mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-17 01:46:07 +00:00
feat: enhance plugin installation process and improve task management
This commit is contained in:
@@ -265,6 +265,8 @@ class PluginsRouterGroup(group.RouterGroup):
|
|||||||
return self.http_status(400, -1, 'Missing asset_url parameter')
|
return self.http_status(400, -1, 'Missing asset_url parameter')
|
||||||
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
ctx = taskmgr.TaskContext.new()
|
||||||
|
ctx.metadata['plugin_name'] = f'{owner}/{repo}'
|
||||||
|
ctx.metadata['install_source'] = 'github'
|
||||||
install_info = {
|
install_info = {
|
||||||
'asset_url': asset_url,
|
'asset_url': asset_url,
|
||||||
'owner': owner,
|
'owner': owner,
|
||||||
@@ -295,12 +297,17 @@ class PluginsRouterGroup(group.RouterGroup):
|
|||||||
|
|
||||||
data = await quart.request.json
|
data = await quart.request.json
|
||||||
|
|
||||||
|
plugin_author = data.get('plugin_author', '')
|
||||||
|
plugin_name = data.get('plugin_name', '')
|
||||||
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
ctx = taskmgr.TaskContext.new()
|
||||||
|
ctx.metadata['plugin_name'] = f'{plugin_author}/{plugin_name}'
|
||||||
|
ctx.metadata['install_source'] = 'marketplace'
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
wrapper = self.ap.task_mgr.create_user_task(
|
||||||
self.ap.plugin_connector.install_plugin(PluginInstallSource.MARKETPLACE, data, task_context=ctx),
|
self.ap.plugin_connector.install_plugin(PluginInstallSource.MARKETPLACE, data, task_context=ctx),
|
||||||
kind='plugin-operation',
|
kind='plugin-operation',
|
||||||
name='plugin-install-marketplace',
|
name='plugin-install-marketplace',
|
||||||
label=f'Installing plugin from marketplace ...{data}',
|
label=f'Installing plugin from marketplace {plugin_author}/{plugin_name}',
|
||||||
context=ctx,
|
context=ctx,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -323,11 +330,13 @@ class PluginsRouterGroup(group.RouterGroup):
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
ctx = taskmgr.TaskContext.new()
|
||||||
|
ctx.metadata['plugin_name'] = file.filename or 'local plugin'
|
||||||
|
ctx.metadata['install_source'] = 'local'
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
wrapper = self.ap.task_mgr.create_user_task(
|
||||||
self.ap.plugin_connector.install_plugin(PluginInstallSource.LOCAL, data, task_context=ctx),
|
self.ap.plugin_connector.install_plugin(PluginInstallSource.LOCAL, data, task_context=ctx),
|
||||||
kind='plugin-operation',
|
kind='plugin-operation',
|
||||||
name='plugin-install-local',
|
name='plugin-install-local',
|
||||||
label=f'Installing plugin from local ...{file.filename}',
|
label=f'Installing plugin from local {file.filename}',
|
||||||
context=ctx,
|
context=ctx,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -118,11 +118,14 @@ class SystemRouterGroup(group.RouterGroup):
|
|||||||
@self.route('/tasks', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
@self.route('/tasks', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||||
async def _() -> str:
|
async def _() -> str:
|
||||||
task_type = quart.request.args.get('type')
|
task_type = quart.request.args.get('type')
|
||||||
|
task_kind = quart.request.args.get('kind')
|
||||||
|
|
||||||
if task_type == '':
|
if task_type == '':
|
||||||
task_type = None
|
task_type = None
|
||||||
|
if task_kind == '':
|
||||||
|
task_kind = None
|
||||||
|
|
||||||
return self.success(data=self.ap.task_mgr.get_tasks_dict(task_type))
|
return self.success(data=self.ap.task_mgr.get_tasks_dict(task_type, task_kind))
|
||||||
|
|
||||||
@self.route('/tasks/<task_id>', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
@self.route('/tasks/<task_id>', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||||
async def _(task_id: str) -> str:
|
async def _(task_id: str) -> str:
|
||||||
|
|||||||
@@ -215,9 +215,14 @@ class AsyncTaskManager:
|
|||||||
def get_tasks_dict(
|
def get_tasks_dict(
|
||||||
self,
|
self,
|
||||||
type: str = None,
|
type: str = None,
|
||||||
|
kind: str = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
return {
|
return {
|
||||||
'tasks': [t.to_dict() for t in self.tasks if type is None or t.task_type == type],
|
'tasks': [
|
||||||
|
t.to_dict()
|
||||||
|
for t in self.tasks
|
||||||
|
if (type is None or t.task_type == type) and (kind is None or t.kind == kind)
|
||||||
|
],
|
||||||
'id_index': TaskWrapper._id_index,
|
'id_index': TaskWrapper._id_index,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Download } from 'lucide-react';
|
import { Download } from 'lucide-react';
|
||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback, useEffect } from 'react';
|
||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
import { systemInfo } from '@/app/infra/http/HttpClient';
|
import { systemInfo } from '@/app/infra/http/HttpClient';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -42,7 +42,12 @@ export default function MarketplacePage() {
|
|||||||
function MarketplaceContent() {
|
function MarketplaceContent() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { refreshPlugins } = useSidebarData();
|
const { refreshPlugins } = useSidebarData();
|
||||||
const { addTask, setSelectedTaskId } = usePluginInstallTasks();
|
const {
|
||||||
|
addTask,
|
||||||
|
setSelectedTaskId,
|
||||||
|
registerOnTaskComplete,
|
||||||
|
unregisterOnTaskComplete,
|
||||||
|
} = usePluginInstallTasks();
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [installInfo, setInstallInfo] = useState<Record<string, string>>({});
|
const [installInfo, setInstallInfo] = useState<Record<string, string>>({});
|
||||||
const [pluginInstallStatus, setPluginInstallStatus] =
|
const [pluginInstallStatus, setPluginInstallStatus] =
|
||||||
@@ -71,26 +76,19 @@ function MarketplaceContent() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function watchTask(taskId: number) {
|
// Register task completion callback for toast and plugin list refresh
|
||||||
let alreadySuccess = false;
|
useEffect(() => {
|
||||||
|
const onComplete = (_taskId: number, success: boolean) => {
|
||||||
const interval = setInterval(() => {
|
if (success) {
|
||||||
httpClient.getAsyncTask(taskId).then((resp) => {
|
toast.success(t('plugins.installSuccess'));
|
||||||
if (resp.runtime.done) {
|
refreshPlugins();
|
||||||
clearInterval(interval);
|
}
|
||||||
if (resp.runtime.exception) {
|
};
|
||||||
// Error is shown via task queue progress dialog
|
registerOnTaskComplete(onComplete);
|
||||||
} else {
|
return () => {
|
||||||
if (!alreadySuccess) {
|
unregisterOnTaskComplete(onComplete);
|
||||||
toast.success(t('plugins.installSuccess'));
|
};
|
||||||
alreadySuccess = true;
|
}, [registerOnTaskComplete, unregisterOnTaskComplete, refreshPlugins, t]);
|
||||||
}
|
|
||||||
refreshPlugins();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleInstallPlugin = useCallback(
|
const handleInstallPlugin = useCallback(
|
||||||
async (plugin: PluginV4) => {
|
async (plugin: PluginV4) => {
|
||||||
@@ -126,7 +124,6 @@ function MarketplaceContent() {
|
|||||||
});
|
});
|
||||||
setSelectedTaskId(taskKey);
|
setSelectedTaskId(taskKey);
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
watchTask(taskId);
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setInstallError(err.msg);
|
setInstallError(err.msg);
|
||||||
|
|||||||
+99
-70
@@ -11,9 +11,9 @@ import { Progress } from '@/components/ui/progress';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Download,
|
Download,
|
||||||
FolderOpen,
|
|
||||||
Package,
|
Package,
|
||||||
FlaskConical,
|
Settings,
|
||||||
|
Rocket,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
XCircle,
|
XCircle,
|
||||||
Loader2,
|
Loader2,
|
||||||
@@ -36,20 +36,20 @@ const STAGES: {
|
|||||||
icon: Download,
|
icon: Download,
|
||||||
i18nKey: 'plugins.installProgress.downloading',
|
i18nKey: 'plugins.installProgress.downloading',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: InstallStage.EXTRACTING,
|
|
||||||
icon: FolderOpen,
|
|
||||||
i18nKey: 'plugins.installProgress.extracting',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: InstallStage.INSTALLING_DEPS,
|
key: InstallStage.INSTALLING_DEPS,
|
||||||
icon: Package,
|
icon: Package,
|
||||||
i18nKey: 'plugins.installProgress.installingDeps',
|
i18nKey: 'plugins.installProgress.installingDeps',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: InstallStage.TESTING,
|
key: InstallStage.INITIALIZING,
|
||||||
icon: FlaskConical,
|
icon: Settings,
|
||||||
i18nKey: 'plugins.installProgress.testing',
|
i18nKey: 'plugins.installProgress.initializing',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: InstallStage.LAUNCHING,
|
||||||
|
icon: Rocket,
|
||||||
|
i18nKey: 'plugins.installProgress.launching',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -88,18 +88,29 @@ function StageRow({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-300',
|
'flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-300',
|
||||||
isActive && !isError && 'bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-800',
|
isActive &&
|
||||||
isCompleted && 'bg-green-50/50 dark:bg-green-950/15 border border-green-100 dark:border-green-900/50',
|
!isError &&
|
||||||
isError && isActive && 'bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900',
|
'bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-800',
|
||||||
|
isCompleted &&
|
||||||
|
'bg-green-50/50 dark:bg-green-950/15 border border-green-100 dark:border-green-900/50',
|
||||||
|
isError &&
|
||||||
|
isActive &&
|
||||||
|
'bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Left: status indicator */}
|
{/* Left: status indicator */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-center w-7 h-7 rounded-full shrink-0',
|
'flex items-center justify-center w-7 h-7 rounded-full shrink-0',
|
||||||
isCompleted && 'bg-green-100 dark:bg-green-900/40 text-green-600 dark:text-green-400',
|
isCompleted &&
|
||||||
isActive && !isError && !isCompleted && 'bg-blue-100 dark:bg-blue-900/40 text-blue-600 dark:text-blue-400',
|
'bg-green-100 dark:bg-green-900/40 text-green-600 dark:text-green-400',
|
||||||
isError && isActive && 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400',
|
isActive &&
|
||||||
|
!isError &&
|
||||||
|
!isCompleted &&
|
||||||
|
'bg-blue-100 dark:bg-blue-900/40 text-blue-600 dark:text-blue-400',
|
||||||
|
isError &&
|
||||||
|
isActive &&
|
||||||
|
'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isCompleted ? (
|
{isCompleted ? (
|
||||||
@@ -119,7 +130,10 @@ function StageRow({
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-sm font-medium',
|
'text-sm font-medium',
|
||||||
isActive && !isError && !isCompleted && 'text-blue-700 dark:text-blue-300',
|
isActive &&
|
||||||
|
!isError &&
|
||||||
|
!isCompleted &&
|
||||||
|
'text-blue-700 dark:text-blue-300',
|
||||||
isCompleted && 'text-green-600 dark:text-green-400',
|
isCompleted && 'text-green-600 dark:text-green-400',
|
||||||
isError && isActive && 'text-red-600 dark:text-red-400',
|
isError && isActive && 'text-red-600 dark:text-red-400',
|
||||||
)}
|
)}
|
||||||
@@ -130,17 +144,24 @@ function StageRow({
|
|||||||
<Icon
|
<Icon
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-3.5 h-3.5 shrink-0',
|
'w-3.5 h-3.5 shrink-0',
|
||||||
isActive && !isError && !isCompleted && 'text-blue-400 dark:text-blue-500',
|
isActive &&
|
||||||
|
!isError &&
|
||||||
|
!isCompleted &&
|
||||||
|
'text-blue-400 dark:text-blue-500',
|
||||||
isCompleted && 'text-green-400 dark:text-green-500',
|
isCompleted && 'text-green-400 dark:text-green-500',
|
||||||
isError && isActive && 'text-red-400 dark:text-red-500',
|
isError && isActive && 'text-red-400 dark:text-red-500',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{detail && (
|
{detail && (
|
||||||
<div className={cn(
|
<div
|
||||||
'text-xs mt-0.5',
|
className={cn(
|
||||||
isCompleted ? 'text-green-600/70 dark:text-green-400/70' : 'text-blue-600/70 dark:text-blue-400/70',
|
'text-xs mt-0.5',
|
||||||
)}>
|
isCompleted
|
||||||
|
? 'text-green-600/70 dark:text-green-400/70'
|
||||||
|
: 'text-blue-600/70 dark:text-blue-400/70',
|
||||||
|
)}
|
||||||
|
>
|
||||||
{detail}
|
{detail}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -259,20 +280,28 @@ function TaskProgressContent({ task }: { task: PluginInstallTask }) {
|
|||||||
{/* Overall progress bar — always blue */}
|
{/* Overall progress bar — always blue */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className={cn(
|
<span
|
||||||
'text-sm font-medium',
|
className={cn(
|
||||||
isDone ? 'text-green-700 dark:text-green-300' : 'text-blue-700 dark:text-blue-300',
|
'text-sm font-medium',
|
||||||
)}>
|
isDone
|
||||||
|
? 'text-green-700 dark:text-green-300'
|
||||||
|
: 'text-blue-700 dark:text-blue-300',
|
||||||
|
)}
|
||||||
|
>
|
||||||
{isDone
|
{isDone
|
||||||
? t('plugins.installProgress.completed')
|
? t('plugins.installProgress.completed')
|
||||||
: isError
|
: isError
|
||||||
? t('plugins.installProgress.failed')
|
? t('plugins.installProgress.failed')
|
||||||
: t('plugins.installProgress.overallProgress')}
|
: t('plugins.installProgress.overallProgress')}
|
||||||
</span>
|
</span>
|
||||||
<span className={cn(
|
<span
|
||||||
'text-sm font-medium',
|
className={cn(
|
||||||
isDone ? 'text-green-600 dark:text-green-400' : 'text-blue-600 dark:text-blue-400',
|
'text-sm font-medium',
|
||||||
)}>
|
isDone
|
||||||
|
? 'text-green-600 dark:text-green-400'
|
||||||
|
: 'text-blue-600 dark:text-blue-400',
|
||||||
|
)}
|
||||||
|
>
|
||||||
{isDone ? '100%' : `${task.overallProgress}%`}
|
{isDone ? '100%' : `${task.overallProgress}%`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -282,52 +311,52 @@ function TaskProgressContent({ task }: { task: PluginInstallTask }) {
|
|||||||
'h-2.5',
|
'h-2.5',
|
||||||
'[&>div]:bg-blue-500 dark:[&>div]:bg-blue-400',
|
'[&>div]:bg-blue-500 dark:[&>div]:bg-blue-400',
|
||||||
'bg-blue-100 dark:bg-blue-900/30',
|
'bg-blue-100 dark:bg-blue-900/30',
|
||||||
isDone && '[&>div]:bg-green-500 dark:[&>div]:bg-green-400 bg-green-100 dark:bg-green-900/30',
|
isDone &&
|
||||||
isError && '[&>div]:bg-red-500 dark:[&>div]:bg-red-400 bg-red-100 dark:bg-red-900/30',
|
'[&>div]:bg-green-500 dark:[&>div]:bg-green-400 bg-green-100 dark:bg-green-900/30',
|
||||||
|
isError &&
|
||||||
|
'[&>div]:bg-red-500 dark:[&>div]:bg-red-400 bg-red-100 dark:bg-red-900/30',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stage display */}
|
{/* Stage display */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
{isDone ? (
|
{isDone
|
||||||
/* When done: show all stages with completed style */
|
? /* When done: show all stages with completed style */
|
||||||
STAGES.map((stageConfig) => (
|
STAGES.map((stageConfig) => (
|
||||||
<StageRow
|
<StageRow
|
||||||
key={stageConfig.key}
|
key={stageConfig.key}
|
||||||
icon={stageConfig.icon}
|
icon={stageConfig.icon}
|
||||||
label={t(stageConfig.i18nKey)}
|
label={t(stageConfig.i18nKey)}
|
||||||
isActive={false}
|
isActive={false}
|
||||||
isCompleted={true}
|
isCompleted={true}
|
||||||
isError={false}
|
isError={false}
|
||||||
detail={getStageDetail(stageConfig.key, true)}
|
detail={getStageDetail(stageConfig.key, true)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : isError ? (
|
: isError
|
||||||
/* Error: show the failed stage */
|
? /* Error: show the failed stage */
|
||||||
currentStageIndex >= 0 && (
|
currentStageIndex >= 0 && (
|
||||||
<StageRow
|
<StageRow
|
||||||
icon={STAGES[currentStageIndex].icon}
|
icon={STAGES[currentStageIndex].icon}
|
||||||
label={t(STAGES[currentStageIndex].i18nKey)}
|
label={t(STAGES[currentStageIndex].i18nKey)}
|
||||||
isActive={true}
|
isActive={true}
|
||||||
isCompleted={false}
|
isCompleted={false}
|
||||||
isError={true}
|
isError={true}
|
||||||
detail={task.error}
|
detail={task.error}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : (
|
: /* In progress: only show the current active stage */
|
||||||
/* In progress: only show the current active stage */
|
currentStageIndex >= 0 && (
|
||||||
currentStageIndex >= 0 && (
|
<StageRow
|
||||||
<StageRow
|
icon={STAGES[currentStageIndex].icon}
|
||||||
icon={STAGES[currentStageIndex].icon}
|
label={t(STAGES[currentStageIndex].i18nKey)}
|
||||||
label={t(STAGES[currentStageIndex].i18nKey)}
|
isActive={true}
|
||||||
isActive={true}
|
isCompleted={false}
|
||||||
isCompleted={false}
|
isError={false}
|
||||||
isError={false}
|
detail={getStageDetail(STAGES[currentStageIndex].key, false)}
|
||||||
detail={getStageDetail(STAGES[currentStageIndex].key, false)}
|
/>
|
||||||
/>
|
)}
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Done banner */}
|
{/* Done banner */}
|
||||||
|
|||||||
+224
-25
@@ -16,9 +16,9 @@ import { AsyncTask } from '@/app/infra/entities/api';
|
|||||||
*/
|
*/
|
||||||
export enum InstallStage {
|
export enum InstallStage {
|
||||||
DOWNLOADING = 'downloading',
|
DOWNLOADING = 'downloading',
|
||||||
EXTRACTING = 'extracting',
|
|
||||||
INSTALLING_DEPS = 'installing_deps',
|
INSTALLING_DEPS = 'installing_deps',
|
||||||
TESTING = 'testing',
|
INITIALIZING = 'initializing',
|
||||||
|
LAUNCHING = 'launching',
|
||||||
DONE = 'done',
|
DONE = 'done',
|
||||||
ERROR = 'error',
|
ERROR = 'error',
|
||||||
}
|
}
|
||||||
@@ -47,6 +47,8 @@ export interface PluginInstallTask {
|
|||||||
currentAction: string; // raw backend action string
|
currentAction: string; // raw backend action string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OnTaskCompleteCallback = (taskId: number, success: boolean) => void;
|
||||||
|
|
||||||
interface PluginInstallTaskContextValue {
|
interface PluginInstallTaskContextValue {
|
||||||
tasks: PluginInstallTask[];
|
tasks: PluginInstallTask[];
|
||||||
addTask: (params: {
|
addTask: (params: {
|
||||||
@@ -59,6 +61,9 @@ interface PluginInstallTaskContextValue {
|
|||||||
clearCompletedTasks: () => void;
|
clearCompletedTasks: () => void;
|
||||||
selectedTaskId: string | null;
|
selectedTaskId: string | null;
|
||||||
setSelectedTaskId: (id: string | null) => void;
|
setSelectedTaskId: (id: string | null) => void;
|
||||||
|
/** Register a callback for when a task completes (for toast/refresh). Cleared on unmount. */
|
||||||
|
registerOnTaskComplete: (cb: OnTaskCompleteCallback) => void;
|
||||||
|
unregisterOnTaskComplete: (cb: OnTaskCompleteCallback) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PluginInstallTaskContext =
|
const PluginInstallTaskContext =
|
||||||
@@ -81,16 +86,11 @@ function mapActionToStage(action: string): InstallStage {
|
|||||||
if (!action) return InstallStage.DOWNLOADING;
|
if (!action) return InstallStage.DOWNLOADING;
|
||||||
const lower = action.toLowerCase();
|
const lower = action.toLowerCase();
|
||||||
if (lower.includes('download')) return InstallStage.DOWNLOADING;
|
if (lower.includes('download')) return InstallStage.DOWNLOADING;
|
||||||
if (lower.includes('extract') || lower.includes('unzip'))
|
|
||||||
return InstallStage.EXTRACTING;
|
|
||||||
if (lower.includes('dependencies') || lower.includes('requirements'))
|
if (lower.includes('dependencies') || lower.includes('requirements'))
|
||||||
return InstallStage.INSTALLING_DEPS;
|
return InstallStage.INSTALLING_DEPS;
|
||||||
if (
|
if (lower.includes('initializ') || lower.includes('setting'))
|
||||||
lower.includes('initializ') ||
|
return InstallStage.INITIALIZING;
|
||||||
lower.includes('launch') ||
|
if (lower.includes('launch')) return InstallStage.LAUNCHING;
|
||||||
lower.includes('test')
|
|
||||||
)
|
|
||||||
return InstallStage.TESTING;
|
|
||||||
if (lower.includes('installed') || lower.includes('complete'))
|
if (lower.includes('installed') || lower.includes('complete'))
|
||||||
return InstallStage.DONE;
|
return InstallStage.DONE;
|
||||||
return InstallStage.DOWNLOADING;
|
return InstallStage.DOWNLOADING;
|
||||||
@@ -103,12 +103,12 @@ function stageToProgress(stage: InstallStage): number {
|
|||||||
switch (stage) {
|
switch (stage) {
|
||||||
case InstallStage.DOWNLOADING:
|
case InstallStage.DOWNLOADING:
|
||||||
return 10;
|
return 10;
|
||||||
case InstallStage.EXTRACTING:
|
|
||||||
return 35;
|
|
||||||
case InstallStage.INSTALLING_DEPS:
|
case InstallStage.INSTALLING_DEPS:
|
||||||
return 55;
|
return 40;
|
||||||
case InstallStage.TESTING:
|
case InstallStage.INITIALIZING:
|
||||||
return 80;
|
return 70;
|
||||||
|
case InstallStage.LAUNCHING:
|
||||||
|
return 85;
|
||||||
case InstallStage.DONE:
|
case InstallStage.DONE:
|
||||||
return 100;
|
return 100;
|
||||||
case InstallStage.ERROR:
|
case InstallStage.ERROR:
|
||||||
@@ -118,6 +118,79 @@ function stageToProgress(stage: InstallStage): number {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract install source from backend task name.
|
||||||
|
*/
|
||||||
|
function extractSourceFromName(
|
||||||
|
name: string,
|
||||||
|
): 'github' | 'marketplace' | 'local' {
|
||||||
|
if (name.includes('github')) return 'github';
|
||||||
|
if (name.includes('marketplace')) return 'marketplace';
|
||||||
|
return 'local';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a backend task name is a plugin install task.
|
||||||
|
*/
|
||||||
|
function isPluginInstallTask(name: string): boolean {
|
||||||
|
return name.startsWith('plugin-install-');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a backend AsyncTask to our PluginInstallTask.
|
||||||
|
*/
|
||||||
|
function asyncTaskToPluginInstallTask(task: AsyncTask): PluginInstallTask {
|
||||||
|
const source = extractSourceFromName(task.name);
|
||||||
|
const md = (task.task_context?.metadata ?? {}) as Record<string, unknown>;
|
||||||
|
const action = task.task_context?.current_action || '';
|
||||||
|
const done = task.runtime.done;
|
||||||
|
const exception = task.runtime.exception;
|
||||||
|
|
||||||
|
const num = (v: unknown) => (typeof v === 'number' ? v : undefined);
|
||||||
|
const str = (v: unknown) => (typeof v === 'string' ? v : undefined);
|
||||||
|
|
||||||
|
let stage: InstallStage;
|
||||||
|
let overallProgress: number;
|
||||||
|
let error: string | undefined;
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
if (exception) {
|
||||||
|
stage = InstallStage.ERROR;
|
||||||
|
overallProgress = 0;
|
||||||
|
error = exception;
|
||||||
|
} else {
|
||||||
|
stage = InstallStage.DONE;
|
||||||
|
overallProgress = 100;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stage = mapActionToStage(action);
|
||||||
|
overallProgress = Math.min(95, stageToProgress(stage));
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginName = str(md.plugin_name) || task.label || `${source} plugin`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `${source}-${task.id}`,
|
||||||
|
taskId: task.id,
|
||||||
|
pluginName,
|
||||||
|
source,
|
||||||
|
stage,
|
||||||
|
overallProgress,
|
||||||
|
downloadCurrent: num(md.download_current),
|
||||||
|
downloadTotal: num(md.download_total),
|
||||||
|
downloadSpeed: num(md.download_speed),
|
||||||
|
depsTotal: num(md.deps_total),
|
||||||
|
depsInstalled: num(md.deps_installed),
|
||||||
|
depsRemaining: num(md.deps_remaining),
|
||||||
|
currentDep: str(md.current_dep),
|
||||||
|
depsDownloadedSize: num(md.deps_downloaded_size),
|
||||||
|
depsSpeed: num(md.deps_speed),
|
||||||
|
error,
|
||||||
|
startedAt: Date.now(),
|
||||||
|
currentAction: action,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function PluginInstallTaskProvider({
|
export function PluginInstallTaskProvider({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
@@ -126,16 +199,46 @@ export function PluginInstallTaskProvider({
|
|||||||
const [tasks, setTasks] = useState<PluginInstallTask[]>([]);
|
const [tasks, setTasks] = useState<PluginInstallTask[]>([]);
|
||||||
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
|
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
|
||||||
const intervalRefs = useRef<Map<string, NodeJS.Timeout>>(new Map());
|
const intervalRefs = useRef<Map<string, NodeJS.Timeout>>(new Map());
|
||||||
|
const syncIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const onTaskCompleteCallbacks = useRef<Set<OnTaskCompleteCallback>>(
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
// Track tasks that have already been marked as completed/failed (to avoid duplicate callbacks)
|
||||||
|
const notifiedTaskIds = useRef<Set<number>>(new Set());
|
||||||
|
// Track task IDs that the user has explicitly dismissed
|
||||||
|
const dismissedTaskIds = useRef<Set<number>>(new Set());
|
||||||
|
|
||||||
// Cleanup all intervals on unmount
|
// Cleanup all intervals on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
intervalRefs.current.forEach((interval) => clearInterval(interval));
|
intervalRefs.current.forEach((interval) => {
|
||||||
|
clearInterval(interval);
|
||||||
|
});
|
||||||
|
if (syncIntervalRef.current) clearInterval(syncIntervalRef.current);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const registerOnTaskComplete = useCallback((cb: OnTaskCompleteCallback) => {
|
||||||
|
onTaskCompleteCallbacks.current.add(cb);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const unregisterOnTaskComplete = useCallback((cb: OnTaskCompleteCallback) => {
|
||||||
|
onTaskCompleteCallbacks.current.delete(cb);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const notifyTaskComplete = useCallback((taskId: number, success: boolean) => {
|
||||||
|
if (notifiedTaskIds.current.has(taskId)) return;
|
||||||
|
notifiedTaskIds.current.add(taskId);
|
||||||
|
onTaskCompleteCallbacks.current.forEach((cb) => {
|
||||||
|
cb(taskId, success);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const pollTask = useCallback(
|
const pollTask = useCallback(
|
||||||
(taskKey: string, taskId: number) => {
|
(taskKey: string, taskId: number) => {
|
||||||
|
// Don't start duplicate polling for the same task
|
||||||
|
if (intervalRefs.current.has(taskKey)) return;
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
httpClient
|
httpClient
|
||||||
.getAsyncTask(taskId)
|
.getAsyncTask(taskId)
|
||||||
@@ -143,7 +246,10 @@ export function PluginInstallTaskProvider({
|
|||||||
const action = res.task_context?.current_action || '';
|
const action = res.task_context?.current_action || '';
|
||||||
const done = res.runtime.done;
|
const done = res.runtime.done;
|
||||||
const exception = res.runtime.exception;
|
const exception = res.runtime.exception;
|
||||||
const md = (res.task_context?.metadata ?? {}) as Record<string, unknown>;
|
const md = (res.task_context?.metadata ?? {}) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
|
||||||
// Extract progress fields from metadata
|
// Extract progress fields from metadata
|
||||||
const num = (v: unknown) => (typeof v === 'number' ? v : undefined);
|
const num = (v: unknown) => (typeof v === 'number' ? v : undefined);
|
||||||
@@ -171,7 +277,8 @@ export function PluginInstallTaskProvider({
|
|||||||
depsInstalled: depsInstalled ?? t.depsInstalled,
|
depsInstalled: depsInstalled ?? t.depsInstalled,
|
||||||
depsRemaining: depsRemaining ?? t.depsRemaining,
|
depsRemaining: depsRemaining ?? t.depsRemaining,
|
||||||
currentDep: currentDep ?? t.currentDep,
|
currentDep: currentDep ?? t.currentDep,
|
||||||
depsDownloadedSize: depsDownloadedSize ?? t.depsDownloadedSize,
|
depsDownloadedSize:
|
||||||
|
depsDownloadedSize ?? t.depsDownloadedSize,
|
||||||
depsSpeed: depsSpeed ?? t.depsSpeed,
|
depsSpeed: depsSpeed ?? t.depsSpeed,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -184,6 +291,7 @@ export function PluginInstallTaskProvider({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (exception) {
|
if (exception) {
|
||||||
|
notifyTaskComplete(taskId, false);
|
||||||
return {
|
return {
|
||||||
...t,
|
...t,
|
||||||
stage: InstallStage.ERROR,
|
stage: InstallStage.ERROR,
|
||||||
@@ -194,6 +302,7 @@ export function PluginInstallTaskProvider({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifyTaskComplete(taskId, true);
|
||||||
return {
|
return {
|
||||||
...t,
|
...t,
|
||||||
stage: InstallStage.DONE,
|
stage: InstallStage.DONE,
|
||||||
@@ -233,9 +342,76 @@ export function PluginInstallTaskProvider({
|
|||||||
|
|
||||||
intervalRefs.current.set(taskKey, interval);
|
intervalRefs.current.set(taskKey, interval);
|
||||||
},
|
},
|
||||||
[],
|
[notifyTaskComplete],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all plugin-operation tasks from backend and sync state.
|
||||||
|
* This is called on mount and periodically to recover tasks after refresh.
|
||||||
|
*/
|
||||||
|
const syncTasksFromBackend = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const resp = await httpClient.getAsyncTasks({ kind: 'plugin-operation' });
|
||||||
|
const backendTasks = (resp.tasks || []).filter((t: AsyncTask) =>
|
||||||
|
isPluginInstallTask(t.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
setTasks((prevTasks) => {
|
||||||
|
const existingTaskIds = new Set(prevTasks.map((t) => t.taskId));
|
||||||
|
const updatedTasks = [...prevTasks];
|
||||||
|
|
||||||
|
for (const bt of backendTasks) {
|
||||||
|
// Skip tasks that the user has dismissed
|
||||||
|
if (dismissedTaskIds.current.has(bt.id)) continue;
|
||||||
|
|
||||||
|
if (!existingTaskIds.has(bt.id)) {
|
||||||
|
// New task from backend (e.g. after page refresh) — add it
|
||||||
|
const newTask = asyncTaskToPluginInstallTask(bt);
|
||||||
|
updatedTasks.push(newTask);
|
||||||
|
|
||||||
|
// If not done, start polling for progress
|
||||||
|
if (!bt.runtime.done) {
|
||||||
|
pollTask(newTask.id, bt.id);
|
||||||
|
} else {
|
||||||
|
// Mark as already notified so we don't re-trigger toasts for old completed tasks
|
||||||
|
notifiedTaskIds.current.add(bt.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Already tracking — if it's done in backend but still active locally, update it
|
||||||
|
const idx = updatedTasks.findIndex((t) => t.taskId === bt.id);
|
||||||
|
if (idx !== -1) {
|
||||||
|
const existing = updatedTasks[idx];
|
||||||
|
if (
|
||||||
|
bt.runtime.done &&
|
||||||
|
existing.stage !== InstallStage.DONE &&
|
||||||
|
existing.stage !== InstallStage.ERROR
|
||||||
|
) {
|
||||||
|
const converted = asyncTaskToPluginInstallTask(bt);
|
||||||
|
converted.startedAt = existing.startedAt;
|
||||||
|
converted.pluginName = existing.pluginName;
|
||||||
|
converted.fileSize = existing.fileSize;
|
||||||
|
updatedTasks[idx] = converted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedTasks;
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Silently ignore sync errors
|
||||||
|
}
|
||||||
|
}, [pollTask]);
|
||||||
|
|
||||||
|
// Initial sync on mount + periodic sync every 3s
|
||||||
|
useEffect(() => {
|
||||||
|
syncTasksFromBackend();
|
||||||
|
syncIntervalRef.current = setInterval(syncTasksFromBackend, 3000);
|
||||||
|
return () => {
|
||||||
|
if (syncIntervalRef.current) clearInterval(syncIntervalRef.current);
|
||||||
|
};
|
||||||
|
}, [syncTasksFromBackend]);
|
||||||
|
|
||||||
const addTask = useCallback(
|
const addTask = useCallback(
|
||||||
(params: {
|
(params: {
|
||||||
taskId: number;
|
taskId: number;
|
||||||
@@ -244,6 +420,10 @@ export function PluginInstallTaskProvider({
|
|||||||
fileSize?: number;
|
fileSize?: number;
|
||||||
}) => {
|
}) => {
|
||||||
const taskKey = `${params.source}-${params.taskId}`;
|
const taskKey = `${params.source}-${params.taskId}`;
|
||||||
|
|
||||||
|
// Remove from dismissed set if re-added
|
||||||
|
dismissedTaskIds.current.delete(params.taskId);
|
||||||
|
|
||||||
const newTask: PluginInstallTask = {
|
const newTask: PluginInstallTask = {
|
||||||
id: taskKey,
|
id: taskKey,
|
||||||
taskId: params.taskId,
|
taskId: params.taskId,
|
||||||
@@ -256,7 +436,11 @@ export function PluginInstallTaskProvider({
|
|||||||
currentAction: '',
|
currentAction: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
setTasks((prev) => [...prev, newTask]);
|
setTasks((prev) => {
|
||||||
|
// Avoid duplicate
|
||||||
|
if (prev.some((t) => t.taskId === params.taskId)) return prev;
|
||||||
|
return [...prev, newTask];
|
||||||
|
});
|
||||||
pollTask(taskKey, params.taskId);
|
pollTask(taskKey, params.taskId);
|
||||||
},
|
},
|
||||||
[pollTask],
|
[pollTask],
|
||||||
@@ -268,15 +452,28 @@ export function PluginInstallTaskProvider({
|
|||||||
clearInterval(iv);
|
clearInterval(iv);
|
||||||
intervalRefs.current.delete(id);
|
intervalRefs.current.delete(id);
|
||||||
}
|
}
|
||||||
setTasks((prev) => prev.filter((t) => t.id !== id));
|
|
||||||
|
setTasks((prev) => {
|
||||||
|
const task = prev.find((t) => t.id === id);
|
||||||
|
if (task) {
|
||||||
|
dismissedTaskIds.current.add(task.taskId);
|
||||||
|
}
|
||||||
|
return prev.filter((t) => t.id !== id);
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const clearCompletedTasks = useCallback(() => {
|
const clearCompletedTasks = useCallback(() => {
|
||||||
setTasks((prev) =>
|
setTasks((prev) => {
|
||||||
prev.filter(
|
const completed = prev.filter(
|
||||||
|
(t) => t.stage === InstallStage.DONE || t.stage === InstallStage.ERROR,
|
||||||
|
);
|
||||||
|
completed.forEach((t) => {
|
||||||
|
dismissedTaskIds.current.add(t.taskId);
|
||||||
|
});
|
||||||
|
return prev.filter(
|
||||||
(t) => t.stage !== InstallStage.DONE && t.stage !== InstallStage.ERROR,
|
(t) => t.stage !== InstallStage.DONE && t.stage !== InstallStage.ERROR,
|
||||||
),
|
);
|
||||||
);
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -288,6 +485,8 @@ export function PluginInstallTaskProvider({
|
|||||||
clearCompletedTasks,
|
clearCompletedTasks,
|
||||||
selectedTaskId,
|
selectedTaskId,
|
||||||
setSelectedTaskId,
|
setSelectedTaskId,
|
||||||
|
registerOnTaskComplete,
|
||||||
|
unregisterOnTaskComplete,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
+15
-21
@@ -5,9 +5,9 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
import {
|
import {
|
||||||
Download,
|
Download,
|
||||||
FolderOpen,
|
|
||||||
Package,
|
Package,
|
||||||
FlaskConical,
|
Settings,
|
||||||
|
Rocket,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
XCircle,
|
XCircle,
|
||||||
Loader2,
|
Loader2,
|
||||||
@@ -30,9 +30,9 @@ import { cn } from '@/lib/utils';
|
|||||||
|
|
||||||
const STAGE_ICONS: Record<string, React.ElementType> = {
|
const STAGE_ICONS: Record<string, React.ElementType> = {
|
||||||
[InstallStage.DOWNLOADING]: Download,
|
[InstallStage.DOWNLOADING]: Download,
|
||||||
[InstallStage.EXTRACTING]: FolderOpen,
|
|
||||||
[InstallStage.INSTALLING_DEPS]: Package,
|
[InstallStage.INSTALLING_DEPS]: Package,
|
||||||
[InstallStage.TESTING]: FlaskConical,
|
[InstallStage.INITIALIZING]: Settings,
|
||||||
|
[InstallStage.LAUNCHING]: Rocket,
|
||||||
[InstallStage.DONE]: CheckCircle2,
|
[InstallStage.DONE]: CheckCircle2,
|
||||||
[InstallStage.ERROR]: XCircle,
|
[InstallStage.ERROR]: XCircle,
|
||||||
};
|
};
|
||||||
@@ -56,12 +56,12 @@ function TaskQueueItem({
|
|||||||
switch (task.stage) {
|
switch (task.stage) {
|
||||||
case InstallStage.DOWNLOADING:
|
case InstallStage.DOWNLOADING:
|
||||||
return t('plugins.installProgress.downloading');
|
return t('plugins.installProgress.downloading');
|
||||||
case InstallStage.EXTRACTING:
|
|
||||||
return t('plugins.installProgress.extracting');
|
|
||||||
case InstallStage.INSTALLING_DEPS:
|
case InstallStage.INSTALLING_DEPS:
|
||||||
return t('plugins.installProgress.installingDeps');
|
return t('plugins.installProgress.installingDeps');
|
||||||
case InstallStage.TESTING:
|
case InstallStage.INITIALIZING:
|
||||||
return t('plugins.installProgress.testing');
|
return t('plugins.installProgress.initializing');
|
||||||
|
case InstallStage.LAUNCHING:
|
||||||
|
return t('plugins.installProgress.launching');
|
||||||
case InstallStage.DONE:
|
case InstallStage.DONE:
|
||||||
return t('plugins.installProgress.completed');
|
return t('plugins.installProgress.completed');
|
||||||
case InstallStage.ERROR:
|
case InstallStage.ERROR:
|
||||||
@@ -79,9 +79,12 @@ function TaskQueueItem({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-center w-7 h-7 rounded-full shrink-0',
|
'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',
|
isDone &&
|
||||||
isError && 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400',
|
'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400',
|
||||||
isRunning && 'bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-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 ? (
|
{isRunning ? (
|
||||||
@@ -138,10 +141,7 @@ export default function PluginInstallTaskQueue() {
|
|||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button variant="outline" className="relative px-4 py-5 cursor-pointer">
|
||||||
variant="outline"
|
|
||||||
className="relative px-4 py-5 cursor-pointer"
|
|
||||||
>
|
|
||||||
<ListTodo className="w-4 h-4 mr-2" />
|
<ListTodo className="w-4 h-4 mr-2" />
|
||||||
{t('plugins.installProgress.taskQueue')}
|
{t('plugins.installProgress.taskQueue')}
|
||||||
{runningCount > 0 && (
|
{runningCount > 0 && (
|
||||||
@@ -152,12 +152,6 @@ export default function PluginInstallTaskQueue() {
|
|||||||
{runningCount}
|
{runningCount}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{runningCount > 0 && (
|
|
||||||
<span className="absolute top-1 right-1 flex h-2.5 w-2.5">
|
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
|
|
||||||
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-primary" />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-[340px] p-2" align="end">
|
<PopoverContent className="w-[340px] p-2" align="end">
|
||||||
|
|||||||
@@ -103,7 +103,12 @@ function PluginListView() {
|
|||||||
pendingPluginInstallAction,
|
pendingPluginInstallAction,
|
||||||
setPendingPluginInstallAction,
|
setPendingPluginInstallAction,
|
||||||
} = useSidebarData();
|
} = useSidebarData();
|
||||||
const { addTask, setSelectedTaskId } = usePluginInstallTasks();
|
const {
|
||||||
|
addTask,
|
||||||
|
setSelectedTaskId,
|
||||||
|
registerOnTaskComplete,
|
||||||
|
unregisterOnTaskComplete,
|
||||||
|
} = usePluginInstallTasks();
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [installSource, setInstallSource] = useState<string>('local');
|
const [installSource, setInstallSource] = useState<string>('local');
|
||||||
const [installInfo] = useState<Record<string, any>>({}); // eslint-disable-line @typescript-eslint/no-explicit-any
|
const [installInfo] = useState<Record<string, any>>({}); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
@@ -160,27 +165,20 @@ function PluginListView() {
|
|||||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
function watchTask(taskId: number) {
|
// Register task completion callback for toast and plugin list refresh
|
||||||
let alreadySuccess = false;
|
useEffect(() => {
|
||||||
|
const onComplete = (_taskId: number, success: boolean) => {
|
||||||
const interval = setInterval(() => {
|
if (success) {
|
||||||
httpClient.getAsyncTask(taskId).then((resp) => {
|
toast.success(t('plugins.installSuccess'));
|
||||||
if (resp.runtime.done) {
|
pluginInstalledRef.current?.refreshPluginList();
|
||||||
clearInterval(interval);
|
refreshPlugins();
|
||||||
if (resp.runtime.exception) {
|
}
|
||||||
// Error is shown via task queue progress dialog
|
};
|
||||||
} else {
|
registerOnTaskComplete(onComplete);
|
||||||
if (!alreadySuccess) {
|
return () => {
|
||||||
toast.success(t('plugins.installSuccess'));
|
unregisterOnTaskComplete(onComplete);
|
||||||
alreadySuccess = true;
|
};
|
||||||
}
|
}, [registerOnTaskComplete, unregisterOnTaskComplete, refreshPlugins, t]);
|
||||||
pluginInstalledRef.current?.refreshPluginList();
|
|
||||||
refreshPlugins();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pluginInstalledRef = useRef<PluginInstalledComponentRef>(null);
|
const pluginInstalledRef = useRef<PluginInstalledComponentRef>(null);
|
||||||
|
|
||||||
@@ -323,7 +321,6 @@ function PluginListView() {
|
|||||||
setSelectedTaskId(taskKey);
|
setSelectedTaskId(taskKey);
|
||||||
resetGithubState();
|
resetGithubState();
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
watchTask(taskId);
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setInstallError(err.msg);
|
setInstallError(err.msg);
|
||||||
@@ -345,7 +342,6 @@ function PluginListView() {
|
|||||||
});
|
});
|
||||||
setSelectedTaskId(taskKey);
|
setSelectedTaskId(taskKey);
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
watchTask(taskId);
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setInstallError(err.msg);
|
setInstallError(err.msg);
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ export interface AsyncTask {
|
|||||||
id: number;
|
id: number;
|
||||||
kind: string;
|
kind: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
label: string;
|
||||||
task_type: string; // system or user
|
task_type: string; // system or user
|
||||||
runtime: AsyncTaskRuntimeInfo;
|
runtime: AsyncTaskRuntimeInfo;
|
||||||
task_context: AsyncTaskTaskContext;
|
task_context: AsyncTaskTaskContext;
|
||||||
|
|||||||
@@ -715,8 +715,15 @@ export class BackendClient extends BaseHttpClient {
|
|||||||
return this.put('/api/v1/system/wizard/progress', progress);
|
return this.put('/api/v1/system/wizard/progress', progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAsyncTasks(): Promise<ApiRespAsyncTasks> {
|
public getAsyncTasks(params?: {
|
||||||
return this.get('/api/v1/system/tasks');
|
type?: string;
|
||||||
|
kind?: string;
|
||||||
|
}): Promise<ApiRespAsyncTasks> {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
if (params?.type) query.set('type', params.type);
|
||||||
|
if (params?.kind) query.set('kind', params.kind);
|
||||||
|
const qs = query.toString();
|
||||||
|
return this.get(`/api/v1/system/tasks${qs ? `?${qs}` : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAsyncTask(id: number): Promise<AsyncTask> {
|
public getAsyncTask(id: number): Promise<AsyncTask> {
|
||||||
|
|||||||
@@ -498,14 +498,15 @@ const enUS = {
|
|||||||
titleGeneric: 'Plugin Installation',
|
titleGeneric: 'Plugin Installation',
|
||||||
overallProgress: 'Overall Progress',
|
overallProgress: 'Overall Progress',
|
||||||
downloading: 'Downloading Plugin',
|
downloading: 'Downloading Plugin',
|
||||||
extracting: 'Extracting Plugin',
|
|
||||||
installingDeps: 'Installing Dependencies',
|
installingDeps: 'Installing Dependencies',
|
||||||
testing: 'Testing Plugin',
|
initializing: 'Initializing Settings',
|
||||||
|
launching: 'Launching Plugin',
|
||||||
completed: 'Completed',
|
completed: 'Completed',
|
||||||
failed: 'Failed',
|
failed: 'Failed',
|
||||||
downloadSize: 'Package size: {{size}}',
|
downloadSize: 'Package size: {{size}}',
|
||||||
depsInfo: '{{count}} dependencies to install',
|
depsInfo: '{{count}} dependencies to install',
|
||||||
depsProgress: '{{installed}}/{{total}} installed · {{remaining}} remaining',
|
depsProgress:
|
||||||
|
'{{installed}}/{{total}} installed · {{remaining}} remaining',
|
||||||
installComplete: 'Plugin installed successfully',
|
installComplete: 'Plugin installed successfully',
|
||||||
dismiss: 'Dismiss',
|
dismiss: 'Dismiss',
|
||||||
background: 'Run in Background',
|
background: 'Run in Background',
|
||||||
|
|||||||
@@ -508,14 +508,15 @@ const esES = {
|
|||||||
titleGeneric: 'Instalación de Plugin',
|
titleGeneric: 'Instalación de Plugin',
|
||||||
overallProgress: 'Progreso general',
|
overallProgress: 'Progreso general',
|
||||||
downloading: 'Descargando Plugin',
|
downloading: 'Descargando Plugin',
|
||||||
extracting: 'Extrayendo Plugin',
|
|
||||||
installingDeps: 'Instalando dependencias',
|
installingDeps: 'Instalando dependencias',
|
||||||
testing: 'Probando Plugin',
|
initializing: 'Inicializando configuración',
|
||||||
|
launching: 'Iniciando Plugin',
|
||||||
completed: 'Completado',
|
completed: 'Completado',
|
||||||
failed: 'Fallido',
|
failed: 'Fallido',
|
||||||
downloadSize: 'Tamaño del paquete: {{size}}',
|
downloadSize: 'Tamaño del paquete: {{size}}',
|
||||||
depsInfo: '{{count}} dependencias por instalar',
|
depsInfo: '{{count}} dependencias por instalar',
|
||||||
depsProgress: '{{installed}}/{{total}} instaladas · {{remaining}} restantes',
|
depsProgress:
|
||||||
|
'{{installed}}/{{total}} instaladas · {{remaining}} restantes',
|
||||||
installComplete: 'Plugin instalado correctamente',
|
installComplete: 'Plugin instalado correctamente',
|
||||||
dismiss: 'Descartar',
|
dismiss: 'Descartar',
|
||||||
background: 'Ejecutar en segundo plano',
|
background: 'Ejecutar en segundo plano',
|
||||||
|
|||||||
@@ -498,14 +498,15 @@
|
|||||||
titleGeneric: 'プラグインのインストール',
|
titleGeneric: 'プラグインのインストール',
|
||||||
overallProgress: '全体の進捗',
|
overallProgress: '全体の進捗',
|
||||||
downloading: 'プラグインをダウンロード中',
|
downloading: 'プラグインをダウンロード中',
|
||||||
extracting: 'プラグインを展開中',
|
|
||||||
installingDeps: '依存関係をインストール中',
|
installingDeps: '依存関係をインストール中',
|
||||||
testing: 'プラグインをテスト中',
|
initializing: '設定を初期化中',
|
||||||
|
launching: 'プラグインを起動中',
|
||||||
completed: '完了',
|
completed: '完了',
|
||||||
failed: '失敗',
|
failed: '失敗',
|
||||||
downloadSize: 'パッケージサイズ: {{size}}',
|
downloadSize: 'パッケージサイズ: {{size}}',
|
||||||
depsInfo: '{{count}} 個の依存関係をインストール',
|
depsInfo: '{{count}} 個の依存関係をインストール',
|
||||||
depsProgress: '{{installed}}/{{total}} インストール済み · 残り {{remaining}} 個',
|
depsProgress:
|
||||||
|
'{{installed}}/{{total}} インストール済み · 残り {{remaining}} 個',
|
||||||
installComplete: 'プラグインのインストール完了',
|
installComplete: 'プラグインのインストール完了',
|
||||||
dismiss: '閉じる',
|
dismiss: '閉じる',
|
||||||
background: 'バックグラウンドで実行',
|
background: 'バックグラウンドで実行',
|
||||||
|
|||||||
@@ -490,9 +490,9 @@ const thTH = {
|
|||||||
titleGeneric: 'การติดตั้งปลั๊กอิน',
|
titleGeneric: 'การติดตั้งปลั๊กอิน',
|
||||||
overallProgress: 'ความคืบหน้าโดยรวม',
|
overallProgress: 'ความคืบหน้าโดยรวม',
|
||||||
downloading: 'กำลังดาวน์โหลดปลั๊กอิน',
|
downloading: 'กำลังดาวน์โหลดปลั๊กอิน',
|
||||||
extracting: 'กำลังแตกไฟล์ปลั๊กอิน',
|
|
||||||
installingDeps: 'กำลังติดตั้งแพ็กเกจ',
|
installingDeps: 'กำลังติดตั้งแพ็กเกจ',
|
||||||
testing: 'กำลังทดสอบปลั๊กอิน',
|
initializing: 'กำลังเริ่มต้นการตั้งค่า',
|
||||||
|
launching: 'กำลังเปิดใช้งานปลั๊กอิน',
|
||||||
completed: 'เสร็จสมบูรณ์',
|
completed: 'เสร็จสมบูรณ์',
|
||||||
failed: 'ล้มเหลว',
|
failed: 'ล้มเหลว',
|
||||||
downloadSize: 'ขนาดแพ็กเกจ: {{size}}',
|
downloadSize: 'ขนาดแพ็กเกจ: {{size}}',
|
||||||
|
|||||||
@@ -502,9 +502,9 @@ const viVN = {
|
|||||||
titleGeneric: 'Cài đặt Plugin',
|
titleGeneric: 'Cài đặt Plugin',
|
||||||
overallProgress: 'Tiến độ tổng thể',
|
overallProgress: 'Tiến độ tổng thể',
|
||||||
downloading: 'Đang tải Plugin',
|
downloading: 'Đang tải Plugin',
|
||||||
extracting: 'Đang giải nén Plugin',
|
|
||||||
installingDeps: 'Đang cài đặt phụ thuộc',
|
installingDeps: 'Đang cài đặt phụ thuộc',
|
||||||
testing: 'Đang kiểm tra Plugin',
|
initializing: 'Đang khởi tạo cài đặt',
|
||||||
|
launching: 'Đang khởi chạy Plugin',
|
||||||
completed: 'Hoàn thành',
|
completed: 'Hoàn thành',
|
||||||
failed: 'Thất bại',
|
failed: 'Thất bại',
|
||||||
downloadSize: 'Kích thước gói: {{size}}',
|
downloadSize: 'Kích thước gói: {{size}}',
|
||||||
|
|||||||
@@ -474,9 +474,9 @@ const zhHans = {
|
|||||||
titleGeneric: '插件安装',
|
titleGeneric: '插件安装',
|
||||||
overallProgress: '总体进度',
|
overallProgress: '总体进度',
|
||||||
downloading: '下载插件',
|
downloading: '下载插件',
|
||||||
extracting: '解压插件',
|
|
||||||
installingDeps: '安装依赖',
|
installingDeps: '安装依赖',
|
||||||
testing: '测试插件',
|
initializing: '初始化配置',
|
||||||
|
launching: '启动插件',
|
||||||
completed: '已完成',
|
completed: '已完成',
|
||||||
failed: '安装失败',
|
failed: '安装失败',
|
||||||
downloadSize: '包大小: {{size}}',
|
downloadSize: '包大小: {{size}}',
|
||||||
|
|||||||
@@ -467,9 +467,9 @@ const zhHant = {
|
|||||||
titleGeneric: '外掛安裝',
|
titleGeneric: '外掛安裝',
|
||||||
overallProgress: '整體進度',
|
overallProgress: '整體進度',
|
||||||
downloading: '下載外掛',
|
downloading: '下載外掛',
|
||||||
extracting: '解壓外掛',
|
|
||||||
installingDeps: '安裝依賴',
|
installingDeps: '安裝依賴',
|
||||||
testing: '測試外掛',
|
initializing: '初始化設定',
|
||||||
|
launching: '啟動外掛',
|
||||||
completed: '已完成',
|
completed: '已完成',
|
||||||
failed: '安裝失敗',
|
failed: '安裝失敗',
|
||||||
downloadSize: '檔案大小: {{size}}',
|
downloadSize: '檔案大小: {{size}}',
|
||||||
|
|||||||
Reference in New Issue
Block a user