From c777d55a1c14bbbb443018323c8e7cb51c5c5e65 Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 25 Oct 2024 16:46:02 +0800 Subject: [PATCH] feat: marketplace install --- .../install-plugin/base/check-task-status.ts | 55 ++++++++++++++++ .../install-from-local-package/index.tsx | 6 +- .../steps/install.tsx | 35 +++++++++-- .../install-from-marketplace/index.tsx | 13 ++-- .../steps/install.tsx | 46 +++++++++++--- .../components/plugins/plugin-page/index.tsx | 7 ++- web/app/components/plugins/types.ts | 63 +++++++++++++------ web/service/plugins.ts | 16 +++++ 8 files changed, 201 insertions(+), 40 deletions(-) create mode 100644 web/app/components/plugins/install-plugin/base/check-task-status.ts diff --git a/web/app/components/plugins/install-plugin/base/check-task-status.ts b/web/app/components/plugins/install-plugin/base/check-task-status.ts new file mode 100644 index 0000000000..365bd9cf36 --- /dev/null +++ b/web/app/components/plugins/install-plugin/base/check-task-status.ts @@ -0,0 +1,55 @@ +import { checkTaskStatus as fetchCheckTaskStatus } from '@/service/plugins' +import type { PluginStatus } from '../../types' +import { TaskStatus } from '../../types' + +const INTERVAL = 10 * 1000 // 10 seconds + +interface Params { + taskId: string + pluginUniqueIdentifier: string +} + +function checkTaskStatus() { + let nextStatus = TaskStatus.running + let isStop = false + + const doCheckStatus = async ({ + taskId, + pluginUniqueIdentifier, + }: Params) => { + if (isStop) return + const { plugins } = await fetchCheckTaskStatus(taskId) + const plugin = plugins.find((p: PluginStatus) => p.plugin_unique_identifier === pluginUniqueIdentifier) + if (!plugin) { + nextStatus = TaskStatus.failed + Promise.reject(new Error('Plugin package not found')) + return + } + nextStatus = plugin.status + if (nextStatus === TaskStatus.running) { + setTimeout(async () => { + await doCheckStatus({ + taskId, + pluginUniqueIdentifier, + }) + }, INTERVAL) + return + } + if (nextStatus === TaskStatus.failed) { + Promise.reject(plugin.message) + return + } + return ({ + status: nextStatus, + }) + } + + return { + check: doCheckStatus, + stop: () => { + isStop = true + }, + } +} + +export default checkTaskStatus diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx index bbfc195a4d..7d89ede48b 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx @@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next' const i18nPrefix = 'plugin.installModal' -type InstallFromLocalPackageProps = { +interface InstallFromLocalPackageProps { file: File onSuccess: () => void onClose: () => void @@ -56,8 +56,10 @@ const InstallFromLocalPackage: React.FC = ({ setStep(InstallStep.installed) }, []) - const handleFailed = useCallback(() => { + const handleFailed = useCallback((errorMsg?: string) => { setStep(InstallStep.installFailed) + if (errorMsg) + setErrorMsg(errorMsg) }, []) return ( diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx index 4d58ab3cb7..08b21ad1ff 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx @@ -5,20 +5,20 @@ import type { PluginDeclaration } from '../../../types' import Card from '../../../card' import { pluginManifestToCardPluginProps } from '../../utils' import Button from '@/app/components/base/button' -import { sleep } from '@/utils' import { Trans, useTranslation } from 'react-i18next' import { RiLoader2Line } from '@remixicon/react' import Badge, { BadgeState } from '@/app/components/base/badge/index' import { installPackageFromLocal } from '@/service/plugins' +import checkTaskStatus from '../../base/check-task-status' const i18nPrefix = 'plugin.installModal' -type Props = { +interface Props { uniqueIdentifier: string payload: PluginDeclaration onCancel: () => void onInstalled: () => void - onFailed: () => void + onFailed: (message?: string) => void } const Installed: FC = ({ @@ -30,18 +30,41 @@ const Installed: FC = ({ }) => { const { t } = useTranslation() const [isInstalling, setIsInstalling] = React.useState(false) + const { + check, + stop, + } = checkTaskStatus() + + const handleCancel = () => { + stop() + onCancel() + } const handleInstall = async () => { if (isInstalling) return setIsInstalling(true) try { - await installPackageFromLocal(uniqueIdentifier) + const { + all_installed: isInstalled, + task_id: taskId, + } = await installPackageFromLocal(uniqueIdentifier) + if (isInstalled) { + onInstalled() + return + } + await check({ + taskId, + pluginUniqueIdentifier: uniqueIdentifier, + }) onInstalled() } catch (e) { + if (typeof e === 'string') { + onFailed(e) + return + } onFailed() } - await sleep(1500) } return ( @@ -67,7 +90,7 @@ const Installed: FC = ({ {/* Action Buttons */}
{!isInstalling && ( - )} diff --git a/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx b/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx index 26b0d117e8..16126e30a0 100644 --- a/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx +++ b/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx @@ -10,15 +10,15 @@ import { useTranslation } from 'react-i18next' const i18nPrefix = 'plugin.installModal' -type InstallFromMarketplaceProps = { - packageId: string +interface InstallFromMarketplaceProps { + uniqueIdentifier: string manifest: PluginDeclaration onSuccess: () => void onClose: () => void } const InstallFromMarketplace: React.FC = ({ - packageId, + uniqueIdentifier, manifest, onSuccess, onClose, @@ -26,6 +26,7 @@ const InstallFromMarketplace: React.FC = ({ const { t } = useTranslation() // readyToInstall -> check installed -> installed/failed const [step, setStep] = useState(InstallStep.readyToInstall) + const [errorMsg, setErrorMsg] = useState(null) // TODO: check installed in beta version. @@ -41,8 +42,10 @@ const InstallFromMarketplace: React.FC = ({ setStep(InstallStep.installed) }, []) - const handleFailed = useCallback(() => { + const handleFailed = useCallback((errorMsg?: string) => { setStep(InstallStep.installFailed) + if (errorMsg) + setErrorMsg(errorMsg) }, []) return ( @@ -60,6 +63,7 @@ const InstallFromMarketplace: React.FC = ({ { step === InstallStep.readyToInstall && ( = ({ ) diff --git a/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx b/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx index df5a551339..3fb83e6e7b 100644 --- a/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx @@ -6,21 +6,24 @@ import type { PluginDeclaration } from '../../../types' import Card from '../../../card' import { pluginManifestToCardPluginProps } from '../../utils' import Button from '@/app/components/base/button' -import { sleep } from '@/utils' import { useTranslation } from 'react-i18next' import { RiLoader2Line } from '@remixicon/react' import Badge, { BadgeState } from '@/app/components/base/badge/index' +import { installPackageFromMarketPlace } from '@/service/plugins' +import checkTaskStatus from '../../base/check-task-status' const i18nPrefix = 'plugin.installModal' -type Props = { +interface Props { + uniqueIdentifier: string payload: PluginDeclaration onCancel: () => void onInstalled: () => void - onFailed: () => void + onFailed: (message?: string) => void } const Installed: FC = ({ + uniqueIdentifier, payload, onCancel, onInstalled, @@ -28,13 +31,42 @@ const Installed: FC = ({ }) => { const { t } = useTranslation() const [isInstalling, setIsInstalling] = React.useState(false) + const { + check, + stop, + } = checkTaskStatus() + + const handleCancel = () => { + stop() + onCancel() + } const handleInstall = async () => { if (isInstalling) return setIsInstalling(true) - await sleep(1500) - onInstalled() - // onFailed() + + try { + const { + all_installed: isInstalled, + task_id: taskId, + } = await installPackageFromMarketPlace(uniqueIdentifier) + if (isInstalled) { + onInstalled() + return + } + await check({ + taskId, + pluginUniqueIdentifier: uniqueIdentifier, + }) + onInstalled() + } + catch (e) { + if (typeof e === 'string') { + onFailed(e) + return + } + onFailed() + } } const toInstallVersion = '1.3.0' @@ -77,7 +109,7 @@ const Installed: FC = ({ {/* Action Buttons */}
{!isInstalling && ( - )} diff --git a/web/app/components/plugins/plugin-page/index.tsx b/web/app/components/plugins/plugin-page/index.tsx index 7335dfbb16..fc15fdc81a 100644 --- a/web/app/components/plugins/plugin-page/index.tsx +++ b/web/app/components/plugins/plugin-page/index.tsx @@ -35,7 +35,7 @@ import { sleep } from '@/utils' const PACKAGE_IDS_KEY = 'package-ids' -export type PluginPageProps = { +export interface PluginPageProps { plugins: React.ReactNode marketplace: React.ReactNode } @@ -74,6 +74,9 @@ const PluginPage = ({ (async () => { await sleep(100) if (packageId) { + // setManifest(toolNotionManifest) + // TODO + // const data = await fetchManifest(encodeURIComponent(packageId)) setManifest(toolNotionManifest) showInstallFromMarketplace() } @@ -229,7 +232,7 @@ const PluginPage = ({ isShowInstallFromMarketplace && ( diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index ba430a87c3..89c557eec7 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -15,7 +15,7 @@ export enum PluginSource { debugging = 'remote', } -export type PluginToolDeclaration = { +export interface PluginToolDeclaration { identity: { author: string name: string @@ -27,17 +27,17 @@ export type PluginToolDeclaration = { credentials_schema: ToolCredential[] // TODO } -export type PluginEndpointDeclaration = { +export interface PluginEndpointDeclaration { settings: ToolCredential[] endpoints: EndpointItem[] } -export type EndpointItem = { +export interface EndpointItem { path: string method: string } -export type EndpointListItem = { +export interface EndpointListItem { id: string created_at: string updated_at: string @@ -53,7 +53,7 @@ export type EndpointListItem = { } // Plugin manifest -export type PluginDeclaration = { +export interface PluginDeclaration { version: string author: string icon: string @@ -70,7 +70,7 @@ export type PluginDeclaration = { model: any // TODO } -export type PluginDetail = { +export interface PluginDetail { id: string created_at: string updated_at: string @@ -87,7 +87,7 @@ export type PluginDetail = { meta?: any } -export type Plugin = { +export interface Plugin { type: PluginType org: string name: string @@ -113,7 +113,7 @@ export enum PermissionType { noOne = 'noOne', } -export type Permissions = { +export interface Permissions { canManagement: PermissionType canDebugger: PermissionType } @@ -125,7 +125,7 @@ export enum InstallStepFromGitHub { installed = 'installed', } -export type InstallState = { +export interface InstallState { step: InstallStepFromGitHub repoUrl: string selectedVersion: string @@ -133,34 +133,34 @@ export type InstallState = { releases: GitHubRepoReleaseResponse[] } -export type GitHubUrlInfo = { +export interface GitHubUrlInfo { isValid: boolean owner?: string repo?: string } // endpoint -export type CreateEndpointRequest = { +export interface CreateEndpointRequest { plugin_unique_identifier: string settings: Record name: string } -export type EndpointOperationResponse = { +export interface EndpointOperationResponse { result: 'success' | 'error' } -export type EndpointsRequest = { +export interface EndpointsRequest { limit: number page: number plugin_id: string } -export type EndpointsResponse = { +export interface EndpointsResponse { endpoints: EndpointListItem[] has_more: boolean limit: number total: number page: number } -export type UpdateEndpointRequest = { +export interface UpdateEndpointRequest { endpoint_id: string settings: Record name: string @@ -175,23 +175,48 @@ export enum InstallStep { installFailed = 'failed', } -export type GitHubAsset = { +export interface GitHubAsset { id: number name: string browser_download_url: string } -export type GitHubRepoReleaseResponse = { +export interface GitHubRepoReleaseResponse { tag_name: string assets: GitHubAsset[] } -export type InstallPackageResponse = { +export interface InstallPackageResponse { plugin_unique_identifier: string + all_installed: boolean + task_id: string } -export type DebugInfo = { +export interface DebugInfo { key: string host: string port: number } + +export enum TaskStatus { + running = 'running', + success = 'success', + failed = 'failed', +} + +export interface PluginStatus { + plugin_unique_identifier: string + plugin_id: string + status: TaskStatus + message: string +} + +export interface TaskStatusResponse { + id: string + created_at: string + updated_at: string + status: string + total_plugins: number + completed_plugins: number + plugins: PluginStatus[] +} diff --git a/web/service/plugins.ts b/web/service/plugins.ts index 655825004d..77664cb8a2 100644 --- a/web/service/plugins.ts +++ b/web/service/plugins.ts @@ -6,6 +6,8 @@ import type { EndpointsRequest, EndpointsResponse, InstallPackageResponse, + PluginDeclaration, + TaskStatusResponse, UpdateEndpointRequest, } from '@/app/components/plugins/types' import type { DebugInfo as DebugInfoTypes } from '@/app/components/plugins/types' @@ -69,6 +71,16 @@ export const installPackageFromLocal = async (uniqueIdentifier: string) => { }) } +export const fetchManifest = async (uniqueIdentifier: string) => { + return get(`/workspaces/current/plugin/fetch-manifest?plugin_unique_identifier=${uniqueIdentifier}`) +} + +export const installPackageFromMarketPlace = async (uniqueIdentifier: string) => { + return post('/workspaces/current/plugin/install/marketplace', { + body: { plugin_unique_identifiers: [uniqueIdentifier] }, + }) +} + export const fetchMarketplaceCollections: Fetcher = ({ url }) => { return get(url) } @@ -76,3 +88,7 @@ export const fetchMarketplaceCollections: Fetcher = ({ url }) => { return get(url) } + +export const checkTaskStatus = async (taskId: string) => { + return get(`/workspaces/current/plugin/tasks/${taskId}`) +}