feat: Add queryNodeTag function and integrate tag retrieval in NodeForm and SubscribeForm components

This commit is contained in:
web 2025-09-03 09:59:54 -07:00
parent 9e01f4f590
commit 4563c570ac
4 changed files with 60 additions and 12 deletions

View File

@ -1,6 +1,6 @@
'use client';
import { filterServerList } from '@/services/admin/server';
import { filterServerList, queryNodeTag } from '@/services/admin/server';
import { zodResolver } from '@hookform/resolvers/zod';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button';
@ -98,6 +98,15 @@ export default function NodeForm(props: {
});
const servers: ServerRow[] = data as ServerRow[];
const { data: tagsData } = useQuery({
queryKey: ['queryNodeTag'],
queryFn: async () => {
const { data } = await queryNodeTag();
return data?.data?.tags || [];
},
});
const existingTags: string[] = tagsData as string[];
const currentServer = useMemo(() => servers?.find((s) => s.id === serverId), [servers, serverId]);
const availableProtocols = useMemo(() => {
@ -298,6 +307,7 @@ export default function NodeForm(props: {
placeholder={t('tags_placeholder')}
value={field.value || []}
onChange={(v) => form.setValue(field.name, v)}
options={existingTags}
/>
</FormControl>
<FormDescription>{t('tags_description')}</FormDescription>

View File

@ -1,6 +1,6 @@
'use client';
import { filterNodeList } from '@/services/admin/server';
import { filterNodeList, queryNodeTag } from '@/services/admin/server';
import { getSubscribeGroupList } from '@/services/admin/subscribe';
import { zodResolver } from '@hookform/resolvers/zod';
import { useQuery } from '@tanstack/react-query';
@ -245,7 +245,16 @@ export default function SubscribeForm<T extends Record<string, any>>({
return (data.data?.list || []) as API.Node[];
},
});
const tagGroups = Array.from(
const { data: allTagsData } = useQuery({
queryKey: ['queryNodeTag'],
queryFn: async () => {
const { data } = await queryNodeTag();
return data?.data?.tags || [];
},
});
const nodeExtractedTags = Array.from(
new Set(
((nodes as API.Node[]) || [])
.flatMap((n) => (Array.isArray(n.tags) ? n.tags : []))
@ -253,6 +262,12 @@ export default function SubscribeForm<T extends Record<string, any>>({
),
) as string[];
const allAvailableTags = (allTagsData as string[]) || [];
const tagGroups = Array.from(new Set([...allAvailableTags, ...nodeExtractedTags])).filter(
Boolean,
);
const unit_time = form.watch('unit_time');
return (
@ -271,7 +286,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
<SheetHeader>
<SheetTitle>{title}</SheetTitle>
</SheetHeader>
<ScrollArea className='h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]'>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className='pt-4'>
<Tabs defaultValue='basic' className='w-full'>
@ -801,8 +816,12 @@ export default function SubscribeForm<T extends Record<string, any>>({
<Accordion type='single' collapsible className='w-full'>
{tagGroups.map((tag) => {
const value = field.value || [];
// Use a synthetic ID for tag grouping selection by name
const tagId = tag;
const nodesWithTag =
(nodes as API.Node[])?.filter((n) =>
(n.tags || []).includes(tag),
) || [];
return (
<AccordionItem key={tag} value={String(tag)}>
<AccordionTrigger>
@ -818,7 +837,12 @@ export default function SubscribeForm<T extends Record<string, any>>({
);
}}
/>
<Label>{tag}</Label>
<Label>
{tag}
<span className='text-muted-foreground ml-2 text-xs'>
({nodesWithTag.length})
</span>
</Label>
</div>
</AccordionTrigger>
<AccordionContent>
@ -830,11 +854,13 @@ export default function SubscribeForm<T extends Record<string, any>>({
key={node.id}
className='flex items-center justify-between gap-3'
>
<span>{node.name}</span>
<span>
<span className='flex-1'>{node.name}</span>
<span className='flex-1'>
{node.address}:{node.port}
</span>
<span>{node.protocol}</span>
<span className='flex-1 text-right'>
{node.protocol}
</span>
</li>
))}
</ul>
@ -876,11 +902,11 @@ export default function SubscribeForm<T extends Record<string, any>>({
}}
/>
<Label className='flex w-full items-center justify-between gap-3'>
<span>{item.name}</span>
<span>
<span className='flex-1'>{item.name}</span>
<span className='flex-1'>
{item.address}:{item.port}
</span>
<span>{item.protocol}</span>
<span className='flex-1 text-right'>{item.protocol}</span>
</Label>
</div>
);

View File

@ -141,6 +141,14 @@ export async function toggleNodeStatus(
});
}
/** Query all node tags GET /v1/admin/server/node/tags */
export async function queryNodeTag(options?: { [key: string]: any }) {
return request<API.Response & { data?: API.QueryNodeTagResponse }>('/v1/admin/server/node/tags', {
method: 'GET',
...(options || {}),
});
}
/** Update Node POST /v1/admin/server/node/update */
export async function updateNode(body: API.UpdateNodeRequest, options?: { [key: string]: any }) {
return request<API.Response & { data?: any }>('/v1/admin/server/node/update', {

View File

@ -1551,6 +1551,10 @@ declare namespace API {
list: Document[];
};
type QueryNodeTagResponse = {
tags: string[];
};
type QueryOrderDetailRequest = {
order_no: string;
};