feat: add workspace page

This commit is contained in:
moonrailgun 2024-08-21 00:18:07 +08:00
parent b862dd7427
commit 491807165c
4 changed files with 153 additions and 0 deletions

View File

@ -28,6 +28,7 @@ import { Route as TelemetryAddImport } from './routes/telemetry/add'
import { Route as TelemetryTelemetryIdImport } from './routes/telemetry/$telemetryId' import { Route as TelemetryTelemetryIdImport } from './routes/telemetry/$telemetryId'
import { Route as SurveyAddImport } from './routes/survey/add' import { Route as SurveyAddImport } from './routes/survey/add'
import { Route as StatusSlugImport } from './routes/status/$slug' import { Route as StatusSlugImport } from './routes/status/$slug'
import { Route as SettingsWorkspaceImport } from './routes/settings/workspace'
import { Route as SettingsUsageImport } from './routes/settings/usage' import { Route as SettingsUsageImport } from './routes/settings/usage'
import { Route as SettingsProfileImport } from './routes/settings/profile' import { Route as SettingsProfileImport } from './routes/settings/profile'
import { Route as SettingsNotificationsImport } from './routes/settings/notifications' import { Route as SettingsNotificationsImport } from './routes/settings/notifications'
@ -132,6 +133,11 @@ const StatusSlugRoute = StatusSlugImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const SettingsWorkspaceRoute = SettingsWorkspaceImport.update({
path: '/workspace',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsUsageRoute = SettingsUsageImport.update({ const SettingsUsageRoute = SettingsUsageImport.update({
path: '/usage', path: '/usage',
getParentRoute: () => SettingsRoute, getParentRoute: () => SettingsRoute,
@ -292,6 +298,10 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SettingsUsageImport preLoaderRoute: typeof SettingsUsageImport
parentRoute: typeof SettingsImport parentRoute: typeof SettingsImport
} }
'/settings/workspace': {
preLoaderRoute: typeof SettingsWorkspaceImport
parentRoute: typeof SettingsImport
}
'/status/$slug': { '/status/$slug': {
preLoaderRoute: typeof StatusSlugImport preLoaderRoute: typeof StatusSlugImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
@ -374,6 +384,7 @@ export const routeTree = rootRoute.addChildren([
SettingsNotificationsRoute, SettingsNotificationsRoute,
SettingsProfileRoute, SettingsProfileRoute,
SettingsUsageRoute, SettingsUsageRoute,
SettingsWorkspaceRoute,
]), ]),
SurveyRoute.addChildren([ SurveyRoute.addChildren([
SurveyAddRoute, SurveyAddRoute,

View File

@ -34,6 +34,11 @@ function PageComponent() {
title: t('Notifications'), title: t('Notifications'),
href: '/settings/notifications', href: '/settings/notifications',
}, },
{
id: 'workspace',
title: t('Workspace'),
href: '/settings/workspace',
},
{ {
id: 'auditLog', id: 'auditLog',
title: t('Audit Log'), title: t('Audit Log'),

View File

@ -0,0 +1,94 @@
import { routeAuthBeforeLoad } from '@/utils/route';
import { createFileRoute } from '@tanstack/react-router';
import { useTranslation } from '@i18next-toolkit/react';
import { CommonWrapper } from '@/components/CommonWrapper';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useCurrentWorkspace } from '../../store/user';
import { CommonHeader } from '@/components/CommonHeader';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Typography } from 'antd';
import { AppRouterOutput, trpc } from '@/api/trpc';
import { createColumnHelper, DataTable } from '@/components/DataTable';
import { useMemo } from 'react';
import { get } from 'lodash-es';
import { Badge } from '@/components/ui/badge';
export const Route = createFileRoute('/settings/workspace')({
beforeLoad: routeAuthBeforeLoad,
component: PageComponent,
});
type MemberInfo = AppRouterOutput['workspace']['members'][number];
const columnHelper = createColumnHelper<MemberInfo>();
function PageComponent() {
const { t } = useTranslation();
const { id, name } = useCurrentWorkspace();
const { data: members = [] } = trpc.workspace.members.useQuery({
workspaceId: id,
});
const columns = useMemo(() => {
return [
columnHelper.accessor(
(data) =>
get(data, ['user', 'nickname']) || get(data, ['user', 'username']),
{
header: t('Name'),
size: 300,
}
),
columnHelper.accessor('user.email', {
header: t('Email'),
size: 130,
cell: (props) => {
return (
<span>
{props.getValue()}
{props.row.original.user.emailVerified && (
<Badge className="ml-1">{t('Verified')}</Badge>
)}
</span>
);
},
}),
columnHelper.accessor('role', {
header: t('Role'),
size: 130,
}),
];
}, [t]);
return (
<CommonWrapper header={<CommonHeader title={t('Workspace')} />}>
<ScrollArea className="h-full overflow-hidden p-4">
<div className="flex flex-col gap-4">
<Card>
<CardHeader className="text-lg font-bold">
{t('Current Workspace:')} {name}
</CardHeader>
<CardContent>
<div>
<span className="mr-2">{t('Workspace ID')}:</span>
<span>
<Typography.Text code={true} copyable={true}>
{id}
</Typography.Text>
</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="text-lg font-bold">
{t('Members')}
</CardHeader>
<CardContent>
<DataTable columns={columns} data={members} />
</CardContent>
</Card>
</div>
</ScrollArea>
</CommonWrapper>
);
}

View File

@ -18,6 +18,7 @@ import { OpenApiMeta } from 'trpc-openapi';
import { getServerCount } from '../../model/serverStatus.js'; import { getServerCount } from '../../model/serverStatus.js';
import { ROLES, slugRegex } from '@tianji/shared'; import { ROLES, slugRegex } from '@tianji/shared';
import { createUserSelect } from '../../model/user.js'; import { createUserSelect } from '../../model/user.js';
import { WorkspacesOnUsersModelSchema } from '../../prisma/zod/workspacesonusers.js';
export const workspaceRouter = router({ export const workspaceRouter = router({
create: protectProedure create: protectProedure
@ -150,6 +151,48 @@ export const workspaceRouter = router({
}, },
}); });
}), }),
members: workspaceProcedure
.meta(
buildWorkspaceOpenapi({
method: 'GET',
path: '/{workspaceId}/members',
})
)
.output(
z.array(
WorkspacesOnUsersModelSchema.merge(
z.object({
user: z.object({
username: z.string(),
nickname: z.string().nullable(),
email: z.string().nullable(),
emailVerified: z.date().nullable(),
}),
})
)
)
)
.query(async ({ input }) => {
const { workspaceId } = input;
const list = await prisma.workspacesOnUsers.findMany({
where: {
workspaceId,
},
include: {
user: {
select: {
username: true,
nickname: true,
email: true,
emailVerified: true,
},
},
},
});
return list;
}),
getUserWorkspaceRole: publicProcedure getUserWorkspaceRole: publicProcedure
.input( .input(
z.object({ z.object({