feat: add group feature in backend

This commit is contained in:
moonrailgun 2024-09-17 19:08:58 +08:00
parent e323e104e0
commit 4d39cb5ef4
10 changed files with 171 additions and 33 deletions

View File

@ -0,0 +1,90 @@
import { AppRouterOutput, trpc } from '@/api/trpc';
import React, { useMemo } from 'react';
import { bodySchema } from './schema';
import { Empty } from 'antd';
import { useTranslation } from '@i18next-toolkit/react';
import { MonitorListItem } from '../MonitorListItem';
interface StatusPageBodyProps {
workspaceId: string;
info: NonNullable<AppRouterOutput['monitor']['getPageInfo']>;
}
export const StatusPageBody: React.FC<StatusPageBodyProps> = React.memo(
(props) => {
const { info } = props;
const { t } = useTranslation();
const body = useMemo(() => {
const res = bodySchema.safeParse(info.body);
if (res.success) {
return res.data;
} else {
return { groups: [] };
}
}, [info.body]);
return (
<div>
{body.groups.map((group) => (
<div key={group.key}>
<div className="mb-2 text-lg font-semibold">{group.title}</div>
<div className="flex flex-col gap-4 rounded-md border border-gray-200 p-2.5 dark:border-gray-700">
{group.children.length === 0 && (
<Empty description={t('No any monitor has been set')} />
)}
{group.children.map((item) => {
if (item.type === 'monitor') {
return (
<StatusItemMonitor
key={item.key}
workspaceId={props.workspaceId}
id={item.id}
showCurrent={item.showCurrent ?? false}
/>
);
}
return null;
})}
</div>
</div>
))}
</div>
);
}
);
StatusPageBody.displayName = 'StatusPageBody';
export const StatusItemMonitor: React.FC<{
id: string;
showCurrent: boolean;
workspaceId: string;
}> = React.memo((props) => {
const { data: list = [], isLoading } = trpc.monitor.getPublicInfo.useQuery({
monitorIds: [props.id],
});
if (isLoading) {
return null;
}
const item = list[0];
if (!item) {
return null;
}
return (
<MonitorListItem
key={item.id}
workspaceId={props.workspaceId}
monitorId={item.id}
monitorName={item.name}
monitorType={item.type}
showCurrentResponse={props.showCurrent}
/>
);
});
StatusItemMonitor.displayName = 'StatusItemMonitor';

View File

