mirror of https://github.com/langgenius/dify.git
refactor(plugin-tasks): consolidate plugin task components and improve state management
- Removed redundant components and consolidated logic for handling plugin tasks. - Updated the rendering logic to directly manage plugin states within the main component. - Enhanced the modal handling for clearing tasks and displaying statuses. - Improved the overall structure and readability of the code by eliminating unnecessary files and simplifying the component hierarchy.
This commit is contained in:
parent
4733a70913
commit
b58a482475
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -1,27 +1,34 @@
|
|||
import {
|
||||
RiCheckboxCircleFill,
|
||||
RiErrorWarningFill,
|
||||
RiInstallLine,
|
||||
RiLoaderLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import {
|
||||
ErrorPluginsSection,
|
||||
RunningPluginsSection,
|
||||
SuccessPluginsSection,
|
||||
} from './plugin-task-list'
|
||||
import PluginTaskTrigger from './plugin-task-trigger'
|
||||
import { usePluginTaskPanel } from './use-plugin-task-panel'
|
||||
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import DownloadingIcon from '@/app/components/header/plugins-nav/downloading-icon'
|
||||
import CardIcon from '@/app/components/plugins/card/base/card-icon'
|
||||
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { usePluginTaskStatus } from './hooks'
|
||||
|
||||
const PluginTasks = () => {
|
||||
const {
|
||||
open,
|
||||
setOpen,
|
||||
tip,
|
||||
taskStatus,
|
||||
handleClearAll,
|
||||
handleClearErrors,
|
||||
handleClearSingle,
|
||||
} = usePluginTaskPanel()
|
||||
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const [open, setOpen] = useState(false)
|
||||
const {
|
||||
errorPlugins,
|
||||
successPlugins,
|
||||
|
|
@ -35,15 +42,68 @@ const PluginTasks = () => {
|
|||
isInstallingWithError,
|
||||
isSuccess,
|
||||
isFailed,
|
||||
} = taskStatus
|
||||
handleClearErrorPlugin,
|
||||
} = usePluginTaskStatus()
|
||||
const { getIconUrl } = useGetIcon()
|
||||
|
||||
const handleClearAllWithModal = useCallback(async () => {
|
||||
// Clear all completed plugins (success and error) but keep running ones
|
||||
const completedPlugins = [...successPlugins, ...errorPlugins]
|
||||
|
||||
// Clear all completed plugins individually
|
||||
for (const plugin of completedPlugins)
|
||||
await handleClearErrorPlugin(plugin.taskId, plugin.plugin_unique_identifier)
|
||||
|
||||
// Only close modal if no plugins are still installing
|
||||
if (runningPluginsLength === 0)
|
||||
setOpen(false)
|
||||
}, [successPlugins, errorPlugins, handleClearErrorPlugin, runningPluginsLength])
|
||||
|
||||
const handleClearErrorsWithModal = useCallback(async () => {
|
||||
// Clear only error plugins, not all plugins
|
||||
for (const plugin of errorPlugins)
|
||||
await handleClearErrorPlugin(plugin.taskId, plugin.plugin_unique_identifier)
|
||||
// Only close modal if no plugins are still installing
|
||||
if (runningPluginsLength === 0)
|
||||
setOpen(false)
|
||||
}, [errorPlugins, handleClearErrorPlugin, runningPluginsLength])
|
||||
|
||||
const handleClearSingleWithModal = useCallback(async (taskId: string, pluginId: string) => {
|
||||
await handleClearErrorPlugin(taskId, pluginId)
|
||||
// Only close modal if no plugins are still installing
|
||||
if (runningPluginsLength === 0)
|
||||
setOpen(false)
|
||||
}, [handleClearErrorPlugin, runningPluginsLength])
|
||||
|
||||
const tip = useMemo(() => {
|
||||
if (isInstallingWithError)
|
||||
return t('plugin.task.installingWithError', { installingLength: runningPluginsLength, successLength: successPluginsLength, errorLength: errorPluginsLength })
|
||||
if (isInstallingWithSuccess)
|
||||
return t('plugin.task.installingWithSuccess', { installingLength: runningPluginsLength, successLength: successPluginsLength })
|
||||
if (isInstalling)
|
||||
return t('plugin.task.installing')
|
||||
if (isFailed)
|
||||
return t('plugin.task.installedError', { errorLength: errorPluginsLength })
|
||||
if (isSuccess)
|
||||
return t('plugin.task.installSuccess', { successLength: successPluginsLength })
|
||||
return t('plugin.task.installed')
|
||||
}, [
|
||||
errorPluginsLength,
|
||||
isFailed,
|
||||
isInstalling,
|
||||
isInstallingWithError,
|
||||
isInstallingWithSuccess,
|
||||
isSuccess,
|
||||
runningPluginsLength,
|
||||
successPluginsLength,
|
||||
t,
|
||||
])
|
||||
|
||||
// Show icon if there are any plugin tasks (completed, running, or failed)
|
||||
// Only hide when there are absolutely no plugin tasks
|
||||
if (totalPluginsLength === 0)
|
||||
return null
|
||||
|
||||
const canOpenPanel = isFailed || isInstalling || isInstallingWithSuccess || isInstallingWithError || isSuccess
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<PortalToFollowElem
|
||||
|
|
@ -57,33 +117,204 @@ const PluginTasks = () => {
|
|||
>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => {
|
||||
if (canOpenPanel)
|
||||
setOpen(!open)
|
||||
if (isFailed || isInstalling || isInstallingWithSuccess || isInstallingWithError || isSuccess)
|
||||
setOpen(v => !v)
|
||||
}}
|
||||
>
|
||||
<PluginTaskTrigger
|
||||
tip={tip}
|
||||
isInstalling={isInstalling}
|
||||
isInstallingWithSuccess={isInstallingWithSuccess}
|
||||
isInstallingWithError={isInstallingWithError}
|
||||
isSuccess={isSuccess}
|
||||
isFailed={isFailed}
|
||||
successPluginsLength={successPluginsLength}
|
||||
runningPluginsLength={runningPluginsLength}
|
||||
errorPluginsLength={errorPluginsLength}
|
||||
totalPluginsLength={totalPluginsLength}
|
||||
<Tooltip
|
||||
popupContent={tip}
|
||||
asChild
|
||||
offset={8}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-8 w-8 items-center justify-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs hover:bg-components-button-secondary-bg-hover',
|
||||
(isInstallingWithError || isFailed) && 'cursor-pointer border-components-button-destructive-secondary-border-hover bg-state-destructive-hover hover:bg-state-destructive-hover-alt',
|
||||
(isInstalling || isInstallingWithSuccess || isSuccess) && 'cursor-pointer hover:bg-components-button-secondary-bg-hover',
|
||||
)}
|
||||
id="plugin-task-trigger"
|
||||
>
|
||||
{
|
||||
(isInstalling || isInstallingWithError) && (
|
||||
<DownloadingIcon />
|
||||
)
|
||||
}
|
||||
{
|
||||
!(isInstalling || isInstallingWithError) && (
|
||||
<RiInstallLine
|
||||
className={cn(
|
||||
'h-4 w-4 text-components-button-secondary-text',
|
||||
(isInstallingWithError || isFailed) && 'text-components-button-destructive-secondary-text',
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className="absolute -right-1 -top-1">
|
||||
{
|
||||
(isInstalling || isInstallingWithSuccess) && (
|
||||
<ProgressCircle
|
||||
percentage={successPluginsLength / totalPluginsLength * 100}
|
||||
circleFillColor="fill-components-progress-brand-bg"
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
isInstallingWithError && (
|
||||
<ProgressCircle
|
||||
percentage={runningPluginsLength / totalPluginsLength * 100}
|
||||
circleFillColor="fill-components-progress-brand-bg"
|
||||
sectorFillColor="fill-components-progress-error-border"
|
||||
circleStrokeColor="stroke-components-progress-error-border"
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
(isSuccess || (successPluginsLength > 0 && runningPluginsLength === 0 && errorPluginsLength === 0)) && (
|
||||
<RiCheckboxCircleFill className="h-3.5 w-3.5 text-text-success" />
|
||||
)
|
||||
}
|
||||
{
|
||||
isFailed && (
|
||||
<RiErrorWarningFill className="h-3.5 w-3.5 text-text-destructive" />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-[11]">
|
||||
<div className="w-[360px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg">
|
||||
<RunningPluginsSection plugins={runningPlugins} />
|
||||
<SuccessPluginsSection plugins={successPlugins} onClearAll={handleClearAll} />
|
||||
<ErrorPluginsSection
|
||||
plugins={errorPlugins}
|
||||
onClearAll={handleClearErrors}
|
||||
onClearSingle={handleClearSingle}
|
||||
{/* Running Plugins */}
|
||||
{runningPlugins.length > 0 && (
|
||||
<>
|
||||
<div className="system-sm-semibold-uppercase sticky top-0 flex h-7 items-center justify-between px-2 pt-1 text-text-secondary">
|
||||
{t('plugin.task.installing')}
|
||||
{' '}
|
||||
(
|
||||
{runningPlugins.length}
|
||||
)
|
||||
</div>
|
||||
<div className="max-h-[200px] overflow-y-auto">
|
||||
{runningPlugins.map(runningPlugin => (
|
||||
<div
|
||||
key={runningPlugin.plugin_unique_identifier}
|
||||
className="flex items-center rounded-lg p-2 hover:bg-state-base-hover"
|
||||
>
|
||||
<div className="relative mr-2 flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge">
|
||||
<RiLoaderLine className="absolute -bottom-0.5 -right-0.5 z-10 h-3 w-3 animate-spin text-text-accent" />
|
||||
<CardIcon
|
||||
size="tiny"
|
||||
src={getIconUrl(runningPlugin.icon)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grow">
|
||||
<div className="system-md-regular truncate text-text-secondary">
|
||||
{runningPlugin.labels[language]}
|
||||
</div>
|
||||
<div className="system-xs-regular text-text-tertiary">
|
||||
{t('plugin.task.installing')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Success Plugins */}
|
||||
{successPlugins.length > 0 && (
|
||||
<>
|
||||
<div className="system-sm-semibold-uppercase sticky top-0 flex h-7 items-center justify-between px-2 pt-1 text-text-secondary">
|
||||
{t('plugin.task.installed')}
|
||||
{' '}
|
||||
(
|
||||
{successPlugins.length}
|
||||
)
|
||||
<Button
|
||||
className="shrink-0"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={() => handleClearAllWithModal()}
|
||||
>
|
||||
{t('plugin.task.clearAll')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-h-[200px] overflow-y-auto">
|
||||
{successPlugins.map(successPlugin => (
|
||||
<div
|
||||
key={successPlugin.plugin_unique_identifier}
|
||||
className="flex items-center rounded-lg p-2 hover:bg-state-base-hover"
|
||||
>
|
||||
<div className="relative mr-2 flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge">
|
||||
<RiCheckboxCircleFill className="absolute -bottom-0.5 -right-0.5 z-10 h-3 w-3 text-text-success" />
|
||||
<CardIcon
|
||||
size="tiny"
|
||||
src={getIconUrl(successPlugin.icon)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grow">
|
||||
<div className="system-md-regular truncate text-text-secondary">
|
||||
{successPlugin.labels[language]}
|
||||
</div>
|
||||
<div className="system-xs-regular text-text-success">
|
||||
{successPlugin.message || t('plugin.task.installed')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Error Plugins */}
|
||||
{errorPlugins.length > 0 && (
|
||||
<>
|
||||
<div className="system-sm-semibold-uppercase sticky top-0 flex h-7 items-center justify-between px-2 pt-1 text-text-secondary">
|
||||
{t('plugin.task.installError', { errorLength: errorPlugins.length })}
|
||||
<Button
|
||||
className="shrink-0"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={() => handleClearErrorsWithModal()}
|
||||
>
|
||||
{t('plugin.task.clearAll')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-h-[200px] overflow-y-auto">
|
||||
{errorPlugins.map(errorPlugin => (
|
||||
<div
|
||||
key={errorPlugin.plugin_unique_identifier}
|
||||
className="flex items-center rounded-lg p-2 hover:bg-state-base-hover"
|
||||
>
|
||||
<div className="relative mr-2 flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge">
|
||||
<RiErrorWarningFill className="absolute -bottom-0.5 -right-0.5 z-10 h-3 w-3 text-text-destructive" />
|
||||
<CardIcon
|
||||
size="tiny"
|
||||
src={getIconUrl(errorPlugin.icon)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grow">
|
||||
<div className="system-md-regular truncate text-text-secondary">
|
||||
{errorPlugin.labels[language]}
|
||||
</div>
|
||||
<div className="system-xs-regular break-all text-text-destructive">
|
||||
{errorPlugin.message}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="shrink-0"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={() => handleClearSingleWithModal(errorPlugin.taskId, errorPlugin.plugin_unique_identifier)}
|
||||
>
|
||||
{t('common.operation.clear')}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,202 +0,0 @@
|
|||
import type { ReactNode } from 'react'
|
||||
import type { PluginStatus } from '@/app/components/plugins/types'
|
||||
import {
|
||||
RiCheckboxCircleFill,
|
||||
RiErrorWarningFill,
|
||||
RiLoaderLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import CardIcon from '@/app/components/plugins/card/base/card-icon'
|
||||
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
|
||||
// Plugin item base component
|
||||
type PluginTaskItemProps = {
|
||||
plugin: PluginStatus
|
||||
statusIcon: ReactNode
|
||||
statusText: string
|
||||
statusClassName?: string
|
||||
action?: ReactNode
|
||||
}
|
||||
|
||||
const PluginTaskItem = ({
|
||||
plugin,
|
||||
statusIcon,
|
||||
statusText,
|
||||
statusClassName = 'text-text-tertiary',
|
||||
action,
|
||||
}: PluginTaskItemProps) => {
|
||||
const language = useGetLanguage()
|
||||
const { getIconUrl } = useGetIcon()
|
||||
|
||||
return (
|
||||
<div className="flex items-center rounded-lg p-2 hover:bg-state-base-hover">
|
||||
<div className="relative mr-2 flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge">
|
||||
{statusIcon}
|
||||
<CardIcon
|
||||
size="tiny"
|
||||
src={getIconUrl(plugin.icon)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grow">
|
||||
<div className="system-md-regular truncate text-text-secondary">
|
||||
{plugin.labels[language]}
|
||||
</div>
|
||||
<div className={`system-xs-regular ${statusClassName}`}>
|
||||
{statusText}
|
||||
</div>
|
||||
</div>
|
||||
{action}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Section header component
|
||||
type SectionHeaderProps = {
|
||||
title: string
|
||||
count: number
|
||||
action?: ReactNode
|
||||
}
|
||||
|
||||
const SectionHeader = ({ title, count, action }: SectionHeaderProps) => (
|
||||
<div className="system-sm-semibold-uppercase sticky top-0 flex h-7 items-center justify-between px-2 pt-1 text-text-secondary">
|
||||
{title}
|
||||
{' '}
|
||||
(
|
||||
{count}
|
||||
)
|
||||
{action}
|
||||
</div>
|
||||
)
|
||||
|
||||
// Running plugins section
|
||||
type RunningPluginsSectionProps = {
|
||||
plugins: PluginStatus[]
|
||||
}
|
||||
|
||||
export const RunningPluginsSection = ({ plugins }: RunningPluginsSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (plugins.length === 0)
|
||||
return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionHeader title={t('plugin.task.installing')} count={plugins.length} />
|
||||
<div className="max-h-[200px] overflow-y-auto">
|
||||
{plugins.map(plugin => (
|
||||
<PluginTaskItem
|
||||
key={plugin.plugin_unique_identifier}
|
||||
plugin={plugin}
|
||||
statusIcon={
|
||||
<RiLoaderLine className="absolute -bottom-0.5 -right-0.5 z-10 h-3 w-3 animate-spin text-text-accent" />
|
||||
}
|
||||
statusText={t('plugin.task.installing')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Success plugins section
|
||||
type SuccessPluginsSectionProps = {
|
||||
plugins: PluginStatus[]
|
||||
onClearAll: () => void
|
||||
}
|
||||
|
||||
export const SuccessPluginsSection = ({ plugins, onClearAll }: SuccessPluginsSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (plugins.length === 0)
|
||||
return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionHeader
|
||||
title={t('plugin.task.installed')}
|
||||
count={plugins.length}
|
||||
action={(
|
||||
<Button
|
||||
className="shrink-0"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={onClearAll}
|
||||
>
|
||||
{t('plugin.task.clearAll')}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<div className="max-h-[200px] overflow-y-auto">
|
||||
{plugins.map(plugin => (
|
||||
<PluginTaskItem
|
||||
key={plugin.plugin_unique_identifier}
|
||||
plugin={plugin}
|
||||
statusIcon={
|
||||
<RiCheckboxCircleFill className="absolute -bottom-0.5 -right-0.5 z-10 h-3 w-3 text-text-success" />
|
||||
}
|
||||
statusText={plugin.message || t('plugin.task.installed')}
|
||||
statusClassName="text-text-success"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Error plugins section
|
||||
type ErrorPluginsSectionProps = {
|
||||
plugins: PluginStatus[]
|
||||
onClearAll: () => void
|
||||
onClearSingle: (taskId: string, pluginId: string) => void
|
||||
}
|
||||
|
||||
export const ErrorPluginsSection = ({ plugins, onClearAll, onClearSingle }: ErrorPluginsSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (plugins.length === 0)
|
||||
return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionHeader
|
||||
title={t('plugin.task.installError', { errorLength: plugins.length })}
|
||||
count={plugins.length}
|
||||
action={(
|
||||
<Button
|
||||
className="shrink-0"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={onClearAll}
|
||||
>
|
||||
{t('plugin.task.clearAll')}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<div className="max-h-[200px] overflow-y-auto">
|
||||
{plugins.map(plugin => (
|
||||
<PluginTaskItem
|
||||
key={plugin.plugin_unique_identifier}
|
||||
plugin={plugin}
|
||||
statusIcon={
|
||||
<RiErrorWarningFill className="absolute -bottom-0.5 -right-0.5 z-10 h-3 w-3 text-text-destructive" />
|
||||
}
|
||||
statusText={plugin.message}
|
||||
statusClassName="break-all text-text-destructive"
|
||||
action={(
|
||||
<Button
|
||||
className="shrink-0"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={() => onClearSingle(plugin.taskId, plugin.plugin_unique_identifier)}
|
||||
>
|
||||
{t('common.operation.clear')}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
import {
|
||||
RiCheckboxCircleFill,
|
||||
RiErrorWarningFill,
|
||||
RiInstallLine,
|
||||
} from '@remixicon/react'
|
||||
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import DownloadingIcon from '@/app/components/header/plugins-nav/downloading-icon'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type PluginTaskTriggerProps = {
|
||||
tip: string
|
||||
isInstalling: boolean
|
||||
isInstallingWithSuccess: boolean
|
||||
isInstallingWithError: boolean
|
||||
isSuccess: boolean
|
||||
isFailed: boolean
|
||||
successPluginsLength: number
|
||||
runningPluginsLength: number
|
||||
errorPluginsLength: number
|
||||
totalPluginsLength: number
|
||||
}
|
||||
|
||||
const PluginTaskTrigger = ({
|
||||
tip,
|
||||
isInstalling,
|
||||
isInstallingWithSuccess,
|
||||
isInstallingWithError,
|
||||
isSuccess,
|
||||
isFailed,
|
||||
successPluginsLength,
|
||||
runningPluginsLength,
|
||||
errorPluginsLength,
|
||||
totalPluginsLength,
|
||||
}: PluginTaskTriggerProps) => {
|
||||
const showDownloadingIcon = isInstalling || isInstallingWithError
|
||||
const hasError = isInstallingWithError || isFailed
|
||||
const showSuccessIndicator = isSuccess || (successPluginsLength > 0 && runningPluginsLength === 0 && errorPluginsLength === 0)
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={tip}
|
||||
asChild
|
||||
offset={8}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-8 w-8 items-center justify-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs hover:bg-components-button-secondary-bg-hover',
|
||||
hasError && 'cursor-pointer border-components-button-destructive-secondary-border-hover bg-state-destructive-hover hover:bg-state-destructive-hover-alt',
|
||||
(isInstalling || isInstallingWithSuccess || isSuccess) && 'cursor-pointer hover:bg-components-button-secondary-bg-hover',
|
||||
)}
|
||||
id="plugin-task-trigger"
|
||||
>
|
||||
{/* Main Icon */}
|
||||
{showDownloadingIcon
|
||||
? <DownloadingIcon />
|
||||
: (
|
||||
<RiInstallLine
|
||||
className={cn(
|
||||
'h-4 w-4 text-components-button-secondary-text',
|
||||
hasError && 'text-components-button-destructive-secondary-text',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Status Indicator */}
|
||||
<div className="absolute -right-1 -top-1">
|
||||
{(isInstalling || isInstallingWithSuccess) && (
|
||||
<ProgressCircle
|
||||
percentage={successPluginsLength / totalPluginsLength * 100}
|
||||
circleFillColor="fill-components-progress-brand-bg"
|
||||
/>
|
||||
)}
|
||||
{isInstallingWithError && (
|
||||
<ProgressCircle
|
||||
percentage={runningPluginsLength / totalPluginsLength * 100}
|
||||
circleFillColor="fill-components-progress-brand-bg"
|
||||
sectorFillColor="fill-components-progress-error-border"
|
||||
circleStrokeColor="stroke-components-progress-error-border"
|
||||
/>
|
||||
)}
|
||||
{showSuccessIndicator && (
|
||||
<RiCheckboxCircleFill className="h-3.5 w-3.5 text-text-success" />
|
||||
)}
|
||||
{isFailed && (
|
||||
<RiErrorWarningFill className="h-3.5 w-3.5 text-text-destructive" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default PluginTaskTrigger
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
import type { PluginStatus } from '@/app/components/plugins/types'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePluginTaskStatus } from './hooks'
|
||||
|
||||
type PluginTaskPanelState = {
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
tip: string
|
||||
taskStatus: ReturnType<typeof usePluginTaskStatus>
|
||||
handleClearAll: () => Promise<void>
|
||||
handleClearErrors: () => Promise<void>
|
||||
handleClearSingle: (taskId: string, pluginId: string) => Promise<void>
|
||||
}
|
||||
|
||||
export const usePluginTaskPanel = (): PluginTaskPanelState => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const taskStatus = usePluginTaskStatus()
|
||||
|
||||
const {
|
||||
errorPlugins,
|
||||
successPlugins,
|
||||
runningPluginsLength,
|
||||
successPluginsLength,
|
||||
errorPluginsLength,
|
||||
isInstalling,
|
||||
isInstallingWithSuccess,
|
||||
isInstallingWithError,
|
||||
isSuccess,
|
||||
isFailed,
|
||||
handleClearErrorPlugin,
|
||||
} = taskStatus
|
||||
|
||||
const closeIfNoRunning = useCallback(() => {
|
||||
if (runningPluginsLength === 0)
|
||||
setOpen(false)
|
||||
}, [runningPluginsLength])
|
||||
|
||||
const clearPlugins = useCallback(async (plugins: PluginStatus[]) => {
|
||||
for (const plugin of plugins)
|
||||
await handleClearErrorPlugin(plugin.taskId, plugin.plugin_unique_identifier)
|
||||
}, [handleClearErrorPlugin])
|
||||
|
||||
const handleClearAll = useCallback(async () => {
|
||||
// Clear all completed plugins (success and error) but keep running ones
|
||||
await clearPlugins([...successPlugins, ...errorPlugins])
|
||||
closeIfNoRunning()
|
||||
}, [successPlugins, errorPlugins, clearPlugins, closeIfNoRunning])
|
||||
|
||||
const handleClearErrors = useCallback(async () => {
|
||||
await clearPlugins(errorPlugins)
|
||||
closeIfNoRunning()
|
||||
}, [errorPlugins, clearPlugins, closeIfNoRunning])
|
||||
|
||||
const handleClearSingle = useCallback(async (taskId: string, pluginId: string) => {
|
||||
await handleClearErrorPlugin(taskId, pluginId)
|
||||
closeIfNoRunning()
|
||||
}, [handleClearErrorPlugin, closeIfNoRunning])
|
||||
|
||||
const tip = useMemo(() => {
|
||||
if (isInstallingWithError)
|
||||
return t('plugin.task.installingWithError', { installingLength: runningPluginsLength, successLength: successPluginsLength, errorLength: errorPluginsLength })
|
||||
if (isInstallingWithSuccess)
|
||||
return t('plugin.task.installingWithSuccess', { installingLength: runningPluginsLength, successLength: successPluginsLength })
|
||||
if (isInstalling)
|
||||
return t('plugin.task.installing')
|
||||
if (isFailed)
|
||||
return t('plugin.task.installedError', { errorLength: errorPluginsLength })
|
||||
if (isSuccess)
|
||||
return t('plugin.task.installSuccess', { successLength: successPluginsLength })
|
||||
return t('plugin.task.installed')
|
||||
}, [
|
||||
errorPluginsLength,
|
||||
isFailed,
|
||||
isInstalling,
|
||||
isInstallingWithError,
|
||||
isInstallingWithSuccess,
|
||||
isSuccess,
|
||||
runningPluginsLength,
|
||||
successPluginsLength,
|
||||
t,
|
||||
])
|
||||
|
||||
return {
|
||||
open,
|
||||
setOpen,
|
||||
tip,
|
||||
taskStatus,
|
||||
handleClearAll,
|
||||
handleClearErrors,
|
||||
handleClearSingle,
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue