feat: marketplace install

This commit is contained in:
Joel 2024-10-25 16:46:02 +08:00
parent cd27ae4319
commit c777d55a1c
8 changed files with 201 additions and 40 deletions

View File

@ -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

View File

@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next'
const i18nPrefix = 'plugin.installModal' const i18nPrefix = 'plugin.installModal'
type InstallFromLocalPackageProps = { interface InstallFromLocalPackageProps {
file: File file: File
onSuccess: () => void onSuccess: () => void
onClose: () => void onClose: () => void
@ -56,8 +56,10 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({
setStep(InstallStep.installed) setStep(InstallStep.installed)
}, []) }, [])
const handleFailed = useCallback(() => { const handleFailed = useCallback((errorMsg?: string) => {
setStep(InstallStep.installFailed) setStep(InstallStep.installFailed)
if (errorMsg)
setErrorMsg(errorMsg)
}, []) }, [])
return ( return (

View File

@ -5,20 +5,20 @@ import type { PluginDeclaration } from '../../../types'
import Card from '../../../card' import Card from '../../../card'
import { pluginManifestToCardPluginProps } from '../../utils' import { pluginManifestToCardPluginProps } from '../../utils'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { sleep } from '@/utils'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { RiLoader2Line } from '@remixicon/react' import { RiLoader2Line } from '@remixicon/react'
import Badge, { BadgeState } from '@/app/components/base/badge/index' import Badge, { BadgeState } from '@/app/components/base/badge/index'
import { installPackageFromLocal } from '@/service/plugins' import { installPackageFromLocal } from '@/service/plugins'
import checkTaskStatus from '../../base/check-task-status'
const i18nPrefix = 'plugin.installModal' const i18nPrefix = 'plugin.installModal'
type Props = { interface Props {
uniqueIdentifier: string uniqueIdentifier: string
payload: PluginDeclaration payload: PluginDeclaration
onCancel: () => void onCancel: () => void
onInstalled: () => void onInstalled: () => void
onFailed: () => void onFailed: (message?: string) => void
} }
const Installed: FC<Props> = ({ const Installed: FC<Props> = ({
@ -30,18 +30,41 @@ const Installed: FC<Props> = ({
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [isInstalling, setIsInstalling] = React.useState(false) const [isInstalling, setIsInstalling] = React.useState(false)
const {
check,
stop,
} = checkTaskStatus()
const handleCancel = () => {
stop()
onCancel()
}
const handleInstall = async () => { const handleInstall = async () => {
if (isInstalling) return if (isInstalling) return
setIsInstalling(true) setIsInstalling(true)
try { try {
await installPackageFromLocal(uniqueIdentifier) const {
all_installed: isInstalled,
task_id: taskId,
} = await installPackageFromLocal(uniqueIdentifier)
if (isInstalled) {
onInstalled()
return
}
await check({
taskId,
pluginUniqueIdentifier: uniqueIdentifier,
})
onInstalled() onInstalled()
} }
catch (e) { catch (e) {
if (typeof e === 'string') {
onFailed(e)
return
}
onFailed() onFailed()
} }
await sleep(1500)
} }
return ( return (
@ -67,7 +90,7 @@ const Installed: FC<Props> = ({
{/* Action Buttons */} {/* Action Buttons */}
<div className='flex p-6 pt-5 justify-end items-center gap-2 self-stretch'> <div className='flex p-6 pt-5 justify-end items-center gap-2 self-stretch'>
{!isInstalling && ( {!isInstalling && (
<Button variant='secondary' className='min-w-[72px]' onClick={onCancel}> <Button variant='secondary' className='min-w-[72px]' onClick={handleCancel}>
{t('common.operation.cancel')} {t('common.operation.cancel')}
</Button> </Button>
)} )}

View File

@ -10,15 +10,15 @@ import { useTranslation } from 'react-i18next'
const i18nPrefix = 'plugin.installModal' const i18nPrefix = 'plugin.installModal'
type InstallFromMarketplaceProps = { interface InstallFromMarketplaceProps {
packageId: string uniqueIdentifier: string
manifest: PluginDeclaration manifest: PluginDeclaration
onSuccess: () => void onSuccess: () => void
onClose: () => void onClose: () => void
} }
const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
packageId, uniqueIdentifier,
manifest, manifest,
onSuccess, onSuccess,
onClose, onClose,
@ -26,6 +26,7 @@ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
const { t } = useTranslation() const { t } = useTranslation()
// readyToInstall -> check installed -> installed/failed // readyToInstall -> check installed -> installed/failed
const [step, setStep] = useState<InstallStep>(InstallStep.readyToInstall) const [step, setStep] = useState<InstallStep>(InstallStep.readyToInstall)
const [errorMsg, setErrorMsg] = useState<string | null>(null)
// TODO: check installed in beta version. // TODO: check installed in beta version.
@ -41,8 +42,10 @@ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
setStep(InstallStep.installed) setStep(InstallStep.installed)
}, []) }, [])
const handleFailed = useCallback(() => { const handleFailed = useCallback((errorMsg?: string) => {
setStep(InstallStep.installFailed) setStep(InstallStep.installFailed)
if (errorMsg)
setErrorMsg(errorMsg)
}, []) }, [])
return ( return (
@ -60,6 +63,7 @@ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
{ {
step === InstallStep.readyToInstall && ( step === InstallStep.readyToInstall && (
<Install <Install
uniqueIdentifier={uniqueIdentifier}
payload={manifest!} payload={manifest!}
onCancel={onClose} onCancel={onClose}
onInstalled={handleInstalled} onInstalled={handleInstalled}
@ -72,6 +76,7 @@ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
<Installed <Installed
payload={manifest!} payload={manifest!}
isFailed={step === InstallStep.installFailed} isFailed={step === InstallStep.installFailed}
errMsg={errorMsg}
onCancel={onSuccess} onCancel={onSuccess}
/> />
) )

View File

@ -6,21 +6,24 @@ import type { PluginDeclaration } from '../../../types'
import Card from '../../../card' import Card from '../../../card'
import { pluginManifestToCardPluginProps } from '../../utils' import { pluginManifestToCardPluginProps } from '../../utils'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { sleep } from '@/utils'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RiLoader2Line } from '@remixicon/react' import { RiLoader2Line } from '@remixicon/react'
import Badge, { BadgeState } from '@/app/components/base/badge/index' 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' const i18nPrefix = 'plugin.installModal'
type Props = { interface Props {
uniqueIdentifier: string
payload: PluginDeclaration payload: PluginDeclaration
onCancel: () => void onCancel: () => void
onInstalled: () => void onInstalled: () => void
onFailed: () => void onFailed: (message?: string) => void
} }
const Installed: FC<Props> = ({ const Installed: FC<Props> = ({
uniqueIdentifier,
payload, payload,
onCancel, onCancel,
onInstalled, onInstalled,
@ -28,13 +31,42 @@ const Installed: FC<Props> = ({
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [isInstalling, setIsInstalling] = React.useState(false) const [isInstalling, setIsInstalling] = React.useState(false)
const {
check,
stop,
} = checkTaskStatus()
const handleCancel = () => {
stop()
onCancel()
}
const handleInstall = async () => { const handleInstall = async () => {
if (isInstalling) return if (isInstalling) return
setIsInstalling(true) setIsInstalling(true)
await sleep(1500)
onInstalled() try {
// onFailed() 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' const toInstallVersion = '1.3.0'
@ -77,7 +109,7 @@ const Installed: FC<Props> = ({
{/* Action Buttons */} {/* Action Buttons */}
<div className='flex p-6 pt-5 justify-end items-center gap-2 self-stretch'> <div className='flex p-6 pt-5 justify-end items-center gap-2 self-stretch'>
{!isInstalling && ( {!isInstalling && (
<Button variant='secondary' className='min-w-[72px]' onClick={onCancel}> <Button variant='secondary' className='min-w-[72px]' onClick={handleCancel}>
{t('common.operation.cancel')} {t('common.operation.cancel')}
</Button> </Button>
)} )}

View File

@ -35,7 +35,7 @@ import { sleep } from '@/utils'
const PACKAGE_IDS_KEY = 'package-ids' const PACKAGE_IDS_KEY = 'package-ids'
export type PluginPageProps = { export interface PluginPageProps {
plugins: React.ReactNode plugins: React.ReactNode
marketplace: React.ReactNode marketplace: React.ReactNode
} }
@ -74,6 +74,9 @@ const PluginPage = ({
(async () => { (async () => {
await sleep(100) await sleep(100)
if (packageId) { if (packageId) {
// setManifest(toolNotionManifest)
// TODO
// const data = await fetchManifest(encodeURIComponent(packageId))
setManifest(toolNotionManifest) setManifest(toolNotionManifest)
showInstallFromMarketplace() showInstallFromMarketplace()
} }
@ -229,7 +232,7 @@ const PluginPage = ({
isShowInstallFromMarketplace && ( isShowInstallFromMarketplace && (
<InstallFromMarketplace <InstallFromMarketplace
manifest={manifest!} manifest={manifest!}
packageId={packageId} uniqueIdentifier={packageId}
onClose={hideInstallFromMarketplace} onClose={hideInstallFromMarketplace}
onSuccess={hideInstallFromMarketplace} onSuccess={hideInstallFromMarketplace}
/> />

View File

@ -15,7 +15,7 @@ export enum PluginSource {
debugging = 'remote', debugging = 'remote',
} }
export type PluginToolDeclaration = { export interface PluginToolDeclaration {
identity: { identity: {
author: string author: string
name: string name: string
@ -27,17 +27,17 @@ export type PluginToolDeclaration = {
credentials_schema: ToolCredential[] // TODO credentials_schema: ToolCredential[] // TODO
} }
export type PluginEndpointDeclaration = { export interface PluginEndpointDeclaration {
settings: ToolCredential[] settings: ToolCredential[]
endpoints: EndpointItem[] endpoints: EndpointItem[]
} }
export type EndpointItem = { export interface EndpointItem {
path: string path: string
method: string method: string
} }
export type EndpointListItem = { export interface EndpointListItem {
id: string id: string
created_at: string created_at: string
updated_at: string updated_at: string
@ -53,7 +53,7 @@ export type EndpointListItem = {
} }
// Plugin manifest // Plugin manifest
export type PluginDeclaration = { export interface PluginDeclaration {
version: string version: string
author: string author: string
icon: string icon: string
@ -70,7 +70,7 @@ export type PluginDeclaration = {
model: any // TODO model: any // TODO
} }
export type PluginDetail = { export interface PluginDetail {
id: string id: string
created_at: string created_at: string
updated_at: string updated_at: string
@ -87,7 +87,7 @@ export type PluginDetail = {
meta?: any meta?: any
} }
export type Plugin = { export interface Plugin {
type: PluginType type: PluginType
org: string org: string
name: string name: string
@ -113,7 +113,7 @@ export enum PermissionType {
noOne = 'noOne', noOne = 'noOne',
} }
export type Permissions = { export interface Permissions {
canManagement: PermissionType canManagement: PermissionType
canDebugger: PermissionType canDebugger: PermissionType
} }
@ -125,7 +125,7 @@ export enum InstallStepFromGitHub {
installed = 'installed', installed = 'installed',
} }
export type InstallState = { export interface InstallState {
step: InstallStepFromGitHub step: InstallStepFromGitHub
repoUrl: string repoUrl: string
selectedVersion: string selectedVersion: string
@ -133,34 +133,34 @@ export type InstallState = {
releases: GitHubRepoReleaseResponse[] releases: GitHubRepoReleaseResponse[]
} }
export type GitHubUrlInfo = { export interface GitHubUrlInfo {
isValid: boolean isValid: boolean
owner?: string owner?: string
repo?: string repo?: string
} }
// endpoint // endpoint
export type CreateEndpointRequest = { export interface CreateEndpointRequest {
plugin_unique_identifier: string plugin_unique_identifier: string
settings: Record<string, any> settings: Record<string, any>
name: string name: string
} }
export type EndpointOperationResponse = { export interface EndpointOperationResponse {
result: 'success' | 'error' result: 'success' | 'error'
} }
export type EndpointsRequest = { export interface EndpointsRequest {
limit: number limit: number
page: number page: number
plugin_id: string plugin_id: string
} }
export type EndpointsResponse = { export interface EndpointsResponse {
endpoints: EndpointListItem[] endpoints: EndpointListItem[]
has_more: boolean has_more: boolean
limit: number limit: number
total: number total: number
page: number page: number
} }
export type UpdateEndpointRequest = { export interface UpdateEndpointRequest {
endpoint_id: string endpoint_id: string
settings: Record<string, any> settings: Record<string, any>
name: string name: string
@ -175,23 +175,48 @@ export enum InstallStep {
installFailed = 'failed', installFailed = 'failed',
} }
export type GitHubAsset = { export interface GitHubAsset {
id: number id: number
name: string name: string
browser_download_url: string browser_download_url: string
} }
export type GitHubRepoReleaseResponse = { export interface GitHubRepoReleaseResponse {
tag_name: string tag_name: string
assets: GitHubAsset[] assets: GitHubAsset[]
} }
export type InstallPackageResponse = { export interface InstallPackageResponse {
plugin_unique_identifier: string plugin_unique_identifier: string
all_installed: boolean
task_id: string
} }
export type DebugInfo = { export interface DebugInfo {
key: string key: string
host: string host: string
port: number 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[]
}

View File

@ -6,6 +6,8 @@ import type {
EndpointsRequest, EndpointsRequest,
EndpointsResponse, EndpointsResponse,
InstallPackageResponse, InstallPackageResponse,
PluginDeclaration,
TaskStatusResponse,
UpdateEndpointRequest, UpdateEndpointRequest,
} from '@/app/components/plugins/types' } from '@/app/components/plugins/types'
import type { DebugInfo as DebugInfoTypes } 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<PluginDeclaration>(`/workspaces/current/plugin/fetch-manifest?plugin_unique_identifier=${uniqueIdentifier}`)
}
export const installPackageFromMarketPlace = async (uniqueIdentifier: string) => {
return post<InstallPackageResponse>('/workspaces/current/plugin/install/marketplace', {
body: { plugin_unique_identifiers: [uniqueIdentifier] },
})
}
export const fetchMarketplaceCollections: Fetcher<MarketplaceCollectionsResponse, { url: string; }> = ({ url }) => { export const fetchMarketplaceCollections: Fetcher<MarketplaceCollectionsResponse, { url: string; }> = ({ url }) => {
return get<MarketplaceCollectionsResponse>(url) return get<MarketplaceCollectionsResponse>(url)
} }
@ -76,3 +88,7 @@ export const fetchMarketplaceCollections: Fetcher<MarketplaceCollectionsResponse
export const fetchMarketplaceCollectionPlugins: Fetcher<MarketplaceCollectionPluginsResponse, { url: string }> = ({ url }) => { export const fetchMarketplaceCollectionPlugins: Fetcher<MarketplaceCollectionPluginsResponse, { url: string }> = ({ url }) => {
return get<MarketplaceCollectionPluginsResponse>(url) return get<MarketplaceCollectionPluginsResponse>(url)
} }
export const checkTaskStatus = async (taskId: string) => {
return get<TaskStatusResponse>(`/workspaces/current/plugin/tasks/${taskId}`)
}