@ -27,7 +27,8 @@ import { Collapsible, CollapsibleContent } from '@/components/ui/collapsible';
import { CollapsibleTrigger } from '@radix-ui/react-collapsible'; import { CollapsibleTrigger } from '@radix-ui/react-collapsible';
import { CaretSortIcon } from '@radix-ui/react-icons'; import { CaretSortIcon } from '@radix-ui/react-icons';
import { DeprecatedBadge } from '@/components/DeprecatedBadge'; import { DeprecatedBadge } from '@/components/DeprecatedBadge';
import { groupItemSchema, MonitorStatusPageServiceList } from './ServiceList'; import { MonitorStatusPageServiceList } from './ServiceList';
import { bodySchema } from './schema';
const Text = Typography.Text; const Text = Typography.Text;
@ -40,11 +41,7 @@ const editFormSchema = z.object({
.regex(domainRegex, 'Invalid domain') .regex(domainRegex, 'Invalid domain')
.or(z.literal('')) .or(z.literal(''))
.optional(), .optional(),
body: z body: bodySchema,
.object({
groups: z.array(groupItemSchema),
})
.default({ groups: [] }),
/** /**
* @deprecated * @deprecated

View File

@ -13,19 +13,7 @@ import { MonitorPicker } from '../MonitorPicker';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { set } from 'lodash-es'; import { set } from 'lodash-es';
import { EditableText } from '@/components/EditableText'; import { EditableText } from '@/components/EditableText';
import { groupItemSchema, leafItemSchema } from './schema';
export const leafItemSchema = z.object({
key: z.string(),
id: z.string(),
type: z.enum(['monitor']),
showCurrent: z.boolean().default(false).optional(),
});
export const groupItemSchema = z.object({
key: z.string(),
title: z.string(),
children: z.array(leafItemSchema),
});
type GroupItemProps = Omit<z.infer<typeof groupItemSchema>, 'key' | 'children'>; type GroupItemProps = Omit<z.infer<typeof groupItemSchema>, 'key' | 'children'>;
type LeafItemProps = Omit<z.infer<typeof leafItemSchema>, 'key'>; type LeafItemProps = Omit<z.infer<typeof leafItemSchema>, 'key'>;
@ -167,7 +155,7 @@ export const MonitorStatusPageServiceList: React.FC<MonitorStatusPageServiceList
{level > 0 && ( {level > 0 && (
<div className={cn('flex items-center gap-2')}> <div className={cn('flex items-center gap-2')}>
<EditableText <EditableText
className="flex-1 overflow-hidden text-ellipsis text-nowrap" className="flex-1 overflow-hidden text-ellipsis text-nowrap font-bold"
defaultValue={group.title} defaultValue={group.title}
onSave={(text) => handleChangeGroupTitle(group.key, text)} onSave={(text) => handleChangeGroupTitle(group.key, text)}
/> />
@ -193,7 +181,10 @@ export const MonitorStatusPageServiceList: React.FC<MonitorStatusPageServiceList
)} )}
<div <div
className={cn(level > 0 && 'border-l-4 border-gray-600 p-2')} className={cn(
level > 0 &&
'border-l-4 border-gray-300 p-2 dark:border-gray-600'
)}
> >
{children} {children}
</div> </div>
@ -203,7 +194,7 @@ export const MonitorStatusPageServiceList: React.FC<MonitorStatusPageServiceList
if (item.type === 'monitor') { if (item.type === 'monitor') {
return ( return (
<div key={item.key}> <div key={item.key}>
{i !== 0 && <Separator />} {i !== 0 && <Separator className="my-2" />}
<div className="mb-2 flex flex-col gap-2"> <div className="mb-2 flex flex-col gap-2">
<MonitorPicker <MonitorPicker

View File

@ -13,6 +13,9 @@ interface StatusPageServicesProps {
showCurrent?: boolean; showCurrent?: boolean;
}[]; }[];
} }
/**
* @deprecated
*/
export const StatusPageServices: React.FC<StatusPageServicesProps> = React.memo( export const StatusPageServices: React.FC<StatusPageServicesProps> = React.memo(
(props) => { (props) => {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -22,6 +22,7 @@ import {
SheetTrigger, SheetTrigger,
} from '@/components/ui/sheet'; } from '@/components/ui/sheet';
import { cn } from '@/utils/style'; import { cn } from '@/utils/style';
import { StatusPageBody } from './Body';
interface MonitorStatusPageProps { interface MonitorStatusPageProps {
slug: string; slug: string;
@ -150,13 +151,21 @@ export const MonitorStatusPage: React.FC<MonitorStatusPageProps> = React.memo(
<MarkdownViewer value={info?.description ?? ''} /> <MarkdownViewer value={info?.description ?? ''} />
</div> </div>
{/* Body */}
{info && (
<StatusPageBody workspaceId={info.workspaceId} info={info} />
)}
{/* deprecated monitor list */}
{info && Array.isArray(monitorList) && monitorList.length > 0 && (
<>
<div className="mb-2 text-lg font-semibold">{t('Services')}</div> <div className="mb-2 text-lg font-semibold">{t('Services')}</div>
{info && (
<StatusPageServices <StatusPageServices
workspaceId={info.workspaceId} workspaceId={info.workspaceId}
monitorList={monitorList} monitorList={monitorList}
/> />
</>
)} )}
</div> </div>
</div> </div>

View File

@ -0,0 +1,20 @@
import { z } from 'zod';
export const leafItemSchema = z.object({
key: z.string(),
id: z.string(),
type: z.enum(['monitor']),
showCurrent: z.boolean().default(false).optional(),
});
export const groupItemSchema = z.object({
key: z.string(),
title: z.string(),
children: z.array(leafItemSchema),
});
export const bodySchema = z
.object({
groups: z.array(groupItemSchema),
})
.default({ groups: [] });

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "MonitorStatusPage" ADD COLUMN "body" JSONB NOT NULL DEFAULT '{}';

View File

@ -421,9 +421,12 @@ model MonitorStatusPage {
slug String @unique // url slug slug String @unique // url slug
title String @db.VarChar(100) title String @db.VarChar(100)
description String @default("") @db.VarChar(1000) description String @default("") @db.VarChar(1000)
/// [CommonPayload]
/// @zod.custom(imports.CommonPayloadSchema)
body Json @default("{}")
/// [MonitorStatusPageList] /// [MonitorStatusPageList]
/// @zod.custom(imports.MonitorStatusPageListSchema) /// @zod.custom(imports.MonitorStatusPageListSchema)
monitorList Json @default("[]") // monitor list monitorList Json @default("[]") // monitor list @deprecated
domain String? // custom domain which can add cname record domain String? // custom domain which can add cname record
createdAt DateTime @default(now()) @db.Timestamptz(6) createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6) updatedAt DateTime @updatedAt @db.Timestamptz(6)

View File

@ -14,6 +14,10 @@ export const MonitorStatusPageModelSchema = z.object({
slug: z.string(), slug: z.string(),
title: z.string(), title: z.string(),
description: z.string(), description: z.string(),
/**
* [CommonPayload]
*/
body: imports.CommonPayloadSchema,
/** /**
* [MonitorStatusPageList] * [MonitorStatusPageList]
*/ */

View File

@ -601,6 +601,7 @@ export const monitorRouter = router({
.merge( .merge(
MonitorStatusPageModelSchema.pick({ MonitorStatusPageModelSchema.pick({
description: true, description: true,
body: true,
monitorList: true, monitorList: true,
domain: true, domain: true,
}).partial() }).partial()
@ -608,8 +609,15 @@ export const monitorRouter = router({
) )
.output(MonitorStatusPageModelSchema) .output(MonitorStatusPageModelSchema)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const { workspaceId, slug, title, description, monitorList, domain } = const {
input; workspaceId,
slug,
title,
description,
body,
monitorList,
domain,
} = input;
const existSlugCount = await prisma.monitorStatusPage.count({ const existSlugCount = await prisma.monitorStatusPage.count({
where: { where: {
@ -631,6 +639,7 @@ export const monitorRouter = router({
slug, slug,
title, title,
description, description,
body,
monitorList, monitorList,
domain: domain || null, // make sure not '' domain: domain || null, // make sure not ''
}, },
@ -661,6 +670,7 @@ export const monitorRouter = router({
slug: true, slug: true,
title: true, title: true,
description: true, description: true,
body: true,
monitorList: true, monitorList: true,
domain: true, domain: true,
}).partial() }).partial()
@ -668,8 +678,16 @@ export const monitorRouter = router({
) )
.output(MonitorStatusPageModelSchema) .output(MonitorStatusPageModelSchema)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const { id, workspaceId, slug, title, description, monitorList, domain } = const {
input; id,
workspaceId,
slug,
title,
description,
body,
monitorList,
domain,
} = input;
if (slug) { if (slug) {
const existSlugCount = await prisma.monitorStatusPage.count({ const existSlugCount = await prisma.monitorStatusPage.count({
@ -699,6 +717,7 @@ export const monitorRouter = router({
slug, slug,
title, title,
description, description,
body,
monitorList, monitorList,
domain: domain || null, domain: domain || null,
}, },