feat: add group feature in backend
This commit is contained in:
parent
e323e104e0
commit
4d39cb5ef4
90
src/client/components/monitor/StatusPage/Body.tsx
Normal file
90
src/client/components/monitor/StatusPage/Body.tsx
Normal 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';
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
||||||
|
@ -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>
|
||||||
|
|
||||||
<div className="mb-2 text-lg font-semibold">{t('Services')}</div>
|
{/* Body */}
|
||||||
|
|
||||||
{info && (
|
{info && (
|
||||||
<StatusPageServices
|
<StatusPageBody workspaceId={info.workspaceId} info={info} />
|
||||||
workspaceId={info.workspaceId}
|
)}
|
||||||
monitorList={monitorList}
|
|
||||||
/>
|
{/* deprecated monitor list */}
|
||||||
|
{info && Array.isArray(monitorList) && monitorList.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="mb-2 text-lg font-semibold">{t('Services')}</div>
|
||||||
|
|
||||||
|
<StatusPageServices
|
||||||
|
workspaceId={info.workspaceId}
|
||||||
|
monitorList={monitorList}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
20
src/client/components/monitor/StatusPage/schema.ts
Normal file
20
src/client/components/monitor/StatusPage/schema.ts
Normal 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: [] });
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "MonitorStatusPage" ADD COLUMN "body" JSONB NOT NULL DEFAULT '{}';
|
@ -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)
|
||||||
|
@ -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]
|
||||||
*/
|
*/
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user