mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
Added project settings & fixed api key generation bug (#38)
* added project setting page, restructured tooltip * removed danger zone * updated page structures, moved environments to project setting * renamed pages and page-clients * fixed meta data, added emails page * fixed api key projectId bug
This commit is contained in:
parent
393c0a2721
commit
8848adf5cb
@ -13,7 +13,7 @@ import { ActionDialog } from "@/components/action-dialog";
|
||||
import Typography from "@/components/ui/typography";
|
||||
|
||||
|
||||
export default function ApiKeysDashboardClient() {
|
||||
export default function PageClient() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const apiKeySets = stackAdminApp.useApiKeySets();
|
||||
|
||||
@ -100,6 +100,8 @@ function ShowKeyDialog(props: {
|
||||
apiKey?: ApiKeySetFirstView,
|
||||
onClose?: () => void,
|
||||
}) {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const project = stackAdminApp.useProject();
|
||||
if (!props.apiKey) return null;
|
||||
|
||||
return <ActionDialog
|
||||
@ -114,7 +116,7 @@ function ShowKeyDialog(props: {
|
||||
Here are your API keys. Copy them to a safe place. You will not be able to view them again.
|
||||
</Typography>
|
||||
<EnvKeys
|
||||
projectId="projectId"
|
||||
projectId={project.id}
|
||||
publishableClientKey={props.apiKey.publishableClientKey}
|
||||
secretServerKey={props.apiKey.secretServerKey}
|
||||
/>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import ApiKeysDashboardClient from "./page-client";
|
||||
import PageClient from "./page-client";
|
||||
|
||||
export const metadata = {
|
||||
title: "API Keys",
|
||||
};
|
||||
|
||||
export default function ApiKeysDashboard() {
|
||||
export default function Page() {
|
||||
return (
|
||||
<ApiKeysDashboardClient />
|
||||
<PageClient />
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import { OAuthProviderConfigJson } from "@stackframe/stack-shared";
|
||||
import { PageLayout } from "../page-layout";
|
||||
import { SettingCard, SettingSwitch } from "@/components/settings";
|
||||
|
||||
export default function ProvidersClient() {
|
||||
export default function PageClient() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const project = stackAdminApp.useProjectAdmin();
|
||||
const oauthProviders = project.evaluatedConfig.oauthProviders;
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import ProvidersClient from "./page-client";
|
||||
import PageClient from "./page-client";
|
||||
|
||||
export const metadata = {
|
||||
title: "Providers",
|
||||
title: "Auth Methods",
|
||||
};
|
||||
|
||||
export default function Providers() {
|
||||
export default function Page() {
|
||||
return (
|
||||
<ProvidersClient />
|
||||
<PageClient />
|
||||
);
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"
|
||||
import Typography from "@/components/ui/typography";
|
||||
import { InputField, SwitchField } from "@/components/form-fields";
|
||||
import { FormDialog } from "@/components/form-dialog";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { TextTooltip } from "@/components/text-tooltip";
|
||||
|
||||
/**
|
||||
* All the different types of OAuth providers that can be created.
|
||||
@ -174,16 +174,9 @@ export function ProviderSettingSwitch(props: Props) {
|
||||
<div className="flex items-center gap-2">
|
||||
{toTitle(props.id)}
|
||||
{isShared && enabled &&
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge variant="secondary">Shared keys</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Shared keys are created by the Stack team for easy development experience
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TextTooltip text="Shared keys are created by the Stack team for easy development experience">
|
||||
<Badge variant="secondary">Shared keys</Badge>
|
||||
</TextTooltip>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -145,7 +145,7 @@ function DeleteDialog(props: {
|
||||
);
|
||||
}
|
||||
|
||||
export default function UrlsAndCallbacksClient() {
|
||||
export default function PageClient() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const project = stackAdminApp.useProjectAdmin();
|
||||
const domains = project.evaluatedConfig.domains;
|
||||
@ -0,0 +1,11 @@
|
||||
import PageClient from "./page-client";
|
||||
|
||||
export const metadata = {
|
||||
title: "Domains & Handlers",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<PageClient />
|
||||
);
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
import EnvironmentClient from "./page-client";
|
||||
|
||||
export const metadata = {
|
||||
title: "Environment",
|
||||
};
|
||||
|
||||
export default function Environment() {
|
||||
return (
|
||||
<EnvironmentClient />
|
||||
);
|
||||
}
|
||||
@ -1,19 +1,17 @@
|
||||
"use client";;
|
||||
import { useAdminApp } from "../use-admin-app";
|
||||
import { PageLayout } from "../page-layout";
|
||||
import { SettingCard, SettingSwitch } from "@/components/settings";
|
||||
import { SettingCard, SettingInput, SettingSwitch } from "@/components/settings";
|
||||
import { Alert } from "@/components/ui/alert";
|
||||
import Typography from "@/components/ui/typography";
|
||||
import { Link } from "@/components/link";
|
||||
|
||||
export default function EnvironmentClient() {
|
||||
export default function PageClient() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const project = stackAdminApp.useProjectAdmin();
|
||||
|
||||
const productionModeErrors = project.getProductionModeErrors();
|
||||
|
||||
return (
|
||||
<PageLayout title="Environment" description="Development and production settings">
|
||||
<PageLayout title="Project Settings" description="Manage your project">
|
||||
<SettingCard title="Production mode" description="Production mode disallows certain configuration options that are useful for development but deemed unsafe for production usage. To prevent accidental misconfigurations it is strongly recommended to enable production mode on your production environments.">
|
||||
<SettingSwitch
|
||||
label="Enable production mode"
|
||||
@ -43,6 +41,35 @@ export default function EnvironmentClient() {
|
||||
</Alert>
|
||||
)}
|
||||
</SettingCard>
|
||||
|
||||
<SettingCard title="Project Information">
|
||||
<SettingInput
|
||||
label="Display Name"
|
||||
onChange={(v) => project.update({ displayName: v })}
|
||||
defaultValue={project.displayName}/>
|
||||
<SettingInput
|
||||
label="Description"
|
||||
onChange={(v) => project.update({ description: v })}
|
||||
defaultValue={project.description}
|
||||
/>
|
||||
</SettingCard>
|
||||
|
||||
{/* <SettingCard title="Danger Zone" description="Be careful with these settings" accordion="Danger Settings">
|
||||
<div>
|
||||
<ActionDialog
|
||||
danger
|
||||
title="Delete Project"
|
||||
trigger={<Button variant="destructive">Delete Project</Button>}
|
||||
okButton={{ label: "Delete Project", onClick: async () => {
|
||||
// await project.delete();
|
||||
// stackAdminApp.router.push("/projects");
|
||||
}}}
|
||||
confirmText="I understand that all the users, teams, and data associated with this project will be permanently deleted. This action cannot be undone."
|
||||
>
|
||||
{`Are you sure that you want to delete the project "${project.displayName}" with the id of "${project.id}"?`}
|
||||
</ActionDialog>
|
||||
</div>
|
||||
</SettingCard> */}
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import PageClient from "./page-client";
|
||||
|
||||
export const metadata = {
|
||||
title: "Project Settings",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<PageClient />
|
||||
);
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
import { redirectHandler } from "@/route-handlers/redirect-handler";
|
||||
|
||||
export const GET = redirectHandler("api-keys");
|
||||
@ -2,11 +2,12 @@
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Book,
|
||||
Globe,
|
||||
KeyRound,
|
||||
LockKeyhole,
|
||||
LucideIcon,
|
||||
Mail,
|
||||
Menu,
|
||||
Settings,
|
||||
Settings2,
|
||||
ShieldEllipsis,
|
||||
User,
|
||||
@ -105,18 +106,11 @@ const navigationItems: (Label | Item | Hidden)[] = [
|
||||
},
|
||||
{
|
||||
name: "Domains & Handlers",
|
||||
href: "/urls-and-callbacks",
|
||||
regex: /^\/projects\/[^\/]+\/urls-and-callbacks$/,
|
||||
href: "/domains",
|
||||
regex: /^\/projects\/[^\/]+\/domains$/,
|
||||
icon: LinkIcon,
|
||||
type: 'item'
|
||||
},
|
||||
{
|
||||
name: "Environment",
|
||||
href: "/environment",
|
||||
regex: /^\/projects\/[^\/]+\/environment$/,
|
||||
icon: Globe,
|
||||
type: 'item'
|
||||
},
|
||||
{
|
||||
name: "API Keys",
|
||||
href: "/api-keys",
|
||||
@ -124,6 +118,13 @@ const navigationItems: (Label | Item | Hidden)[] = [
|
||||
icon: KeyRound,
|
||||
type: 'item'
|
||||
},
|
||||
{
|
||||
name: "Project Settings",
|
||||
href: "/project-settings",
|
||||
regex: /^\/projects\/[^\/]+\/project-settings$/,
|
||||
icon: Settings,
|
||||
type: 'item'
|
||||
}
|
||||
];
|
||||
|
||||
export function NavItem({ item, href, onClick }: { item: Item, href: string, onClick?: () => void}) {
|
||||
|
||||
@ -10,7 +10,7 @@ import { InputField } from "@/components/form-fields";
|
||||
import { TeamPermissionTable } from "@/components/data-table/team-permission-table";
|
||||
|
||||
|
||||
export default function ClientPage() {
|
||||
export default function PageClient() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const permissions = stackAdminApp.usePermissionDefinitions();
|
||||
const [createPermissionModalOpen, setCreatePermissionModalOpen] = React.useState(false);
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import ClientPage from "./page-client";
|
||||
import PageClient from "./page-client";
|
||||
|
||||
export const metadata = {
|
||||
title: "Team Permissions",
|
||||
};
|
||||
|
||||
export default function TeamPermissions() {
|
||||
export default function Page() {
|
||||
return (
|
||||
<ClientPage />
|
||||
<PageClient />
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { useAdminApp } from "../use-admin-app";
|
||||
import { PageLayout } from "../page-layout";
|
||||
import { SettingCard, SettingSwitch } from "@/components/settings";
|
||||
|
||||
export default function TeamSettingsClient() {
|
||||
export default function PageClient() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const project = stackAdminApp.useProjectAdmin();
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import TeamSettingsClient from "./page-client";
|
||||
import PageClient from "./page-client";
|
||||
|
||||
export const metadata = {
|
||||
title: "Team Settings",
|
||||
};
|
||||
|
||||
export default function TeamSettings() {
|
||||
export default function Page() {
|
||||
return (
|
||||
<TeamSettingsClient />
|
||||
<PageClient />
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import { PageLayout } from '../../page-layout';
|
||||
import { TeamMemberTable } from '@/components/data-table/team-member-table';
|
||||
|
||||
|
||||
export default function ClientPage(props: { teamId: string }) {
|
||||
export default function PageClient(props: { teamId: string }) {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const team = stackAdminApp.useTeam(props.teamId);
|
||||
const users = team?.useMembers();
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import ClientPage from "./page-client";
|
||||
import PageClient from "./page-client";
|
||||
|
||||
export const metadata = {
|
||||
title: "Team Members",
|
||||
@ -6,6 +6,6 @@ export const metadata = {
|
||||
|
||||
export default function Page({ params }: { params: { teamId: string } }) {
|
||||
return (
|
||||
<ClientPage teamId={params.teamId} />
|
||||
<PageClient teamId={params.teamId} />
|
||||
);
|
||||
}
|
||||
@ -4,7 +4,7 @@ import { PageLayout } from "../page-layout";
|
||||
import { TeamTable } from "@/components/data-table/team-table";
|
||||
|
||||
|
||||
export default function ClientPage() {
|
||||
export default function PageClient() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const teams = stackAdminApp.useTeams();
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import ClientPage from "./page-client";
|
||||
import PageClient from "./page-client";
|
||||
|
||||
export const metadata = {
|
||||
title: "Teams",
|
||||
};
|
||||
|
||||
export default function TeamDashboard() {
|
||||
export default function Page() {
|
||||
return (
|
||||
<ClientPage />
|
||||
<PageClient />
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
import UrlsAndCallbacksClient from "./page-client";
|
||||
|
||||
export const metadata = {
|
||||
title: "Domains & Handlers",
|
||||
};
|
||||
|
||||
export default function UrlsAndCallbacks() {
|
||||
return (
|
||||
<UrlsAndCallbacksClient />
|
||||
);
|
||||
}
|
||||
@ -7,7 +7,7 @@ import { Link } from "@/components/link";
|
||||
import { UserTable } from "@/components/data-table/user-table";
|
||||
|
||||
|
||||
export default function UsersDashboardClient() {
|
||||
export default function PageClient() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const allUsers = stackAdminApp.useServerUsers();
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import UsersDashboardClient from "./page-client";
|
||||
import PageClient from "./page-client";
|
||||
|
||||
export const metadata = {
|
||||
title: "Users",
|
||||
};
|
||||
|
||||
export default function UsersDashboard() {
|
||||
export default function Page() {
|
||||
return (
|
||||
<UsersDashboardClient />
|
||||
<PageClient />
|
||||
);
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ import { ActionDialog } from "../action-dialog";
|
||||
import Typography from "../ui/typography";
|
||||
import { standardFilterFn } from "./elements/utils";
|
||||
import { CircleAlert } from "lucide-react";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
|
||||
import { TextTooltip } from "../text-tooltip";
|
||||
|
||||
export type ExtendedServerUser = ServerUser & {
|
||||
authType: string,
|
||||
@ -155,17 +155,9 @@ export const commonUserColumns: ColumnDef<ExtendedServerUser>[] = [
|
||||
{
|
||||
accessorKey: "primaryEmail",
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Primary Email" />,
|
||||
cell: ({ row }) => <TextCell size={180} icon={row.original.emailVerified === "unverified" &&
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<CircleAlert className="text-zinc-500 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Email not verified
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>}>
|
||||
cell: ({ row }) => <TextCell
|
||||
size={180}
|
||||
icon={row.original.emailVerified === "unverified" && <TextTooltip text="Email not verified"><CircleAlert className="text-zinc-500 h-4 w-4" /></TextTooltip>}>
|
||||
{row.original.primaryEmail}
|
||||
</TextCell>,
|
||||
},
|
||||
|
||||
@ -9,8 +9,12 @@ import {
|
||||
import { Switch } from "./ui/switch";
|
||||
import { Settings } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import { useId, useState } from "react";
|
||||
import React, { useEffect, useId, useRef, useState } from "react";
|
||||
import { Label } from "./ui/label";
|
||||
import { DelayedInput, Input } from "./ui/input";
|
||||
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import { Accordion } from "@radix-ui/react-accordion";
|
||||
import { AccordionContent, AccordionItem, AccordionTrigger } from "./ui/accordion";
|
||||
|
||||
|
||||
export function SettingCard(props: {
|
||||
@ -18,6 +22,7 @@ export function SettingCard(props: {
|
||||
description?: string,
|
||||
actions?: React.ReactNode,
|
||||
children?: React.ReactNode,
|
||||
accordion?: string,
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
@ -25,8 +30,19 @@ export function SettingCard(props: {
|
||||
<CardTitle>{props.title}</CardTitle>
|
||||
{props.description && <CardDescription>{props.description}</CardDescription>}
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{props.children}
|
||||
{props.accordion ?
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>{props.accordion}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
{props.children}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion> :
|
||||
props.children}
|
||||
|
||||
</CardContent>
|
||||
{props.actions && <CardFooter>
|
||||
<div className="w-full flex justify-end">
|
||||
@ -69,12 +85,33 @@ export function SettingSwitch(props: {
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingIconButton(props: {
|
||||
onClick?: () => void | Promise<void>,
|
||||
}) {
|
||||
export const SettingIconButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>((props, ref) => {
|
||||
return (
|
||||
<Button variant='ghost' size='sm' className="p-1 h-full" onClick={props.onClick}>
|
||||
<Button variant='ghost' size='sm' className="p-1 h-full" onClick={props.onClick} ref={ref}>
|
||||
<Settings className="w-4 h-4 text-muted-foreground" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
});
|
||||
SettingIconButton.displayName = "SettingIconButton";
|
||||
|
||||
export function SettingInput(props: {
|
||||
label: string,
|
||||
defaultValue?: string,
|
||||
onChange: (value: string) => void | Promise<void>,
|
||||
actions?: React.ReactNode,
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>{props.label}</Label>
|
||||
<DelayedInput
|
||||
className="max-w-[400px]"
|
||||
defaultValue={props.defaultValue}
|
||||
onChange={(e) => runAsynchronously(props.onChange(e.target.value))}
|
||||
/>
|
||||
{props.actions}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
19
packages/stack-server/src/components/text-tooltip.tsx
Normal file
19
packages/stack-server/src/components/text-tooltip.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
||||
|
||||
export function TextTooltip(props: {
|
||||
text: string,
|
||||
children: React.ReactNode,
|
||||
}) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{props.children}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{props.text}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@ -23,3 +23,29 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
|
||||
|
||||
export interface DelayedInputProps extends InputProps {
|
||||
delay?: number,
|
||||
}
|
||||
|
||||
export const DelayedInput = React.forwardRef<HTMLInputElement, DelayedInputProps>(
|
||||
({ delay = 500, defaultValue, ...props }, ref) => {
|
||||
const [value, setValue] = React.useState(defaultValue ?? "");
|
||||
|
||||
const timeout = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(e.target.value);
|
||||
if (timeout.current) {
|
||||
clearTimeout(timeout.current);
|
||||
}
|
||||
timeout.current = setTimeout(() => {
|
||||
props.onChange?.(e);
|
||||
}, delay);
|
||||
};
|
||||
|
||||
return <Input ref={ref} {...props} value={value} onChange={onChange} />;
|
||||
}
|
||||
);
|
||||
DelayedInput.displayName = "DelayedInput";
|
||||
Loading…
Reference in New Issue
Block a user