Introduce Plugins (#13836)
Signed-off-by: yihong0618 <zouzou0208@gmail.com> Signed-off-by: -LAN- <laipz8200@outlook.com> Signed-off-by: xhe <xw897002528@gmail.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: takatost <takatost@gmail.com> Co-authored-by: kurokobo <kuro664@gmail.com> Co-authored-by: Novice Lee <novicelee@NoviPro.local> Co-authored-by: zxhlyh <jasonapring2015@outlook.com> Co-authored-by: AkaraChen <akarachen@outlook.com> Co-authored-by: Yi <yxiaoisme@gmail.com> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: twwu <twwu@dify.ai> Co-authored-by: Hiroshi Fujita <fujita-h@users.noreply.github.com> Co-authored-by: AkaraChen <85140972+AkaraChen@users.noreply.github.com> Co-authored-by: NFish <douxc512@gmail.com> Co-authored-by: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Co-authored-by: 非法操作 <hjlarry@163.com> Co-authored-by: Novice <857526207@qq.com> Co-authored-by: Hiroki Nagai <82458324+nagaihiroki-git@users.noreply.github.com> Co-authored-by: Gen Sato <52241300+halogen22@users.noreply.github.com> Co-authored-by: eux <euxuuu@gmail.com> Co-authored-by: huangzhuo1949 <167434202+huangzhuo1949@users.noreply.github.com> Co-authored-by: huangzhuo <huangzhuo1@xiaomi.com> Co-authored-by: lotsik <lotsik@mail.ru> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: nite-knite <nkCoding@gmail.com> Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: gakkiyomi <gakkiyomi@aliyun.com> Co-authored-by: CN-P5 <heibai2006@gmail.com> Co-authored-by: CN-P5 <heibai2006@qq.com> Co-authored-by: Chuehnone <1897025+chuehnone@users.noreply.github.com> Co-authored-by: yihong <zouzou0208@gmail.com> Co-authored-by: Kevin9703 <51311316+Kevin9703@users.noreply.github.com> Co-authored-by: -LAN- <laipz8200@outlook.com> Co-authored-by: Boris Feld <lothiraldan@gmail.com> Co-authored-by: mbo <himabo@gmail.com> Co-authored-by: mabo <mabo@aeyes.ai> Co-authored-by: Warren Chen <warren.chen830@gmail.com> Co-authored-by: JzoNgKVO <27049666+JzoNgKVO@users.noreply.github.com> Co-authored-by: jiandanfeng <chenjh3@wangsu.com> Co-authored-by: zhu-an <70234959+xhdd123321@users.noreply.github.com> Co-authored-by: zhaoqingyu.1075 <zhaoqingyu.1075@bytedance.com> Co-authored-by: 海狸大師 <86974027+yenslife@users.noreply.github.com> Co-authored-by: Xu Song <xusong.vip@gmail.com> Co-authored-by: rayshaw001 <396301947@163.com> Co-authored-by: Ding Jiatong <dingjiatong@gmail.com> Co-authored-by: Bowen Liang <liangbowen@gf.com.cn> Co-authored-by: JasonVV <jasonwangiii@outlook.com> Co-authored-by: le0zh <newlight@qq.com> Co-authored-by: zhuxinliang <zhuxinliang@didiglobal.com> Co-authored-by: k-zaku <zaku99@outlook.jp> Co-authored-by: luckylhb90 <luckylhb90@gmail.com> Co-authored-by: hobo.l <hobo.l@binance.com> Co-authored-by: jiangbo721 <365065261@qq.com> Co-authored-by: 刘江波 <jiangbo721@163.com> Co-authored-by: Shun Miyazawa <34241526+miya@users.noreply.github.com> Co-authored-by: EricPan <30651140+Egfly@users.noreply.github.com> Co-authored-by: crazywoola <427733928@qq.com> Co-authored-by: sino <sino2322@gmail.com> Co-authored-by: Jhvcc <37662342+Jhvcc@users.noreply.github.com> Co-authored-by: lowell <lowell.hu@zkteco.in> Co-authored-by: Boris Polonsky <BorisPolonsky@users.noreply.github.com> Co-authored-by: Ademílson Tonato <ademilsonft@outlook.com> Co-authored-by: Ademílson Tonato <ademilson.tonato@refurbed.com> Co-authored-by: IWAI, Masaharu <iwaim.sub@gmail.com> Co-authored-by: Yueh-Po Peng (Yabi) <94939112+y10ab1@users.noreply.github.com> Co-authored-by: Jason <ggbbddjm@gmail.com> Co-authored-by: Xin Zhang <sjhpzx@gmail.com> Co-authored-by: yjc980121 <3898524+yjc980121@users.noreply.github.com> Co-authored-by: heyszt <36215648+hieheihei@users.noreply.github.com> Co-authored-by: Abdullah AlOsaimi <osaimiacc@gmail.com> Co-authored-by: Abdullah AlOsaimi <189027247+osaimi@users.noreply.github.com> Co-authored-by: Yingchun Lai <laiyingchun@apache.org> Co-authored-by: Hash Brown <hi@xzd.me> Co-authored-by: zuodongxu <192560071+zuodongxu@users.noreply.github.com> Co-authored-by: Masashi Tomooka <tmokmss@users.noreply.github.com> Co-authored-by: aplio <ryo.091219@gmail.com> Co-authored-by: Obada Khalili <54270856+obadakhalili@users.noreply.github.com> Co-authored-by: Nam Vu <zuzoovn@gmail.com> Co-authored-by: Kei YAMAZAKI <1715090+kei-yamazaki@users.noreply.github.com> Co-authored-by: TechnoHouse <13776377+deephbz@users.noreply.github.com> Co-authored-by: Riddhimaan-Senapati <114703025+Riddhimaan-Senapati@users.noreply.github.com> Co-authored-by: MaFee921 <31881301+2284730142@users.noreply.github.com> Co-authored-by: te-chan <t-nakanome@sakura-is.co.jp> Co-authored-by: HQidea <HQidea@users.noreply.github.com> Co-authored-by: Joshbly <36315710+Joshbly@users.noreply.github.com> Co-authored-by: xhe <xw897002528@gmail.com> Co-authored-by: weiwenyan-dev <154779315+weiwenyan-dev@users.noreply.github.com> Co-authored-by: ex_wenyan.wei <ex_wenyan.wei@tcl.com> Co-authored-by: engchina <12236799+engchina@users.noreply.github.com> Co-authored-by: engchina <atjapan2015@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: 呆萌闷油瓶 <253605712@qq.com> Co-authored-by: Kemal <kemalmeler@outlook.com> Co-authored-by: Lazy_Frog <4590648+lazyFrogLOL@users.noreply.github.com> Co-authored-by: Yi Xiao <54782454+YIXIAO0@users.noreply.github.com> Co-authored-by: Steven sun <98230804+Tuyohai@users.noreply.github.com> Co-authored-by: steven <sunzwj@digitalchina.com> Co-authored-by: Kalo Chin <91766386+fdb02983rhy@users.noreply.github.com> Co-authored-by: Katy Tao <34019945+KatyTao@users.noreply.github.com> Co-authored-by: depy <42985524+h4ckdepy@users.noreply.github.com> Co-authored-by: 胡春东 <gycm520@gmail.com> Co-authored-by: Junjie.M <118170653@qq.com> Co-authored-by: MuYu <mr.muzea@gmail.com> Co-authored-by: Naoki Takashima <39912547+takatea@users.noreply.github.com> Co-authored-by: Summer-Gu <37869445+gubinjie@users.noreply.github.com> Co-authored-by: Fei He <droxer.he@gmail.com> Co-authored-by: ybalbert001 <120714773+ybalbert001@users.noreply.github.com> Co-authored-by: Yuanbo Li <ybalbert@amazon.com> Co-authored-by: douxc <7553076+douxc@users.noreply.github.com> Co-authored-by: liuzhenghua <1090179900@qq.com> Co-authored-by: Wu Jiayang <62842862+Wu-Jiayang@users.noreply.github.com> Co-authored-by: Your Name <you@example.com> Co-authored-by: kimjion <45935338+kimjion@users.noreply.github.com> Co-authored-by: AugNSo <song.tiankai@icloud.com> Co-authored-by: llinvokerl <38915183+llinvokerl@users.noreply.github.com> Co-authored-by: liusurong.lsr <liusurong.lsr@alibaba-inc.com> Co-authored-by: Vasu Negi <vasu-negi@users.noreply.github.com> Co-authored-by: Hundredwz <1808096180@qq.com> Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { checkTaskStatus as fetchCheckTaskStatus } from '@/service/plugins'
|
||||
import type { PluginStatus } from '../../types'
|
||||
import { TaskStatus } from '../../types'
|
||||
import { sleep } from '@/utils'
|
||||
|
||||
const INTERVAL = 10 * 1000 // 10 seconds
|
||||
|
||||
type Params = {
|
||||
taskId: string
|
||||
pluginUniqueIdentifier: string
|
||||
}
|
||||
|
||||
function checkTaskStatus() {
|
||||
let nextStatus = TaskStatus.running
|
||||
let isStop = false
|
||||
|
||||
const doCheckStatus = async ({
|
||||
taskId,
|
||||
pluginUniqueIdentifier,
|
||||
}: Params) => {
|
||||
if (isStop) {
|
||||
return {
|
||||
status: TaskStatus.success,
|
||||
}
|
||||
}
|
||||
const res = await fetchCheckTaskStatus(taskId)
|
||||
const { plugins } = res.task
|
||||
const plugin = plugins.find((p: PluginStatus) => p.plugin_unique_identifier === pluginUniqueIdentifier)
|
||||
if (!plugin) {
|
||||
nextStatus = TaskStatus.failed
|
||||
return {
|
||||
status: TaskStatus.failed,
|
||||
error: 'Plugin package not found',
|
||||
}
|
||||
}
|
||||
nextStatus = plugin.status
|
||||
if (nextStatus === TaskStatus.running) {
|
||||
await sleep(INTERVAL)
|
||||
return await doCheckStatus({
|
||||
taskId,
|
||||
pluginUniqueIdentifier,
|
||||
})
|
||||
}
|
||||
if (nextStatus === TaskStatus.failed) {
|
||||
return {
|
||||
status: TaskStatus.failed,
|
||||
error: plugin.message,
|
||||
}
|
||||
}
|
||||
return ({
|
||||
status: TaskStatus.success,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
check: doCheckStatus,
|
||||
stop: () => {
|
||||
isStop = true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default checkTaskStatus
|
||||
60
web/app/components/plugins/install-plugin/base/installed.tsx
Normal file
60
web/app/components/plugins/install-plugin/base/installed.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Card from '../../card'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../../types'
|
||||
import { pluginManifestInMarketToPluginProps, pluginManifestToCardPluginProps } from '../utils'
|
||||
import Badge, { BadgeState } from '@/app/components/base/badge/index'
|
||||
|
||||
type Props = {
|
||||
payload?: Plugin | PluginDeclaration | PluginManifestInMarket | null
|
||||
isMarketPayload?: boolean
|
||||
isFailed: boolean
|
||||
errMsg?: string | null
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const Installed: FC<Props> = ({
|
||||
payload,
|
||||
isMarketPayload,
|
||||
isFailed,
|
||||
errMsg,
|
||||
onCancel,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleClose = () => {
|
||||
onCancel()
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col px-6 py-3 justify-center items-start gap-4 self-stretch'>
|
||||
<p className='text-text-secondary system-md-regular'>{(isFailed && errMsg) ? errMsg : t(`plugin.installModal.${isFailed ? 'installFailedDesc' : 'installedSuccessfullyDesc'}`)}</p>
|
||||
{payload && (
|
||||
<div className='flex p-2 items-start content-start gap-1 self-stretch flex-wrap rounded-2xl bg-background-section-burn'>
|
||||
<Card
|
||||
className='w-full'
|
||||
payload={isMarketPayload ? pluginManifestInMarketToPluginProps(payload as PluginManifestInMarket) : pluginManifestToCardPluginProps(payload as PluginDeclaration)}
|
||||
installed={!isFailed}
|
||||
installFailed={isFailed}
|
||||
titleLeft={<Badge className='mx-1' size="s" state={BadgeState.Default}>{(payload as PluginDeclaration).version || (payload as PluginManifestInMarket).latest_version}</Badge>}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Action Buttons */}
|
||||
<div className='flex p-6 pt-5 justify-end items-center gap-2 self-stretch'>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px]'
|
||||
onClick={handleClose}
|
||||
>
|
||||
{t('common.operation.close')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(Installed)
|
||||
@@ -0,0 +1,45 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { Group } from '../../../base/icons/src/vender/other'
|
||||
import { LoadingPlaceholder } from '@/app/components/plugins/card/base/placeholder'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const LoadingError: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Checkbox
|
||||
className='shrink-0'
|
||||
checked={false}
|
||||
disabled
|
||||
/>
|
||||
<div className='grow relative p-4 pb-3 border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg hover-bg-components-panel-on-panel-item-bg rounded-xl shadow-xs'>
|
||||
<div className="flex">
|
||||
<div
|
||||
className='relative flex w-10 h-10 p-1 justify-center items-center gap-2 rounded-[10px]
|
||||
border-[0.5px] border-state-destructive-border bg-state-destructive-hover backdrop-blur-sm'>
|
||||
<div className='flex w-5 h-5 justify-center items-center'>
|
||||
<Group className='text-text-quaternary' />
|
||||
</div>
|
||||
<div className='absolute bottom-[-4px] right-[-4px] rounded-full border-[2px] border-components-panel-bg bg-state-destructive-solid'>
|
||||
<RiCloseLine className='w-3 h-3 text-text-primary-on-surface' />
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-3 grow">
|
||||
<div className="flex items-center h-5 system-md-semibold text-text-destructive">
|
||||
{t('plugin.installModal.pluginLoadError')}
|
||||
</div>
|
||||
<div className='mt-0.5 system-xs-regular text-text-tertiary'>
|
||||
{t('plugin.installModal.pluginLoadErrorDesc')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<LoadingPlaceholder className="mt-3 w-[420px]" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(LoadingError)
|
||||
23
web/app/components/plugins/install-plugin/base/loading.tsx
Normal file
23
web/app/components/plugins/install-plugin/base/loading.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import Placeholder from '../../card/base/placeholder'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Checkbox
|
||||
className='shrink-0'
|
||||
checked={false}
|
||||
disabled
|
||||
/>
|
||||
<div className='grow relative p-4 pb-3 border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg hover-bg-components-panel-on-panel-item-bg rounded-xl shadow-xs'>
|
||||
<Placeholder
|
||||
wrapClassName='w-full'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Loading)
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useCallback } from 'react'
|
||||
import { apiPrefix } from '@/config'
|
||||
import { useSelector } from '@/context/app-context'
|
||||
|
||||
const useGetIcon = () => {
|
||||
const currentWorkspace = useSelector(s => s.currentWorkspace)
|
||||
const getIconUrl = useCallback((fileName: string) => {
|
||||
return `${apiPrefix}/workspaces/current/plugin/icon?tenant_id=${currentWorkspace.id}&filename=${fileName}`
|
||||
}, [currentWorkspace.id])
|
||||
|
||||
return {
|
||||
getIconUrl,
|
||||
}
|
||||
}
|
||||
|
||||
export default useGetIcon
|
||||
34
web/app/components/plugins/install-plugin/base/version.tsx
Normal file
34
web/app/components/plugins/install-plugin/base/version.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import Badge, { BadgeState } from '@/app/components/base/badge/index'
|
||||
import type { VersionProps } from '../../types'
|
||||
|
||||
const Version: FC<VersionProps> = ({
|
||||
hasInstalled,
|
||||
installedVersion,
|
||||
toInstallVersion,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
!hasInstalled
|
||||
? (
|
||||
<Badge className='mx-1' size="s" state={BadgeState.Default}>{toInstallVersion}</Badge>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<Badge className='mx-1' size="s" state={BadgeState.Warning}>
|
||||
{`${installedVersion} -> ${toInstallVersion}`}
|
||||
</Badge>
|
||||
{/* <div className='flex px-0.5 justify-center items-center gap-0.5'>
|
||||
<div className='text-text-warning system-xs-medium'>Used in 3 apps</div>
|
||||
<RiInformation2Line className='w-4 h-4 text-text-tertiary' />
|
||||
</div> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(Version)
|
||||
107
web/app/components/plugins/install-plugin/hooks.ts
Normal file
107
web/app/components/plugins/install-plugin/hooks.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import Toast, { type IToastProps } from '@/app/components/base/toast'
|
||||
import { uploadGitHub } from '@/service/plugins'
|
||||
import { compareVersion, getLatestVersion } from '@/utils/semver'
|
||||
import type { GitHubRepoReleaseResponse } from '../types'
|
||||
import { GITHUB_ACCESS_TOKEN } from '@/config'
|
||||
|
||||
const formatReleases = (releases: any) => {
|
||||
return releases.map((release: any) => ({
|
||||
tag_name: release.tag_name,
|
||||
assets: release.assets.map((asset: any) => ({
|
||||
browser_download_url: asset.browser_download_url,
|
||||
name: asset.name,
|
||||
})),
|
||||
}))
|
||||
}
|
||||
|
||||
export const useGitHubReleases = () => {
|
||||
const fetchReleases = async (owner: string, repo: string) => {
|
||||
try {
|
||||
if (!GITHUB_ACCESS_TOKEN) {
|
||||
// Fetch releases without authentication from client
|
||||
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases`)
|
||||
if (!res.ok) throw new Error('Failed to fetch repository releases')
|
||||
const data = await res.json()
|
||||
return formatReleases(data)
|
||||
}
|
||||
else {
|
||||
// Fetch releases with authentication from server
|
||||
const res = await fetch(`/repos/${owner}/${repo}/releases`)
|
||||
const bodyJson = await res.json()
|
||||
if (bodyJson.status !== 200) throw new Error(bodyJson.data.message)
|
||||
return formatReleases(bodyJson.data)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error.message,
|
||||
})
|
||||
}
|
||||
else {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: 'Failed to fetch repository releases',
|
||||
})
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const checkForUpdates = (fetchedReleases: GitHubRepoReleaseResponse[], currentVersion: string) => {
|
||||
let needUpdate = false
|
||||
const toastProps: IToastProps = {
|
||||
type: 'info',
|
||||
message: 'No new version available',
|
||||
}
|
||||
if (fetchedReleases.length === 0) {
|
||||
toastProps.type = 'error'
|
||||
toastProps.message = 'Input releases is empty'
|
||||
return { needUpdate, toastProps }
|
||||
}
|
||||
const versions = fetchedReleases.map(release => release.tag_name)
|
||||
const latestVersion = getLatestVersion(versions)
|
||||
try {
|
||||
needUpdate = compareVersion(latestVersion, currentVersion) === 1
|
||||
if (needUpdate)
|
||||
toastProps.message = `New version available: ${latestVersion}`
|
||||
}
|
||||
catch {
|
||||
needUpdate = false
|
||||
toastProps.type = 'error'
|
||||
toastProps.message = 'Fail to compare versions, please check the version format'
|
||||
}
|
||||
return { needUpdate, toastProps }
|
||||
}
|
||||
|
||||
return { fetchReleases, checkForUpdates }
|
||||
}
|
||||
|
||||
export const useGitHubUpload = () => {
|
||||
const handleUpload = async (
|
||||
repoUrl: string,
|
||||
selectedVersion: string,
|
||||
selectedPackage: string,
|
||||
onSuccess?: (GitHubPackage: { manifest: any; unique_identifier: string }) => void,
|
||||
) => {
|
||||
try {
|
||||
const response = await uploadGitHub(repoUrl, selectedVersion, selectedPackage)
|
||||
const GitHubPackage = {
|
||||
manifest: response.manifest,
|
||||
unique_identifier: response.unique_identifier,
|
||||
}
|
||||
if (onSuccess) onSuccess(GitHubPackage)
|
||||
return GitHubPackage
|
||||
}
|
||||
catch (error) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: 'Error uploading package',
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return { handleUpload }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useCheckInstalled as useDoCheckInstalled } from '@/service/use-plugins'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import type { VersionInfo } from '../../types'
|
||||
type Props = {
|
||||
pluginIds: string[],
|
||||
enabled: boolean
|
||||
}
|
||||
const useCheckInstalled = (props: Props) => {
|
||||
const { data, isLoading, error } = useDoCheckInstalled(props)
|
||||
|
||||
const installedInfo = useMemo(() => {
|
||||
if (!data)
|
||||
return undefined
|
||||
|
||||
const res: Record<string, VersionInfo> = {}
|
||||
data?.plugins.forEach((plugin) => {
|
||||
res[plugin.plugin_id] = {
|
||||
installedId: plugin.id,
|
||||
installedVersion: plugin.declaration.version,
|
||||
uniqueIdentifier: plugin.plugin_unique_identifier,
|
||||
}
|
||||
})
|
||||
return res
|
||||
}, [data])
|
||||
return {
|
||||
installedInfo,
|
||||
isLoading,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
export default useCheckInstalled
|
||||
@@ -0,0 +1,57 @@
|
||||
import { sleep } from '@/utils'
|
||||
|
||||
const animTime = 750
|
||||
const modalClassName = 'install-modal'
|
||||
const COUNT_DOWN_TIME = 15000 // 15s
|
||||
|
||||
function getElemCenter(elem: HTMLElement) {
|
||||
const rect = elem.getBoundingClientRect()
|
||||
return {
|
||||
x: rect.left + rect.width / 2 + window.scrollX,
|
||||
y: rect.top + rect.height / 2 + window.scrollY,
|
||||
}
|
||||
}
|
||||
|
||||
const useFoldAnimInto = (onClose: () => void) => {
|
||||
let countDownRunId: number
|
||||
const clearCountDown = () => {
|
||||
clearTimeout(countDownRunId)
|
||||
}
|
||||
// modalElem fold into plugin install task btn
|
||||
const foldIntoAnim = async () => {
|
||||
clearCountDown()
|
||||
const modalElem = document.querySelector(`.${modalClassName}`) as HTMLElement
|
||||
const pluginTaskTriggerElem = document.getElementById('plugin-task-trigger') || document.querySelector('.plugins-nav-button')
|
||||
|
||||
if (!modalElem || !pluginTaskTriggerElem) {
|
||||
onClose()
|
||||
return
|
||||
}
|
||||
|
||||
const modelCenter = getElemCenter(modalElem)
|
||||
const modalElemRect = modalElem.getBoundingClientRect()
|
||||
const pluginTaskTriggerCenter = getElemCenter(pluginTaskTriggerElem)
|
||||
const xDiff = pluginTaskTriggerCenter.x - modelCenter.x
|
||||
const yDiff = pluginTaskTriggerCenter.y - modelCenter.y
|
||||
const scale = 1 / Math.max(modalElemRect.width, modalElemRect.height)
|
||||
modalElem.style.transition = `all cubic-bezier(0.4, 0, 0.2, 1) ${animTime}ms`
|
||||
modalElem.style.transform = `translate(${xDiff}px, ${yDiff}px) scale(${scale})`
|
||||
await sleep(animTime)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const countDownFoldIntoAnim = async () => {
|
||||
countDownRunId = window.setTimeout(() => {
|
||||
foldIntoAnim()
|
||||
}, COUNT_DOWN_TIME)
|
||||
}
|
||||
|
||||
return {
|
||||
modalClassName,
|
||||
foldIntoAnim,
|
||||
clearCountDown,
|
||||
countDownFoldIntoAnim,
|
||||
}
|
||||
}
|
||||
|
||||
export default useFoldAnimInto
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import useFoldAnimInto from './use-fold-anim-into'
|
||||
|
||||
const useHideLogic = (onClose: () => void) => {
|
||||
const {
|
||||
modalClassName,
|
||||
foldIntoAnim: doFoldAnimInto,
|
||||
clearCountDown,
|
||||
countDownFoldIntoAnim,
|
||||
} = useFoldAnimInto(onClose)
|
||||
|
||||
const [isInstalling, doSetIsInstalling] = useState(false)
|
||||
const setIsInstalling = useCallback((isInstalling: boolean) => {
|
||||
if (!isInstalling)
|
||||
clearCountDown()
|
||||
doSetIsInstalling(isInstalling)
|
||||
}, [clearCountDown])
|
||||
|
||||
const foldAnimInto = useCallback(() => {
|
||||
if (isInstalling) {
|
||||
doFoldAnimInto()
|
||||
return
|
||||
}
|
||||
onClose()
|
||||
}, [doFoldAnimInto, isInstalling, onClose])
|
||||
|
||||
const handleStartToInstall = useCallback(() => {
|
||||
setIsInstalling(true)
|
||||
countDownFoldIntoAnim()
|
||||
}, [countDownFoldIntoAnim, setIsInstalling])
|
||||
|
||||
return {
|
||||
modalClassName,
|
||||
foldAnimInto,
|
||||
setIsInstalling,
|
||||
handleStartToInstall,
|
||||
}
|
||||
}
|
||||
|
||||
export default useHideLogic
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import { useInvalidateAllBuiltInTools, useInvalidateAllToolProviders } from '@/service/use-tools'
|
||||
import { useInvalidateStrategyProviders } from '@/service/use-strategy'
|
||||
import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../../types'
|
||||
import { PluginType } from '../../types'
|
||||
|
||||
const useRefreshPluginList = () => {
|
||||
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
||||
const { mutate: refetchLLMModelList } = useModelList(ModelTypeEnum.textGeneration)
|
||||
const { mutate: refetchEmbeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
|
||||
const { mutate: refetchRerankModelList } = useModelList(ModelTypeEnum.rerank)
|
||||
const { refreshModelProviders } = useProviderContext()
|
||||
|
||||
const invalidateAllToolProviders = useInvalidateAllToolProviders()
|
||||
const invalidateAllBuiltInTools = useInvalidateAllBuiltInTools()
|
||||
|
||||
const invalidateStrategyProviders = useInvalidateStrategyProviders()
|
||||
return {
|
||||
refreshPluginList: (manifest?: PluginManifestInMarket | Plugin | PluginDeclaration | null, refreshAllType?: boolean) => {
|
||||
// installed list
|
||||
invalidateInstalledPluginList()
|
||||
|
||||
if (!manifest) return
|
||||
|
||||
// tool page, tool select
|
||||
if (PluginType.tool.includes(manifest.category) || refreshAllType) {
|
||||
invalidateAllToolProviders()
|
||||
invalidateAllBuiltInTools()
|
||||
// TODO: update suggested tools. It's a function in hook useMarketplacePlugins,handleUpdatePlugins
|
||||
}
|
||||
|
||||
// model select
|
||||
if (PluginType.model.includes(manifest.category) || refreshAllType) {
|
||||
refreshModelProviders()
|
||||
refetchLLMModelList()
|
||||
refetchEmbeddingModelList()
|
||||
refetchRerankModelList()
|
||||
}
|
||||
|
||||
// agent select
|
||||
if (PluginType.agent.includes(manifest.category) || refreshAllType)
|
||||
invalidateStrategyProviders()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default useRefreshPluginList
|
||||
@@ -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, 'flex min-w-[560px] p-0 flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadows-shadow-xl')}
|
||||
closable
|
||||
>
|
||||
<div className='flex pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'>
|
||||
<div className='self-stretch text-text-primary title-2xl-semi-bold'>
|
||||
{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,62 @@
|
||||
'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)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data])
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
onFetchError()
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error])
|
||||
if (!payload) return <Loading />
|
||||
return (
|
||||
<LoadedItem
|
||||
payload={payload}
|
||||
versionInfo={versionInfo}
|
||||
checked={checked}
|
||||
onCheckedChange={onCheckedChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(Item)
|
||||
@@ -0,0 +1,51 @@
|
||||
'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'
|
||||
|
||||
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,
|
||||
}
|
||||
return (
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Checkbox
|
||||
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}
|
||||
/>
|
||||
</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}
|
||||
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, InstallStatusResponse, 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<InstallStatusResponse[]>([])
|
||||
const handleInstalled = useCallback((plugins: Plugin[], installStatus: InstallStatusResponse[]) => {
|
||||
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,221 @@
|
||||
'use client'
|
||||
import type { FC } 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 { useFetchPluginsInMarketPlaceByIds, 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'
|
||||
|
||||
type Props = {
|
||||
allPlugins: Dependency[]
|
||||
selectedPlugins: Plugin[]
|
||||
onSelect: (plugin: Plugin, selectedIndex: number) => void
|
||||
onLoadedAllPlugin: (installedInfo: Record<string, VersionInfo>) => void
|
||||
isFromMarketPlace?: boolean
|
||||
}
|
||||
|
||||
const InstallByDSLList: FC<Props> = ({
|
||||
allPlugins,
|
||||
selectedPlugins,
|
||||
onSelect,
|
||||
onLoadedAllPlugin,
|
||||
isFromMarketPlace,
|
||||
}) => {
|
||||
// DSL has id, to get plugin info to show more info
|
||||
const { isLoading: isFetchingMarketplaceDataById, data: infoGetById, error: infoByIdError } = useFetchPluginsInMarketPlaceByIds(allPlugins.filter(d => d.type === 'marketplace').map(d => (d as GitHubItemAndMarketPlaceDependency).value.marketplace_plugin_unique_identifier!))
|
||||
// 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.plugins) {
|
||||
const payloads = infoGetById?.data.plugins
|
||||
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])
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [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])
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isFetchingDataByMeta])
|
||||
|
||||
useEffect(() => {
|
||||
// get info all failed
|
||||
if (infoByMetaError || infoByIdError)
|
||||
setErrorIndexes([...errorIndexes, ...marketPlaceInDSLIndex])
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [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!)
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoadedAllData, installedInfo])
|
||||
|
||||
const handleSelect = useCallback((index: number) => {
|
||||
return () => {
|
||||
onSelect(plugins[index]!, index)
|
||||
}
|
||||
}, [onSelect, plugins])
|
||||
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}
|
||||
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 React.memo(InstallByDSLList)
|
||||
@@ -0,0 +1,116 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import type { Dependency, InstallStatusResponse, Plugin, VersionInfo } from '../../../types'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import InstallMulti from './install-multi'
|
||||
import { useInstallOrUpdate } from '@/service/use-plugins'
|
||||
import useRefreshPluginList from '../../hooks/use-refresh-plugin-list'
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
|
||||
type Props = {
|
||||
allPlugins: Dependency[]
|
||||
onStartToInstall?: () => void
|
||||
onInstalled: (plugins: Plugin[], installStatus: InstallStatusResponse[]) => void
|
||||
onCancel: () => void
|
||||
isFromMarketPlace?: boolean
|
||||
isHideButton?: boolean
|
||||
}
|
||||
|
||||
const Install: FC<Props> = ({
|
||||
allPlugins,
|
||||
onStartToInstall,
|
||||
onInstalled,
|
||||
onCancel,
|
||||
isFromMarketPlace,
|
||||
isHideButton,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [selectedPlugins, setSelectedPlugins] = React.useState<Plugin[]>([])
|
||||
const [selectedIndexes, setSelectedIndexes] = React.useState<number[]>([])
|
||||
const selectedPluginsNum = selectedPlugins.length
|
||||
const { refreshPluginList } = useRefreshPluginList()
|
||||
const handleSelect = (plugin: Plugin, selectedIndex: 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)
|
||||
}
|
||||
|
||||
const [canInstall, setCanInstall] = React.useState(false)
|
||||
const [installedInfo, setInstalledInfo] = useState<Record<string, VersionInfo> | undefined>(undefined)
|
||||
|
||||
const handleLoadedAllPlugin = useCallback((installedInfo: Record<string, VersionInfo> | undefined) => {
|
||||
setInstalledInfo(installedInfo)
|
||||
setCanInstall(true)
|
||||
}, [])
|
||||
|
||||
// Install from marketplace and github
|
||||
const { mutate: installOrUpdate, isPending: isInstalling } = useInstallOrUpdate({
|
||||
onSuccess: (res: InstallStatusResponse[]) => {
|
||||
onInstalled(selectedPlugins, res.map((r, i) => {
|
||||
return ({
|
||||
...r,
|
||||
isFromMarketPlace: allPlugins[selectedIndexes[i]].type === 'marketplace',
|
||||
})
|
||||
}))
|
||||
const hasInstallSuccess = res.some(r => r.success)
|
||||
if (hasInstallSuccess)
|
||||
refreshPluginList(undefined, true)
|
||||
},
|
||||
})
|
||||
const handleInstall = () => {
|
||||
onStartToInstall?.()
|
||||
installOrUpdate({
|
||||
payload: allPlugins.filter((_d, index) => selectedIndexes.includes(index)),
|
||||
plugin: selectedPlugins,
|
||||
installedInfo: installedInfo!,
|
||||
})
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col px-6 py-3 justify-center items-start gap-4 self-stretch'>
|
||||
<div className='text-text-secondary system-md-regular'>
|
||||
<p>{t(`${i18nPrefix}.${selectedPluginsNum > 1 ? 'readyToInstallPackages' : 'readyToInstallPackage'}`, { num: selectedPluginsNum })}</p>
|
||||
</div>
|
||||
<div className='w-full p-2 rounded-2xl bg-background-section-burn space-y-1'>
|
||||
<InstallMulti
|
||||
allPlugins={allPlugins}
|
||||
selectedPlugins={selectedPlugins}
|
||||
onSelect={handleSelect}
|
||||
onLoadedAllPlugin={handleLoadedAllPlugin}
|
||||
isFromMarketPlace={isFromMarketPlace}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Action Buttons */}
|
||||
{!isHideButton && (
|
||||
<div className='flex p-6 pt-5 justify-end items-center gap-2 self-stretch'>
|
||||
{!canInstall && (
|
||||
<Button variant='secondary' className='min-w-[72px]' onClick={onCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px] flex space-x-0.5'
|
||||
disabled={!canInstall || isInstalling || selectedPlugins.length === 0}
|
||||
onClick={handleInstall}
|
||||
>
|
||||
{isInstalling && <RiLoader2Line className='w-4 h-4 animate-spin-slow' />}
|
||||
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(Install)
|
||||
@@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { InstallStatusResponse, 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: InstallStatusResponse[]
|
||||
onCancel: () => void
|
||||
isHideButton?: boolean
|
||||
}
|
||||
|
||||
const Installed: FC<Props> = ({
|
||||
list,
|
||||
installStatus,
|
||||
onCancel,
|
||||
isHideButton,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { getIconUrl } = useGetIcon()
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col px-6 py-3 justify-center items-start gap-4 self-stretch'>
|
||||
{/* <p className='text-text-secondary system-md-regular'>{(isFailed && errMsg) ? errMsg : t(`plugin.installModal.${isFailed ? 'installFailedDesc' : 'installedSuccessfullyDesc'}`)}</p> */}
|
||||
<div className='flex p-2 items-start content-start gap-1 self-stretch flex-wrap rounded-2xl bg-background-section-burn space-y-1'>
|
||||
{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 p-6 pt-5 justify-end items-center gap-2 self-stretch'>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px]'
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('common.operation.close')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Installed)
|
||||
@@ -0,0 +1,235 @@
|
||||
'use client'
|
||||
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import type { InstallState } from '@/app/components/plugins/types'
|
||||
import { useGitHubReleases } from '../hooks'
|
||||
import { convertRepoToUrl, parseGitHubUrl } from '../utils'
|
||||
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../types'
|
||||
import { InstallStepFromGitHub } from '../../types'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import SetURL from './steps/setURL'
|
||||
import SelectPackage from './steps/selectPackage'
|
||||
import Installed from '../base/installed'
|
||||
import Loaded from './steps/loaded'
|
||||
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
|
||||
import cn from '@/utils/classnames'
|
||||
import useHideLogic from '../hooks/use-hide-logic'
|
||||
|
||||
const i18nPrefix = 'plugin.installFromGitHub'
|
||||
|
||||
type InstallFromGitHubProps = {
|
||||
updatePayload?: UpdateFromGitHubPayload
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
}
|
||||
|
||||
const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, onClose, onSuccess }) => {
|
||||
const { t } = useTranslation()
|
||||
const { getIconUrl } = useGetIcon()
|
||||
const { fetchReleases } = useGitHubReleases()
|
||||
const { refreshPluginList } = useRefreshPluginList()
|
||||
|
||||
const {
|
||||
modalClassName,
|
||||
foldAnimInto,
|
||||
setIsInstalling,
|
||||
handleStartToInstall,
|
||||
} = useHideLogic(onClose)
|
||||
|
||||
const [state, setState] = useState<InstallState>({
|
||||
step: updatePayload ? InstallStepFromGitHub.selectPackage : InstallStepFromGitHub.setUrl,
|
||||
repoUrl: updatePayload?.originalPackageInfo?.repo
|
||||
? convertRepoToUrl(updatePayload.originalPackageInfo.repo)
|
||||
: '',
|
||||
selectedVersion: '',
|
||||
selectedPackage: '',
|
||||
releases: updatePayload ? updatePayload.originalPackageInfo.releases : [],
|
||||
})
|
||||
const [uniqueIdentifier, setUniqueIdentifier] = useState<string | null>(null)
|
||||
const [manifest, setManifest] = useState<PluginDeclaration | null>(null)
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null)
|
||||
|
||||
const versions: Item[] = state.releases.map(release => ({
|
||||
value: release.tag_name,
|
||||
name: release.tag_name,
|
||||
}))
|
||||
|
||||
const packages: Item[] = state.selectedVersion
|
||||
? (state.releases
|
||||
.find(release => release.tag_name === state.selectedVersion)
|
||||
?.assets
|
||||
.map(asset => ({
|
||||
value: asset.name,
|
||||
name: asset.name,
|
||||
})) || [])
|
||||
: []
|
||||
|
||||
const getTitle = useCallback(() => {
|
||||
if (state.step === InstallStepFromGitHub.installed)
|
||||
return t(`${i18nPrefix}.installedSuccessfully`)
|
||||
if (state.step === InstallStepFromGitHub.installFailed)
|
||||
return t(`${i18nPrefix}.installFailed`)
|
||||
|
||||
return updatePayload ? t(`${i18nPrefix}.updatePlugin`) : t(`${i18nPrefix}.installPlugin`)
|
||||
}, [state.step, t, updatePayload])
|
||||
|
||||
const handleUrlSubmit = async () => {
|
||||
const { isValid, owner, repo } = parseGitHubUrl(state.repoUrl)
|
||||
if (!isValid || !owner || !repo) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('plugin.error.inValidGitHubUrl'),
|
||||
})
|
||||
return
|
||||
}
|
||||
try {
|
||||
const fetchedReleases = await fetchReleases(owner, repo)
|
||||
if (fetchedReleases.length > 0) {
|
||||
setState(prevState => ({
|
||||
...prevState,
|
||||
releases: fetchedReleases,
|
||||
step: InstallStepFromGitHub.selectPackage,
|
||||
}))
|
||||
}
|
||||
else {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('plugin.error.noReleasesFound'),
|
||||
})
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('plugin.error.fetchReleasesError'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleError = (e: any, isInstall: boolean) => {
|
||||
const message = e?.response?.message || t('plugin.installModal.installFailedDesc')
|
||||
setErrorMsg(message)
|
||||
setState(prevState => ({ ...prevState, step: isInstall ? InstallStepFromGitHub.installFailed : InstallStepFromGitHub.uploadFailed }))
|
||||
}
|
||||
|
||||
const handleUploaded = async (GitHubPackage: any) => {
|
||||
try {
|
||||
const icon = await getIconUrl(GitHubPackage.manifest.icon)
|
||||
setManifest({
|
||||
...GitHubPackage.manifest,
|
||||
icon,
|
||||
})
|
||||
setUniqueIdentifier(GitHubPackage.uniqueIdentifier)
|
||||
setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.readyToInstall }))
|
||||
}
|
||||
catch (e) {
|
||||
handleError(e, false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUploadFail = useCallback((errorMsg: string) => {
|
||||
setErrorMsg(errorMsg)
|
||||
setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.uploadFailed }))
|
||||
}, [])
|
||||
|
||||
const handleInstalled = useCallback((notRefresh?: boolean) => {
|
||||
setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installed }))
|
||||
if (!notRefresh)
|
||||
refreshPluginList(manifest)
|
||||
setIsInstalling(false)
|
||||
onSuccess()
|
||||
}, [manifest, onSuccess, refreshPluginList, setIsInstalling])
|
||||
|
||||
const handleFailed = useCallback((errorMsg?: string) => {
|
||||
setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installFailed }))
|
||||
setIsInstalling(false)
|
||||
if (errorMsg)
|
||||
setErrorMsg(errorMsg)
|
||||
}, [setIsInstalling])
|
||||
|
||||
const handleBack = () => {
|
||||
setState((prevState) => {
|
||||
switch (prevState.step) {
|
||||
case InstallStepFromGitHub.selectPackage:
|
||||
return { ...prevState, step: InstallStepFromGitHub.setUrl }
|
||||
case InstallStepFromGitHub.readyToInstall:
|
||||
return { ...prevState, step: InstallStepFromGitHub.selectPackage }
|
||||
default:
|
||||
return prevState
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={true}
|
||||
onClose={foldAnimInto}
|
||||
className={cn(modalClassName, `flex min-w-[560px] p-0 flex-col items-start rounded-2xl border-[0.5px]
|
||||
border-components-panel-border bg-components-panel-bg shadows-shadow-xl`)}
|
||||
closable
|
||||
>
|
||||
<div className='flex pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'>
|
||||
<div className='flex flex-col items-start gap-1 grow'>
|
||||
<div className='self-stretch text-text-primary title-2xl-semi-bold'>
|
||||
{getTitle()}
|
||||
</div>
|
||||
<div className='self-stretch text-text-tertiary system-xs-regular'>
|
||||
{!([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step)) && t('plugin.installFromGitHub.installNote')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step))
|
||||
? <Installed
|
||||
payload={manifest}
|
||||
isFailed={[InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installFailed].includes(state.step)}
|
||||
errMsg={errorMsg}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
: <div className={`flex px-6 py-3 flex-col justify-center items-start self-stretch ${state.step === InstallStepFromGitHub.installed ? 'gap-2' : 'gap-4'}`}>
|
||||
{state.step === InstallStepFromGitHub.setUrl && (
|
||||
<SetURL
|
||||
repoUrl={state.repoUrl}
|
||||
onChange={value => setState(prevState => ({ ...prevState, repoUrl: value }))}
|
||||
onNext={handleUrlSubmit}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
)}
|
||||
{state.step === InstallStepFromGitHub.selectPackage && (
|
||||
<SelectPackage
|
||||
updatePayload={updatePayload!}
|
||||
repoUrl={state.repoUrl}
|
||||
selectedVersion={state.selectedVersion}
|
||||
versions={versions}
|
||||
onSelectVersion={item => setState(prevState => ({ ...prevState, selectedVersion: item.value as string }))}
|
||||
selectedPackage={state.selectedPackage}
|
||||
packages={packages}
|
||||
onSelectPackage={item => setState(prevState => ({ ...prevState, selectedPackage: item.value as string }))}
|
||||
onUploaded={handleUploaded}
|
||||
onFailed={handleUploadFail}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
)}
|
||||
{state.step === InstallStepFromGitHub.readyToInstall && (
|
||||
<Loaded
|
||||
updatePayload={updatePayload!}
|
||||
uniqueIdentifier={uniqueIdentifier!}
|
||||
payload={manifest as any}
|
||||
repoUrl={state.repoUrl}
|
||||
selectedVersion={state.selectedVersion}
|
||||
selectedPackage={state.selectedPackage}
|
||||
onBack={handleBack}
|
||||
onStartToInstall={handleStartToInstall}
|
||||
onInstalled={handleInstalled}
|
||||
onFailed={handleFailed}
|
||||
/>
|
||||
)}
|
||||
</div>}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default InstallFromGitHub
|
||||
@@ -0,0 +1,180 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect } from 'react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { type Plugin, type PluginDeclaration, TaskStatus, type UpdateFromGitHubPayload } from '../../../types'
|
||||
import Card from '../../../card'
|
||||
import { pluginManifestToCardPluginProps } from '../../utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { updateFromGitHub } from '@/service/plugins'
|
||||
import { useInstallPackageFromGitHub } from '@/service/use-plugins'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import { usePluginTaskList } from '@/service/use-plugins'
|
||||
import checkTaskStatus from '../../base/check-task-status'
|
||||
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
|
||||
import { parseGitHubUrl } from '../../utils'
|
||||
import Version from '../../base/version'
|
||||
|
||||
type LoadedProps = {
|
||||
updatePayload: UpdateFromGitHubPayload
|
||||
uniqueIdentifier: string
|
||||
payload: PluginDeclaration | Plugin
|
||||
repoUrl: string
|
||||
selectedVersion: string
|
||||
selectedPackage: string
|
||||
onBack: () => void
|
||||
onStartToInstall?: () => void
|
||||
onInstalled: (notRefresh?: boolean) => void
|
||||
onFailed: (message?: string) => void
|
||||
}
|
||||
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
|
||||
const Loaded: React.FC<LoadedProps> = ({
|
||||
updatePayload,
|
||||
uniqueIdentifier,
|
||||
payload,
|
||||
repoUrl,
|
||||
selectedVersion,
|
||||
selectedPackage,
|
||||
onBack,
|
||||
onStartToInstall,
|
||||
onInstalled,
|
||||
onFailed,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const toInstallVersion = payload.version
|
||||
const pluginId = (payload as Plugin).plugin_id
|
||||
const { installedInfo, isLoading } = useCheckInstalled({
|
||||
pluginIds: [pluginId],
|
||||
enabled: !!pluginId,
|
||||
})
|
||||
const installedInfoPayload = installedInfo?.[pluginId]
|
||||
const installedVersion = installedInfoPayload?.installedVersion
|
||||
const hasInstalled = !!installedVersion
|
||||
|
||||
const [isInstalling, setIsInstalling] = React.useState(false)
|
||||
const { mutateAsync: installPackageFromGitHub } = useInstallPackageFromGitHub()
|
||||
const { handleRefetch } = usePluginTaskList(payload.category)
|
||||
const { check } = checkTaskStatus()
|
||||
|
||||
useEffect(() => {
|
||||
if (hasInstalled && uniqueIdentifier === installedInfoPayload.uniqueIdentifier)
|
||||
onInstalled()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hasInstalled])
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (isInstalling) return
|
||||
setIsInstalling(true)
|
||||
onStartToInstall?.()
|
||||
|
||||
try {
|
||||
const { owner, repo } = parseGitHubUrl(repoUrl)
|
||||
let taskId
|
||||
let isInstalled
|
||||
if (updatePayload) {
|
||||
const { all_installed, task_id } = await updateFromGitHub(
|
||||
`${owner}/${repo}`,
|
||||
selectedVersion,
|
||||
selectedPackage,
|
||||
updatePayload.originalPackageInfo.id,
|
||||
uniqueIdentifier,
|
||||
)
|
||||
|
||||
taskId = task_id
|
||||
isInstalled = all_installed
|
||||
}
|
||||
else {
|
||||
if (hasInstalled) {
|
||||
const {
|
||||
all_installed,
|
||||
task_id,
|
||||
} = await updateFromGitHub(
|
||||
`${owner}/${repo}`,
|
||||
selectedVersion,
|
||||
selectedPackage,
|
||||
installedInfoPayload.uniqueIdentifier,
|
||||
uniqueIdentifier,
|
||||
)
|
||||
taskId = task_id
|
||||
isInstalled = all_installed
|
||||
}
|
||||
else {
|
||||
const { all_installed, task_id } = await installPackageFromGitHub({
|
||||
repoUrl: `${owner}/${repo}`,
|
||||
selectedVersion,
|
||||
selectedPackage,
|
||||
uniqueIdentifier,
|
||||
})
|
||||
|
||||
taskId = task_id
|
||||
isInstalled = all_installed
|
||||
}
|
||||
}
|
||||
if (isInstalled) {
|
||||
onInstalled()
|
||||
return
|
||||
}
|
||||
|
||||
handleRefetch()
|
||||
|
||||
const { status, error } = await check({
|
||||
taskId,
|
||||
pluginUniqueIdentifier: uniqueIdentifier,
|
||||
})
|
||||
if (status === TaskStatus.failed) {
|
||||
onFailed(error)
|
||||
return
|
||||
}
|
||||
onInstalled(true)
|
||||
}
|
||||
catch (e) {
|
||||
if (typeof e === 'string') {
|
||||
onFailed(e)
|
||||
return
|
||||
}
|
||||
onFailed()
|
||||
}
|
||||
finally {
|
||||
setIsInstalling(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='text-text-secondary system-md-regular'>
|
||||
<p>{t(`${i18nPrefix}.readyToInstall`)}</p>
|
||||
</div>
|
||||
<div className='flex p-2 items-start content-start gap-1 self-stretch flex-wrap rounded-2xl bg-background-section-burn'>
|
||||
<Card
|
||||
className='w-full'
|
||||
payload={pluginManifestToCardPluginProps(payload as PluginDeclaration)}
|
||||
titleLeft={!isLoading && <Version
|
||||
hasInstalled={hasInstalled}
|
||||
installedVersion={installedVersion}
|
||||
toInstallVersion={toInstallVersion}
|
||||
/>}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex justify-end items-center gap-2 self-stretch mt-4'>
|
||||
{!isInstalling && (
|
||||
<Button variant='secondary' className='min-w-[72px]' onClick={onBack}>
|
||||
{t('plugin.installModal.back')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px] flex space-x-0.5'
|
||||
onClick={handleInstall}
|
||||
disabled={isInstalling || isLoading}
|
||||
>
|
||||
{isInstalling && <RiLoader2Line className='w-4 h-4 animate-spin-slow' />}
|
||||
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loaded
|
||||
@@ -0,0 +1,125 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../../types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useGitHubUpload } from '../../hooks'
|
||||
|
||||
const i18nPrefix = 'plugin.installFromGitHub'
|
||||
|
||||
type SelectPackageProps = {
|
||||
updatePayload: UpdateFromGitHubPayload
|
||||
repoUrl: string
|
||||
selectedVersion: string
|
||||
versions: Item[]
|
||||
onSelectVersion: (item: Item) => void
|
||||
selectedPackage: string
|
||||
packages: Item[]
|
||||
onSelectPackage: (item: Item) => void
|
||||
onUploaded: (result: {
|
||||
uniqueIdentifier: string
|
||||
manifest: PluginDeclaration
|
||||
}) => void
|
||||
onFailed: (errorMsg: string) => void
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
const SelectPackage: React.FC<SelectPackageProps> = ({
|
||||
updatePayload,
|
||||
repoUrl,
|
||||
selectedVersion,
|
||||
versions,
|
||||
onSelectVersion,
|
||||
selectedPackage,
|
||||
packages,
|
||||
onSelectPackage,
|
||||
onUploaded,
|
||||
onFailed,
|
||||
onBack,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isEdit = Boolean(updatePayload)
|
||||
const [isUploading, setIsUploading] = React.useState(false)
|
||||
const { handleUpload } = useGitHubUpload()
|
||||
|
||||
const handleUploadPackage = async () => {
|
||||
if (isUploading) return
|
||||
setIsUploading(true)
|
||||
try {
|
||||
const repo = repoUrl.replace('https://github.com/', '')
|
||||
await handleUpload(repo, selectedVersion, selectedPackage, (GitHubPackage) => {
|
||||
onUploaded({
|
||||
uniqueIdentifier: GitHubPackage.unique_identifier,
|
||||
manifest: GitHubPackage.manifest,
|
||||
})
|
||||
})
|
||||
}
|
||||
catch (e: any) {
|
||||
if (e.response?.message)
|
||||
onFailed(e.response?.message)
|
||||
else
|
||||
onFailed(t(`${i18nPrefix}.uploadFailed`))
|
||||
}
|
||||
finally {
|
||||
setIsUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
htmlFor='version'
|
||||
className='flex flex-col justify-center items-start self-stretch text-text-secondary'
|
||||
>
|
||||
<span className='system-sm-semibold'>{t(`${i18nPrefix}.selectVersion`)}</span>
|
||||
</label>
|
||||
<PortalSelect
|
||||
value={selectedVersion}
|
||||
onSelect={onSelectVersion}
|
||||
items={versions}
|
||||
installedValue={updatePayload?.originalPackageInfo.version}
|
||||
placeholder={t(`${i18nPrefix}.selectVersionPlaceholder`) || ''}
|
||||
popupClassName='w-[512px] z-[1001]'
|
||||
/>
|
||||
<label
|
||||
htmlFor='package'
|
||||
className='flex flex-col justify-center items-start self-stretch text-text-secondary'
|
||||
>
|
||||
<span className='system-sm-semibold'>{t(`${i18nPrefix}.selectPackage`)}</span>
|
||||
</label>
|
||||
<PortalSelect
|
||||
value={selectedPackage}
|
||||
onSelect={onSelectPackage}
|
||||
items={packages}
|
||||
readonly={!selectedVersion}
|
||||
placeholder={t(`${i18nPrefix}.selectPackagePlaceholder`) || ''}
|
||||
popupClassName='w-[512px] z-[1001]'
|
||||
/>
|
||||
<div className='flex justify-end items-center gap-2 self-stretch mt-4'>
|
||||
{!isEdit
|
||||
&& <Button
|
||||
variant='secondary'
|
||||
className='min-w-[72px]'
|
||||
onClick={onBack}
|
||||
disabled={isUploading}
|
||||
>
|
||||
{t('plugin.installModal.back')}
|
||||
</Button>
|
||||
}
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px]'
|
||||
onClick={handleUploadPackage}
|
||||
disabled={!selectedVersion || !selectedPackage || isUploading}
|
||||
>
|
||||
{t('plugin.installModal.next')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectPackage
|
||||
@@ -0,0 +1,56 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type SetURLProps = {
|
||||
repoUrl: string
|
||||
onChange: (value: string) => void
|
||||
onNext: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const SetURL: React.FC<SetURLProps> = ({ repoUrl, onChange, onNext, onCancel }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
htmlFor='repoUrl'
|
||||
className='flex flex-col justify-center items-start self-stretch text-text-secondary'
|
||||
>
|
||||
<span className='system-sm-semibold'>{t('plugin.installFromGitHub.gitHubRepo')}</span>
|
||||
</label>
|
||||
<input
|
||||
type='url'
|
||||
id='repoUrl'
|
||||
name='repoUrl'
|
||||
value={repoUrl}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
className='flex items-center self-stretch rounded-lg border border-components-input-border-active
|
||||
bg-components-input-bg-active shadows-shadow-xs p-2 gap-[2px] flex-grow overflow-hidden
|
||||
text-components-input-text-filled text-ellipsis system-sm-regular'
|
||||
placeholder='Please enter GitHub repo URL'
|
||||
/>
|
||||
<div className='flex justify-end items-center gap-2 self-stretch mt-4'>
|
||||
<Button
|
||||
variant='secondary'
|
||||
className='min-w-[72px]'
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('plugin.installModal.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px]'
|
||||
onClick={onNext}
|
||||
disabled={!repoUrl.trim()}
|
||||
>
|
||||
{t('plugin.installModal.next')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SetURL
|
||||
@@ -0,0 +1,133 @@
|
||||
'use client'
|
||||
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import type { Dependency, PluginDeclaration } from '../../types'
|
||||
import { InstallStep } from '../../types'
|
||||
import Uploading from './steps/uploading'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
|
||||
import ReadyToInstallPackage from './ready-to-install'
|
||||
import ReadyToInstallBundle from '../install-bundle/ready-to-install'
|
||||
import useHideLogic from '../hooks/use-hide-logic'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
|
||||
type InstallFromLocalPackageProps = {
|
||||
file: File
|
||||
onSuccess: () => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({
|
||||
file,
|
||||
onClose,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
// uploading -> !uploadFailed -> readyToInstall -> installed/failed
|
||||
const [step, setStep] = useState<InstallStep>(InstallStep.uploading)
|
||||
const [uniqueIdentifier, setUniqueIdentifier] = useState<string | null>(null)
|
||||
const [manifest, setManifest] = useState<PluginDeclaration | null>(null)
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null)
|
||||
const isBundle = file.name.endsWith('.difybndl')
|
||||
const [dependencies, setDependencies] = useState<Dependency[]>([])
|
||||
|
||||
const {
|
||||
modalClassName,
|
||||
foldAnimInto,
|
||||
setIsInstalling,
|
||||
handleStartToInstall,
|
||||
} = useHideLogic(onClose)
|
||||
|
||||
const getTitle = useCallback(() => {
|
||||
if (step === InstallStep.uploadFailed)
|
||||
return t(`${i18nPrefix}.uploadFailed`)
|
||||
if (isBundle && step === InstallStep.installed)
|
||||
return t(`${i18nPrefix}.installComplete`)
|
||||
if (step === InstallStep.installed)
|
||||
return t(`${i18nPrefix}.installedSuccessfully`)
|
||||
if (step === InstallStep.installFailed)
|
||||
return t(`${i18nPrefix}.installFailed`)
|
||||
|
||||
return t(`${i18nPrefix}.installPlugin`)
|
||||
}, [isBundle, step, t])
|
||||
|
||||
const { getIconUrl } = useGetIcon()
|
||||
|
||||
const handlePackageUploaded = useCallback(async (result: {
|
||||
uniqueIdentifier: string
|
||||
manifest: PluginDeclaration
|
||||
}) => {
|
||||
const {
|
||||
manifest,
|
||||
uniqueIdentifier,
|
||||
} = result
|
||||
const icon = await getIconUrl(manifest!.icon)
|
||||
setUniqueIdentifier(uniqueIdentifier)
|
||||
setManifest({
|
||||
...manifest,
|
||||
icon,
|
||||
})
|
||||
setStep(InstallStep.readyToInstall)
|
||||
}, [getIconUrl])
|
||||
|
||||
const handleBundleUploaded = useCallback((result: Dependency[]) => {
|
||||
setDependencies(result)
|
||||
setStep(InstallStep.readyToInstall)
|
||||
}, [])
|
||||
|
||||
const handleUploadFail = useCallback((errorMsg: string) => {
|
||||
setErrorMsg(errorMsg)
|
||||
setStep(InstallStep.uploadFailed)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={true}
|
||||
onClose={foldAnimInto}
|
||||
className={cn(modalClassName, 'flex min-w-[560px] p-0 flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadows-shadow-xl')}
|
||||
closable
|
||||
>
|
||||
<div className='flex pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'>
|
||||
<div className='self-stretch text-text-primary title-2xl-semi-bold'>
|
||||
{getTitle()}
|
||||
</div>
|
||||
</div>
|
||||
{step === InstallStep.uploading && (
|
||||
<Uploading
|
||||
isBundle={isBundle}
|
||||
file={file}
|
||||
onCancel={onClose}
|
||||
onPackageUploaded={handlePackageUploaded}
|
||||
onBundleUploaded={handleBundleUploaded}
|
||||
onFailed={handleUploadFail}
|
||||
/>
|
||||
)}
|
||||
{isBundle ? (
|
||||
<ReadyToInstallBundle
|
||||
step={step}
|
||||
onStepChange={setStep}
|
||||
onStartToInstall={handleStartToInstall}
|
||||
setIsInstalling={setIsInstalling}
|
||||
onClose={onClose}
|
||||
allPlugins={dependencies}
|
||||
/>
|
||||
) : (
|
||||
<ReadyToInstallPackage
|
||||
step={step}
|
||||
onStepChange={setStep}
|
||||
onStartToInstall={handleStartToInstall}
|
||||
setIsInstalling={setIsInstalling}
|
||||
onClose={onClose}
|
||||
uniqueIdentifier={uniqueIdentifier}
|
||||
manifest={manifest}
|
||||
errorMsg={errorMsg}
|
||||
onError={setErrorMsg}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default InstallFromLocalPackage
|
||||
@@ -0,0 +1,76 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import type { PluginDeclaration } from '../../types'
|
||||
import { InstallStep } from '../../types'
|
||||
import Install from './steps/install'
|
||||
import Installed from '../base/installed'
|
||||
import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
|
||||
|
||||
type Props = {
|
||||
step: InstallStep
|
||||
onStepChange: (step: InstallStep) => void,
|
||||
onStartToInstall: () => void
|
||||
setIsInstalling: (isInstalling: boolean) => void
|
||||
onClose: () => void
|
||||
uniqueIdentifier: string | null,
|
||||
manifest: PluginDeclaration | null,
|
||||
errorMsg: string | null,
|
||||
onError: (errorMsg: string) => void,
|
||||
}
|
||||
|
||||
const ReadyToInstall: FC<Props> = ({
|
||||
step,
|
||||
onStepChange,
|
||||
onStartToInstall,
|
||||
setIsInstalling,
|
||||
onClose,
|
||||
uniqueIdentifier,
|
||||
manifest,
|
||||
errorMsg,
|
||||
onError,
|
||||
}) => {
|
||||
const { refreshPluginList } = useRefreshPluginList()
|
||||
|
||||
const handleInstalled = useCallback((notRefresh?: boolean) => {
|
||||
onStepChange(InstallStep.installed)
|
||||
if (!notRefresh)
|
||||
refreshPluginList(manifest)
|
||||
setIsInstalling(false)
|
||||
}, [manifest, onStepChange, refreshPluginList, setIsInstalling])
|
||||
|
||||
const handleFailed = useCallback((errorMsg?: string) => {
|
||||
onStepChange(InstallStep.installFailed)
|
||||
setIsInstalling(false)
|
||||
if (errorMsg)
|
||||
onError(errorMsg)
|
||||
}, [onError, onStepChange, setIsInstalling])
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
step === InstallStep.readyToInstall && (
|
||||
<Install
|
||||
uniqueIdentifier={uniqueIdentifier!}
|
||||
payload={manifest!}
|
||||
onCancel={onClose}
|
||||
onInstalled={handleInstalled}
|
||||
onFailed={handleFailed}
|
||||
onStartToInstall={onStartToInstall}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
([InstallStep.uploadFailed, InstallStep.installed, InstallStep.installFailed].includes(step)) && (
|
||||
<Installed
|
||||
payload={manifest}
|
||||
isFailed={[InstallStep.uploadFailed, InstallStep.installFailed].includes(step)}
|
||||
errMsg={errorMsg}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(ReadyToInstall)
|
||||
@@ -0,0 +1,150 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { type PluginDeclaration, TaskStatus } from '../../../types'
|
||||
import Card from '../../../card'
|
||||
import { pluginManifestToCardPluginProps } from '../../utils'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import checkTaskStatus from '../../base/check-task-status'
|
||||
import { useInstallPackageFromLocal, usePluginTaskList } from '@/service/use-plugins'
|
||||
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
|
||||
import { uninstallPlugin } from '@/service/plugins'
|
||||
import Version from '../../base/version'
|
||||
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
|
||||
type Props = {
|
||||
uniqueIdentifier: string
|
||||
payload: PluginDeclaration
|
||||
onCancel: () => void
|
||||
onStartToInstall?: () => void
|
||||
onInstalled: (notRefresh?: boolean) => void
|
||||
onFailed: (message?: string) => void
|
||||
}
|
||||
|
||||
const Installed: FC<Props> = ({
|
||||
uniqueIdentifier,
|
||||
payload,
|
||||
onCancel,
|
||||
onStartToInstall,
|
||||
onInstalled,
|
||||
onFailed,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const toInstallVersion = payload.version
|
||||
const pluginId = `${payload.author}/${payload.name}`
|
||||
const { installedInfo, isLoading } = useCheckInstalled({
|
||||
pluginIds: [pluginId],
|
||||
enabled: !!pluginId,
|
||||
})
|
||||
const installedInfoPayload = installedInfo?.[pluginId]
|
||||
const installedVersion = installedInfoPayload?.installedVersion
|
||||
const hasInstalled = !!installedVersion
|
||||
|
||||
useEffect(() => {
|
||||
if (hasInstalled && uniqueIdentifier === installedInfoPayload.uniqueIdentifier)
|
||||
onInstalled()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hasInstalled])
|
||||
|
||||
const [isInstalling, setIsInstalling] = React.useState(false)
|
||||
const { mutateAsync: installPackageFromLocal } = useInstallPackageFromLocal()
|
||||
|
||||
const {
|
||||
check,
|
||||
stop,
|
||||
} = checkTaskStatus()
|
||||
|
||||
const handleCancel = () => {
|
||||
stop()
|
||||
onCancel()
|
||||
}
|
||||
|
||||
const { handleRefetch } = usePluginTaskList(payload.category)
|
||||
const handleInstall = async () => {
|
||||
if (isInstalling) return
|
||||
setIsInstalling(true)
|
||||
onStartToInstall?.()
|
||||
|
||||
try {
|
||||
if (hasInstalled)
|
||||
await uninstallPlugin(installedInfoPayload.installedId)
|
||||
|
||||
const {
|
||||
all_installed,
|
||||
task_id,
|
||||
} = await installPackageFromLocal(uniqueIdentifier)
|
||||
const taskId = task_id
|
||||
const isInstalled = all_installed
|
||||
|
||||
if (isInstalled) {
|
||||
onInstalled()
|
||||
return
|
||||
}
|
||||
handleRefetch()
|
||||
const { status, error } = await check({
|
||||
taskId,
|
||||
pluginUniqueIdentifier: uniqueIdentifier,
|
||||
})
|
||||
if (status === TaskStatus.failed) {
|
||||
onFailed(error)
|
||||
return
|
||||
}
|
||||
onInstalled(true)
|
||||
}
|
||||
catch (e) {
|
||||
if (typeof e === 'string') {
|
||||
onFailed(e)
|
||||
return
|
||||
}
|
||||
onFailed()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col px-6 py-3 justify-center items-start gap-4 self-stretch'>
|
||||
<div className='text-text-secondary system-md-regular'>
|
||||
<p>{t(`${i18nPrefix}.readyToInstall`)}</p>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.fromTrustSource`}
|
||||
components={{ trustSource: <span className='system-md-semibold' /> }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex p-2 items-start content-start gap-1 self-stretch flex-wrap rounded-2xl bg-background-section-burn'>
|
||||
<Card
|
||||
className='w-full'
|
||||
payload={pluginManifestToCardPluginProps(payload)}
|
||||
titleLeft={!isLoading && <Version
|
||||
hasInstalled={hasInstalled}
|
||||
installedVersion={installedVersion}
|
||||
toInstallVersion={toInstallVersion}
|
||||
/>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Action Buttons */}
|
||||
<div className='flex p-6 pt-5 justify-end items-center gap-2 self-stretch'>
|
||||
{!isInstalling && (
|
||||
<Button variant='secondary' className='min-w-[72px]' onClick={handleCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px] flex space-x-0.5'
|
||||
disabled={isInstalling || isLoading}
|
||||
onClick={handleInstall}
|
||||
>
|
||||
{isInstalling && <RiLoader2Line className='w-4 h-4 animate-spin-slow' />}
|
||||
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(Installed)
|
||||
@@ -0,0 +1,99 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import Card from '../../../card'
|
||||
import type { Dependency, PluginDeclaration } from '../../../types'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { uploadFile } from '@/service/plugins'
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
|
||||
type Props = {
|
||||
isBundle: boolean
|
||||
file: File
|
||||
onCancel: () => void
|
||||
onPackageUploaded: (result: {
|
||||
uniqueIdentifier: string
|
||||
manifest: PluginDeclaration
|
||||
}) => void
|
||||
onBundleUploaded: (result: Dependency[]) => void
|
||||
onFailed: (errorMsg: string) => void
|
||||
}
|
||||
|
||||
const Uploading: FC<Props> = ({
|
||||
isBundle,
|
||||
file,
|
||||
onCancel,
|
||||
onPackageUploaded,
|
||||
onBundleUploaded,
|
||||
onFailed,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const fileName = file.name
|
||||
const handleUpload = async () => {
|
||||
try {
|
||||
await uploadFile(file, isBundle)
|
||||
}
|
||||
catch (e: any) {
|
||||
if (e.response?.message) {
|
||||
onFailed(e.response?.message)
|
||||
}
|
||||
else { // Why it would into this branch?
|
||||
const res = e.response
|
||||
if (isBundle) {
|
||||
onBundleUploaded(res)
|
||||
return
|
||||
}
|
||||
onPackageUploaded({
|
||||
uniqueIdentifier: res.unique_identifier,
|
||||
manifest: res.manifest,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
handleUpload()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col px-6 py-3 justify-center items-start gap-4 self-stretch'>
|
||||
<div className='flex items-center gap-1 self-stretch'>
|
||||
<RiLoader2Line className='text-text-accent w-4 h-4 animate-spin-slow' />
|
||||
<div className='text-text-secondary system-md-regular'>
|
||||
{t(`${i18nPrefix}.uploadingPackage`, {
|
||||
packageName: fileName,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex p-2 items-start content-start gap-1 self-stretch flex-wrap rounded-2xl bg-background-section-burn'>
|
||||
<Card
|
||||
className='w-full'
|
||||
payload={{ name: fileName } as any}
|
||||
isLoading
|
||||
loadingFileName={fileName}
|
||||
installed={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className='flex p-6 pt-5 justify-end items-center gap-2 self-stretch'>
|
||||
<Button variant='secondary' className='min-w-[72px]' onClick={onCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px]'
|
||||
disabled
|
||||
>
|
||||
{t(`${i18nPrefix}.install`)}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Uploading)
|
||||
@@ -0,0 +1,125 @@
|
||||
'use client'
|
||||
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import type { Dependency, Plugin, PluginManifestInMarket } from '../../types'
|
||||
import { InstallStep } from '../../types'
|
||||
import Install from './steps/install'
|
||||
import Installed from '../base/installed'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
|
||||
import ReadyToInstallBundle from '../install-bundle/ready-to-install'
|
||||
import cn from '@/utils/classnames'
|
||||
import useHideLogic from '../hooks/use-hide-logic'
|
||||
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
|
||||
type InstallFromMarketplaceProps = {
|
||||
uniqueIdentifier: string
|
||||
manifest: PluginManifestInMarket | Plugin
|
||||
isBundle?: boolean
|
||||
dependencies?: Dependency[]
|
||||
onSuccess: () => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
|
||||
uniqueIdentifier,
|
||||
manifest,
|
||||
isBundle,
|
||||
dependencies,
|
||||
onSuccess,
|
||||
onClose,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
// readyToInstall -> check installed -> installed/failed
|
||||
const [step, setStep] = useState<InstallStep>(InstallStep.readyToInstall)
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null)
|
||||
const { refreshPluginList } = useRefreshPluginList()
|
||||
|
||||
const {
|
||||
modalClassName,
|
||||
foldAnimInto,
|
||||
setIsInstalling,
|
||||
handleStartToInstall,
|
||||
} = useHideLogic(onClose)
|
||||
|
||||
const getTitle = useCallback(() => {
|
||||
if (isBundle && step === InstallStep.installed)
|
||||
return t(`${i18nPrefix}.installComplete`)
|
||||
if (step === InstallStep.installed)
|
||||
return t(`${i18nPrefix}.installedSuccessfully`)
|
||||
if (step === InstallStep.installFailed)
|
||||
return t(`${i18nPrefix}.installFailed`)
|
||||
return t(`${i18nPrefix}.installPlugin`)
|
||||
}, [isBundle, step, t])
|
||||
|
||||
const handleInstalled = useCallback((notRefresh?: boolean) => {
|
||||
setStep(InstallStep.installed)
|
||||
if (!notRefresh)
|
||||
refreshPluginList(manifest)
|
||||
setIsInstalling(false)
|
||||
}, [manifest, refreshPluginList, setIsInstalling])
|
||||
|
||||
const handleFailed = useCallback((errorMsg?: string) => {
|
||||
setStep(InstallStep.installFailed)
|
||||
setIsInstalling(false)
|
||||
if (errorMsg)
|
||||
setErrorMsg(errorMsg)
|
||||
}, [setIsInstalling])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={true}
|
||||
onClose={foldAnimInto}
|
||||
wrapperClassName='z-[9999]'
|
||||
className={cn(modalClassName, 'flex min-w-[560px] p-0 flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadows-shadow-xl')}
|
||||
closable
|
||||
>
|
||||
<div className='flex pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'>
|
||||
<div className='self-stretch text-text-primary title-2xl-semi-bold'>
|
||||
{getTitle()}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
isBundle ? (
|
||||
<ReadyToInstallBundle
|
||||
step={step}
|
||||
onStepChange={setStep}
|
||||
onStartToInstall={handleStartToInstall}
|
||||
setIsInstalling={setIsInstalling}
|
||||
onClose={onClose}
|
||||
allPlugins={dependencies!}
|
||||
isFromMarketPlace
|
||||
/>
|
||||
) : (<>
|
||||
{
|
||||
step === InstallStep.readyToInstall && (
|
||||
<Install
|
||||
uniqueIdentifier={uniqueIdentifier}
|
||||
payload={manifest!}
|
||||
onCancel={onClose}
|
||||
onInstalled={handleInstalled}
|
||||
onFailed={handleFailed}
|
||||
onStartToInstall={handleStartToInstall}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
[InstallStep.installed, InstallStep.installFailed].includes(step) && (
|
||||
<Installed
|
||||
payload={manifest!}
|
||||
isMarketPayload
|
||||
isFailed={step === InstallStep.installFailed}
|
||||
errMsg={errorMsg}
|
||||
onCancel={onSuccess}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Modal >
|
||||
)
|
||||
}
|
||||
|
||||
export default InstallFromMarketplace
|
||||
@@ -0,0 +1,158 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
// import { RiInformation2Line } from '@remixicon/react'
|
||||
import { type Plugin, type PluginManifestInMarket, TaskStatus } from '../../../types'
|
||||
import Card from '../../../card'
|
||||
import { pluginManifestInMarketToPluginProps } from '../../utils'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import { useInstallPackageFromMarketPlace, useUpdatePackageFromMarketPlace } from '@/service/use-plugins'
|
||||
import checkTaskStatus from '../../base/check-task-status'
|
||||
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
|
||||
import Version from '../../base/version'
|
||||
import { usePluginTaskList } from '@/service/use-plugins'
|
||||
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
|
||||
type Props = {
|
||||
uniqueIdentifier: string
|
||||
payload: PluginManifestInMarket | Plugin
|
||||
onCancel: () => void
|
||||
onStartToInstall?: () => void
|
||||
onInstalled: (notRefresh?: boolean) => void
|
||||
onFailed: (message?: string) => void
|
||||
}
|
||||
|
||||
const Installed: FC<Props> = ({
|
||||
uniqueIdentifier,
|
||||
payload,
|
||||
onCancel,
|
||||
onStartToInstall,
|
||||
onInstalled,
|
||||
onFailed,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const toInstallVersion = payload.version || payload.latest_version
|
||||
const pluginId = (payload as Plugin).plugin_id
|
||||
const { installedInfo, isLoading } = useCheckInstalled({
|
||||
pluginIds: [pluginId],
|
||||
enabled: !!pluginId,
|
||||
})
|
||||
const installedInfoPayload = installedInfo?.[pluginId]
|
||||
const installedVersion = installedInfoPayload?.installedVersion
|
||||
const hasInstalled = !!installedVersion
|
||||
|
||||
const { mutateAsync: installPackageFromMarketPlace } = useInstallPackageFromMarketPlace()
|
||||
const { mutateAsync: updatePackageFromMarketPlace } = useUpdatePackageFromMarketPlace()
|
||||
const [isInstalling, setIsInstalling] = React.useState(false)
|
||||
const {
|
||||
check,
|
||||
stop,
|
||||
} = checkTaskStatus()
|
||||
const { handleRefetch } = usePluginTaskList(payload.category)
|
||||
|
||||
useEffect(() => {
|
||||
if (hasInstalled && uniqueIdentifier === installedInfoPayload.uniqueIdentifier)
|
||||
onInstalled()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hasInstalled])
|
||||
|
||||
const handleCancel = () => {
|
||||
stop()
|
||||
onCancel()
|
||||
}
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (isInstalling) return
|
||||
onStartToInstall?.()
|
||||
setIsInstalling(true)
|
||||
try {
|
||||
let taskId
|
||||
let isInstalled
|
||||
if (hasInstalled) {
|
||||
const {
|
||||
all_installed,
|
||||
task_id,
|
||||
} = await updatePackageFromMarketPlace({
|
||||
original_plugin_unique_identifier: installedInfoPayload.uniqueIdentifier,
|
||||
new_plugin_unique_identifier: uniqueIdentifier,
|
||||
})
|
||||
taskId = task_id
|
||||
isInstalled = all_installed
|
||||
}
|
||||
else {
|
||||
const {
|
||||
all_installed,
|
||||
task_id,
|
||||
} = await installPackageFromMarketPlace(uniqueIdentifier)
|
||||
taskId = task_id
|
||||
isInstalled = all_installed
|
||||
}
|
||||
|
||||
if (isInstalled) {
|
||||
onInstalled()
|
||||
return
|
||||
}
|
||||
|
||||
handleRefetch()
|
||||
|
||||
const { status, error } = await check({
|
||||
taskId,
|
||||
pluginUniqueIdentifier: uniqueIdentifier,
|
||||
})
|
||||
if (status === TaskStatus.failed) {
|
||||
onFailed(error)
|
||||
return
|
||||
}
|
||||
onInstalled(true)
|
||||
}
|
||||
catch (e) {
|
||||
if (typeof e === 'string') {
|
||||
onFailed(e)
|
||||
return
|
||||
}
|
||||
onFailed()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col px-6 py-3 justify-center items-start gap-4 self-stretch'>
|
||||
<div className='text-text-secondary system-md-regular'>
|
||||
<p>{t(`${i18nPrefix}.readyToInstall`)}</p>
|
||||
</div>
|
||||
<div className='flex p-2 items-start content-start gap-1 self-stretch flex-wrap rounded-2xl bg-background-section-burn'>
|
||||
<Card
|
||||
className='w-full'
|
||||
payload={pluginManifestInMarketToPluginProps(payload as PluginManifestInMarket)}
|
||||
titleLeft={!isLoading && <Version
|
||||
hasInstalled={hasInstalled}
|
||||
installedVersion={installedVersion}
|
||||
toInstallVersion={toInstallVersion}
|
||||
/>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Action Buttons */}
|
||||
<div className='flex p-6 pt-5 justify-end items-center gap-2 self-stretch'>
|
||||
{!isInstalling && (
|
||||
<Button variant='secondary' className='min-w-[72px]' onClick={handleCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px] flex space-x-0.5'
|
||||
disabled={isInstalling || isLoading}
|
||||
onClick={handleInstall}
|
||||
>
|
||||
{isInstalling && <RiLoader2Line className='w-4 h-4 animate-spin-slow' />}
|
||||
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(Installed)
|
||||
59
web/app/components/plugins/install-plugin/utils.ts
Normal file
59
web/app/components/plugins/install-plugin/utils.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../types'
|
||||
import type { GitHubUrlInfo } from '@/app/components/plugins/types'
|
||||
|
||||
export const pluginManifestToCardPluginProps = (pluginManifest: PluginDeclaration): Plugin => {
|
||||
return {
|
||||
plugin_id: pluginManifest.plugin_unique_identifier,
|
||||
type: pluginManifest.category,
|
||||
category: pluginManifest.category,
|
||||
name: pluginManifest.name,
|
||||
version: pluginManifest.version,
|
||||
latest_version: '',
|
||||
latest_package_identifier: '',
|
||||
org: pluginManifest.author,
|
||||
label: pluginManifest.label,
|
||||
brief: pluginManifest.description,
|
||||
icon: pluginManifest.icon,
|
||||
verified: pluginManifest.verified,
|
||||
introduction: '',
|
||||
repository: '',
|
||||
install_count: 0,
|
||||
endpoint: {
|
||||
settings: [],
|
||||
},
|
||||
tags: [],
|
||||
}
|
||||
}
|
||||
|
||||
export const pluginManifestInMarketToPluginProps = (pluginManifest: PluginManifestInMarket): Plugin => {
|
||||
return {
|
||||
plugin_id: pluginManifest.plugin_unique_identifier,
|
||||
type: pluginManifest.category,
|
||||
category: pluginManifest.category,
|
||||
name: pluginManifest.name,
|
||||
version: pluginManifest.latest_version,
|
||||
latest_version: pluginManifest.latest_version,
|
||||
latest_package_identifier: '',
|
||||
org: pluginManifest.org,
|
||||
label: pluginManifest.label,
|
||||
brief: pluginManifest.brief,
|
||||
icon: pluginManifest.icon,
|
||||
verified: pluginManifest.verified,
|
||||
introduction: pluginManifest.introduction,
|
||||
repository: '',
|
||||
install_count: 0,
|
||||
endpoint: {
|
||||
settings: [],
|
||||
},
|
||||
tags: [],
|
||||
}
|
||||
}
|
||||
|
||||
export const parseGitHubUrl = (url: string): GitHubUrlInfo => {
|
||||
const match = url.match(/^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/?$/)
|
||||
return match ? { isValid: true, owner: match[1], repo: match[2] } : { isValid: false }
|
||||
}
|
||||
|
||||
export const convertRepoToUrl = (repo: string) => {
|
||||
return repo ? `https://github.com/${repo}` : ''
|
||||
}
|
||||
Reference in New Issue
Block a user