| @account_initialization_required | @account_initialization_required | ||||
| def get(self): | def get(self): | ||||
| tenant_id = current_user.current_tenant_id | tenant_id = current_user.current_tenant_id | ||||
| parser = reqparse.RequestParser() | |||||
| parser.add_argument("page", type=int, required=False, location="args", default=1) | |||||
| parser.add_argument("page_size", type=int, required=False, location="args", default=256) | |||||
| args = parser.parse_args() | |||||
| try: | try: | ||||
| plugins = PluginService.list(tenant_id) | |||||
| plugins_with_total = PluginService.list_with_total(tenant_id, args["page"], args["page_size"]) | |||||
| except PluginDaemonClientSideError as e: | except PluginDaemonClientSideError as e: | ||||
| raise ValueError(e) | raise ValueError(e) | ||||
| return jsonable_encoder({"plugins": plugins}) | |||||
| return jsonable_encoder({"plugins": plugins_with_total.list, "total": plugins_with_total.total}) | |||||
| class PluginListLatestVersionsApi(Resource): | class PluginListLatestVersionsApi(Resource): |
| from core.model_runtime.entities.model_entities import AIModelEntity | from core.model_runtime.entities.model_entities import AIModelEntity | ||||
| from core.model_runtime.entities.provider_entities import ProviderEntity | from core.model_runtime.entities.provider_entities import ProviderEntity | ||||
| from core.plugin.entities.base import BasePluginEntity | from core.plugin.entities.base import BasePluginEntity | ||||
| from core.plugin.entities.plugin import PluginDeclaration | |||||
| from core.plugin.entities.plugin import PluginDeclaration, PluginEntity | |||||
| from core.tools.entities.common_entities import I18nObject | from core.tools.entities.common_entities import I18nObject | ||||
| from core.tools.entities.tool_entities import ToolProviderEntityWithPlugin | from core.tools.entities.tool_entities import ToolProviderEntityWithPlugin | ||||
| class PluginOAuthCredentialsResponse(BaseModel): | class PluginOAuthCredentialsResponse(BaseModel): | ||||
| credentials: Mapping[str, Any] = Field(description="The credentials of the OAuth.") | credentials: Mapping[str, Any] = Field(description="The credentials of the OAuth.") | ||||
| class PluginListResponse(BaseModel): | |||||
| list: list[PluginEntity] | |||||
| total: int |
| PluginInstallation, | PluginInstallation, | ||||
| PluginInstallationSource, | PluginInstallationSource, | ||||
| ) | ) | ||||
| from core.plugin.entities.plugin_daemon import PluginInstallTask, PluginInstallTaskStartResponse, PluginUploadResponse | |||||
| from core.plugin.entities.plugin_daemon import ( | |||||
| PluginInstallTask, | |||||
| PluginInstallTaskStartResponse, | |||||
| PluginListResponse, | |||||
| PluginUploadResponse, | |||||
| ) | |||||
| from core.plugin.impl.base import BasePluginClient | from core.plugin.impl.base import BasePluginClient | ||||
| ) | ) | ||||
| def list_plugins(self, tenant_id: str) -> list[PluginEntity]: | def list_plugins(self, tenant_id: str) -> list[PluginEntity]: | ||||
| return self._request_with_plugin_daemon_response( | |||||
| result = self._request_with_plugin_daemon_response( | |||||
| "GET", | "GET", | ||||
| f"plugin/{tenant_id}/management/list", | f"plugin/{tenant_id}/management/list", | ||||
| list[PluginEntity], | |||||
| PluginListResponse, | |||||
| params={"page": 1, "page_size": 256}, | params={"page": 1, "page_size": 256}, | ||||
| ) | ) | ||||
| return result.list | |||||
| def list_plugins_with_total(self, tenant_id: str, page: int, page_size: int) -> PluginListResponse: | |||||
| return self._request_with_plugin_daemon_response( | |||||
| "GET", | |||||
| f"plugin/{tenant_id}/management/list", | |||||
| PluginListResponse, | |||||
| params={"page": page, "page_size": page_size}, | |||||
| ) | |||||
| def upload_pkg( | def upload_pkg( | ||||
| self, | self, |
| PluginInstallation, | PluginInstallation, | ||||
| PluginInstallationSource, | PluginInstallationSource, | ||||
| ) | ) | ||||
| from core.plugin.entities.plugin_daemon import PluginInstallTask, PluginUploadResponse | |||||
| from core.plugin.entities.plugin_daemon import PluginInstallTask, PluginListResponse, PluginUploadResponse | |||||
| from core.plugin.impl.asset import PluginAssetManager | from core.plugin.impl.asset import PluginAssetManager | ||||
| from core.plugin.impl.debugging import PluginDebuggingClient | from core.plugin.impl.debugging import PluginDebuggingClient | ||||
| from core.plugin.impl.plugin import PluginInstaller | from core.plugin.impl.plugin import PluginInstaller | ||||
| plugins = manager.list_plugins(tenant_id) | plugins = manager.list_plugins(tenant_id) | ||||
| return plugins | return plugins | ||||
| @staticmethod | |||||
| def list_with_total(tenant_id: str, page: int, page_size: int) -> PluginListResponse: | |||||
| """ | |||||
| list all plugins of the tenant | |||||
| """ | |||||
| manager = PluginInstaller() | |||||
| plugins = manager.list_plugins_with_total(tenant_id, page, page_size) | |||||
| return plugins | |||||
| @staticmethod | @staticmethod | ||||
| def list_installations_from_ids(tenant_id: str, ids: Sequence[str]) -> Sequence[PluginInstallation]: | def list_installations_from_ids(tenant_id: str, ids: Sequence[str]) -> Sequence[PluginInstallation]: | ||||
| """ | """ |
| 'use client' | 'use client' | ||||
| import { useMemo } from 'react' | import { useMemo } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | |||||
| import type { FilterState } from './filter-management' | import type { FilterState } from './filter-management' | ||||
| import FilterManagement from './filter-management' | import FilterManagement from './filter-management' | ||||
| import List from './list' | import List from './list' | ||||
| import { useInstalledLatestVersion, useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins' | |||||
| import { useInstalledLatestVersion, useInstalledPluginListWithPagination, useInvalidateInstalledPluginList } from '@/service/use-plugins' | |||||
| import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel' | import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel' | ||||
| import { usePluginPageContext } from './context' | import { usePluginPageContext } from './context' | ||||
| import { useDebounceFn } from 'ahooks' | import { useDebounceFn } from 'ahooks' | ||||
| import Button from '@/app/components/base/button' | |||||
| import Empty from './empty' | import Empty from './empty' | ||||
| import Loading from '../../base/loading' | import Loading from '../../base/loading' | ||||
| import { PluginSource } from '../types' | import { PluginSource } from '../types' | ||||
| const PluginsPanel = () => { | const PluginsPanel = () => { | ||||
| const { t } = useTranslation() | |||||
| const filters = usePluginPageContext(v => v.filters) as FilterState | const filters = usePluginPageContext(v => v.filters) as FilterState | ||||
| const setFilters = usePluginPageContext(v => v.setFilters) | const setFilters = usePluginPageContext(v => v.setFilters) | ||||
| const { data: pluginList, isLoading: isPluginListLoading } = useInstalledPluginList() | |||||
| const { data: pluginList, isLoading: isPluginListLoading, isFetching, isLastPage, loadNextPage } = useInstalledPluginListWithPagination() | |||||
| const { data: installedLatestVersion } = useInstalledLatestVersion( | const { data: installedLatestVersion } = useInstalledLatestVersion( | ||||
| pluginList?.plugins | pluginList?.plugins | ||||
| .filter(plugin => plugin.source === PluginSource.marketplace) | .filter(plugin => plugin.source === PluginSource.marketplace) | ||||
| /> | /> | ||||
| </div> | </div> | ||||
| {isPluginListLoading ? <Loading type='app' /> : (filteredList?.length ?? 0) > 0 ? ( | {isPluginListLoading ? <Loading type='app' /> : (filteredList?.length ?? 0) > 0 ? ( | ||||
| <div className='flex grow flex-wrap content-start items-start gap-2 self-stretch px-12'> | |||||
| <div className='flex grow flex-wrap content-start items-start justify-center gap-2 self-stretch px-12'> | |||||
| <div className='w-full'> | <div className='w-full'> | ||||
| <List pluginList={filteredList || []} /> | <List pluginList={filteredList || []} /> | ||||
| </div> | </div> | ||||
| {!isLastPage && !isFetching && ( | |||||
| <Button onClick={loadNextPage}> | |||||
| {t('workflow.common.loadMore')} | |||||
| </Button> | |||||
| )} | |||||
| {isFetching && <div className='system-md-semibold text-text-secondary'>{t('appLog.detail.loading')}</div>} | |||||
| </div> | </div> | ||||
| ) : ( | ) : ( | ||||
| <Empty /> | <Empty /> |
| plugins: PluginDetail[] | plugins: PluginDetail[] | ||||
| } | } | ||||
| export type InstalledPluginListWithTotalResponse = { | |||||
| plugins: PluginDetail[] | |||||
| total: number | |||||
| } | |||||
| export type InstalledLatestVersionResponse = { | export type InstalledLatestVersionResponse = { | ||||
| versions: { | versions: { | ||||
| [plugin_id: string]: { | [plugin_id: string]: { |
| InstallPackageResponse, | InstallPackageResponse, | ||||
| InstalledLatestVersionResponse, | InstalledLatestVersionResponse, | ||||
| InstalledPluginListResponse, | InstalledPluginListResponse, | ||||
| InstalledPluginListWithTotalResponse, | |||||
| PackageDependency, | PackageDependency, | ||||
| Permissions, | Permissions, | ||||
| Plugin, | Plugin, | ||||
| import { get, getMarketplace, post, postMarketplace } from './base' | import { get, getMarketplace, post, postMarketplace } from './base' | ||||
| import type { MutateOptions, QueryOptions } from '@tanstack/react-query' | import type { MutateOptions, QueryOptions } from '@tanstack/react-query' | ||||
| import { | import { | ||||
| useInfiniteQuery, | |||||
| useMutation, | useMutation, | ||||
| useQuery, | useQuery, | ||||
| useQueryClient, | useQueryClient, | ||||
| }) | }) | ||||
| } | } | ||||
| export const useInstalledPluginListWithPagination = (pageSize = 100) => { | |||||
| const fetchPlugins = async ({ pageParam = 1 }) => { | |||||
| const response = await get<InstalledPluginListWithTotalResponse>( | |||||
| `/workspaces/current/plugin/list?page=${pageParam}&page_size=${pageSize}`, | |||||
| ) | |||||
| return response | |||||
| } | |||||
| const { | |||||
| data, | |||||
| error, | |||||
| fetchNextPage, | |||||
| hasNextPage, | |||||
| isFetchingNextPage, | |||||
| isLoading, | |||||
| } = useInfiniteQuery({ | |||||
| queryKey: ['installed-plugins', pageSize], | |||||
| queryFn: fetchPlugins, | |||||
| getNextPageParam: (lastPage, pages) => { | |||||
| const totalItems = lastPage.total | |||||
| const currentPage = pages.length | |||||
| const itemsLoaded = currentPage * pageSize | |||||
| if (itemsLoaded >= totalItems) | |||||
| return | |||||
| return currentPage + 1 | |||||
| }, | |||||
| initialPageParam: 1, | |||||
| }) | |||||
| const plugins = data?.pages.flatMap(page => page.plugins) ?? [] | |||||
| return { | |||||
| data: { | |||||
| plugins, | |||||
| }, | |||||
| isLastPage: !hasNextPage, | |||||
| loadNextPage: () => { | |||||
| fetchNextPage() | |||||
| }, | |||||
| isLoading, | |||||
| isFetching: isFetchingNextPage, | |||||
| error, | |||||
| } | |||||
| } | |||||
| export const useInstalledLatestVersion = (pluginIds: string[]) => { | export const useInstalledLatestVersion = (pluginIds: string[]) => { | ||||
| return useQuery<InstalledLatestVersionResponse>({ | return useQuery<InstalledLatestVersionResponse>({ | ||||
| queryKey: [NAME_SPACE, 'installedLatestVersion', pluginIds], | queryKey: [NAME_SPACE, 'installedLatestVersion', pluginIds], |