Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A more detailed dashboard user edit page #332

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { Typography } from "@stackframe/stack-ui";
import { Typography, cn } from "@stackframe/stack-ui";

export function PageLayout(props: {
children: React.ReactNode,
title: string,
description?: string,
actions?: React.ReactNode,
width?: "full" | "lg" | "md" | "sm",
}) {
const widthClass = {
full: "w-full",
lg: "max-w-[1250px] w-[1250px]",
md: "max-w-[800px] w-[800px]",
sm: "max-w-[600px] w-[600px]",
}[props.width ?? "lg"];

return (
<div className="py-4 px-4 md:px-6 flex justify-center">
<div className="max-w-[1250px] w-[1250px] min-w-0">
<div className={cn(widthClass, "min-w-0")}>
<div className="flex flex-col gap-4 sm:flex-row sm:justify-between sm:items-end">
<div>
<Typography type="h2">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use client";

import NotFound from "@/app/not-found";
import * as yup from "yup";
import { PageLayout } from "../../page-layout";
import { useAdminApp } from "../../use-admin-app";


export default function PageClient(props: { userId: string }) {
const stackAdminApp = useAdminApp();

if (!yup.string().uuid().isValidSync(props.userId)) {
return <NotFound />;
}

const user = stackAdminApp.useUser(props.userId);

if (!user) {
return <NotFound />;
}

return (
<PageLayout title="User Profile" width="md">
test
</PageLayout>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import PageClient from "./page-client";

export const metadata = {
title: "User Profile",
};

export default async function Page(props: { params: { userId: string } }) {
return (
<PageClient userId={props.params.userId} />
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,107 @@

import { UserTable } from "@/components/data-table/user-table";
import { FormDialog } from "@/components/form-dialog";
import { InputField, SwitchField } from "@/components/form-fields";
import { InputField, SwitchField, TextAreaField } from "@/components/form-fields";
import { StyledLink } from "@/components/link";
import { Alert, Button } from "@stackframe/stack-ui";
import { KnownErrors } from "@stackframe/stack-shared";
import { jsonStringOrEmptySchema } from "@stackframe/stack-shared/dist/schema-fields";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, Button, Typography, useToast } from "@stackframe/stack-ui";
import * as yup from "yup";
import { PageLayout } from "../page-layout";
import { useAdminApp } from "../use-admin-app";
import { UserDialog } from "@/components/user-dialog";

export function UserDialog(props: {
open?: boolean,
onOpenChange?: (open: boolean) => void,
trigger?: React.ReactNode,
}) {
const { toast } = useToast();
const adminApp = useAdminApp();
const project = adminApp.useProject();

const formSchema = yup.object({
primaryEmail: yup.string().email("Primary Email must be a valid email address").required("Primary email is required"),
displayName: yup.string().optional(),
signedUpAt: yup.date().required(),
clientMetadata: jsonStringOrEmptySchema.default("null"),
clientReadOnlyMetadata: jsonStringOrEmptySchema.default("null"),
serverMetadata: jsonStringOrEmptySchema.default("null"),
primaryEmailVerified: yup.boolean().optional(),
password: yup.string().optional(),
otpAuthEnabled: yup.boolean().test({
name: 'otp-verified',
message: "Primary email must be verified if OTP/magic link sign-in is enabled",
test: (value, context) => {
return context.parent.primaryEmailVerified || !value;
},
}).optional(),
passwordEnabled: yup.boolean().optional(),
});

async function handleSubmit(values: yup.InferType<typeof formSchema>) {
const userValues = {
...values,
primaryEmailAuthEnabled: true,
clientMetadata: values.clientMetadata ? JSON.parse(values.clientMetadata) : undefined,
clientReadOnlyMetadata: values.clientReadOnlyMetadata ? JSON.parse(values.clientReadOnlyMetadata) : undefined,
serverMetadata: values.serverMetadata ? JSON.parse(values.serverMetadata) : undefined
};

try {
await adminApp.createUser(userValues);
} catch (error) {
if (error instanceof KnownErrors.UserEmailAlreadyExists) {
toast({
title: "Email already exists",
description: "Please choose a different email address",
variant: "destructive",
});
return 'prevent-close';
}
}
}

return <FormDialog
open={props.open}
onOpenChange={props.onOpenChange}
trigger={props.trigger}
title={"Create User"}
formSchema={formSchema}
okButton={{ label: "Create" }}
render={(form) => (
<>
<div className="flex gap-4 items-end">
<div className="flex-1">
<InputField control={form.control} label="Primary email" name="primaryEmail" required />
</div>
<div className="mb-2">
<SwitchField control={form.control} label="Verified" name="primaryEmailVerified" />
</div>
</div>

<InputField control={form.control} label="Display name" name="displayName" />

{project.config.magicLinkEnabled && <SwitchField control={form.control} label="OTP/magic link sign-in" name="otpAuthEnabled" />}
{project.config.credentialEnabled && <SwitchField control={form.control} label="Password sign-in" name="passwordEnabled" />}
{form.watch("passwordEnabled") ? <InputField control={form.control} label={"Password"} name="password" type="password" /> : null}
{!form.watch("primaryEmailVerified") && form.watch("otpAuthEnabled") && <Typography variant="secondary">Primary email must be verified if OTP/magic link sign-in is enabled</Typography>}

<Accordion type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger>Metadata</AccordionTrigger>
<AccordionContent className="space-y-4">
<TextAreaField rows={3} control={form.control} label="Client metadata" name="clientMetadata" placeholder="null" monospace />
<TextAreaField rows={3} control={form.control} label="Client read only metadata" name="clientReadOnlyMetadata" placeholder="null" monospace />
<TextAreaField rows={3} control={form.control} label="Server metadata" name="serverMetadata" placeholder="null" monospace />
</AccordionContent>
</AccordionItem>
</Accordion>
</>
)}
onSubmit={handleSubmit}
cancelButton
/>;
}


export default function PageClient() {
Expand All @@ -19,7 +113,6 @@ export default function PageClient() {
<PageLayout
title="Users"
actions={<UserDialog
type="create"
trigger={<Button>Create User</Button>}
/>}
>
Expand Down
3 changes: 0 additions & 3 deletions apps/dashboard/src/app/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ export default function NotFound() {
title="Oh no! 404"
description="Page not found."
redirectUrl="/"
secondaryDescription={<>
Did you mean to <Link href="/handler/sign-in" className="underline">log in</Link>?
</>}
redirectText="Go to home"
/>;
}
7 changes: 3 additions & 4 deletions apps/dashboard/src/components/data-table/user-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { ServerUser } from '@stackframe/stack';
import { deindent } from '@stackframe/stack-shared/dist/utils/strings';
import { ActionCell, ActionDialog, AvatarCell, BadgeCell, CopyField, DataTableColumnHeader, DataTableManualPagination, DateCell, SearchToolbarItem, SimpleTooltip, TextCell, Typography } from "@stackframe/stack-ui";
import { ColumnDef, ColumnFiltersState, Row, SortingState, Table } from "@tanstack/react-table";
import { useRouter } from 'next/navigation';
import { useState } from "react";
import { UserDialog } from '../user-dialog';

export type ExtendedServerUser = ServerUser & {
authTypes: string[],
Expand Down Expand Up @@ -61,14 +61,13 @@ function ImpersonateUserDialog(props: {
}

function UserActions({ row }: { row: Row<ExtendedServerUser> }) {
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [impersonateSnippet, setImpersonateSnippet] = useState<string | null>(null);
const app = useAdminApp();
const router = useRouter();

return (
<>
<UserDialog user={row.original} type="edit" open={isEditModalOpen} onOpenChange={setIsEditModalOpen} />
<DeleteUserDialog user={row.original} open={isDeleteModalOpen} onOpenChange={setIsDeleteModalOpen} />
<ImpersonateUserDialog user={row.original} impersonateSnippet={impersonateSnippet} onClose={() => setImpersonateSnippet(null)} />
<ActionCell
Expand All @@ -89,7 +88,7 @@ function UserActions({ row }: { row: Row<ExtendedServerUser> }) {
'-',
{
item: "Edit",
onClick: () => setIsEditModalOpen(true),
onClick: () => { router.push(`/projects/${app.projectId}/users/${row.original.id}`); },
},
...row.original.isMultiFactorRequired ? [{
item: "Remove 2FA",
Expand Down
134 changes: 0 additions & 134 deletions apps/dashboard/src/components/user-dialog.tsx

This file was deleted.

Loading