diff --git a/web/app/components/base/tab-slider/index.tsx b/web/app/components/base/tab-slider/index.tsx index 00abb9e28d..93dbbe3f62 100644 --- a/web/app/components/base/tab-slider/index.tsx +++ b/web/app/components/base/tab-slider/index.tsx @@ -67,7 +67,9 @@ const TabSlider: FC = ({ }} > {option.text} + {/* if no plugin installed, the badge won't show */} {option.value === 'plugins' + && pluginList.length > 0 && void - meta: MetaData + meta?: MetaData } const Action: FC = ({ pluginId, @@ -99,9 +99,9 @@ const Action: FC = ({ {isShowPluginInfo && ( )} diff --git a/web/app/components/plugins/plugin-item/index.tsx b/web/app/components/plugins/plugin-item/index.tsx index 9c7a20c92e..5a40056a05 100644 --- a/web/app/components/plugins/plugin-item/index.tsx +++ b/web/app/components/plugins/plugin-item/index.tsx @@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next' import { usePluginPageContext } from '../plugin-page/context' import { Github } from '../../base/icons/src/public/common' import Badge from '../../base/badge' -import { type InstalledPlugin, PluginSource } from '../types' +import { type PluginDetail, PluginSource } from '../types' import CornerMark from '../card/base/corner-mark' import Description from '../card/base/description' import OrgInfo from '../card/base/org-info' @@ -26,7 +26,7 @@ import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config' type Props = { className?: string - plugin: InstalledPlugin + plugin: PluginDetail } const PluginItem: FC = ({ @@ -50,6 +50,7 @@ const PluginItem: FC = ({ } = plugin const { category, author, name, label, description, icon, verified } = plugin.declaration // Only plugin installed from GitHub need to check if it's the new version + // todo check version manually const hasNewVersion = useMemo(() => { return source === PluginSource.github && latest_version !== version }, [source, latest_version, version]) @@ -124,7 +125,7 @@ const PluginItem: FC = ({
{source === PluginSource.github && <> - +
{t('plugin.from')}
diff --git a/web/app/components/plugins/plugin-page/context.tsx b/web/app/components/plugins/plugin-page/context.tsx index 8d73fb1a19..8587012b79 100644 --- a/web/app/components/plugins/plugin-page/context.tsx +++ b/web/app/components/plugins/plugin-page/context.tsx @@ -2,6 +2,7 @@ import type { ReactNode } from 'react' import { + useMemo, useRef, useState, } from 'react' @@ -9,22 +10,28 @@ import { createContext, useContextSelector, } from 'use-context-selector' -import type { InstalledPlugin, Permissions } from '../types' +import { useSelector as useAppContextSelector } from '@/context/app-context' +import type { Permissions, PluginDetail } from '../types' import type { FilterState } from './filter-management' import { PermissionType } from '../types' import { fetchInstalledPluginList } from '@/service/plugins' import useSWR from 'swr' +import { useTranslation } from 'react-i18next' +import { useTabSearchParams } from '@/hooks/use-tab-searchparams' export type PluginPageContextValue = { containerRef: React.RefObject permissions: Permissions setPermissions: (permissions: PluginPageContextValue['permissions']) => void - currentPluginDetail: InstalledPlugin | undefined - setCurrentPluginDetail: (plugin: InstalledPlugin) => void - installedPluginList: InstalledPlugin[] + currentPluginDetail: PluginDetail | undefined + setCurrentPluginDetail: (plugin: PluginDetail) => void + installedPluginList: PluginDetail[] mutateInstalledPluginList: () => void filters: FilterState setFilters: (filter: FilterState) => void + activeTab: string + setActiveTab: (tab: string) => void + options: Array<{ value: string, text: string }> } export const PluginPageContext = createContext({ @@ -44,6 +51,9 @@ export const PluginPageContext = createContext({ searchQuery: '', }, setFilters: () => {}, + activeTab: '', + setActiveTab: () => {}, + options: [], }) type PluginPageContextProviderProps = { @@ -57,6 +67,7 @@ export function usePluginPageContext(selector: (value: PluginPageContextValue) = export const PluginPageContextProvider = ({ children, }: PluginPageContextProviderProps) => { + const { t } = useTranslation() const containerRef = useRef(null) const [permissions, setPermissions] = useState({ install_permission: PermissionType.noOne, @@ -68,7 +79,22 @@ export const PluginPageContextProvider = ({ searchQuery: '', }) const { data, mutate: mutateInstalledPluginList } = useSWR({ url: '/workspaces/current/plugin/list' }, fetchInstalledPluginList) - const [currentPluginDetail, setCurrentPluginDetail] = useState() + const [currentPluginDetail, setCurrentPluginDetail] = useState() + + const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) + const options = useMemo(() => { + return [ + { value: 'plugins', text: t('common.menus.plugins') }, + ...( + enable_marketplace + ? [{ value: 'discover', text: 'Explore Marketplace' }] + : [] + ), + ] + }, [t, enable_marketplace]) + const [activeTab, setActiveTab] = useTabSearchParams({ + defaultTab: options[0].value, + }) return ( {children} diff --git a/web/app/components/plugins/plugin-page/empty/index.tsx b/web/app/components/plugins/plugin-page/empty/index.tsx new file mode 100644 index 0000000000..ea296d9f31 --- /dev/null +++ b/web/app/components/plugins/plugin-page/empty/index.tsx @@ -0,0 +1,114 @@ +import React, { useMemo, useRef, useState } from 'react' +import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' +import { FileZip } from '@/app/components/base/icons/src/vender/solid/files' +import { Github } from '@/app/components/base/icons/src/vender/solid/general' +import InstallFromGitHub from '@/app/components/plugins/install-plugin/install-from-github' +import InstallFromLocalPackage from '@/app/components/plugins/install-plugin/install-from-local-package' +import { usePluginPageContext } from '../context' +import type { PluginDetail } from '../../types' +import { Group } from '@/app/components/base/icons/src/vender/other' +import { useSelector as useAppContextSelector } from '@/context/app-context' +import Line from '../../marketplace/empty/line' + +const Empty = () => { + const fileInputRef = useRef(null) + const [selectedAction, setSelectedAction] = useState(null) + const [selectedFile, setSelectedFile] = useState(null) + const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) + const setActiveTab = usePluginPageContext(v => v.setActiveTab) + + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0] + if (file) { + setSelectedFile(file) + setSelectedAction('local') + } + } + const filters = usePluginPageContext(v => v.filters) + const pluginList = usePluginPageContext(v => v.installedPluginList) as PluginDetail[] + + const text = useMemo(() => { + if (pluginList.length === 0) + return 'No plugins installed' + if (filters.categories.length > 0 || filters.tags.length > 0 || filters.searchQuery) + return 'No plugins found' + }, [pluginList, filters]) + + return ( +
+ {/* skeleton */} +
+ {Array.from({ length: 20 }).fill(0).map((_, i) => ( +
+ ))} +
+ {/* mask */} +
+
+
+
+ + + + + +
+
+ {text} +
+
+ +
+ {[ + ...( + (enable_marketplace || true) + ? [{ icon: MagicBox, text: 'Marketplace', action: 'marketplace' }] + : [] + ), + { icon: Github, text: 'GitHub', action: 'github' }, + { icon: FileZip, text: 'Local Package File', action: 'local' }, + ].map(({ icon: Icon, text, action }) => ( +
{ + if (action === 'local') + fileInputRef.current?.click() + else if (action === 'marketplace') + setActiveTab('discover') + else + setSelectedAction(action) + }} + > + + {`Install from ${text}`} +
+ ))} +
+
+
+ {selectedAction === 'github' && setSelectedAction(null)} />} + {selectedAction === 'local' && selectedFile + && ( setSelectedAction(null)} + onSuccess={() => { }} + /> + ) + } +
+
+ ) +} + +Empty.displayName = 'Empty' + +export default React.memo(Empty) diff --git a/web/app/components/plugins/plugin-page/index.tsx b/web/app/components/plugins/plugin-page/index.tsx index c5f8d382b3..50723b580b 100644 --- a/web/app/components/plugins/plugin-page/index.tsx +++ b/web/app/components/plugins/plugin-page/index.tsx @@ -18,7 +18,6 @@ import usePermission from './use-permission' import DebugInfo from './debug-info' import { usePluginTasksStore } from './store' import InstallInfo from './install-info' -import { useTabSearchParams } from '@/hooks/use-tab-searchparams' import Button from '@/app/components/base/button' import TabSlider from '@/app/components/base/tab-slider' import Tooltip from '@/app/components/base/tooltip' @@ -100,22 +99,11 @@ const PluginPage = ({ }] = useBoolean() const [currentFile, setCurrentFile] = useState(null) const containerRef = usePluginPageContext(v => v.containerRef) + const options = usePluginPageContext(v => v.options) + const [activeTab, setActiveTab] = usePluginPageContext(v => [v.activeTab, v.setActiveTab]) const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) const [installed, total] = [2, 3] // Replace this with the actual progress const progressPercentage = (installed / total) * 100 - const options = useMemo(() => { - return [ - { value: 'plugins', text: t('common.menus.plugins') }, - ...( - enable_marketplace - ? [{ value: 'discover', text: 'Explore Marketplace' }] - : [] - ), - ] - }, [t, enable_marketplace]) - const [activeTab, setActiveTab] = useTabSearchParams({ - defaultTab: options[0].value, - }) const uploaderProps = useUploader({ onFileChange: setCurrentFile, diff --git a/web/app/components/plugins/plugin-page/list/index.tsx b/web/app/components/plugins/plugin-page/list/index.tsx index 23f6e403e5..57fea8c8b5 100644 --- a/web/app/components/plugins/plugin-page/list/index.tsx +++ b/web/app/components/plugins/plugin-page/list/index.tsx @@ -1,9 +1,9 @@ import type { FC } from 'react' import PluginItem from '../../plugin-item' -import type { InstalledPlugin } from '../../types' +import type { PluginDetail } from '../../types' type IPluginListProps = { - pluginList: InstalledPlugin[] + pluginList: PluginDetail[] } const PluginList: FC = ({ pluginList }) => { diff --git a/web/app/components/plugins/plugin-page/plugins-panel.tsx b/web/app/components/plugins/plugin-page/plugins-panel.tsx index 77f531b122..11a8c3d198 100644 --- a/web/app/components/plugins/plugin-page/plugins-panel.tsx +++ b/web/app/components/plugins/plugin-page/plugins-panel.tsx @@ -1,16 +1,17 @@ 'use client' import { useMemo } from 'react' -import type { InstalledPlugin } from '../types' +import type { PluginDetail } from '../types' import type { FilterState } from './filter-management' import FilterManagement from './filter-management' import List from './list' import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel' import { usePluginPageContext } from './context' import { useDebounceFn } from 'ahooks' +import Empty from './empty' const PluginsPanel = () => { const [filters, setFilters] = usePluginPageContext(v => [v.filters, v.setFilters]) - const pluginList = usePluginPageContext(v => v.installedPluginList) as InstalledPlugin[] + const pluginList = usePluginPageContext(v => v.installedPluginList) as PluginDetail[] const mutateInstalledPluginList = usePluginPageContext(v => v.mutateInstalledPluginList) const { run: handleFilterChange } = useDebounceFn((filters: FilterState) => { @@ -37,11 +38,15 @@ const PluginsPanel = () => { onFilterChange={handleFilterChange} />
-
-
- + {filteredList.length > 0 ? ( +
+
+ +
-
+ ) : ( + + )} mutateInstalledPluginList()}/> ) diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index e0ed3bf862..8ccba6ef30 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -83,6 +83,24 @@ export type PluginManifestInMarket = { install_count: number } +export type PluginDetail = { + id: string + created_at: string + updated_at: string + name: string + plugin_id: string + plugin_unique_identifier: string + declaration: PluginDeclaration + installation_id: string + tenant_id: string + endpoints_setups: number + endpoints_active: number + version: string + latest_version: string + source: PluginSource + meta?: MetaData +} + export type Plugin = { type: PluginType org: string @@ -238,21 +256,8 @@ export type MetaData = { package: string } -export type InstalledPlugin = { - plugin_id: string - plugin_unique_identifier: string - installation_id: string - declaration: PluginDeclaration - source: PluginSource - tenant_id: string - version: string - latest_version: string - endpoints_active: number - meta: MetaData -} - export type InstalledPluginListResponse = { - plugins: InstalledPlugin[] + plugins: PluginDetail[] } export type UninstallPluginResponse = {