Optimize the plugin system (#2090)

* Optimize the plugin system

* feat: enhance plugin installation process and improve task management

* fix: linter err

---------

Co-authored-by: Junyan Qin <rockchinq@gmail.com>
This commit is contained in:
Typer_Body
2026-03-29 23:58:34 +08:00
committed by GitHub
parent b0a9be77b0
commit 1c419e3591
24 changed files with 2619 additions and 3339 deletions

View File

@@ -0,0 +1,192 @@
'use client';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Progress } from '@/components/ui/progress';
import {
Download,
Package,
Settings,
Rocket,
CheckCircle2,
XCircle,
Loader2,
X,
ListTodo,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Badge } from '@/components/ui/badge';
import {
usePluginInstallTasks,
InstallStage,
PluginInstallTask,
} from './PluginInstallTaskContext';
import { cn } from '@/lib/utils';
const STAGE_ICONS: Record<string, React.ElementType> = {
[InstallStage.DOWNLOADING]: Download,
[InstallStage.INSTALLING_DEPS]: Package,
[InstallStage.INITIALIZING]: Settings,
[InstallStage.LAUNCHING]: Rocket,
[InstallStage.DONE]: CheckCircle2,
[InstallStage.ERROR]: XCircle,
};
function TaskQueueItem({
task,
onClick,
onRemove,
}: {
task: PluginInstallTask;
onClick: () => void;
onRemove: () => void;
}) {
const { t } = useTranslation();
const isDone = task.stage === InstallStage.DONE;
const isError = task.stage === InstallStage.ERROR;
const isRunning = !isDone && !isError;
const StageIcon = STAGE_ICONS[task.stage] || Download;
const stageLabel = (() => {
switch (task.stage) {
case InstallStage.DOWNLOADING:
return t('plugins.installProgress.downloading');
case InstallStage.INSTALLING_DEPS:
return t('plugins.installProgress.installingDeps');
case InstallStage.INITIALIZING:
return t('plugins.installProgress.initializing');
case InstallStage.LAUNCHING:
return t('plugins.installProgress.launching');
case InstallStage.DONE:
return t('plugins.installProgress.completed');
case InstallStage.ERROR:
return t('plugins.installProgress.failed');
default:
return '';
}
})();
return (
<div
className="flex items-center gap-2.5 px-3 py-2 rounded-lg hover:bg-muted/60 cursor-pointer transition-colors group"
onClick={onClick}
>
<div
className={cn(
'flex items-center justify-center w-7 h-7 rounded-full shrink-0',
isDone &&
'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400',
isError &&
'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400',
isRunning &&
'bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400',
)}
>
{isRunning ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : (
<StageIcon className="w-3.5 h-3.5" />
)}
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">{task.pluginName}</div>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">{stageLabel}</span>
{isRunning && (
<span className="text-xs text-muted-foreground">
{task.overallProgress}%
</span>
)}
</div>
{isRunning && (
<Progress value={task.overallProgress} className="h-1 mt-1" />
)}
</div>
{(isDone || isError) && (
<Button
variant="ghost"
size="icon"
className="w-6 h-6 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => {
e.stopPropagation();
onRemove();
}}
>
<X className="w-3 h-3" />
</Button>
)}
</div>
);
}
export default function PluginInstallTaskQueue() {
const { t } = useTranslation();
const { tasks, setSelectedTaskId, removeTask, clearCompletedTasks } =
usePluginInstallTasks();
const runningCount = tasks.filter(
(t) => t.stage !== InstallStage.DONE && t.stage !== InstallStage.ERROR,
).length;
const hasCompleted = tasks.some(
(t) => t.stage === InstallStage.DONE || t.stage === InstallStage.ERROR,
);
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="relative px-4 py-5 cursor-pointer">
<ListTodo className="w-4 h-4 mr-2" />
{t('plugins.installProgress.taskQueue')}
{runningCount > 0 && (
<Badge
variant="default"
className="ml-2 h-5 min-w-5 px-1.5 text-xs"
>
{runningCount}
</Badge>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[340px] p-2" align="end">
<div className="flex items-center justify-between px-2 py-1.5 mb-1">
<span className="text-sm font-semibold">
{t('plugins.installProgress.taskQueue')}
</span>
{hasCompleted && (
<Button
variant="ghost"
size="sm"
className="h-6 text-xs px-2"
onClick={clearCompletedTasks}
>
{t('plugins.installProgress.clearCompleted')}
</Button>
)}
</div>
<div className="max-h-[300px] overflow-y-auto space-y-0.5">
{tasks.length === 0 ? (
<div className="py-6 text-center text-sm text-muted-foreground">
{t('plugins.installProgress.noTasks')}
</div>
) : (
tasks.map((task) => (
<TaskQueueItem
key={task.id}
task={task}
onClick={() => setSelectedTaskId(task.id)}
onRemove={() => removeTask(task.id)}
/>
))
)}
</div>
</PopoverContent>
</Popover>
);
}