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} /> }
- ]
- }))}
- />
- //
- //
- //
- //
- // |
- // ID
- // |
- //
- // Avatar
- // |
- //
- // Display Name
- // |
- //
- // Email
- // |
- // {/*
- // Provider
- // | */}
- //
- // Sign Up Time
- // |
- //
- // |
- //
- //
- //
- // {props.rows.map(user => (
- //
- // |
- // {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]}
/>
+