diff --git a/src/client/components/DeprecatedBadge.tsx b/src/client/components/DeprecatedBadge.tsx new file mode 100644 index 0000000..455089c --- /dev/null +++ b/src/client/components/DeprecatedBadge.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Badge } from './ui/badge'; +import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; +import { useTranslation } from '@i18next-toolkit/react'; + +interface DeprecatedBadgeProps { + tip?: string; +} + +export const DeprecatedBadge: React.FC = React.memo( + (props) => { + const { t } = useTranslation(); + + const el = ( + + {t('Deprecated')} + + ); + + if (!props.tip) { + return el; + } else { + return ( + + {el} + +

{props.tip}

+
+
+ ); + } + } +); +DeprecatedBadge.displayName = 'DeprecatedBadge'; diff --git a/src/client/components/Sortable/SortableContext.tsx b/src/client/components/Sortable/SortableContext.tsx index 61029c7..9e81a15 100644 --- a/src/client/components/Sortable/SortableContext.tsx +++ b/src/client/components/Sortable/SortableContext.tsx @@ -2,15 +2,15 @@ import React from 'react'; import { DragDropContext, DropResult } from 'react-beautiful-dnd'; import { useEvent } from '@/hooks/useEvent'; import { reorder } from '@/utils/reorder'; -import { BaseSortableData } from './types'; +import { SortableItem } from './types'; -interface SortableContextProps { +interface SortableContextProps { list: T[]; onChange: (list: T[]) => void; children: React.ReactNode; } -export const SortableContext = ( +export const SortableContext = ( props: SortableContextProps ) => { const { list, onChange, children } = props; @@ -37,50 +37,50 @@ export const SortableContext = ( const final = [...list]; const sourceGroupIndex = final.findIndex( - (group) => group.id === result.source.droppableId + (group) => group.key === result.source.droppableId ); if (sourceGroupIndex === -1) { return; } const destinationGroupIndex = final.findIndex( - (group) => group.id === result.destination?.droppableId + (group) => group.key === result.destination?.droppableId ); if (destinationGroupIndex === -1) { return; } if (sourceGroupIndex === destinationGroupIndex) { - if (!('items' in final[sourceGroupIndex])) { + if (!('children' in final[sourceGroupIndex])) { return; } // same group - final[sourceGroupIndex].items = reorder( - final[sourceGroupIndex].items!, + final[sourceGroupIndex].children = reorder( + final[sourceGroupIndex].children!, result.source.index, result.destination.index ); } else { // cross group if ( - !('items' in final[sourceGroupIndex]) || - !('items' in final[destinationGroupIndex]) + !('children' in final[sourceGroupIndex]) || + !('children' in final[destinationGroupIndex]) ) { return; } const sourceGroupItems = Array.from( - final[sourceGroupIndex].items ?? [] + final[sourceGroupIndex].children ?? [] ); const [removed] = sourceGroupItems.splice(result.source.index, 1); const destinationGroupItems = Array.from( - final[destinationGroupIndex].items ?? [] + final[destinationGroupIndex].children ?? [] ); destinationGroupItems.splice(result.destination.index, 0, removed); - final[sourceGroupIndex].items = sourceGroupItems; - final[destinationGroupIndex].items = destinationGroupItems; + final[sourceGroupIndex].children = sourceGroupItems; + final[destinationGroupIndex].children = destinationGroupItems; } onChange(final); diff --git a/src/client/components/Sortable/SortableGroup.tsx b/src/client/components/Sortable/SortableGroup.tsx index ea66762..749a2d0 100644 --- a/src/client/components/Sortable/SortableGroup.tsx +++ b/src/client/components/Sortable/SortableGroup.tsx @@ -3,90 +3,106 @@ import { SortableContext } from './SortableContext'; import { StrictModeDroppable } from './StrictModeDroppable'; import { useEvent } from '@/hooks/useEvent'; import { Draggable } from 'react-beautiful-dnd'; -import { BaseSortableData, ExtractGroup, ExtractItem } from './types'; +import { SortableGroupItem, SortableItem, SortableLeafItem } from './types'; -interface SortableGroupProps { - list: T[]; - onChange: (list: T[]) => void; +interface SortableGroupProps { + list: SortableItem[]; + onChange: (list: SortableItem[]) => void; renderGroup: ( - group: ExtractGroup, - children: React.ReactNode + group: SortableGroupItem, + children: React.ReactNode, + level: number + ) => React.ReactNode; + renderItem: ( + item: SortableLeafItem, + index: number, + group: SortableGroupItem ) => React.ReactNode; - renderItem: (item: ExtractItem) => React.ReactNode; } -export const SortableGroup = ( - props: SortableGroupProps +export const SortableGroup = ( + props: SortableGroupProps ) => { const { list, onChange, renderGroup, renderItem } = props; - const renderItemEl = useEvent((item: ExtractItem, index: number) => { - return ( - - {(dragProvided) => ( -
- {renderItem(item)} -
- )} -
- ); - }); + const renderItemEl = useEvent( + ( + item: SortableLeafItem, + index: number, + group: SortableGroupItem + ) => { + return ( + + {(dragProvided) => ( +
+ {renderItem(item, index, group)} +
+ )} +
+ ); + } + ); - const renderGroupEl = useEvent((group: ExtractGroup, level = 0) => { - return ( - - {(dropProvided) => ( -
- {renderGroup( - group, - <> - {group.items.map((item, index) => - item.type === 'item' ? ( - renderItemEl(item as ExtractItem, index) - ) : ( - - {(dragProvided) => ( -
- {renderGroupEl(item as ExtractGroup, level + 1)} -
- )} -
- ) - )} - - )} + const renderGroupEl = useEvent( + (group: SortableGroupItem, level = 0) => { + return ( + + {(dropProvided) => ( +
+ {renderGroup( + group, + <> + {group.children.map((item, index) => + !('children' in item) ? ( + renderItemEl(item, index, group) + ) : ( + + {(dragProvided) => ( +
+ {renderGroupEl(item, level + 1)} +
+ )} +
+ ) + )} + , + level + )} - {dropProvided.placeholder} -
- )} -
- ); - }); + {dropProvided.placeholder} +
+ )} +
+ ); + } + ); return ( - list={list} onChange={onChange}> + > + list={list} + onChange={onChange} + > {renderGroupEl( { - id: 'root', - type: 'root' as const, - items: list, - } as any, + key: 'root', + children: list, + } as SortableGroupItem, 0 )} diff --git a/src/client/components/Sortable/types.ts b/src/client/components/Sortable/types.ts index 00098be..e4655db 100644 --- a/src/client/components/Sortable/types.ts +++ b/src/client/components/Sortable/types.ts @@ -1,33 +1,14 @@ import { Id } from 'react-beautiful-dnd'; -export type BaseSortableItem = { - type: 'item'; - id: Id; -}; +export type SortableItem = + | SortableLeafItem + | SortableGroupItem; -export type BaseSortableGroup = { - type: 'group'; - id: Id; - title?: string; - items: ((BaseSortableGroup & GroupProps) | (BaseSortableItem & ItemProps))[]; -}; +export type SortableGroupItem = { + key: Id; + children: SortableItem[]; +} & GroupProps; -export type BaseSortableRoot = { - type: 'root'; - id: Id; - title?: string; - items: (BaseSortableItem & GroupProps)[]; -}; - -export type BaseSortableData = - | BaseSortableRoot - | BaseSortableGroup - | BaseSortableItem; - -export type SortableData = - | BaseSortableRoot - | (BaseSortableGroup & GroupProps) - | (BaseSortableItem & ItemProps); - -export type ExtractGroup = T extends { type: 'group' } ? T : never; -export type ExtractItem = T extends { type: 'item' } ? T : never; +export type SortableLeafItem = { + key: Id; +} & ItemProps; diff --git a/src/client/components/monitor/StatusPage/EditForm.tsx b/src/client/components/monitor/StatusPage/EditForm.tsx index 790ecbb..1236b02 100644 --- a/src/client/components/monitor/StatusPage/EditForm.tsx +++ b/src/client/components/monitor/StatusPage/EditForm.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { MonitorPicker, MonitorPickerOld } from '../MonitorPicker'; +import { MonitorPicker } from '../MonitorPicker'; import { useTranslation } from '@i18next-toolkit/react'; import { Button } from '@/components/ui/button'; import { LuMinusCircle, LuPlus } from 'react-icons/lu'; @@ -23,17 +23,11 @@ import { domainRegex, slugRegex } from '@tianji/shared'; import { useElementSize } from '@/hooks/useResizeObserver'; import { Switch } from '@/components/ui/switch'; import { Separator } from '@/components/ui/separator'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { ColorTag } from '@/components/ColorTag'; import { Collapsible, CollapsibleContent } from '@/components/ui/collapsible'; import { CollapsibleTrigger } from '@radix-ui/react-collapsible'; import { CaretSortIcon } from '@radix-ui/react-icons'; +import { DeprecatedBadge } from '@/components/DeprecatedBadge'; +import { groupItemSchema, MonitorStatusPageServiceList } from './ServiceList'; const Text = Typography.Text; @@ -46,6 +40,15 @@ const editFormSchema = z.object({ .regex(domainRegex, 'Invalid domain') .or(z.literal('')) .optional(), + body: z + .object({ + groups: z.array(groupItemSchema), + }) + .default({ groups: [] }), + + /** + * @deprecated + */ monitorList: z.array( z.object({ id: z.string(), @@ -77,10 +80,20 @@ export const MonitorStatusPageEditForm: React.FC description: '', domain: '', monitorList: [], + body: { groups: [] }, }, }); - const { fields, append, remove } = useFieldArray({ + const showDeprecatedMonitorList = props.initialValues + ? Array.isArray(props.initialValues.monitorList) && + props.initialValues.monitorList.length > 0 + : false; + + const { + fields: oldMonitorFields, + append, + remove, + } = useFieldArray({ control: form.control, name: 'monitorList', keyName: 'key', @@ -191,74 +204,98 @@ export const MonitorStatusPageEditForm: React.FC - {/* MonitorList */} + {/* Body */} ( + name="body.groups" + render={({ field }) => ( - {t('Monitor List')} - {fields.map((field, i) => ( - <> - {i !== 0 && } + {t('Body')} + + + + + + )} + /> -
- ( - - )} - /> + {/* MonitorList */} + {showDeprecatedMonitorList && ( + ( + + + {t('Monitor List')} + + + {oldMonitorFields.map((field, i) => ( + <> + {i !== 0 && } -
+
( - )} /> - - {t('Show Latest Value')} - +
+ ( + + )} + /> - remove(i)} - /> + + {t('Show Latest Value')} + + + remove(i)} + /> +
-
- - ))} + + ))} - + - -
- )} - /> + + + )} + /> + )}
+
0 && 'border-l-4 border-gray-600 p-2')} + > + {children} +
+
+ )} + renderItem={(item, i, group) => { + if (item.type === 'monitor') { + return ( +
+ {i !== 0 && } + +
+ + handleUpdateItem(group.key, item.key, 'id', val) + } + /> + +
+ + handleUpdateItem( + group.key, + item.key, + 'showCurrent', + val + ) + } + /> + + + {t('Show Latest Value')} + + + handleDeleteItem(group.key, item.key)} + /> +
+
+
+ ); + } + + return null; + }} + /> + + {Array.isArray(props.value) && props.value.length === 0 && ( +

+ {t('No any group has been created, click button to create one')} +

+ )} + + + + ); + }); MonitorStatusPageServiceList.displayName = 'MonitorStatusPageServiceList'; diff --git a/src/client/components/ui/form.tsx b/src/client/components/ui/form.tsx index 0ba4189..43a13c8 100644 --- a/src/client/components/ui/form.tsx +++ b/src/client/components/ui/form.tsx @@ -87,7 +87,7 @@ FormItem.displayName = "FormItem" const FormLabel = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef & {optional?: boolean} + React.ComponentPropsWithoutRef & { optional?: boolean } >(({ className, ...props }, ref) => { const { error, formItemId } = useFormField() const { t } = useTranslation() diff --git a/src/client/routes/playground.tsx b/src/client/routes/playground.tsx index cadfd7b..6edf629 100644 --- a/src/client/routes/playground.tsx +++ b/src/client/routes/playground.tsx @@ -1,6 +1,10 @@ import { createFileRoute, redirect } from '@tanstack/react-router'; import { isDev } from '@/utils/env'; -import { MonitorStatusPageServiceList } from '@/components/monitor/StatusPage/ServiceList'; +import { + MonitorStatusPageServiceItem, + MonitorStatusPageServiceList, +} from '@/components/monitor/StatusPage/ServiceList'; +import { useState } from 'react'; export const Route = createFileRoute('/playground')({ beforeLoad: () => { @@ -13,13 +17,35 @@ export const Route = createFileRoute('/playground')({ component: PageComponent, }); -function PageComponent(this: { - beforeLoad: () => void; - component: () => import('react/jsx-runtime').JSX.Element; -}) { +function PageComponent() { + const [list, setList] = useState([ + { + title: 'Group 1', + key: 'group1', + children: [ + { + key: 'item1', + id: 'fooo', + type: 'monitor', + }, + ], + }, + { + title: 'Group 2', + key: 'group2', + children: [ + { + key: 'item2', + id: 'barr', + type: 'monitor', + }, + ], + }, + ]); + return (
- +
); }