feat: refactor sortable group component and add edit body component
This commit is contained in:
parent
72a1e7b024
commit
946ecaf9f9
34
src/client/components/DeprecatedBadge.tsx
Normal file
34
src/client/components/DeprecatedBadge.tsx
Normal file
@ -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<DeprecatedBadgeProps> = React.memo(
|
||||||
|
(props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const el = (
|
||||||
|
<Badge className="mx-1 px-1 py-0.5 text-xs" variant="secondary">
|
||||||
|
{t('Deprecated')}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!props.tip) {
|
||||||
|
return el;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>{el}</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{props.tip}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
DeprecatedBadge.displayName = 'DeprecatedBadge';
|
@ -2,15 +2,15 @@ import React from 'react';
|
|||||||
import { DragDropContext, DropResult } from 'react-beautiful-dnd';
|
import { DragDropContext, DropResult } from 'react-beautiful-dnd';
|
||||||
import { useEvent } from '@/hooks/useEvent';
|
import { useEvent } from '@/hooks/useEvent';
|
||||||
import { reorder } from '@/utils/reorder';
|
import { reorder } from '@/utils/reorder';
|
||||||
import { BaseSortableData } from './types';
|
import { SortableItem } from './types';
|
||||||
|
|
||||||
interface SortableContextProps<T extends BaseSortableData = BaseSortableData> {
|
interface SortableContextProps<T extends SortableItem> {
|
||||||
list: T[];
|
list: T[];
|
||||||
onChange: (list: T[]) => void;
|
onChange: (list: T[]) => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SortableContext = <T extends BaseSortableData>(
|
export const SortableContext = <T extends SortableItem>(
|
||||||
props: SortableContextProps<T>
|
props: SortableContextProps<T>
|
||||||
) => {
|
) => {
|
||||||
const { list, onChange, children } = props;
|
const { list, onChange, children } = props;
|
||||||
@ -37,50 +37,50 @@ export const SortableContext = <T extends BaseSortableData>(
|
|||||||
|
|
||||||
const final = [...list];
|
const final = [...list];
|
||||||
const sourceGroupIndex = final.findIndex(
|
const sourceGroupIndex = final.findIndex(
|
||||||
(group) => group.id === result.source.droppableId
|
(group) => group.key === result.source.droppableId
|
||||||
);
|
);
|
||||||
if (sourceGroupIndex === -1) {
|
if (sourceGroupIndex === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const destinationGroupIndex = final.findIndex(
|
const destinationGroupIndex = final.findIndex(
|
||||||
(group) => group.id === result.destination?.droppableId
|
(group) => group.key === result.destination?.droppableId
|
||||||
);
|
);
|
||||||
if (destinationGroupIndex === -1) {
|
if (destinationGroupIndex === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sourceGroupIndex === destinationGroupIndex) {
|
if (sourceGroupIndex === destinationGroupIndex) {
|
||||||
if (!('items' in final[sourceGroupIndex])) {
|
if (!('children' in final[sourceGroupIndex])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// same group
|
// same group
|
||||||
final[sourceGroupIndex].items = reorder(
|
final[sourceGroupIndex].children = reorder(
|
||||||
final[sourceGroupIndex].items!,
|
final[sourceGroupIndex].children!,
|
||||||
result.source.index,
|
result.source.index,
|
||||||
result.destination.index
|
result.destination.index
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// cross group
|
// cross group
|
||||||
if (
|
if (
|
||||||
!('items' in final[sourceGroupIndex]) ||
|
!('children' in final[sourceGroupIndex]) ||
|
||||||
!('items' in final[destinationGroupIndex])
|
!('children' in final[destinationGroupIndex])
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceGroupItems = Array.from(
|
const sourceGroupItems = Array.from(
|
||||||
final[sourceGroupIndex].items ?? []
|
final[sourceGroupIndex].children ?? []
|
||||||
);
|
);
|
||||||
const [removed] = sourceGroupItems.splice(result.source.index, 1);
|
const [removed] = sourceGroupItems.splice(result.source.index, 1);
|
||||||
|
|
||||||
const destinationGroupItems = Array.from(
|
const destinationGroupItems = Array.from(
|
||||||
final[destinationGroupIndex].items ?? []
|
final[destinationGroupIndex].children ?? []
|
||||||
);
|
);
|
||||||
destinationGroupItems.splice(result.destination.index, 0, removed);
|
destinationGroupItems.splice(result.destination.index, 0, removed);
|
||||||
|
|
||||||
final[sourceGroupIndex].items = sourceGroupItems;
|
final[sourceGroupIndex].children = sourceGroupItems;
|
||||||
final[destinationGroupIndex].items = destinationGroupItems;
|
final[destinationGroupIndex].children = destinationGroupItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(final);
|
onChange(final);
|
||||||
|
@ -3,90 +3,106 @@ import { SortableContext } from './SortableContext';
|
|||||||
import { StrictModeDroppable } from './StrictModeDroppable';
|
import { StrictModeDroppable } from './StrictModeDroppable';
|
||||||
import { useEvent } from '@/hooks/useEvent';
|
import { useEvent } from '@/hooks/useEvent';
|
||||||
import { Draggable } from 'react-beautiful-dnd';
|
import { Draggable } from 'react-beautiful-dnd';
|
||||||
import { BaseSortableData, ExtractGroup, ExtractItem } from './types';
|
import { SortableGroupItem, SortableItem, SortableLeafItem } from './types';
|
||||||
|
|
||||||
interface SortableGroupProps<T extends BaseSortableData> {
|
interface SortableGroupProps<GroupProps, ItemProps> {
|
||||||
list: T[];
|
list: SortableItem<GroupProps, ItemProps>[];
|
||||||
onChange: (list: T[]) => void;
|
onChange: (list: SortableItem<GroupProps, ItemProps>[]) => void;
|
||||||
renderGroup: (
|
renderGroup: (
|
||||||
group: ExtractGroup<T>,
|
group: SortableGroupItem<GroupProps, ItemProps>,
|
||||||
children: React.ReactNode
|
children: React.ReactNode,
|
||||||
|
level: number
|
||||||
|
) => React.ReactNode;
|
||||||
|
renderItem: (
|
||||||
|
item: SortableLeafItem<ItemProps>,
|
||||||
|
index: number,
|
||||||
|
group: SortableGroupItem<GroupProps, ItemProps>
|
||||||
) => React.ReactNode;
|
) => React.ReactNode;
|
||||||
renderItem: (item: ExtractItem<T>) => React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SortableGroup = <T extends BaseSortableData>(
|
export const SortableGroup = <GroupProps, ItemProps>(
|
||||||
props: SortableGroupProps<T>
|
props: SortableGroupProps<GroupProps, ItemProps>
|
||||||
) => {
|
) => {
|
||||||
const { list, onChange, renderGroup, renderItem } = props;
|
const { list, onChange, renderGroup, renderItem } = props;
|
||||||
|
|
||||||
const renderItemEl = useEvent((item: ExtractItem<T>, index: number) => {
|
const renderItemEl = useEvent(
|
||||||
return (
|
(
|
||||||
<Draggable key={item.id} draggableId={item.id} index={index}>
|
item: SortableLeafItem<ItemProps>,
|
||||||
{(dragProvided) => (
|
index: number,
|
||||||
<div
|
group: SortableGroupItem<GroupProps, ItemProps>
|
||||||
ref={dragProvided.innerRef}
|
) => {
|
||||||
{...dragProvided.draggableProps}
|
return (
|
||||||
{...dragProvided.dragHandleProps}
|
<Draggable key={item.key} draggableId={item.key} index={index}>
|
||||||
>
|
{(dragProvided) => (
|
||||||
{renderItem(item)}
|
<div
|
||||||
</div>
|
ref={dragProvided.innerRef}
|
||||||
)}
|
{...dragProvided.draggableProps}
|
||||||
</Draggable>
|
{...dragProvided.dragHandleProps}
|
||||||
);
|
>
|
||||||
});
|
{renderItem(item, index, group)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const renderGroupEl = useEvent((group: ExtractGroup<T>, level = 0) => {
|
const renderGroupEl = useEvent(
|
||||||
return (
|
(group: SortableGroupItem<GroupProps, ItemProps>, level = 0) => {
|
||||||
<StrictModeDroppable
|
return (
|
||||||
droppableId={group.id}
|
<StrictModeDroppable
|
||||||
type={group.type}
|
droppableId={group.key}
|
||||||
key={group.id}
|
type={level === 0 ? 'root' : 'group'}
|
||||||
>
|
key={group.key}
|
||||||
{(dropProvided) => (
|
>
|
||||||
<div ref={dropProvided.innerRef} {...dropProvided.droppableProps}>
|
{(dropProvided) => (
|
||||||
{renderGroup(
|
<div ref={dropProvided.innerRef} {...dropProvided.droppableProps}>
|
||||||
group,
|
{renderGroup(
|
||||||
<>
|
group,
|
||||||
{group.items.map((item, index) =>
|
<>
|
||||||
item.type === 'item' ? (
|
{group.children.map((item, index) =>
|
||||||
renderItemEl(item as ExtractItem<T>, index)
|
!('children' in item) ? (
|
||||||
) : (
|
renderItemEl(item, index, group)
|
||||||
<Draggable
|
) : (
|
||||||
draggableId={item.id}
|
<Draggable
|
||||||
key={item.id}
|
draggableId={item.key}
|
||||||
index={index}
|
key={item.key}
|
||||||
>
|
index={index}
|
||||||
{(dragProvided) => (
|
>
|
||||||
<div
|
{(dragProvided) => (
|
||||||
ref={dragProvided.innerRef}
|
<div
|
||||||
{...dragProvided.draggableProps}
|
ref={dragProvided.innerRef}
|
||||||
{...dragProvided.dragHandleProps}
|
{...dragProvided.draggableProps}
|
||||||
>
|
{...dragProvided.dragHandleProps}
|
||||||
{renderGroupEl(item as ExtractGroup<T>, level + 1)}
|
>
|
||||||
</div>
|
{renderGroupEl(item, level + 1)}
|
||||||
)}
|
</div>
|
||||||
</Draggable>
|
)}
|
||||||
)
|
</Draggable>
|
||||||
)}
|
)
|
||||||
</>
|
)}
|
||||||
)}
|
</>,
|
||||||
|
level
|
||||||
|
)}
|
||||||
|
|
||||||
{dropProvided.placeholder}
|
{dropProvided.placeholder}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</StrictModeDroppable>
|
</StrictModeDroppable>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SortableContext<T> list={list} onChange={onChange}>
|
<SortableContext<SortableItem<GroupProps, ItemProps>>
|
||||||
|
list={list}
|
||||||
|
onChange={onChange}
|
||||||
|
>
|
||||||
{renderGroupEl(
|
{renderGroupEl(
|
||||||
{
|
{
|
||||||
id: 'root',
|
key: 'root',
|
||||||
type: 'root' as const,
|
children: list,
|
||||||
items: list,
|
} as SortableGroupItem<GroupProps, ItemProps>,
|
||||||
} as any,
|
|
||||||
0
|
0
|
||||||
)}
|
)}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
|
@ -1,33 +1,14 @@
|
|||||||
import { Id } from 'react-beautiful-dnd';
|
import { Id } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
export type BaseSortableItem = {
|
export type SortableItem<GroupProps = unknown, ItemProps = unknown> =
|
||||||
type: 'item';
|
| SortableLeafItem<ItemProps>
|
||||||
id: Id;
|
| SortableGroupItem<GroupProps, ItemProps>;
|
||||||
};
|
|
||||||
|
|
||||||
export type BaseSortableGroup<GroupProps = unknown, ItemProps = unknown> = {
|
export type SortableGroupItem<GroupProps = unknown, ItemProps = unknown> = {
|
||||||
type: 'group';
|
key: Id;
|
||||||
id: Id;
|
children: SortableItem<GroupProps, ItemProps>[];
|
||||||
title?: string;
|
} & GroupProps;
|
||||||
items: ((BaseSortableGroup & GroupProps) | (BaseSortableItem & ItemProps))[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BaseSortableRoot<GroupProps = unknown> = {
|
export type SortableLeafItem<ItemProps = unknown> = {
|
||||||
type: 'root';
|
key: Id;
|
||||||
id: Id;
|
} & ItemProps;
|
||||||
title?: string;
|
|
||||||
items: (BaseSortableItem & GroupProps)[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BaseSortableData =
|
|
||||||
| BaseSortableRoot
|
|
||||||
| BaseSortableGroup
|
|
||||||
| BaseSortableItem;
|
|
||||||
|
|
||||||
export type SortableData<GroupProps = unknown, ItemProps = unknown> =
|
|
||||||
| BaseSortableRoot<GroupProps>
|
|
||||||
| (BaseSortableGroup<GroupProps, ItemProps> & GroupProps)
|
|
||||||
| (BaseSortableItem & ItemProps);
|
|
||||||
|
|
||||||
export type ExtractGroup<T> = T extends { type: 'group' } ? T : never;
|
|
||||||
export type ExtractItem<T> = T extends { type: 'item' } ? T : never;
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MonitorPicker, MonitorPickerOld } from '../MonitorPicker';
|
import { MonitorPicker } from '../MonitorPicker';
|
||||||
import { useTranslation } from '@i18next-toolkit/react';
|
import { useTranslation } from '@i18next-toolkit/react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { LuMinusCircle, LuPlus } from 'react-icons/lu';
|
import { LuMinusCircle, LuPlus } from 'react-icons/lu';
|
||||||
@ -23,17 +23,11 @@ import { domainRegex, slugRegex } from '@tianji/shared';
|
|||||||
import { useElementSize } from '@/hooks/useResizeObserver';
|
import { useElementSize } from '@/hooks/useResizeObserver';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Separator } from '@/components/ui/separator';
|
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 { 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 { groupItemSchema, MonitorStatusPageServiceList } from './ServiceList';
|
||||||
|
|
||||||
const Text = Typography.Text;
|
const Text = Typography.Text;
|
||||||
|
|
||||||
@ -46,6 +40,15 @@ const editFormSchema = z.object({
|
|||||||
.regex(domainRegex, 'Invalid domain')
|
.regex(domainRegex, 'Invalid domain')
|
||||||
.or(z.literal(''))
|
.or(z.literal(''))
|
||||||
.optional(),
|
.optional(),
|
||||||
|
body: z
|
||||||
|
.object({
|
||||||
|
groups: z.array(groupItemSchema),
|
||||||
|
})
|
||||||
|
.default({ groups: [] }),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
monitorList: z.array(
|
monitorList: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@ -77,10 +80,20 @@ export const MonitorStatusPageEditForm: React.FC<MonitorStatusPageEditFormProps>
|
|||||||
description: '',
|
description: '',
|
||||||
domain: '',
|
domain: '',
|
||||||
monitorList: [],
|
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,
|
control: form.control,
|
||||||
name: 'monitorList',
|
name: 'monitorList',
|
||||||
keyName: 'key',
|
keyName: 'key',
|
||||||
@ -191,74 +204,98 @@ export const MonitorStatusPageEditForm: React.FC<MonitorStatusPageEditFormProps>
|
|||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
{/* MonitorList */}
|
{/* Body */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="monitorList"
|
name="body.groups"
|
||||||
render={() => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('Monitor List')}</FormLabel>
|
<FormLabel>{t('Body')}</FormLabel>
|
||||||
{fields.map((field, i) => (
|
<FormControl>
|
||||||
<>
|
<MonitorStatusPageServiceList
|
||||||
{i !== 0 && <Separator />}
|
{...field}
|
||||||
|
value={field.value ?? []}
|
||||||
|
onChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div key={field.key} className="mb-2 flex flex-col gap-2">
|
{/* MonitorList */}
|
||||||
<Controller
|
{showDeprecatedMonitorList && (
|
||||||
control={form.control}
|
<FormField
|
||||||
name={`monitorList.${i}.id`}
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="monitorList"
|
||||||
<MonitorPicker
|
render={() => (
|
||||||
{...field}
|
<FormItem className="opacity-50">
|
||||||
value={field.value}
|
<FormLabel>
|
||||||
onValueChange={field.onChange}
|
{t('Monitor List')}
|
||||||
/>
|
<DeprecatedBadge tip={t('Please use Body field')} />
|
||||||
)}
|
</FormLabel>
|
||||||
/>
|
{oldMonitorFields.map((field, i) => (
|
||||||
|
<>
|
||||||
|
{i !== 0 && <Separator />}
|
||||||
|
|
||||||
<div className="flex flex-1 items-center">
|
<div key={field.key} className="mb-2 flex flex-col gap-2">
|
||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`monitorList.${i}.showCurrent`}
|
name={`monitorList.${i}.id`}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Switch
|
<MonitorPicker
|
||||||
checked={field.value}
|
{...field}
|
||||||
onCheckedChange={field.onChange}
|
value={field.value}
|
||||||
|
onValueChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className="ml-1 flex-1 align-middle text-sm">
|
<div className="flex flex-1 items-center">
|
||||||
{t('Show Latest Value')}
|
<Controller
|
||||||
</span>
|
control={form.control}
|
||||||
|
name={`monitorList.${i}.showCurrent`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<LuMinusCircle
|
<span className="ml-1 flex-1 align-middle text-sm">
|
||||||
className="cursor-pointer text-lg"
|
{t('Show Latest Value')}
|
||||||
onClick={() => remove(i)}
|
</span>
|
||||||
/>
|
|
||||||
|
<LuMinusCircle
|
||||||
|
className="cursor-pointer text-lg"
|
||||||
|
onClick={() => remove(i)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</>
|
))}
|
||||||
))}
|
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="dashed"
|
variant="dashed"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
append({
|
append({
|
||||||
id: '',
|
id: '',
|
||||||
showCurrent: false,
|
showCurrent: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
style={{ width: '60%' }}
|
style={{ width: '60%' }}
|
||||||
Icon={LuPlus}
|
Icon={LuPlus}
|
||||||
>
|
>
|
||||||
{t('Add Monitor')}
|
{t('Add Monitor')}
|
||||||
</Button>
|
</Button>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="!mt-8 flex justify-end gap-2">
|
<div className="!mt-8 flex justify-end gap-2">
|
||||||
<Button type="submit" loading={isLoading}>
|
<Button type="submit" loading={isLoading}>
|
||||||
|
@ -1,59 +1,243 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { SortableData } from '@/components/Sortable/types';
|
import { SortableItem } from '@/components/Sortable/types';
|
||||||
import { SortableGroup } from '@/components/Sortable/SortableGroup';
|
import { SortableGroup } from '@/components/Sortable/SortableGroup';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { useEvent } from '@/hooks/useEvent';
|
||||||
|
import { v1 as uuid } from 'uuid';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useTranslation } from '@i18next-toolkit/react';
|
||||||
|
import { LuMinusCircle, LuPlusCircle, LuTrash } from 'react-icons/lu';
|
||||||
|
import { cn } from '@/utils/style';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { MonitorPicker } from '../MonitorPicker';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { set } from 'lodash-es';
|
||||||
|
|
||||||
type MonitorStatusPageServiceItem = SortableData<{}, { title: string }>;
|
export const leafItemSchema = z.object({
|
||||||
|
key: z.string(),
|
||||||
export const MonitorStatusPageServiceList: React.FC = React.memo(() => {
|
id: z.string(),
|
||||||
const [list, setList] = useState<MonitorStatusPageServiceItem[]>([
|
type: z.enum(['monitor']),
|
||||||
{
|
showCurrent: z.boolean().default(false).optional(),
|
||||||
type: 'group',
|
|
||||||
title: 'Group 1',
|
|
||||||
id: 'group1',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'item1',
|
|
||||||
type: 'item',
|
|
||||||
title: 'Item 1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'item2',
|
|
||||||
type: 'item',
|
|
||||||
title: 'Item 2',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'group',
|
|
||||||
title: 'Group 2',
|
|
||||||
id: 'group2',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'item3',
|
|
||||||
type: 'item',
|
|
||||||
title: 'Item 3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'item4',
|
|
||||||
type: 'item',
|
|
||||||
title: 'Item 4',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
] as MonitorStatusPageServiceItem[]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SortableGroup
|
|
||||||
list={list}
|
|
||||||
onChange={(list) => setList(list)}
|
|
||||||
renderGroup={(group, children) => (
|
|
||||||
<div>
|
|
||||||
<div>{group.title}</div>
|
|
||||||
<div className="p-2">{children}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
renderItem={(item) => <div>{item.id}</div>}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 LeafItemProps = Omit<z.infer<typeof leafItemSchema>, 'key'>;
|
||||||
|
|
||||||
|
export type MonitorStatusPageServiceItem = SortableItem<
|
||||||
|
GroupItemProps,
|
||||||
|
LeafItemProps
|
||||||
|
>;
|
||||||
|
|
||||||
|
interface MonitorStatusPageServiceListProps {
|
||||||
|
value: MonitorStatusPageServiceItem[];
|
||||||
|
onChange: (list: MonitorStatusPageServiceItem[]) => void;
|
||||||
|
}
|
||||||
|
export const MonitorStatusPageServiceList: React.FC<MonitorStatusPageServiceListProps> =
|
||||||
|
React.memo((props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const handleAddGroup = useEvent(() => {
|
||||||
|
props.onChange([
|
||||||
|
...props.value,
|
||||||
|
{
|
||||||
|
key: uuid(),
|
||||||
|
title: 'Default',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAddItem = useEvent((groupKey: string) => {
|
||||||
|
const index = props.value.findIndex((item) => item.key === groupKey);
|
||||||
|
if (index === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newList = [...props.value];
|
||||||
|
|
||||||
|
if (!('children' in newList[index])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newList[index].children = [
|
||||||
|
...newList[index].children,
|
||||||
|
{
|
||||||
|
key: uuid(),
|
||||||
|
id: '',
|
||||||
|
type: 'monitor',
|
||||||
|
showCurrent: false,
|
||||||
|
},
|
||||||
|
] as MonitorStatusPageServiceItem[];
|
||||||
|
|
||||||
|
props.onChange(newList);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDeleteGroup = useEvent((groupKey: string) => {
|
||||||
|
const index = props.value.findIndex((item) => item.key === groupKey);
|
||||||
|
if (index === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newList = [...props.value];
|
||||||
|
|
||||||
|
newList.splice(index, 1);
|
||||||
|
|
||||||
|
props.onChange(newList);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDeleteItem = useEvent((groupKey: string, itemKey: string) => {
|
||||||
|
const newList = [...props.value];
|
||||||
|
const groupIndex = newList.findIndex((item) => item.key === groupKey);
|
||||||
|
if (groupIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const group = newList[groupIndex];
|
||||||
|
if (!('children' in group)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemIndex = group.children.findIndex(
|
||||||
|
(item) => item.key === itemKey
|
||||||
|
);
|
||||||
|
if (itemIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
group.children.splice(itemIndex, 1);
|
||||||
|
|
||||||
|
props.onChange(newList);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleUpdateItem = useEvent(
|
||||||
|
(groupKey: string, itemKey: string, fieldName: string, value: any) => {
|
||||||
|
const newList = [...props.value];
|
||||||
|
const groupIndex = newList.findIndex((item) => item.key === groupKey);
|
||||||
|
if (groupIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const group = newList[groupIndex];
|
||||||
|
if (!('children' in group)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemIndex = group.children.findIndex(
|
||||||
|
(item) => item.key === itemKey
|
||||||
|
);
|
||||||
|
if (itemIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(group, `children[${itemIndex}].${fieldName}`, value);
|
||||||
|
|
||||||
|
props.onChange(newList);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border p-2">
|
||||||
|
<SortableGroup<GroupItemProps, LeafItemProps>
|
||||||
|
list={props.value}
|
||||||
|
onChange={props.onChange}
|
||||||
|
renderGroup={(group, children, level) => (
|
||||||
|
<div>
|
||||||
|
<div className={cn('flex items-center gap-2')}>
|
||||||
|
<span>{group.title}</span>
|
||||||
|
|
||||||
|
{level > 0 && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
className="h-6 w-6"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
type="button"
|
||||||
|
Icon={LuPlusCircle}
|
||||||
|
onClick={() => handleAddItem(group.key)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="h-6 w-6"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
type="button"
|
||||||
|
Icon={LuTrash}
|
||||||
|
onClick={() => handleDeleteGroup(group.key)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(level > 0 && 'border-l-4 border-gray-600 p-2')}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
renderItem={(item, i, group) => {
|
||||||
|
if (item.type === 'monitor') {
|
||||||
|
return (
|
||||||
|
<div key={item.key}>
|
||||||
|
{i !== 0 && <Separator />}
|
||||||
|
|
||||||
|
<div className="mb-2 flex flex-col gap-2">
|
||||||
|
<MonitorPicker
|
||||||
|
value={item.id}
|
||||||
|
onValueChange={(val) =>
|
||||||
|
handleUpdateItem(group.key, item.key, 'id', val)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-1 items-center">
|
||||||
|
<Switch
|
||||||
|
checked={item.showCurrent ?? false}
|
||||||
|
onCheckedChange={(val) =>
|
||||||
|
handleUpdateItem(
|
||||||
|
group.key,
|
||||||
|
item.key,
|
||||||
|
'showCurrent',
|
||||||
|
val
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className="ml-1 flex-1 align-middle text-sm">
|
||||||
|
{t('Show Latest Value')}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<LuMinusCircle
|
||||||
|
className="cursor-pointer text-lg"
|
||||||
|
onClick={() => handleDeleteItem(group.key, item.key)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{Array.isArray(props.value) && props.value.length === 0 && (
|
||||||
|
<p className="text-xs opacity-50">
|
||||||
|
{t('No any group has been created, click button to create one')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="mt-2"
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleAddGroup}
|
||||||
|
>
|
||||||
|
{t('Add Group')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
MonitorStatusPageServiceList.displayName = 'MonitorStatusPageServiceList';
|
MonitorStatusPageServiceList.displayName = 'MonitorStatusPageServiceList';
|
||||||
|
@ -87,7 +87,7 @@ FormItem.displayName = "FormItem"
|
|||||||
|
|
||||||
const FormLabel = React.forwardRef<
|
const FormLabel = React.forwardRef<
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & {optional?: boolean}
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & { optional?: boolean }
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
const { error, formItemId } = useFormField()
|
const { error, formItemId } = useFormField()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { createFileRoute, redirect } from '@tanstack/react-router';
|
import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||||
import { isDev } from '@/utils/env';
|
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')({
|
export const Route = createFileRoute('/playground')({
|
||||||
beforeLoad: () => {
|
beforeLoad: () => {
|
||||||
@ -13,13 +17,35 @@ export const Route = createFileRoute('/playground')({
|
|||||||
component: PageComponent,
|
component: PageComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
function PageComponent(this: {
|
function PageComponent() {
|
||||||
beforeLoad: () => void;
|
const [list, setList] = useState<MonitorStatusPageServiceItem[]>([
|
||||||
component: () => import('react/jsx-runtime').JSX.Element;
|
{
|
||||||
}) {
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<MonitorStatusPageServiceList />
|
<MonitorStatusPageServiceList value={list} onChange={setList} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user