feat: tag debugging plugins in webui

This commit is contained in:
Junyan Qin
2025-08-15 19:11:49 +08:00
parent 2b8eb5f01c
commit e1a78e8ff9
10 changed files with 314 additions and 14 deletions

View File

@@ -22,6 +22,7 @@
"@hookform/resolvers": "^5.0.1", "@hookform/resolvers": "^5.0.1",
"@radix-ui/react-checkbox": "^1.3.1", "@radix-ui/react-checkbox": "^1.3.1",
"@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.13", "@radix-ui/react-hover-card": "^1.1.13",
"@radix-ui/react-label": "^2.1.6", "@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-popover": "^1.1.14",

View File

@@ -7,7 +7,13 @@ import PluginSortDialog from '@/app/home/plugins/plugin-sort/PluginSortDialog';
import styles from './plugins.module.css'; import styles from './plugins.module.css';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { PlusIcon } from 'lucide-react'; import { PlusIcon, ChevronDownIcon, UploadIcon, StoreIcon } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -32,6 +38,7 @@ export default function PluginConfigPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [sortModalOpen, setSortModalOpen] = useState(false); const [sortModalOpen, setSortModalOpen] = useState(false);
const [activeTab, setActiveTab] = useState('installed');
const [pluginInstallStatus, setPluginInstallStatus] = const [pluginInstallStatus, setPluginInstallStatus] =
useState<PluginInstallStatus>(PluginInstallStatus.WAIT_INPUT); useState<PluginInstallStatus>(PluginInstallStatus.WAIT_INPUT);
const [installError, setInstallError] = useState<string | null>(null); const [installError, setInstallError] = useState<string | null>(null);
@@ -83,7 +90,7 @@ export default function PluginConfigPage() {
return ( return (
<div className={styles.pageContainer}> <div className={styles.pageContainer}>
<Tabs defaultValue="installed" className="w-full"> <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<div className="flex flex-row justify-between items-center px-[0.8rem]"> <div className="flex flex-row justify-between items-center px-[0.8rem]">
<TabsList className="shadow-md py-5 bg-[#f0f0f0]"> <TabsList className="shadow-md py-5 bg-[#f0f0f0]">
<TabsTrigger value="installed" className="px-6 py-4 cursor-pointer"> <TabsTrigger value="installed" className="px-6 py-4 cursor-pointer">
@@ -104,18 +111,34 @@ export default function PluginConfigPage() {
> >
{t('plugins.arrange')} {t('plugins.arrange')}
</Button> </Button>
<Button <DropdownMenu>
variant="default" <DropdownMenuTrigger asChild>
className="px-6 py-4 cursor-pointer" <Button variant="default" className="px-6 py-4 cursor-pointer">
onClick={() => { <PlusIcon className="w-4 h-4" />
setModalOpen(true); {t('plugins.install')}
setPluginInstallStatus(PluginInstallStatus.WAIT_INPUT); <ChevronDownIcon className="ml-2 w-4 h-4" />
setInstallError(null); </Button>
}} </DropdownMenuTrigger>
> <DropdownMenuContent align="end">
<PlusIcon className="w-4 h-4" /> <DropdownMenuItem
{t('plugins.install')} onClick={() => {
</Button> // TODO: 本地上传功能待实现
console.log('本地上传功能待实现');
}}
>
<UploadIcon className="w-4 h-4" />
{t('plugins.uploadLocal')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setActiveTab('market');
}}
>
<StoreIcon className="w-4 h-4" />
{t('plugins.marketplace')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</div> </div>
<TabsContent value="installed"> <TabsContent value="installed">

View File

@@ -9,6 +9,7 @@ export interface IPluginCardVO {
tools: object[]; tools: object[];
event_handlers: object; event_handlers: object;
repository: string; repository: string;
debug: boolean;
} }
export class PluginCardVO implements IPluginCardVO { export class PluginCardVO implements IPluginCardVO {
@@ -18,6 +19,7 @@ export class PluginCardVO implements IPluginCardVO {
version: string; version: string;
enabled: boolean; enabled: boolean;
priority: number; priority: number;
debug: boolean;
status: string; status: string;
tools: object[]; tools: object[];
event_handlers: object; event_handlers: object;
@@ -34,5 +36,6 @@ export class PluginCardVO implements IPluginCardVO {
this.status = prop.status; this.status = prop.status;
this.tools = prop.tools; this.tools = prop.tools;
this.version = prop.version; this.version = prop.version;
this.debug = prop.debug;
} }
} }

View File

@@ -50,6 +50,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
zh_Hans: '', zh_Hans: '',
}, },
), ),
debug: plugin.debug,
enabled: plugin.enabled, enabled: plugin.enabled,
name: plugin.manifest.manifest.metadata.name, name: plugin.manifest.manifest.metadata.name,
version: plugin.manifest.manifest.metadata.version ?? '', version: plugin.manifest.manifest.metadata.version ?? '',

View File

@@ -58,6 +58,14 @@ export default function PluginCardComponent({
<Badge variant="outline" className="text-[0.7rem]"> <Badge variant="outline" className="text-[0.7rem]">
v{cardVO.version} v{cardVO.version}
</Badge> </Badge>
{cardVO.debug && (
<Badge
variant="outline"
className="text-[0.7rem] border-orange-400 text-orange-400"
>
{t('plugins.debugging')}
</Badge>
)}
</div> </div>
</div> </div>

View File

@@ -7,6 +7,7 @@ export interface Plugin {
manifest: { manifest: {
manifest: ComponentManifest; manifest: ComponentManifest;
}; };
debug: boolean;
enabled: boolean; enabled: boolean;
components: { components: {
component_config: object; component_config: object;

View File

@@ -0,0 +1,257 @@
'use client';
import * as React from 'react';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
);
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
);
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
);
}
function DropdownMenuItem({
className,
inset,
variant = 'default',
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: 'default' | 'destructive';
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
);
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
className,
)}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn('bg-border -mx-1 my-1 h-px', className)}
{...props}
/>
);
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<'span'>) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
'text-muted-foreground ml-auto text-xs tracking-widest',
className,
)}
{...props}
/>
);
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
className,
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};

View File

@@ -179,6 +179,8 @@ const enUS = {
eventCount: 'Events: {{count}}', eventCount: 'Events: {{count}}',
toolCount: 'Tools: {{count}}', toolCount: 'Tools: {{count}}',
starCount: 'Stars: {{count}}', starCount: 'Stars: {{count}}',
uploadLocal: 'Upload Local',
debugging: 'Debugging',
}, },
pipelines: { pipelines: {
title: 'Pipelines', title: 'Pipelines',

View File

@@ -179,6 +179,8 @@ const jaJP = {
eventCount: 'イベント:{{count}}', eventCount: 'イベント:{{count}}',
toolCount: 'ツール:{{count}}', toolCount: 'ツール:{{count}}',
starCount: 'スター:{{count}}', starCount: 'スター:{{count}}',
uploadLocal: 'ローカルアップロード',
debugging: 'デバッグ中',
}, },
pipelines: { pipelines: {
title: 'パイプライン', title: 'パイプライン',

View File

@@ -175,6 +175,8 @@ const zhHans = {
eventCount: '事件:{{count}}', eventCount: '事件:{{count}}',
toolCount: '工具:{{count}}', toolCount: '工具:{{count}}',
starCount: '星标:{{count}}', starCount: '星标:{{count}}',
uploadLocal: '本地上传',
debugging: '调试中',
}, },
pipelines: { pipelines: {
title: '流水线', title: '流水线',