refactor: update install status handling in plugin installation process (#27594)

This commit is contained in:
Wu Tianwei
2025-10-29 18:31:02 +08:00
committed by GitHub
parent f260627660
commit 4ca7ba000c
13 changed files with 157 additions and 32 deletions

View File

@@ -214,6 +214,7 @@ const SettingBuiltInTool: FC<Props> = ({
pluginPayload={{
provider: collection.name,
category: AuthCategory.tool,
providerType: collection.type,
}}
credentialId={credentialId}
onAuthorizationItemClick={onAuthorizationItemClick}

View File

@@ -23,6 +23,10 @@ const meta = {
args: {
items: ITEMS,
value: 'all',
// eslint-disable-next-line no-empty-function
onSelect: () => {},
// eslint-disable-next-line no-empty-function
onClear: () => {},
},
} satisfies Meta<typeof Chip>
@@ -69,6 +73,13 @@ const [selection, setSelection] = useState('all')
}
export const WithoutLeftIcon: Story = {
args: {
showLeftIcon: false,
// eslint-disable-next-line no-empty-function
onSelect: () => {},
// eslint-disable-next-line no-empty-function
onClear: () => {},
},
render: args => (
<ChipDemo
{...args}

View File

@@ -103,7 +103,6 @@ export const Default: Story = {
</button>
</>
),
children: null,
},
}
@@ -112,7 +111,6 @@ export const WithoutFooter: Story = {
args: {
footer: undefined,
title: 'Read-only summary',
children: null,
},
parameters: {
docs: {
@@ -130,7 +128,6 @@ export const CustomStyling: Story = {
bodyClassName: 'bg-gray-50 rounded-xl p-5',
footerClassName: 'justify-between px-4 pb-4 pt-4',
titleClassName: 'text-lg text-primary-600',
children: null,
footer: (
<>
<span className="text-xs text-gray-400">Last synced 2 minutes ago</span>
@@ -144,7 +141,6 @@ export const CustomStyling: Story = {
</div>
</>
),
children: null,
},
parameters: {
docs: {

View File

@@ -20,6 +20,7 @@ import { useDataSourceAuthUpdate } from './hooks'
import Confirm from '@/app/components/base/confirm'
import { useGetDataSourceOAuthUrl } from '@/service/use-datasource'
import { openOAuthPopup } from '@/hooks/use-oauth'
import { CollectionType } from '@/app/components/tools/types'
type CardProps = {
item: DataSourceAuth
@@ -42,6 +43,7 @@ const Card = ({
const pluginPayload = {
category: AuthCategory.datasource,
provider: `${item.plugin_id}/${item.name}`,
providerType: CollectionType.datasource,
}
const { handleAuthUpdate } = useDataSourceAuthUpdate({
pluginId: item.plugin_id,

View File

@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react'
import { InstallStep } from '../../types'
import Install from './steps/install'
import Installed from './steps/installed'
import type { Dependency, InstallStatusResponse, Plugin } from '../../types'
import type { Dependency, InstallStatus, Plugin } from '../../types'
type Props = {
step: InstallStep
@@ -26,8 +26,8 @@ const ReadyToInstall: FC<Props> = ({
isFromMarketPlace,
}) => {
const [installedPlugins, setInstalledPlugins] = useState<Plugin[]>([])
const [installStatus, setInstallStatus] = useState<InstallStatusResponse[]>([])
const handleInstalled = useCallback((plugins: Plugin[], installStatus: InstallStatusResponse[]) => {
const [installStatus, setInstallStatus] = useState<InstallStatus[]>([])
const handleInstalled = useCallback((plugins: Plugin[], installStatus: InstallStatus[]) => {
setInstallStatus(installStatus)
setInstalledPlugins(plugins)
onStepChange(InstallStep.installed)

View File

@@ -2,23 +2,31 @@
import type { FC } from 'react'
import { useRef } from 'react'
import React, { useCallback, useState } from 'react'
import type { Dependency, InstallStatusResponse, Plugin, VersionInfo } from '../../../types'
import {
type Dependency,
type InstallStatus,
type InstallStatusResponse,
type Plugin,
TaskStatus,
type VersionInfo,
} from '../../../types'
import Button from '@/app/components/base/button'
import { RiLoader2Line } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import type { ExposeRefs } from './install-multi'
import InstallMulti from './install-multi'
import { useInstallOrUpdate } from '@/service/use-plugins'
import { useInstallOrUpdate, usePluginTaskList } from '@/service/use-plugins'
import useRefreshPluginList from '../../hooks/use-refresh-plugin-list'
import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-reference-setting'
import { useMittContextSelector } from '@/context/mitt-context'
import Checkbox from '@/app/components/base/checkbox'
import checkTaskStatus from '../../base/check-task-status'
const i18nPrefix = 'plugin.installModal'
type Props = {
allPlugins: Dependency[]
onStartToInstall?: () => void
onInstalled: (plugins: Plugin[], installStatus: InstallStatusResponse[]) => void
onInstalled: (plugins: Plugin[], installStatus: InstallStatus[]) => void
onCancel: () => void
isFromMarketPlace?: boolean
isHideButton?: boolean
@@ -55,22 +63,64 @@ const Install: FC<Props> = ({
setCanInstall(true)
}, [])
const {
check,
stop,
} = checkTaskStatus()
const handleCancel = useCallback(() => {
stop()
onCancel()
}, [onCancel, stop])
const { handleRefetch } = usePluginTaskList()
// Install from marketplace and github
const { mutate: installOrUpdate, isPending: isInstalling } = useInstallOrUpdate({
onSuccess: (res: InstallStatusResponse[]) => {
onSuccess: async (res: InstallStatusResponse[]) => {
const isAllSettled = res.every(r => r.status === TaskStatus.success || r.status === TaskStatus.failed)
// if all settled, return the install status
if (isAllSettled) {
onInstalled(selectedPlugins, res.map((r, i) => {
return ({
...r,
success: r.status === TaskStatus.success,
isFromMarketPlace: allPlugins[selectedIndexes[i]].type === 'marketplace',
})
}))
const hasInstallSuccess = res.some(r => r.success)
const hasInstallSuccess = res.some(r => r.status === TaskStatus.success)
if (hasInstallSuccess) {
refreshPluginList(undefined, true)
emit('plugin:install:success', selectedPlugins.map((p) => {
return `${p.plugin_id}/${p.name}`
}))
}
return
}
// if not all settled, keep checking the status of the plugins
handleRefetch()
const installStatus = await Promise.all(res.map(async (item, index) => {
if (item.status !== TaskStatus.running) {
return {
success: item.status === TaskStatus.success,
isFromMarketPlace: allPlugins[selectedIndexes[index]].type === 'marketplace',
}
}
const { status } = await check({
taskId: item.taskId,
pluginUniqueIdentifier: item.uniqueIdentifier,
})
return {
success: status === TaskStatus.success,
isFromMarketPlace: allPlugins[selectedIndexes[index]].type === 'marketplace',
}
}))
onInstalled(selectedPlugins, installStatus)
const hasInstallSuccess = installStatus.some(r => r.success)
if (hasInstallSuccess) {
emit('plugin:install:success', selectedPlugins.map((p) => {
return `${p.plugin_id}/${p.name}`
}))
}
},
})
const handleInstall = () => {
@@ -150,7 +200,7 @@ const Install: FC<Props> = ({
</div>
<div className='flex items-center justify-end gap-2 self-stretch'>
{!canInstall && (
<Button variant='secondary' className='min-w-[72px]' onClick={onCancel}>
<Button variant='secondary' className='min-w-[72px]' onClick={handleCancel}>
{t('common.operation.cancel')}
</Button>
)}

View File

@@ -1,7 +1,7 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import type { InstallStatusResponse, Plugin } from '../../../types'
import type { InstallStatus, Plugin } from '../../../types'
import Card from '@/app/components/plugins/card'
import Button from '@/app/components/base/button'
import { useTranslation } from 'react-i18next'
@@ -11,7 +11,7 @@ import { MARKETPLACE_API_PREFIX } from '@/config'
type Props = {
list: Plugin[]
installStatus: InstallStatusResponse[]
installStatus: InstallStatus[]
onCancel: () => void
isHideButton?: boolean
}

View File

@@ -331,6 +331,7 @@ const DetailHeader = ({
pluginPayload={{
provider: provider?.name || '',
category: AuthCategory.tool,
providerType: provider?.type || '',
}}
/>
)

View File

@@ -314,6 +314,7 @@ const ToolSelector: FC<Props> = ({
pluginPayload={{
provider: currentProvider.name,
category: AuthCategory.tool,
providerType: currentProvider.type,
}}
credentialId={value?.credential_id}
onAuthorizationItemClick={handleAuthorizationItemClick}

View File

@@ -280,6 +280,12 @@ export type InstallPackageResponse = {
}
export type InstallStatusResponse = {
status: TaskStatus,
taskId: string,
uniqueIdentifier: string,
}
export type InstallStatus = {
success: boolean,
isFromMarketPlace?: boolean
}

1
web/global.d.ts vendored
View File

@@ -1,6 +1,7 @@
import './types/i18n'
import './types/jsx'
import './types/mdx'
import './types/assets'
declare module 'lamejs';
declare module 'lamejs/src/js/MPEGMode';

View File

@@ -10,6 +10,7 @@ import type {
Dependency,
GitHubItemAndMarketPlaceDependency,
InstallPackageResponse,
InstallStatusResponse,
InstalledLatestVersionResponse,
InstalledPluginListWithTotalResponse,
PackageDependency,
@@ -233,7 +234,7 @@ export const useUploadGitHub = (payload: {
export const useInstallOrUpdate = ({
onSuccess,
}: {
onSuccess?: (res: { success: boolean }[]) => void
onSuccess?: (res: InstallStatusResponse[]) => void
}) => {
const { mutateAsync: updatePackageFromMarketPlace } = useUpdatePackageFromMarketPlace()
@@ -251,6 +252,8 @@ export const useInstallOrUpdate = ({
const installedPayload = installedInfo[orgAndName]
const isInstalled = !!installedPayload
let uniqueIdentifier = ''
let taskId = ''
let isFinishedInstallation = false
if (item.type === 'github') {
const data = item as GitHubItemAndMarketPlaceDependency
@@ -268,12 +271,14 @@ export const useInstallOrUpdate = ({
// has the same version, but not installed
if (uniqueIdentifier === installedPayload?.uniqueIdentifier) {
return {
success: true,
status: TaskStatus.success,
taskId: '',
uniqueIdentifier: '',
}
}
}
if (!isInstalled) {
await post<InstallPackageResponse>('/workspaces/current/plugin/install/github', {
const { task_id, all_installed } = await post<InstallPackageResponse>('/workspaces/current/plugin/install/github', {
body: {
repo: data.value.repo!,
version: data.value.release! || data.value.version!,
@@ -281,6 +286,8 @@ export const useInstallOrUpdate = ({
plugin_unique_identifier: uniqueIdentifier,
},
})
taskId = task_id
isFinishedInstallation = all_installed
}
}
if (item.type === 'marketplace') {
@@ -288,15 +295,19 @@ export const useInstallOrUpdate = ({
uniqueIdentifier = data.value.marketplace_plugin_unique_identifier! || plugin[i]?.plugin_id
if (uniqueIdentifier === installedPayload?.uniqueIdentifier) {
return {
success: true,
status: TaskStatus.success,
taskId: '',
uniqueIdentifier: '',
}
}
if (!isInstalled) {
await post<InstallPackageResponse>('/workspaces/current/plugin/install/marketplace', {
const { task_id, all_installed } = await post<InstallPackageResponse>('/workspaces/current/plugin/install/marketplace', {
body: {
plugin_unique_identifiers: [uniqueIdentifier],
},
})
taskId = task_id
isFinishedInstallation = all_installed
}
}
if (item.type === 'package') {
@@ -304,38 +315,59 @@ export const useInstallOrUpdate = ({
uniqueIdentifier = data.value.unique_identifier
if (uniqueIdentifier === installedPayload?.uniqueIdentifier) {
return {
success: true,
status: TaskStatus.success,
taskId: '',
uniqueIdentifier: '',
}
}
if (!isInstalled) {
await post<InstallPackageResponse>('/workspaces/current/plugin/install/pkg', {
const { task_id, all_installed } = await post<InstallPackageResponse>('/workspaces/current/plugin/install/pkg', {
body: {
plugin_unique_identifiers: [uniqueIdentifier],
},
})
taskId = task_id
isFinishedInstallation = all_installed
}
}
if (isInstalled) {
if (item.type === 'package') {
await uninstallPlugin(installedPayload.installedId)
await post<InstallPackageResponse>('/workspaces/current/plugin/install/pkg', {
const { task_id, all_installed } = await post<InstallPackageResponse>('/workspaces/current/plugin/install/pkg', {
body: {
plugin_unique_identifiers: [uniqueIdentifier],
},
})
taskId = task_id
isFinishedInstallation = all_installed
}
else {
await updatePackageFromMarketPlace({
const { task_id, all_installed } = await updatePackageFromMarketPlace({
original_plugin_unique_identifier: installedPayload?.uniqueIdentifier,
new_plugin_unique_identifier: uniqueIdentifier,
})
taskId = task_id
isFinishedInstallation = all_installed
}
}
if (isFinishedInstallation) {
return {
status: TaskStatus.success,
taskId: '',
uniqueIdentifier: '',
}
}
else {
return {
status: TaskStatus.running,
taskId,
uniqueIdentifier,
}
}
return ({ success: true })
}
// eslint-disable-next-line unused-imports/no-unused-vars
catch (e) {
return Promise.resolve({ success: false })
return Promise.resolve({ status: TaskStatus.failed, taskId: '', uniqueIdentifier: '' })
}
}))
},

24
web/types/assets.d.ts vendored Normal file
View File

@@ -0,0 +1,24 @@
declare module '*.svg' {
const value: any
export default value
}
declare module '*.png' {
const value: any
export default value
}
declare module '*.jpg' {
const value: any
export default value
}
declare module '*.jpeg' {
const value: any
export default value
}
declare module '*.gif' {
const value: any
export default value
}