diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/auth/users/users-table.tsx b/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/auth/users/users-table.tsx index c7ecbba46..5a9a82686 100644 --- a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/auth/users/users-table.tsx +++ b/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/auth/users/users-table.tsx @@ -1,173 +1,132 @@ -"use client";; +"use client"; + import * as React from 'react'; -import { - Avatar, - Box, - Checkbox, - DialogActions, - DialogContent, - DialogTitle, - Divider, - Dropdown, - FormControl, - FormLabel, - IconButton, - Input, - ListDivider, - ListItemDecorator, - Menu, - MenuButton, - MenuItem, - Modal, - ModalDialog, - Stack, -} from '@mui/joy'; -import { fromNowDetailed, getInputDatetimeLocalString } from '@stackframe/stack-shared/dist/utils/dates'; +import { DataGrid, GridColDef, GridToolbar } from '@mui/x-data-grid'; +import { Avatar, Box, Checkbox, DialogActions, DialogContent, DialogTitle, Divider, Dropdown, FormControl, FormLabel, IconButton, Input, ListDivider, ListItemDecorator, Menu, MenuButton, MenuItem, Modal, ModalDialog, Stack, Tooltip } from '@mui/joy'; +import { getInputDatetimeLocalString } from '@stackframe/stack-shared/dist/utils/dates'; import { Icon } from '@/components/icon'; import { AsyncButton } from '@/components/async-button'; import { Dialog } from '@/components/dialog'; import { useAdminApp } from '../../useAdminInterface'; import { runAsynchronously } from '@stackframe/stack-shared/src/utils/promises'; import { ServerUserJson } from '@stackframe/stack-shared'; -import Table from '@/components/table'; export function UsersTable(props: { rows: ServerUserJson[], onInvalidate(): void, }) { - return ( - ({ - id: user.id, - row: [ - { content: user.id }, - { - content: - }, - { content: user.displayName }, - { content: user.primaryEmail }, - { content: fromNowDetailed(new Date(user.signedUpAtMillis)).result }, - { content: props.onInvalidate()} user={user} /> } - ] - }))} - /> - // - //
- // - // - // - // - // - // - // {/* */} - // - // - // - // - // - // {props.rows.map(user => ( - // - // - // - // - // - // {/* */} - // - // - // - // ))} - // - //
- // ID - // - // Avatar - // - // Display Name - // - // Email - // - // Provider - // - // Sign Up Time - // - //
- // {user.id} - // - // - // - // {user.displayName} - // - // {user.primaryEmail} - // - // + const stackAdminApp = useAdminApp(); - // - // - // {fromNowDetailed(new Date(user.signedUpAtMillis)).result} - // - // props.onInvalidate()} user={user} /> - //
- // + const columns: (GridColDef & { + stackOnProcessUpdate?: (updatedRow: ServerUserJson, oldRow: ServerUserJson) => Promise, + })[] = [ + { + field: 'profilePicture', + headerName: 'Profile picture', + renderHeader: () => <>, + width: 50, + filterable: false, + sortable: false, + renderCell: (params) => params.row.profileImageUrl ? ( + + ) : ( + { + params.row.displayName + ?.split(/\s/) + .map((x: string) => x[0]) + .filter((x: string) => x) + .join("") + } + ), + }, + { + field: 'id', + headerName: 'ID', + width: 100, + }, + { + field: 'displayName', + headerName: 'Display name', + width: 150, + flex: 1, + editable: true, + stackOnProcessUpdate: async (updatedRow, originalRow) => { + await stackAdminApp.setServerUserCustomizableData(originalRow.id, { displayName: updatedRow.displayName }); + }, + }, + { + field: 'primaryEmail', + headerName: 'E-Mail', + width: 200, + editable: true, + stackOnProcessUpdate: async (updatedRow, originalRow) => { + await stackAdminApp.setServerUserCustomizableData(originalRow.id, { primaryEmail: updatedRow.primaryEmail, primaryEmailVerified: false }); + }, + renderCell: (params) => ( + <> + + {params.row.primaryEmail} + + {!params.row.primaryEmailVerified && ( + <> + + + + + + + + + )} + + ), + }, + { + field: 'signedUpAtMillis', + headerName: 'Signed up', + type: 'dateTime', + valueFormatter: (params) => new Date(params.value as number).toLocaleString(), + width: 200, + }, + { + field: 'actions', + headerName: 'Actions', + renderHeader: () => <>, + type: 'actions', + width: 48, + getActions: (params) => [ + props.onInvalidate()} /> + ], + }, + ]; + + return ( + { + for (const column of columns) { + if (column.editable && column.stackOnProcessUpdate) { + await column.stackOnProcessUpdate(updatedRow, originalRow); + } + } + props.onInvalidate(); + return originalRow; + }} + pageSizeOptions={[5, 15, 25]} + /> ); } -function Actions(props: { user: ServerUserJson, onInvalidate: () => void }) { +function Actions(props: { params: any, onInvalidate: () => void}) { const stackAdminApp = useAdminApp(); const [isEditModalOpen, setIsEditModalOpen] = React.useState(false); @@ -199,7 +158,7 @@ function Actions(props: { user: ServerUserJson, onInvalidate: () => void }) { setIsEditModalOpen(false)} onInvalidate={() => props.onInvalidate()} @@ -213,13 +172,13 @@ function Actions(props: { user: ServerUserJson, onInvalidate: () => void }) { okButton={{ label: "Delete user", onClick: async () => { - await stackAdminApp.deleteServerUser(props.user.id); + await stackAdminApp.deleteServerUser(props.params.row.id); props.onInvalidate(); } }} cancelButton={true} > - Are you sure you want to delete the user '{props.user.displayName}' with ID {props.user.id}? This action cannot be undone. + Are you sure you want to delete the user '{props.params.row.displayName}' with ID {props.params.row.id}? This action cannot be undone. ); @@ -266,7 +225,7 @@ function EditUserModal(props: { user: ServerUserJson, open: boolean, onClose: () > - ID: {props.user.id} + ID: {props.user.id} Display name diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/layout.tsx b/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/layout.tsx index a692ab436..ef350742b 100644 --- a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/layout.tsx +++ b/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/layout.tsx @@ -73,9 +73,9 @@ export default function Layout(props: { children: React.ReactNode, params: { pro minWidth={0} overflow='auto' > - + {props.children} - + diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/settings/api-keys/api-keys-table.tsx b/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/settings/api-keys/api-keys-table.tsx index de39a643a..861316964 100644 --- a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/settings/api-keys/api-keys-table.tsx +++ b/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/settings/api-keys/api-keys-table.tsx @@ -1,71 +1,123 @@ -"use client";; +"use client"; + import * as React from 'react'; +import { DataGrid, GridColDef, GridToolbar } from '@mui/x-data-grid'; import { ApiKeySetSummary } from '@stackframe/stack-shared'; -import { Checkbox, Stack, Tooltip, Typography } from '@mui/joy'; +import { Box, Checkbox, Stack, Tooltip, Typography } from '@mui/joy'; import { Dialog } from '@/components/dialog'; import { useAdminApp } from '../../useAdminInterface'; -import Table from '@/components/table'; export function ApiKeysTable(props: { rows: ApiKeySetSummary[], onInvalidate(): void, }) { const stackAdminApp = useAdminApp(); + const [revokeDialogApiKeySet, setRevokeDialogApiKeySet] = React.useState(null); + const columns: GridColDef[] = [ + { + field: 'description', + headerName: 'Description', + width: 250, + flex: 1, + }, + { + field: 'availableKeys', + headerName: 'Key values', + width: 200, + filterable: false, + sortable: false, + valueGetter: (params) => { + return [ + ["Client", params.row.publishableClientKey?.lastFour], + ["Server", params.row.secretServerKey?.lastFour], + ["Admin", params.row.superSecretAdminKey?.lastFour], + ].filter(([, value]) => value).map(([key, value]) => `${key}: ${value}`).join("\n"); + }, + renderCell: (params) => { + return ( + + + {params.row.publishableClientKey && ( + + Client: ••••••••••{params.row.publishableClientKey.lastFour} + + )} + {params.row.secretServerKey && ( + + Server: ••••••••••{params.row.secretServerKey.lastFour} + + )} + {params.row.superSecretAdminKey && ( + + Admin: •••••••••••{params.row.superSecretAdminKey.lastFour} + + )} + + + ); + }, + }, + { + field: 'createdAt', + headerName: 'Created', + width: 200, + }, + { + field: 'expiresAt', + headerName: 'Expires', + width: 200, + type: 'dateTime', + }, + { + field: 'isValid', + headerName: 'Is Valid', + width: 100, + type: 'boolean', + valueGetter: (params) => params.row.isValid(), + renderCell: (params) => { + const invalidReason = params.row.whyInvalid(); + if (invalidReason) { + return ( + + {{ + "expired": "Expired", + "manually-revoked": "Revoked", + }[invalidReason as string] ?? "Invalid"} + + ); + } else { + return ( + + setRevokeDialogApiKeySet(params.row)} /> + + ); + } + }, + }, + ]; + return ( <> - ({ - id: key.id, - row: [ - { content: key.description }, - { content: - {[ - ["Client", key.publishableClientKey?.lastFour], - ["Server", key.secretServerKey?.lastFour], - ["Admin", key.superSecretAdminKey?.lastFour], - ].filter(([, value]) => value).map(([key, value]) => ({key}: •••••••••{value}))} - }, - { content: key.createdAt.toLocaleString() }, - { content: key.expiresAt.toLocaleString() }, - { content: key.whyInvalid() ? ( - - {{ - "expired": "Expired", - "manually-revoked": "Revoked", - }[key.whyInvalid() as string] ?? "Invalid"} - - ) : ( - - setRevokeDialogApiKeySet(key)} /> - - )} - ] - }))} + }} + pageSizeOptions={[5, 15, 25]} /> +