fix(admin): organize admin panel (#7840)

This commit is contained in:
forehalo
2024-08-13 05:45:02 +00:00
parent 6dea831d8a
commit 0ec1995add
45 changed files with 746 additions and 955 deletions

View File

@@ -1,134 +0,0 @@
import { Button } from '@affine/admin/components/ui/button';
import { Input } from '@affine/admin/components/ui/input';
import { Label } from '@affine/admin/components/ui/label';
import { Separator } from '@affine/admin/components/ui/separator';
import { Switch } from '@affine/admin/components/ui/switch';
import { FeatureType } from '@affine/graphql';
import { CheckIcon, XIcon } from 'lucide-react';
import { useCallback, useState } from 'react';
import { useRightPanel } from '../../layout';
import { useUserManagement } from './use-user-management';
export function CreateUserPanel() {
const { closePanel } = useRightPanel();
const [name, setName] = useState('');
const [password, setPassword] = useState('');
const [email, setEmail] = useState('');
const [features, setFeatures] = useState<FeatureType[]>([]);
const disableSave = !name || !email;
const { createUser } = useUserManagement();
const handleConfirm = useCallback(() => {
createUser({
name,
email,
password,
features,
callback: closePanel,
});
}, [closePanel, createUser, email, features, name, password]);
const onEarlyAccessChange = useCallback(
(checked: boolean) => {
setFeatures(
checked
? [...features, FeatureType.AIEarlyAccess]
: features.filter(f => f !== FeatureType.AIEarlyAccess)
);
},
[features]
);
const onAdminChange = useCallback(
(checked: boolean) => {
setFeatures(
checked
? [...features, FeatureType.Admin]
: features.filter(f => f !== FeatureType.Admin)
);
},
[features]
);
return (
<div className="flex flex-col h-full gap-1">
<div className=" flex justify-between items-center py-[10px] px-6">
<Button
type="button"
size="icon"
className="w-7 h-7"
variant="ghost"
onClick={closePanel}
>
<XIcon size={20} />
</Button>
<span className="text-base font-medium">Create Account</span>
<Button
type="submit"
size="icon"
className="w-7 h-7"
variant="ghost"
onClick={handleConfirm}
disabled={disableSave}
>
<CheckIcon size={20} />
</Button>
</div>
<Separator />
<div className="p-4 flex-grow overflow-y-auto space-y-[10px]">
<div className="flex flex-col rounded-md border py-4 gap-4">
<div className="px-5 space-y-3">
<Label className="text-sm font-medium">Name</Label>
<Input
type="text"
className="py-2 px-3 text-base font-normal"
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
<Separator />
<div className="px-5 space-y-3">
<Label className="text-sm font-medium">Email</Label>
<Input
type="email"
className="py-2 px-3 ext-base font-normal"
value={email}
onChange={e => setEmail(e.target.value)}
/>
</div>{' '}
<Separator />
<div className="px-5 space-y-3">
<Label className="text-sm font-medium">Password</Label>
<Input
type="password"
className="py-2 px-3 ext-base font-normal"
value={password}
onChange={e => setPassword(e.target.value)}
/>
</div>
</div>
<div className="border rounded-md">
<Label className="flex items-center justify-between px-4 py-3">
<span>Enable AI Access</span>
<Switch
checked={features.includes(FeatureType.AIEarlyAccess)}
onCheckedChange={onEarlyAccessChange}
/>
</Label>
<Separator />
<Label className="flex items-center justify-between px-4 py-3">
<span>Admin</span>
<Switch
checked={features.includes(FeatureType.Admin)}
onCheckedChange={onAdminChange}
/>
</Label>
</div>
</div>
</div>
);
}

View File

@@ -3,6 +3,7 @@ import {
AvatarFallback,
AvatarImage,
} from '@affine/admin/components/ui/avatar';
import type { UserType } from '@affine/graphql';
import { FeatureType } from '@affine/graphql';
import type { ColumnDef } from '@tanstack/react-table';
import clsx from 'clsx';
@@ -15,7 +16,6 @@ import {
} from 'lucide-react';
import type { ReactNode } from 'react';
import type { User } from '../schema';
import { DataTableRowActions } from './data-table-row-actions';
const StatusItem = ({
@@ -51,7 +51,7 @@ const StatusItem = ({
</div>
);
export const columns: ColumnDef<User>[] = [
export const columns: ColumnDef<UserType>[] = [
{
accessorKey: 'info',
cell: ({ row }) => (
@@ -88,13 +88,13 @@ export const columns: ColumnDef<User>[] = [
},
{
accessorKey: 'property',
cell: ({ row }) => (
cell: ({ row: { original: user } }) => (
<div className="flex items-center gap-2">
<div className="flex flex-col gap-2 text-xs max-md:hidden">
<div className="flex justify-end opacity-25">{row.original.id}</div>
<div className="flex justify-end opacity-25">{user.id}</div>
<div className="flex gap-3 items-center justify-end">
<StatusItem
condition={row.original.hasPassword}
condition={user.hasPassword}
IconTrue={<LockIcon size={10} />}
IconFalse={<UnlockIcon size={10} />}
textTrue="Password Set"
@@ -102,7 +102,7 @@ export const columns: ColumnDef<User>[] = [
/>
<StatusItem
condition={row.original.emailVerified}
condition={user.emailVerified}
IconTrue={<MailIcon size={10} />}
IconFalse={<MailWarningIcon size={10} />}
textTrue="Email Verified"
@@ -110,7 +110,7 @@ export const columns: ColumnDef<User>[] = [
/>
</div>
</div>
<DataTableRowActions row={row} />
<DataTableRowActions user={user} />
</div>
),
},

View File

@@ -6,7 +6,6 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@affine/admin/components/ui/dropdown-menu';
import type { Row } from '@tanstack/react-table';
import {
LockIcon,
MoreVerticalIcon,
@@ -17,29 +16,26 @@ import { useCallback, useState } from 'react';
import { toast } from 'sonner';
import { useRightPanel } from '../../layout';
import { userSchema } from '../schema';
import type { UserType } from '../schema';
import { DeleteAccountDialog } from './delete-account';
import { DiscardChanges } from './discard-changes';
import { EditPanel } from './edit-panel';
import { ResetPasswordDialog } from './reset-password';
import { useUserManagement } from './use-user-management';
import { useDeleteUser, useResetUserPassword } from './use-user-management';
import { UpdateUserForm } from './user-form';
interface DataTableRowActionsProps<TData> {
row: Row<TData>;
interface DataTableRowActionsProps {
user: UserType;
}
export function DataTableRowActions<TData>({
row,
}: DataTableRowActionsProps<TData>) {
export function DataTableRowActions({ user }: DataTableRowActionsProps) {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false);
const [discardDialogOpen, setDiscardDialogOpen] = useState(false);
const user = userSchema.parse(row.original);
const { setRightPanelContent, openPanel, isOpen, closePanel } =
useRightPanel();
const { deleteUser, resetPasswordLink, onResetPassword } =
useUserManagement();
const deleteUser = useDeleteUser();
const { resetPasswordLink, onResetPassword } = useResetUserPassword();
const openResetPasswordDialog = useCallback(() => {
onResetPassword(user.id, () => setResetPasswordDialogOpen(true));
@@ -82,8 +78,9 @@ export function DataTableRowActions<TData>({
const handleConfirm = useCallback(() => {
setRightPanelContent(
<EditPanel
<UpdateUserForm
user={user}
onComplete={closePanel}
onResetPassword={openResetPasswordDialog}
onDeleteAccount={openDeleteDialog}
/>
@@ -96,6 +93,7 @@ export function DataTableRowActions<TData>({
openPanel();
}
}, [
closePanel,
discardDialogOpen,
handleDiscardChangesCancel,
isOpen,

View File

@@ -7,8 +7,8 @@ import type { SetStateAction } from 'react';
import { startTransition, useCallback, useEffect, useState } from 'react';
import { useRightPanel } from '../../layout';
import { CreateUserPanel } from './ceate-user-panel';
import { DiscardChanges } from './discard-changes';
import { CreateUserForm } from './user-form';
interface DataTableToolbarProps<TData> {
data: TData[];
@@ -38,17 +38,18 @@ export function DataTableToolbar<TData>({
const [value, setValue] = useState('');
const [dialogOpen, setDialogOpen] = useState(false);
const debouncedValue = useDebouncedValue(value, 500);
const { setRightPanelContent, openPanel, isOpen } = useRightPanel();
const { setRightPanelContent, openPanel, closePanel, isOpen } =
useRightPanel();
const handleConfirm = useCallback(() => {
setRightPanelContent(<CreateUserPanel />);
setRightPanelContent(<CreateUserForm onComplete={closePanel} />);
if (dialogOpen) {
setDialogOpen(false);
}
if (!isOpen) {
openPanel();
}
}, [setRightPanelContent, dialogOpen, isOpen, openPanel]);
}, [setRightPanelContent, closePanel, dialogOpen, isOpen, openPanel]);
const result = useQuery({
query: getUserByEmailQuery,

View File

@@ -1,155 +0,0 @@
import { Button } from '@affine/admin/components/ui/button';
import { Input } from '@affine/admin/components/ui/input';
import { Label } from '@affine/admin/components/ui/label';
import { Separator } from '@affine/admin/components/ui/separator';
import { Switch } from '@affine/admin/components/ui/switch';
import { FeatureType } from '@affine/graphql';
import { CheckIcon, ChevronRightIcon, XIcon } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { useRightPanel } from '../../layout';
import type { User } from '../schema';
import { useUserManagement } from './use-user-management';
interface EditPanelProps {
user: User;
onResetPassword: () => void;
onDeleteAccount: () => void;
}
export function EditPanel({
user,
onResetPassword,
onDeleteAccount,
}: EditPanelProps) {
const { closePanel } = useRightPanel();
const [name, setName] = useState(user.name);
const [email, setEmail] = useState(user.email);
const [features, setFeatures] = useState(user.features);
const { updateUser } = useUserManagement();
const disableSave =
name === user.name && email === user.email && features === user.features;
const onConfirm = useCallback(() => {
updateUser({
userId: user.id,
name,
email,
features,
callback: closePanel,
});
}, [closePanel, email, features, name, updateUser, user.id]);
const onEarlyAccessChange = useCallback(
(checked: boolean) => {
if (checked) {
setFeatures([...features, FeatureType.AIEarlyAccess]);
} else {
setFeatures(features.filter(f => f !== FeatureType.AIEarlyAccess));
}
},
[features]
);
const onAdminChange = useCallback(
(checked: boolean) => {
if (checked) {
setFeatures([...features, FeatureType.Admin]);
} else {
setFeatures(features.filter(f => f !== FeatureType.Admin));
}
},
[features]
);
useEffect(() => {
setName(user.name);
setEmail(user.email);
setFeatures(user.features);
}, [user]);
return (
<div className="flex flex-col h-full gap-1">
<div className=" flex justify-between items-center py-[10px] px-6 ">
<Button
type="button"
size="icon"
className="w-7 h-7"
variant="ghost"
onClick={closePanel}
>
<XIcon size={20} />
</Button>
<span className="text-base font-medium">Edit Account</span>
<Button
type="submit"
size="icon"
className="w-7 h-7"
variant="ghost"
onClick={onConfirm}
disabled={disableSave}
>
<CheckIcon size={20} />
</Button>
</div>
<Separator />
<div className="p-4 flex-grow overflow-y-auto space-y-[10px]">
<div className="flex flex-col rounded-md border py-4 gap-4">
<div className="px-5 space-y-3">
<Label className="text-sm font-medium">Name</Label>
<Input
type="text"
className="py-2 px-3 text-base font-normal"
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
<Separator />
<div className="px-5 space-y-3">
<Label className="text-sm font-medium">Email</Label>
<Input
type="email"
className="py-2 px-3 ext-base font-normal"
value={email}
onChange={e => setEmail(e.target.value)}
/>
</div>
</div>
<Button
className="w-full flex items-center justify-between text-sm font-medium px-4 py-3"
variant="outline"
onClick={onResetPassword}
>
<span>Reset Password</span>
<ChevronRightIcon size={16} />
</Button>
<div className="border rounded-md">
<Label className="flex items-center justify-between px-4 py-3">
<span>Enable AI Access</span>
<Switch
checked={features.includes(FeatureType.AIEarlyAccess)}
onCheckedChange={onEarlyAccessChange}
/>
</Label>
<Separator />
<Label className="flex items-center justify-between px-4 py-3">
<span>Admin</span>
<Switch
checked={features.includes(FeatureType.Admin)}
onCheckedChange={onAdminChange}
/>
</Label>
</div>
<Button
className="w-full text-red-500 px-4 py-3 rounded-md flex items-center justify-between text-sm font-medium hover:text-red-500"
variant="outline"
onClick={onDeleteAccount}
>
<span>Delete Account</span>
<ChevronRightIcon size={16} />
</Button>
</div>
</div>
);
}

View File

@@ -4,161 +4,103 @@ import {
useMutation,
} from '@affine/core/hooks/use-mutation';
import {
addToAdminMutation,
addToEarlyAccessMutation,
createChangePasswordUrlMutation,
createUserMutation,
deleteUserMutation,
EarlyAccessType,
FeatureType,
listUsersQuery,
removeAdminMutation,
removeEarlyAccessMutation,
updateAccountFeaturesMutation,
updateAccountMutation,
} from '@affine/graphql';
import { useCallback, useMemo, useState } from 'react';
import { toast } from 'sonner';
import type { UserInput } from '../schema';
export const useCreateUser = () => {
const { trigger: createUser } = useMutation({
const {
trigger: createAccount,
isMutating: creating,
error,
} = useMutation({
mutation: createUserMutation,
});
const { trigger: addToEarlyAccess } = useMutation({
mutation: addToEarlyAccessMutation,
});
const { trigger: addToAdmin } = useMutation({
mutation: addToAdminMutation,
const { trigger: updateAccountFeatures } = useMutation({
mutation: updateAccountFeaturesMutation,
});
const revalidate = useMutateQueryResource();
const updateFeatures = useCallback(
(email: string, features: FeatureType[]) => {
const shouldAddToAdmin = features.includes(FeatureType.Admin);
const shouldAddToAIEarlyAccess = features.includes(
FeatureType.AIEarlyAccess
);
return Promise.all([
shouldAddToAdmin && addToAdmin({ email }),
shouldAddToAIEarlyAccess &&
addToEarlyAccess({ email, type: EarlyAccessType.AI }),
]);
},
[addToAdmin, addToEarlyAccess]
);
const create = useAsyncCallback(
async ({
name,
email,
password,
features,
callback,
}: {
name: string;
email: string;
password: string;
features: FeatureType[];
callback?: () => void;
}) => {
await createUser({
input: {
name,
email,
password,
},
})
.then(async () => {
await updateFeatures(email, features);
await revalidate(listUsersQuery);
toast('User created successfully');
callback?.();
})
.catch(e => {
toast(e.message);
console.error(e);
async ({ name, email, features }: UserInput) => {
try {
const account = await createAccount({
input: {
name,
email,
},
});
await updateAccountFeatures({
userId: account.createUser.id,
features,
});
await revalidate(listUsersQuery);
toast('Account updated successfully');
} catch (e) {
toast.error('Failed to update account: ' + (e as Error).message);
}
},
[createUser, revalidate, updateFeatures]
[createAccount, revalidate]
);
return create;
return { creating: creating || !!error, create };
};
interface UpdateUserProps {
userId: string;
name: string;
email: string;
features: FeatureType[];
callback?: () => void;
}
export const useUpdateUser = () => {
const { trigger: updateAccount } = useMutation({
const {
trigger: updateAccount,
isMutating: updating,
error,
} = useMutation({
mutation: updateAccountMutation,
});
const { trigger: addToEarlyAccess } = useMutation({
mutation: addToEarlyAccessMutation,
});
const { trigger: removeEarlyAccess } = useMutation({
mutation: removeEarlyAccessMutation,
});
const { trigger: addToAdmin } = useMutation({
mutation: addToAdminMutation,
});
const { trigger: removeAdmin } = useMutation({
mutation: removeAdminMutation,
const { trigger: updateAccountFeatures } = useMutation({
mutation: updateAccountFeaturesMutation,
});
const revalidate = useMutateQueryResource();
const updateFeatures = useCallback(
({ email, features }: { email: string; features: FeatureType[] }) => {
const shoutAddToAdmin = features.includes(FeatureType.Admin);
const shoutAddToAIEarlyAccess = features.includes(
FeatureType.AIEarlyAccess
);
return Promise.all([
shoutAddToAdmin ? addToAdmin({ email }) : removeAdmin({ email }),
shoutAddToAIEarlyAccess
? addToEarlyAccess({ email, type: EarlyAccessType.AI })
: removeEarlyAccess({ email, type: EarlyAccessType.AI }),
]);
},
[addToAdmin, addToEarlyAccess, removeAdmin, removeEarlyAccess]
);
const update = useAsyncCallback(
async ({ userId, name, email, features, callback }: UpdateUserProps) => {
updateAccount({
id: userId,
input: {
name,
email,
},
})
.then(async () => {
await updateFeatures({ email, features });
await revalidate(listUsersQuery);
toast('Account updated successfully');
callback?.();
})
.catch(e => {
toast.error('Failed to update account: ' + e.message);
async ({
userId,
name,
email,
features,
}: UserInput & { userId: string }) => {
try {
await updateAccount({
id: userId,
input: {
name,
email,
},
});
await updateAccountFeatures({
userId,
features,
});
await revalidate(listUsersQuery);
toast('Account updated successfully');
} catch (e) {
toast.error('Failed to update account: ' + (e as Error).message);
}
},
[revalidate, updateAccount, updateFeatures]
[revalidate, updateAccount]
);
return update;
return { updating: updating || !!error, update };
};
export const useResetUserPassword = () => {
@@ -217,20 +159,3 @@ export const useDeleteUser = () => {
return deleteById;
};
export const useUserManagement = () => {
const createUser = useCreateUser();
const updateUser = useUpdateUser();
const deleteUser = useDeleteUser();
const { resetPasswordLink, onResetPassword } = useResetUserPassword();
return useMemo(() => {
return {
createUser,
updateUser,
deleteUser,
resetPasswordLink,
onResetPassword,
};
}, [createUser, deleteUser, onResetPassword, resetPasswordLink, updateUser]);
};

View File

@@ -0,0 +1,288 @@
import { Button } from '@affine/admin/components/ui/button';
import { Input } from '@affine/admin/components/ui/input';
import { Label } from '@affine/admin/components/ui/label';
import { Separator } from '@affine/admin/components/ui/separator';
import { Switch } from '@affine/admin/components/ui/switch';
import type { FeatureType } from '@affine/graphql';
import { CheckIcon, ChevronRightIcon, XIcon } from 'lucide-react';
import type { ChangeEvent } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useServerConfig } from '../../common';
import type { UserInput, UserType } from '../schema';
import { useCreateUser, useUpdateUser } from './use-user-management';
type UserFormProps = {
title: string;
defaultValue?: Partial<UserInput>;
onClose: () => void;
onConfirm: (user: UserInput) => void;
onValidate: (user: Partial<UserInput>) => boolean;
actions?: React.ReactNode;
};
function UserForm({
title,
defaultValue,
onClose,
onConfirm,
onValidate,
actions,
}: UserFormProps) {
const serverConfig = useServerConfig();
const [changes, setChanges] = useState<Partial<UserInput>>({
features: defaultValue?.features ?? [],
});
const setField = useCallback(
<K extends keyof UserInput>(
field: K,
value: UserInput[K] | ((prev: UserInput[K] | undefined) => UserInput[K])
) => {
setChanges(changes => ({
...changes,
[field]:
typeof value === 'function' ? value(changes[field] as any) : value,
}));
},
[]
);
const canSave = useMemo(() => {
return onValidate(changes);
}, [onValidate, changes]);
const handleConfirm = useCallback(() => {
if (!canSave) {
return;
}
// @ts-expect-error checked
onConfirm(changes);
}, [canSave, changes, onConfirm]);
const onFeatureChanged = useCallback(
(feature: FeatureType, checked: boolean) => {
setField('features', (features = []) => {
if (checked) {
return [...features, feature];
}
return features.filter(f => f !== feature);
});
},
[setField]
);
return (
<div className="flex flex-col h-full gap-1">
<div className=" flex justify-between items-center py-[10px] px-6">
<Button
type="button"
size="icon"
className="w-7 h-7"
variant="ghost"
onClick={onClose}
>
<XIcon size={20} />
</Button>
<span className="text-base font-medium">{title}</span>
<Button
type="submit"
size="icon"
className="w-7 h-7"
variant="ghost"
onClick={handleConfirm}
disabled={!canSave}
>
<CheckIcon size={20} />
</Button>
</div>
<Separator />
<div className="p-4 flex-grow overflow-y-auto space-y-[10px]">
<div className="flex flex-col rounded-md border py-4 gap-4">
<InputItem
label="Name"
field="name"
value={changes.name ?? defaultValue?.name}
onChange={setField}
/>
<Separator />
<InputItem
label="Email"
field="email"
value={changes.email ?? defaultValue?.email}
onChange={setField}
/>
</div>
<div className="border rounded-md">
{serverConfig.availableUserFeatures.map((feature, i) => (
<div key={feature}>
<ToggleItem
name={feature}
checked={(
changes.features ??
defaultValue?.features ??
[]
).includes(feature)}
onChange={onFeatureChanged}
/>
{i < serverConfig.availableUserFeatures.length - 1 && (
<Separator />
)}
</div>
))}
</div>
{actions}
</div>
</div>
);
}
function ToggleItem({
name,
checked,
onChange,
}: {
name: FeatureType;
checked: boolean;
onChange: (name: FeatureType, value: boolean) => void;
}) {
const onToggle = useCallback(
(checked: boolean) => {
onChange(name, checked);
},
[name, onChange]
);
return (
<Label className="flex items-center justify-between px-4 py-3">
<span>{name}</span>
<Switch checked={checked} onCheckedChange={onToggle} />
</Label>
);
}
function InputItem({
label,
field,
value,
onChange,
}: {
label: string;
field: keyof UserInput;
value?: string;
onChange: (field: keyof UserInput, value: string) => void;
}) {
const onValueChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
onChange(field, e.target.value);
},
[field, onChange]
);
return (
<div className="px-5 space-y-3">
<Label className="text-sm font-medium">{label}</Label>
<Input
type="text"
className="py-2 px-3 text-base font-normal"
defaultValue={value}
onChange={onValueChange}
/>
</div>
);
}
const validateCreateUser = (user: Partial<UserInput>) => {
return !!user.name && !!user.email && !!user.features;
};
const validateUpdateUser = (user: Partial<UserInput>) => {
return !!user.name || !!user.email;
};
export function CreateUserForm({ onComplete }: { onComplete: () => void }) {
const { create, creating } = useCreateUser();
useEffect(() => {
if (creating) {
return () => {
onComplete();
};
}
return;
}, [creating, onComplete]);
return (
<UserForm
title="Create User"
onClose={onComplete}
onConfirm={create}
onValidate={validateCreateUser}
/>
);
}
export function UpdateUserForm({
user,
onResetPassword,
onDeleteAccount,
onComplete,
}: {
user: UserType;
onResetPassword: () => void;
onDeleteAccount: () => void;
onComplete: () => void;
}) {
const { update, updating } = useUpdateUser();
const onUpdateUser = useCallback(
(updates: UserInput) => {
update({
...updates,
userId: user.id,
});
},
[user, update]
);
useEffect(() => {
if (updating) {
return () => {
onComplete();
};
}
return;
}, [updating, onComplete]);
return (
<UserForm
title="Update User"
defaultValue={user}
onClose={onComplete}
onConfirm={onUpdateUser}
onValidate={validateUpdateUser}
actions={
<>
<Button
className="w-full flex items-center justify-between text-sm font-medium px-4 py-3"
variant="outline"
onClick={onResetPassword}
>
<span>Reset Password</span>
<ChevronRightIcon size={16} />
</Button>
<Button
className="w-full text-red-500 px-4 py-3 rounded-md flex items-center justify-between text-sm font-medium hover:text-red-500"
variant="outline"
onClick={onDeleteAccount}
>
<span>Delete Account</span>
<ChevronRightIcon size={16} />
</Button>
</>
}
/>
);
}

View File

@@ -37,6 +37,7 @@ export function AccountPage() {
<DataTable
data={users}
// @ts-expect-error do not complains
columns={columns}
pagination={pagination}
onPaginationChange={setPagination}

View File

@@ -1,34 +1,8 @@
import { FeatureType } from '@affine/graphql';
import { z } from 'zod';
import type { FeatureType, ListUsersQuery } from '@affine/graphql';
const featureTypeValues = Object.values(FeatureType) as [
FeatureType,
...FeatureType[],
];
const featureTypeEnum = z.enum(featureTypeValues);
export const userSchema = z.object({
__typename: z.literal('UserType').optional(),
id: z.string(),
name: z.string(),
email: z.string(),
features: z.array(featureTypeEnum),
hasPassword: z.boolean().nullable(),
emailVerified: z.boolean(),
avatarUrl: z.string().nullable(),
quota: z
.object({
__typename: z.literal('UserQuota').optional(),
humanReadable: z.object({
__typename: z.literal('UserQuotaHumanReadable').optional(),
blobLimit: z.string(),
historyPeriod: z.string(),
memberLimit: z.string(),
name: z.string(),
storageQuota: z.string(),
}),
})
.nullable(),
});
export type User = z.infer<typeof userSchema>;
export type UserType = ListUsersQuery['users'][0];
export type UserInput = {
name: string;
email: string;
features: FeatureType[];
};

View File

@@ -2,31 +2,21 @@ import { Button } from '@affine/admin/components/ui/button';
import { Input } from '@affine/admin/components/ui/input';
import { Label } from '@affine/admin/components/ui/label';
import { useMutateQueryResource } from '@affine/core/hooks/use-mutation';
import { useQuery } from '@affine/core/hooks/use-query';
import {
FeatureType,
getCurrentUserFeaturesQuery,
getUserFeaturesQuery,
serverConfigQuery,
} from '@affine/graphql';
import { useCallback, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { useCurrentUser, useServerConfig } from '../common';
import logo from './logo.svg';
export function Auth() {
const {
data: { currentUser },
} = useQuery({
query: getCurrentUserFeaturesQuery,
});
const {
data: { serverConfig },
} = useQuery({
query: serverConfigQuery,
});
const currentUser = useCurrentUser();
const serverConfig = useServerConfig();
const revalidate = useMutateQueryResource();
const emailRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);

View File

@@ -0,0 +1,21 @@
import { useQueryImmutable } from '@affine/core/hooks/use-query';
import {
adminServerConfigQuery,
getCurrentUserFeaturesQuery,
} from '@affine/graphql';
export const useServerConfig = () => {
const { data } = useQueryImmutable({
query: adminServerConfigQuery,
});
return data.serverConfig;
};
export const useCurrentUser = () => {
const { data } = useQueryImmutable({
query: getCurrentUserFeaturesQuery,
});
return data.currentUser;
};

View File

@@ -6,7 +6,7 @@ import {
} from '@affine/admin/components/ui/card';
import { ScrollArea } from '@affine/admin/components/ui/scroll-area';
import { Separator } from '@affine/admin/components/ui/separator';
import { useQuery } from '@affine/core/hooks/use-query';
import { useQueryImmutable } from '@affine/core/hooks/use-query';
import { getServerServiceConfigsQuery } from '@affine/graphql';
import { Layout } from '../layout';
@@ -171,7 +171,7 @@ const MailerCard = ({ mailerConfig }: { mailerConfig?: MailerConfig }) => {
};
export function ServerServiceConfig() {
const { data } = useQuery({
const { data } = useQueryImmutable({
query: getServerServiceConfigsQuery,
});
const server = data.serverServiceConfigs.find(

View File

@@ -7,11 +7,7 @@ import { Separator } from '@affine/admin/components/ui/separator';
import { TooltipProvider } from '@affine/admin/components/ui/tooltip';
import { cn } from '@affine/admin/utils';
import { useQuery } from '@affine/core/hooks/use-query';
import {
FeatureType,
getCurrentUserFeaturesQuery,
serverConfigQuery,
} from '@affine/graphql';
import { FeatureType, getCurrentUserFeaturesQuery } from '@affine/graphql';
import { AlignJustifyIcon } from 'lucide-react';
import type { ReactNode, RefObject } from 'react';
import {
@@ -36,6 +32,7 @@ import {
SheetTrigger,
} from '../components/ui/sheet';
import { Logo } from './accounts/components/logo';
import { useServerConfig } from './common';
import { NavContext } from './nav/context';
import { Nav } from './nav/nav';
@@ -85,6 +82,13 @@ export function useMediaQuery(query: string) {
}
export function Layout({ content }: LayoutProps) {
const serverConfig = useServerConfig();
const {
data: { currentUser },
} = useQuery({
query: getCurrentUserFeaturesQuery,
});
const [rightPanelContent, setRightPanelContent] = useState<ReactNode>(null);
const [open, setOpen] = useState(false);
const rightPanelRef = useRef<ImperativePanelHandle>(null);
@@ -122,16 +126,6 @@ export function Layout({ content }: LayoutProps) {
[closePanel, openPanel]
);
const {
data: { serverConfig },
} = useQuery({
query: serverConfigQuery,
});
const {
data: { currentUser },
} = useQuery({
query: getCurrentUserFeaturesQuery,
});
const navigate = useNavigate();
useEffect(() => {

View File

@@ -12,29 +12,17 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@affine/admin/components/ui/dropdown-menu';
import { useQuery } from '@affine/core/hooks/use-query';
import {
FeatureType,
getCurrentUserFeaturesQuery,
serverConfigQuery,
} from '@affine/graphql';
import { FeatureType } from '@affine/graphql';
import { CircleUser, MoreVertical } from 'lucide-react';
import { useCallback, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
export function UserDropdown() {
const {
data: { currentUser },
} = useQuery({
query: getCurrentUserFeaturesQuery,
});
import { useCurrentUser, useServerConfig } from '../common';
const {
data: { serverConfig },
} = useQuery({
query: serverConfigQuery,
});
export function UserDropdown() {
const currentUser = useCurrentUser();
const serverConfig = useServerConfig();
const navigate = useNavigate();

View File

@@ -7,12 +7,12 @@ import {
} from '@affine/admin/components/ui/carousel';
import { validateEmailAndPassword } from '@affine/admin/utils';
import { useMutateQueryResource } from '@affine/core/hooks/use-mutation';
import { useQuery } from '@affine/core/hooks/use-query';
import { serverConfigQuery } from '@affine/graphql';
import { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { useServerConfig } from '../common';
import { CreateAdmin } from './create-admin';
export enum CarouselSteps {
@@ -72,10 +72,8 @@ export const Form = () => {
const [invalidEmail, setInvalidEmail] = useState(false);
const [invalidPassword, setInvalidPassword] = useState(false);
const { data } = useQuery({
query: serverConfigQuery,
});
const passwordLimits = data.serverConfig.credentialsRequirement.password;
const serverConfig = useServerConfig();
const passwordLimits = serverConfig.credentialsRequirement.password;
const isCreateAdminStep = current - 1 === CarouselSteps.CreateAdmin;
@@ -95,7 +93,7 @@ export const Form = () => {
api.on('select', () => {
setCurrent(api.selectedScrollSnap() + 1);
});
}, [api, data.serverConfig.initialized, navigate]);
}, [api, serverConfig.initialized, navigate]);
const createAdmin = useCallback(async () => {
try {
@@ -170,14 +168,14 @@ export const Form = () => {
const onPrevious = useCallback(() => {
if (current === count) {
if (data.serverConfig.initialized === true) {
if (serverConfig.initialized === true) {
return navigate('/admin', { replace: true });
}
toast.error('Goto Admin Panel failed, please try again.');
return;
}
api?.scrollPrev();
}, [api, count, current, data.serverConfig.initialized, navigate]);
}, [api, count, current, serverConfig.initialized, navigate]);
return (
<div className="flex flex-col justify-between h-full w-full lg:pl-36 max-lg:items-center ">