dify
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { InstallStep } from '../../types'
|
||||
import type { Dependency } from '../../types'
|
||||
import ReadyToInstall from './ready-to-install'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useHideLogic from '../hooks/use-hide-logic'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
|
||||
export enum InstallType {
|
||||
fromLocal = 'fromLocal',
|
||||
fromMarketplace = 'fromMarketplace',
|
||||
fromDSL = 'fromDSL',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
installType?: InstallType
|
||||
fromDSLPayload: Dependency[]
|
||||
// plugins?: PluginDeclaration[]
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const InstallBundle: FC<Props> = ({
|
||||
installType = InstallType.fromMarketplace,
|
||||
fromDSLPayload,
|
||||
onClose,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [step, setStep] = useState<InstallStep>(installType === InstallType.fromMarketplace ? InstallStep.readyToInstall : InstallStep.uploading)
|
||||
|
||||
const {
|
||||
modalClassName,
|
||||
foldAnimInto,
|
||||
setIsInstalling,
|
||||
handleStartToInstall,
|
||||
} = useHideLogic(onClose)
|
||||
|
||||
const getTitle = useCallback(() => {
|
||||
if (step === InstallStep.uploadFailed)
|
||||
return t(`${i18nPrefix}.uploadFailed`)
|
||||
if (step === InstallStep.installed)
|
||||
return t(`${i18nPrefix}.installComplete`)
|
||||
|
||||
return t(`${i18nPrefix}.installPlugin`)
|
||||
}, [step, t])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={true}
|
||||
onClose={foldAnimInto}
|
||||
className={cn(modalClassName, 'shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0')}
|
||||
closable
|
||||
>
|
||||
<div className='flex items-start gap-2 self-stretch pb-3 pl-6 pr-14 pt-6'>
|
||||
<div className='title-2xl-semi-bold self-stretch text-text-primary'>
|
||||
{getTitle()}
|
||||
</div>
|
||||
</div>
|
||||
<ReadyToInstall
|
||||
step={step}
|
||||
onStepChange={setStep}
|
||||
onStartToInstall={handleStartToInstall}
|
||||
setIsInstalling={setIsInstalling}
|
||||
allPlugins={fromDSLPayload}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(InstallBundle)
|
||||
@@ -0,0 +1,59 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import type { GitHubItemAndMarketPlaceDependency, Plugin } from '../../../types'
|
||||
import { pluginManifestToCardPluginProps } from '../../utils'
|
||||
import { useUploadGitHub } from '@/service/use-plugins'
|
||||
import Loading from '../../base/loading'
|
||||
import LoadedItem from './loaded-item'
|
||||
import type { VersionProps } from '@/app/components/plugins/types'
|
||||
|
||||
type Props = {
|
||||
checked: boolean
|
||||
onCheckedChange: (plugin: Plugin) => void
|
||||
dependency: GitHubItemAndMarketPlaceDependency
|
||||
versionInfo: VersionProps
|
||||
onFetchedPayload: (payload: Plugin) => void
|
||||
onFetchError: () => void
|
||||
}
|
||||
|
||||
const Item: FC<Props> = ({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
dependency,
|
||||
versionInfo,
|
||||
onFetchedPayload,
|
||||
onFetchError,
|
||||
}) => {
|
||||
const info = dependency.value
|
||||
const { data, error } = useUploadGitHub({
|
||||
repo: info.repo!,
|
||||
version: info.release! || info.version!,
|
||||
package: info.packages! || info.package!,
|
||||
})
|
||||
const [payload, setPayload] = React.useState<Plugin | null>(null)
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const payload = {
|
||||
...pluginManifestToCardPluginProps(data.manifest),
|
||||
plugin_id: data.unique_identifier,
|
||||
}
|
||||
onFetchedPayload(payload)
|
||||
setPayload({ ...payload, from: dependency.type })
|
||||
}
|
||||
}, [data])
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
onFetchError()
|
||||
}, [error])
|
||||
if (!payload) return <Loading />
|
||||
return (
|
||||
<LoadedItem
|
||||
payload={payload}
|
||||
versionInfo={versionInfo}
|
||||
checked={checked}
|
||||
onCheckedChange={onCheckedChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(Item)
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { Plugin } from '../../../types'
|
||||
import Card from '../../../card'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import useGetIcon from '../../base/use-get-icon'
|
||||
import { MARKETPLACE_API_PREFIX } from '@/config'
|
||||
import Version from '../../base/version'
|
||||
import type { VersionProps } from '../../../types'
|
||||
import usePluginInstallLimit from '../../hooks/use-install-plugin-limit'
|
||||
|
||||
type Props = {
|
||||
checked: boolean
|
||||
onCheckedChange: (plugin: Plugin) => void
|
||||
payload: Plugin
|
||||
isFromMarketPlace?: boolean
|
||||
versionInfo: VersionProps
|
||||
}
|
||||
|
||||
const LoadedItem: FC<Props> = ({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
payload,
|
||||
isFromMarketPlace,
|
||||
versionInfo: particleVersionInfo,
|
||||
}) => {
|
||||
const { getIconUrl } = useGetIcon()
|
||||
const versionInfo = {
|
||||
...particleVersionInfo,
|
||||
toInstallVersion: payload.version,
|
||||
}
|
||||
const { canInstall } = usePluginInstallLimit(payload)
|
||||
return (
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Checkbox
|
||||
disabled={!canInstall}
|
||||
className='shrink-0'
|
||||
checked={checked}
|
||||
onCheck={() => onCheckedChange(payload)}
|
||||
/>
|
||||
<Card
|
||||
className='grow'
|
||||
payload={{
|
||||
...payload,
|
||||
icon: isFromMarketPlace ? `${MARKETPLACE_API_PREFIX}/plugins/${payload.org}/${payload.name}/icon` : getIconUrl(payload.icon),
|
||||
}}
|
||||
titleLeft={payload.version ? <Version {...versionInfo} /> : null}
|
||||
limitedInstall={!canInstall}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(LoadedItem)
|
||||
@@ -0,0 +1,36 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { Plugin } from '../../../types'
|
||||
import Loading from '../../base/loading'
|
||||
import LoadedItem from './loaded-item'
|
||||
import type { VersionProps } from '@/app/components/plugins/types'
|
||||
|
||||
type Props = {
|
||||
checked: boolean
|
||||
onCheckedChange: (plugin: Plugin) => void
|
||||
payload?: Plugin
|
||||
version: string
|
||||
versionInfo: VersionProps
|
||||
}
|
||||
|
||||
const MarketPlaceItem: FC<Props> = ({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
payload,
|
||||
version,
|
||||
versionInfo,
|
||||
}) => {
|
||||
if (!payload) return <Loading />
|
||||
return (
|
||||
<LoadedItem
|
||||
checked={checked}
|
||||
onCheckedChange={onCheckedChange}
|
||||
payload={{ ...payload, version }}
|
||||
isFromMarketPlace
|
||||
versionInfo={versionInfo}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(MarketPlaceItem)
|
||||
@@ -0,0 +1,41 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { Plugin } from '../../../types'
|
||||
import type { PackageDependency } from '../../../types'
|
||||
import { pluginManifestToCardPluginProps } from '../../utils'
|
||||
import LoadedItem from './loaded-item'
|
||||
import LoadingError from '../../base/loading-error'
|
||||
import type { VersionProps } from '@/app/components/plugins/types'
|
||||
|
||||
type Props = {
|
||||
checked: boolean
|
||||
onCheckedChange: (plugin: Plugin) => void
|
||||
payload: PackageDependency
|
||||
isFromMarketPlace?: boolean
|
||||
versionInfo: VersionProps
|
||||
}
|
||||
|
||||
const PackageItem: FC<Props> = ({
|
||||
payload,
|
||||
checked,
|
||||
onCheckedChange,
|
||||
isFromMarketPlace,
|
||||
versionInfo,
|
||||
}) => {
|
||||
if (!payload.value?.manifest)
|
||||
return <LoadingError />
|
||||
|
||||
const plugin = pluginManifestToCardPluginProps(payload.value.manifest)
|
||||
return (
|
||||
<LoadedItem
|
||||
payload={{ ...plugin, from: payload.type }}
|
||||
checked={checked}
|
||||
onCheckedChange={onCheckedChange}
|
||||
isFromMarketPlace={isFromMarketPlace}
|
||||
versionInfo={versionInfo}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(PackageItem)
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { InstallStep } from '../../types'
|
||||
import Install from './steps/install'
|
||||
import Installed from './steps/installed'
|
||||
import type { Dependency, InstallStatus, Plugin } from '../../types'
|
||||
|
||||
type Props = {
|
||||
step: InstallStep
|
||||
onStepChange: (step: InstallStep) => void,
|
||||
onStartToInstall: () => void
|
||||
setIsInstalling: (isInstalling: boolean) => void
|
||||
allPlugins: Dependency[]
|
||||
onClose: () => void
|
||||
isFromMarketPlace?: boolean
|
||||
}
|
||||
|
||||
const ReadyToInstall: FC<Props> = ({
|
||||
step,
|
||||
onStepChange,
|
||||
onStartToInstall,
|
||||
setIsInstalling,
|
||||
allPlugins,
|
||||
onClose,
|
||||
isFromMarketPlace,
|
||||
}) => {
|
||||
const [installedPlugins, setInstalledPlugins] = useState<Plugin[]>([])
|
||||
const [installStatus, setInstallStatus] = useState<InstallStatus[]>([])
|
||||
const handleInstalled = useCallback((plugins: Plugin[], installStatus: InstallStatus[]) => {
|
||||
setInstallStatus(installStatus)
|
||||
setInstalledPlugins(plugins)
|
||||
onStepChange(InstallStep.installed)
|
||||
setIsInstalling(false)
|
||||
}, [onStepChange, setIsInstalling])
|
||||
return (
|
||||
<>
|
||||
{step === InstallStep.readyToInstall && (
|
||||
<Install
|
||||
allPlugins={allPlugins}
|
||||
onCancel={onClose}
|
||||
onStartToInstall={onStartToInstall}
|
||||
onInstalled={handleInstalled}
|
||||
isFromMarketPlace={isFromMarketPlace}
|
||||
/>
|
||||
)}
|
||||
{step === InstallStep.installed && (
|
||||
<Installed
|
||||
list={installedPlugins}
|
||||
installStatus={installStatus}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(ReadyToInstall)
|
||||
@@ -0,0 +1,272 @@
|
||||
'use client'
|
||||
import { useImperativeHandle } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '../../../types'
|
||||
import MarketplaceItem from '../item/marketplace-item'
|
||||
import GithubItem from '../item/github-item'
|
||||
import { useFetchPluginsInMarketPlaceByInfo } from '@/service/use-plugins'
|
||||
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
|
||||
import { produce } from 'immer'
|
||||
import PackageItem from '../item/package-item'
|
||||
import LoadingError from '../../base/loading-error'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { pluginInstallLimit } from '../../hooks/use-install-plugin-limit'
|
||||
|
||||
type Props = {
|
||||
allPlugins: Dependency[]
|
||||
selectedPlugins: Plugin[]
|
||||
onSelect: (plugin: Plugin, selectedIndex: number, allCanInstallPluginsLength: number) => void
|
||||
onSelectAll: (plugins: Plugin[], selectedIndexes: number[]) => void
|
||||
onDeSelectAll: () => void
|
||||
onLoadedAllPlugin: (installedInfo: Record<string, VersionInfo>) => void
|
||||
isFromMarketPlace?: boolean
|
||||
ref?: React.Ref<ExposeRefs>
|
||||
}
|
||||
|
||||
export type ExposeRefs = {
|
||||
selectAllPlugins: () => void
|
||||
deSelectAllPlugins: () => void
|
||||
}
|
||||
|
||||
const InstallByDSLList = ({
|
||||
allPlugins,
|
||||
selectedPlugins,
|
||||
onSelect,
|
||||
onSelectAll,
|
||||
onDeSelectAll,
|
||||
onLoadedAllPlugin,
|
||||
isFromMarketPlace,
|
||||
ref,
|
||||
}: Props) => {
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
// DSL has id, to get plugin info to show more info
|
||||
const { isLoading: isFetchingMarketplaceDataById, data: infoGetById, error: infoByIdError } = useFetchPluginsInMarketPlaceByInfo(allPlugins.filter(d => d.type === 'marketplace').map((d) => {
|
||||
const dependecy = (d as GitHubItemAndMarketPlaceDependency).value
|
||||
// split org, name, version by / and :
|
||||
// and remove @ and its suffix
|
||||
const [orgPart, nameAndVersionPart] = dependecy.marketplace_plugin_unique_identifier!.split('@')[0].split('/')
|
||||
const [name, version] = nameAndVersionPart.split(':')
|
||||
return {
|
||||
organization: orgPart,
|
||||
plugin: name,
|
||||
version,
|
||||
}
|
||||
}))
|
||||
// has meta(org,name,version), to get id
|
||||
const { isLoading: isFetchingDataByMeta, data: infoByMeta, error: infoByMetaError } = useFetchPluginsInMarketPlaceByInfo(allPlugins.filter(d => d.type === 'marketplace').map(d => (d as GitHubItemAndMarketPlaceDependency).value!))
|
||||
|
||||
const [plugins, doSetPlugins] = useState<(Plugin | undefined)[]>((() => {
|
||||
const hasLocalPackage = allPlugins.some(d => d.type === 'package')
|
||||
if (!hasLocalPackage)
|
||||
return []
|
||||
|
||||
const _plugins = allPlugins.map((d) => {
|
||||
if (d.type === 'package') {
|
||||
return {
|
||||
...(d as any).value.manifest,
|
||||
plugin_id: (d as any).value.unique_identifier,
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
})
|
||||
return _plugins
|
||||
})())
|
||||
|
||||
const pluginsRef = React.useRef<(Plugin | undefined)[]>(plugins)
|
||||
|
||||
const setPlugins = useCallback((p: (Plugin | undefined)[]) => {
|
||||
doSetPlugins(p)
|
||||
pluginsRef.current = p
|
||||
}, [])
|
||||
|
||||
const [errorIndexes, setErrorIndexes] = useState<number[]>([])
|
||||
|
||||
const handleGitHubPluginFetched = useCallback((index: number) => {
|
||||
return (p: Plugin) => {
|
||||
const nextPlugins = produce(pluginsRef.current, (draft) => {
|
||||
draft[index] = p
|
||||
})
|
||||
setPlugins(nextPlugins)
|
||||
}
|
||||
}, [setPlugins])
|
||||
|
||||
const handleGitHubPluginFetchError = useCallback((index: number) => {
|
||||
return () => {
|
||||
setErrorIndexes([...errorIndexes, index])
|
||||
}
|
||||
}, [errorIndexes])
|
||||
|
||||
const marketPlaceInDSLIndex = useMemo(() => {
|
||||
const res: number[] = []
|
||||
allPlugins.forEach((d, index) => {
|
||||
if (d.type === 'marketplace')
|
||||
res.push(index)
|
||||
})
|
||||
return res
|
||||
}, [allPlugins])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetchingMarketplaceDataById && infoGetById?.data.list) {
|
||||
const sortedList = allPlugins.filter(d => d.type === 'marketplace').map((d) => {
|
||||
const p = d as GitHubItemAndMarketPlaceDependency
|
||||
const id = p.value.marketplace_plugin_unique_identifier?.split(':')[0]
|
||||
const retPluginInfo = infoGetById.data.list.find(item => item.plugin.plugin_id === id)?.plugin
|
||||
return { ...retPluginInfo, from: d.type } as Plugin
|
||||
})
|
||||
const payloads = sortedList
|
||||
const failedIndex: number[] = []
|
||||
const nextPlugins = produce(pluginsRef.current, (draft) => {
|
||||
marketPlaceInDSLIndex.forEach((index, i) => {
|
||||
if (payloads[i]) {
|
||||
draft[index] = {
|
||||
...payloads[i],
|
||||
version: payloads[i]!.version || payloads[i]!.latest_version,
|
||||
}
|
||||
}
|
||||
else { failedIndex.push(index) }
|
||||
})
|
||||
})
|
||||
setPlugins(nextPlugins)
|
||||
|
||||
if (failedIndex.length > 0)
|
||||
setErrorIndexes([...errorIndexes, ...failedIndex])
|
||||
}
|
||||
}, [isFetchingMarketplaceDataById])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetchingDataByMeta && infoByMeta?.data.list) {
|
||||
const payloads = infoByMeta?.data.list
|
||||
const failedIndex: number[] = []
|
||||
const nextPlugins = produce(pluginsRef.current, (draft) => {
|
||||
marketPlaceInDSLIndex.forEach((index, i) => {
|
||||
if (payloads[i]) {
|
||||
const item = payloads[i]
|
||||
draft[index] = {
|
||||
...item.plugin,
|
||||
plugin_id: item.version.unique_identifier,
|
||||
}
|
||||
}
|
||||
else {
|
||||
failedIndex.push(index)
|
||||
}
|
||||
})
|
||||
})
|
||||
setPlugins(nextPlugins)
|
||||
if (failedIndex.length > 0)
|
||||
setErrorIndexes([...errorIndexes, ...failedIndex])
|
||||
}
|
||||
}, [isFetchingDataByMeta])
|
||||
|
||||
useEffect(() => {
|
||||
// get info all failed
|
||||
if (infoByMetaError || infoByIdError)
|
||||
setErrorIndexes([...errorIndexes, ...marketPlaceInDSLIndex])
|
||||
}, [infoByMetaError, infoByIdError])
|
||||
|
||||
const isLoadedAllData = (plugins.filter(p => !!p).length + errorIndexes.length) === allPlugins.length
|
||||
|
||||
const { installedInfo } = useCheckInstalled({
|
||||
pluginIds: plugins?.filter(p => !!p).map((d) => {
|
||||
return `${d?.org || d?.author}/${d?.name}`
|
||||
}) || [],
|
||||
enabled: isLoadedAllData,
|
||||
})
|
||||
|
||||
const getVersionInfo = useCallback((pluginId: string) => {
|
||||
const pluginDetail = installedInfo?.[pluginId]
|
||||
const hasInstalled = !!pluginDetail
|
||||
return {
|
||||
hasInstalled,
|
||||
installedVersion: pluginDetail?.installedVersion,
|
||||
toInstallVersion: '',
|
||||
}
|
||||
}, [installedInfo])
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoadedAllData && installedInfo)
|
||||
onLoadedAllPlugin(installedInfo!)
|
||||
}, [isLoadedAllData, installedInfo])
|
||||
|
||||
const handleSelect = useCallback((index: number) => {
|
||||
return () => {
|
||||
const canSelectPlugins = plugins.filter((p) => {
|
||||
const { canInstall } = pluginInstallLimit(p!, systemFeatures)
|
||||
return canInstall
|
||||
})
|
||||
onSelect(plugins[index]!, index, canSelectPlugins.length)
|
||||
}
|
||||
}, [onSelect, plugins, systemFeatures])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
selectAllPlugins: () => {
|
||||
const selectedIndexes: number[] = []
|
||||
const selectedPlugins: Plugin[] = []
|
||||
allPlugins.forEach((d, index) => {
|
||||
const p = plugins[index]
|
||||
if (!p)
|
||||
return
|
||||
const { canInstall } = pluginInstallLimit(p, systemFeatures)
|
||||
if (canInstall) {
|
||||
selectedIndexes.push(index)
|
||||
selectedPlugins.push(p)
|
||||
}
|
||||
})
|
||||
onSelectAll(selectedPlugins, selectedIndexes)
|
||||
},
|
||||
deSelectAllPlugins: () => {
|
||||
onDeSelectAll()
|
||||
},
|
||||
}))
|
||||
|
||||
return (
|
||||
<>
|
||||
{allPlugins.map((d, index) => {
|
||||
if (errorIndexes.includes(index)) {
|
||||
return (
|
||||
<LoadingError key={index} />
|
||||
)
|
||||
}
|
||||
const plugin = plugins[index]
|
||||
if (d.type === 'github') {
|
||||
return (<GithubItem
|
||||
key={index}
|
||||
checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)}
|
||||
onCheckedChange={handleSelect(index)}
|
||||
dependency={d as GitHubItemAndMarketPlaceDependency}
|
||||
onFetchedPayload={handleGitHubPluginFetched(index)}
|
||||
onFetchError={handleGitHubPluginFetchError(index)}
|
||||
versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)}
|
||||
/>)
|
||||
}
|
||||
|
||||
if (d.type === 'marketplace') {
|
||||
return (
|
||||
<MarketplaceItem
|
||||
key={index}
|
||||
checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)}
|
||||
onCheckedChange={handleSelect(index)}
|
||||
payload={{ ...plugin, from: d.type } as Plugin}
|
||||
version={(d as GitHubItemAndMarketPlaceDependency).value.version! || plugin?.version || ''}
|
||||
versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Local package
|
||||
return (
|
||||
<PackageItem
|
||||
key={index}
|
||||
checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)}
|
||||
onCheckedChange={handleSelect(index)}
|
||||
payload={d as PackageDependency}
|
||||
isFromMarketPlace={isFromMarketPlace}
|
||||
versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default InstallByDSLList
|
||||
@@ -0,0 +1,223 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useRef } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
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, 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: InstallStatus[]) => void
|
||||
onCancel: () => void
|
||||
isFromMarketPlace?: boolean
|
||||
isHideButton?: boolean
|
||||
}
|
||||
|
||||
const Install: FC<Props> = ({
|
||||
allPlugins,
|
||||
onStartToInstall,
|
||||
onInstalled,
|
||||
onCancel,
|
||||
isFromMarketPlace,
|
||||
isHideButton,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const emit = useMittContextSelector(s => s.emit)
|
||||
const [selectedPlugins, setSelectedPlugins] = React.useState<Plugin[]>([])
|
||||
const [selectedIndexes, setSelectedIndexes] = React.useState<number[]>([])
|
||||
const selectedPluginsNum = selectedPlugins.length
|
||||
const installMultiRef = useRef<ExposeRefs>(null)
|
||||
const { refreshPluginList } = useRefreshPluginList()
|
||||
const [isSelectAll, setIsSelectAll] = useState(false)
|
||||
const handleClickSelectAll = useCallback(() => {
|
||||
if (isSelectAll)
|
||||
installMultiRef.current?.deSelectAllPlugins()
|
||||
else
|
||||
installMultiRef.current?.selectAllPlugins()
|
||||
}, [isSelectAll])
|
||||
const [canInstall, setCanInstall] = React.useState(false)
|
||||
const [installedInfo, setInstalledInfo] = useState<Record<string, VersionInfo> | undefined>(undefined)
|
||||
|
||||
const handleLoadedAllPlugin = useCallback((installedInfo: Record<string, VersionInfo> | undefined) => {
|
||||
handleClickSelectAll()
|
||||
setInstalledInfo(installedInfo)
|
||||
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: 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 ({
|
||||
success: r.status === TaskStatus.success,
|
||||
isFromMarketPlace: allPlugins[selectedIndexes[i]].type === 'marketplace',
|
||||
})
|
||||
}))
|
||||
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 = () => {
|
||||
onStartToInstall?.()
|
||||
installOrUpdate({
|
||||
payload: allPlugins.filter((_d, index) => selectedIndexes.includes(index)),
|
||||
plugin: selectedPlugins,
|
||||
installedInfo: installedInfo!,
|
||||
})
|
||||
}
|
||||
const [isIndeterminate, setIsIndeterminate] = useState(false)
|
||||
const handleSelectAll = useCallback((plugins: Plugin[], selectedIndexes: number[]) => {
|
||||
setSelectedPlugins(plugins)
|
||||
setSelectedIndexes(selectedIndexes)
|
||||
setIsSelectAll(true)
|
||||
setIsIndeterminate(false)
|
||||
}, [])
|
||||
const handleDeSelectAll = useCallback(() => {
|
||||
setSelectedPlugins([])
|
||||
setSelectedIndexes([])
|
||||
setIsSelectAll(false)
|
||||
setIsIndeterminate(false)
|
||||
}, [])
|
||||
|
||||
const handleSelect = useCallback((plugin: Plugin, selectedIndex: number, allPluginsLength: number) => {
|
||||
const isSelected = !!selectedPlugins.find(p => p.plugin_id === plugin.plugin_id)
|
||||
let nextSelectedPlugins
|
||||
if (isSelected)
|
||||
nextSelectedPlugins = selectedPlugins.filter(p => p.plugin_id !== plugin.plugin_id)
|
||||
else
|
||||
nextSelectedPlugins = [...selectedPlugins, plugin]
|
||||
setSelectedPlugins(nextSelectedPlugins)
|
||||
const nextSelectedIndexes = isSelected ? selectedIndexes.filter(i => i !== selectedIndex) : [...selectedIndexes, selectedIndex]
|
||||
setSelectedIndexes(nextSelectedIndexes)
|
||||
if (nextSelectedPlugins.length === 0) {
|
||||
setIsSelectAll(false)
|
||||
setIsIndeterminate(false)
|
||||
}
|
||||
else if (nextSelectedPlugins.length === allPluginsLength) {
|
||||
setIsSelectAll(true)
|
||||
setIsIndeterminate(false)
|
||||
}
|
||||
else {
|
||||
setIsIndeterminate(true)
|
||||
setIsSelectAll(false)
|
||||
}
|
||||
}, [selectedPlugins, selectedIndexes])
|
||||
|
||||
const { canInstallPluginFromMarketplace } = useCanInstallPluginFromMarketplace()
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'>
|
||||
<div className='system-md-regular text-text-secondary'>
|
||||
<p>{t(`${i18nPrefix}.${selectedPluginsNum > 1 ? 'readyToInstallPackages' : 'readyToInstallPackage'}`, { num: selectedPluginsNum })}</p>
|
||||
</div>
|
||||
<div className='w-full space-y-1 rounded-2xl bg-background-section-burn p-2'>
|
||||
<InstallMulti
|
||||
ref={installMultiRef}
|
||||
allPlugins={allPlugins}
|
||||
selectedPlugins={selectedPlugins}
|
||||
onSelect={handleSelect}
|
||||
onSelectAll={handleSelectAll}
|
||||
onDeSelectAll={handleDeSelectAll}
|
||||
onLoadedAllPlugin={handleLoadedAllPlugin}
|
||||
isFromMarketPlace={isFromMarketPlace}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Action Buttons */}
|
||||
{!isHideButton && (
|
||||
<div className='flex items-center justify-between gap-2 self-stretch p-6 pt-5'>
|
||||
<div className='px-2'>
|
||||
{canInstall && <div className='flex items-center gap-x-2' onClick={handleClickSelectAll}>
|
||||
<Checkbox checked={isSelectAll} indeterminate={isIndeterminate} />
|
||||
<p className='system-sm-medium cursor-pointer text-text-secondary'>{isSelectAll ? t('common.operation.deSelectAll') : t('common.operation.selectAll')}</p>
|
||||
</div>}
|
||||
</div>
|
||||
<div className='flex items-center justify-end gap-2 self-stretch'>
|
||||
{!canInstall && (
|
||||
<Button variant='secondary' className='min-w-[72px]' onClick={handleCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='primary'
|
||||
className='flex min-w-[72px] space-x-0.5'
|
||||
disabled={!canInstall || isInstalling || selectedPlugins.length === 0 || !canInstallPluginFromMarketplace}
|
||||
onClick={handleInstall}
|
||||
>
|
||||
{isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />}
|
||||
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(Install)
|
||||
@@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
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'
|
||||
import Badge, { BadgeState } from '@/app/components/base/badge/index'
|
||||
import useGetIcon from '../../base/use-get-icon'
|
||||
import { MARKETPLACE_API_PREFIX } from '@/config'
|
||||
|
||||
type Props = {
|
||||
list: Plugin[]
|
||||
installStatus: InstallStatus[]
|
||||
onCancel: () => void
|
||||
isHideButton?: boolean
|
||||
}
|
||||
|
||||
const Installed: FC<Props> = ({
|
||||
list,
|
||||
installStatus,
|
||||
onCancel,
|
||||
isHideButton,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { getIconUrl } = useGetIcon()
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'>
|
||||
{/* <p className='text-text-secondary system-md-regular'>{(isFailed && errMsg) ? errMsg : t(`plugin.installModal.${isFailed ? 'installFailedDesc' : 'installedSuccessfullyDesc'}`)}</p> */}
|
||||
<div className='flex flex-wrap content-start items-start gap-1 space-y-1 self-stretch rounded-2xl bg-background-section-burn p-2'>
|
||||
{list.map((plugin, index) => {
|
||||
return (
|
||||
<Card
|
||||
key={plugin.plugin_id}
|
||||
className='w-full'
|
||||
payload={{
|
||||
...plugin,
|
||||
icon: installStatus[index].isFromMarketPlace ? `${MARKETPLACE_API_PREFIX}/plugins/${plugin.org}/${plugin.name}/icon` : getIconUrl(plugin.icon),
|
||||
}}
|
||||
installed={installStatus[index].success}
|
||||
installFailed={!installStatus[index].success}
|
||||
titleLeft={plugin.version ? <Badge className='mx-1' size="s" state={BadgeState.Default}>{plugin.version}</Badge> : null}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{/* Action Buttons */}
|
||||
{!isHideButton && (
|
||||
<div className='flex items-center justify-end gap-2 self-stretch p-6 pt-5'>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px]'
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('common.operation.close')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Installed)
|
||||
Reference in New Issue
Block a user