From 6c161c6ae766bb1fc5e532a6c7a07ff26ae4eeaa Mon Sep 17 00:00:00 2001 From: jialin Date: Wed, 22 Apr 2026 21:57:48 +0800 Subject: [PATCH] build: sync config --- config/config.ts | 3 + config/routes.ts | 20 ++++ package.json | 7 ++ scripts/run-enterprise-build.cjs | 47 +++++++++ scripts/run-enterprise-dev.cjs | 69 +++++++++++++ scripts/sync-enterprise-config.cjs | 96 +++++++++++++++++++ src/app.tsx | 28 +++++- src/components/templates/card-list.tsx | 4 +- src/config/theme/constants.ts | 1 + src/config/theme/dark.ts | 2 +- src/config/theme/index.ts | 5 +- src/config/theme/light.ts | 2 +- src/hooks/use-user-settings-storage.ts | 17 ++++ src/hooks/use-user-settings.ts | 4 +- src/layouts/extraRender.tsx | 15 +++ src/layouts/index.tsx | 20 +++- src/pages/api-keys/hooks/use-keys-columns.tsx | 3 +- .../backends/components/backend-list.tsx | 4 +- src/pages/backends/config/index.ts | 2 +- .../benchmark/components/row-actions.tsx | 8 +- src/pages/benchmark/forms/index.tsx | 8 +- src/pages/cluster-management/clusters.tsx | 2 +- .../components/cloud-options.tsx | 4 +- .../components/pool-form.tsx | 4 +- .../components/volumes-config.tsx | 4 +- .../config/cloud-options-config.ts | 2 +- src/pages/cluster-management/config/index.ts | 2 +- .../cluster-management/config/providers.ts | 2 +- .../hooks/use-cluster-columns.tsx | 6 +- .../hooks/use-pools-columns.tsx | 9 +- .../components/catalog/catalog-list.tsx | 4 +- .../instance-cells/actions-cell.tsx | 2 +- .../downloading-status-cell.tsx | 3 +- src/pages/llmodels/components/table-list.tsx | 4 +- src/pages/llmodels/config/button-actions.ts | 2 +- src/pages/llmodels/deployments.tsx | 3 +- src/pages/llmodels/forms/index.tsx | 8 +- .../llmodels/hooks/use-models-columns.tsx | 12 ++- .../llmodels/hooks/use-view-instance-logs.ts | 2 +- src/pages/maas-provider/config/index.ts | 2 +- src/pages/maas-provider/forms/index.tsx | 12 ++- src/pages/model-routes/config/index.ts | 2 +- src/pages/model-routes/forms/index.tsx | 12 ++- .../model-routes/hooks/use-routes-columns.tsx | 9 +- src/pages/model-routes/index.tsx | 2 +- src/pages/playground/images/edit.tsx | 2 +- src/pages/settings/index.tsx | 22 +++++ src/pages/users/hooks/use-users-columns.tsx | 8 +- src/plugins/index.ts | 23 +++++ src/plugins/locale-merger.ts | 34 +++++++ src/plugins/manager.ts | 81 ++++++++++++++++ src/plugins/optional-empty-module.ts | 2 + src/plugins/route-merger.ts | 57 +++++++++++ src/plugins/types.ts | 95 ++++++++++++++++++ tsconfig.json | 18 +--- 55 files changed, 731 insertions(+), 90 deletions(-) create mode 100644 scripts/run-enterprise-build.cjs create mode 100644 scripts/run-enterprise-dev.cjs create mode 100644 scripts/sync-enterprise-config.cjs create mode 100644 src/config/theme/constants.ts create mode 100644 src/hooks/use-user-settings-storage.ts create mode 100644 src/pages/settings/index.tsx create mode 100644 src/plugins/index.ts create mode 100644 src/plugins/locale-merger.ts create mode 100644 src/plugins/manager.ts create mode 100644 src/plugins/optional-empty-module.ts create mode 100644 src/plugins/route-merger.ts create mode 100644 src/plugins/types.ts diff --git a/config/config.ts b/config/config.ts index f8a93a26..d636fa1f 100644 --- a/config/config.ts +++ b/config/config.ts @@ -19,6 +19,9 @@ export default defineConfig({ history: { type: 'hash' }, + define: { + 'process.env.ENABLE_ENTERPRISE': process.env.ENABLE_ENTERPRISE + }, analyze: { analyzerMode: 'server', analyzerPort: 8888, diff --git a/config/routes.ts b/config/routes.ts index d0d63288..c60234e1 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -302,6 +302,26 @@ export default [ } ] }, + { + name: 'apikeys', + path: '/api-keys', + key: 'apikeys', + hideInMenu: true, + selectedIcon: 'icon-key-filled', + icon: 'icon-key', + defaultIcon: 'icon-key', + component: './api-keys' + }, + { + name: 'settings', + path: '/settings', + key: 'settings', + hideInMenu: true, + selectedIcon: 'icon-settings-filled', + icon: 'icon-settings', + defaultIcon: 'icon-settings', + component: './settings' + }, { name: 'profile', path: '/profile', diff --git a/package.json b/package.json index fd69418f..a9994a41 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,19 @@ "author": "jialin", "scripts": { "build": "max build", + "build:enterprise": "node ./scripts/run-enterprise-build.cjs", "check:locales": "npx tsx ./src/locales/check.ts", "dev": "max dev", + "dev:enterprise": "node ./scripts/run-enterprise-dev.cjs", "format": "prettier --cache --write .", "postinstall": "max setup", "prepare": "husky", "preview": "max preview", "setup": "max setup", + "sync:enterprise-config:clean": "node ./scripts/sync-enterprise-config.cjs clean", + "sync:enterprise-config": "node ./scripts/sync-enterprise-config.cjs", + "sync:enterprise-global:clean": "npm run sync:enterprise-config:clean", + "sync:enterprise-global": "npm run sync:enterprise-config", "start": "npm run dev" }, "dependencies": { @@ -94,6 +100,7 @@ "babel-plugin-named-asset-import": "^0.3.8", "case-sensitive-paths-webpack-plugin": "^2.4.0", "compression-webpack-plugin": "^11.1.0", + "cross-env": "^7.0.3", "css-loader": "^7.1.2", "eslint": "^9.39.4", "eslint-config-prettier": "^10.1.8", diff --git a/scripts/run-enterprise-build.cjs b/scripts/run-enterprise-build.cjs new file mode 100644 index 00000000..050d2f8e --- /dev/null +++ b/scripts/run-enterprise-build.cjs @@ -0,0 +1,47 @@ +const { spawnSync } = require('node:child_process'); +const path = require('node:path'); + +const syncScript = path.resolve(__dirname, './sync-enterprise-config.cjs'); + +const runNodeScript = (args) => { + const result = spawnSync(process.execPath, [syncScript, ...args], { + stdio: 'inherit' + }); + + if (result.error) { + throw result.error; + } + + if (typeof result.status === 'number' && result.status !== 0) { + process.exit(result.status); + } +}; + +const runBuild = () => + spawnSync('max', ['build'], { + env: { + ...process.env, + ENABLE_ENTERPRISE: 'true' + }, + shell: true, + stdio: 'inherit' + }); + +let buildResult; + +try { + runNodeScript([]); + buildResult = runBuild(); +} finally { + try { + runNodeScript(['clean']); + } catch (error) { + console.error('Failed to clean enterprise config:', error); + } +} + +if (buildResult?.error) { + throw buildResult.error; +} + +process.exit(buildResult?.status ?? 0); diff --git a/scripts/run-enterprise-dev.cjs b/scripts/run-enterprise-dev.cjs new file mode 100644 index 00000000..11663ede --- /dev/null +++ b/scripts/run-enterprise-dev.cjs @@ -0,0 +1,69 @@ +const { spawn, spawnSync } = require('node:child_process'); +const path = require('node:path'); + +const syncScript = path.resolve(__dirname, './sync-enterprise-config.cjs'); + +const runNodeScript = (args) => { + const result = spawnSync(process.execPath, [syncScript, ...args], { + stdio: 'inherit' + }); + + if (typeof result.status === 'number' && result.status !== 0) { + process.exit(result.status); + } + + if (result.error) { + throw result.error; + } +}; + +runNodeScript([]); + +const child = spawn('max', ['dev'], { + env: { + ...process.env, + ENABLE_ENTERPRISE: 'true' + }, + shell: true, + stdio: 'inherit' +}); + +let cleaned = false; + +const clean = () => { + if (cleaned) { + return; + } + cleaned = true; + try { + runNodeScript(['clean']); + } catch (error) { + console.error('Failed to clean enterprise config:', error); + } +}; + +const forwardSignal = (signal) => { + if (!child.killed) { + child.kill(signal); + } +}; + +['SIGINT', 'SIGTERM', 'SIGHUP'].forEach((signal) => { + process.on(signal, () => { + forwardSignal(signal); + }); +}); + +child.on('error', (error) => { + clean(); + throw error; +}); + +child.on('exit', (code, signal) => { + clean(); + if (signal) { + process.kill(process.pid, signal); + return; + } + process.exit(code ?? 0); +}); diff --git a/scripts/sync-enterprise-config.cjs b/scripts/sync-enterprise-config.cjs new file mode 100644 index 00000000..9c8b7e8c --- /dev/null +++ b/scripts/sync-enterprise-config.cjs @@ -0,0 +1,96 @@ +const fs = require('node:fs'); +const path = require('node:path'); + +const workspaceGlobalPath = path.resolve(__dirname, '../../../global.tsx'); +const workspaceDependenciesPath = path.resolve(__dirname, '../../../dependencies.json'); +const workspaceTsconfigPath = path.resolve(__dirname, '../../../tsconfig.json'); +const targetGlobalPath = path.resolve(__dirname, '../src/global.tsx'); +const targetPackageJsonPath = path.resolve(__dirname, '../package.json'); +const targetTsconfigPath = path.resolve(__dirname, '../tsconfig.json'); +const mode = process.argv[2] || 'append'; +const blockStart = '// ENTERPRISE_PLUGIN_BLOCK_START'; +const blockEnd = '// ENTERPRISE_PLUGIN_BLOCK_END'; + +const workspaceSource = fs.readFileSync(workspaceGlobalPath, 'utf8').trim(); +const blockContent = `\n${blockStart}\n${workspaceSource}\n${blockEnd}\n`; + +const stripBlock = (content) => { + const pattern = new RegExp(`\\n?${blockStart}[\\s\\S]*?${blockEnd}\\n?`, 'g'); + return content.replace(pattern, '\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n'; +}; + +const currentContent = fs.readFileSync(targetGlobalPath, 'utf8'); +const strippedContent = stripBlock(currentContent); + +const readJson = (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf8')); +const writeJson = (filePath, data) => { + fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`); +}; + +const syncPackageJson = () => { + const sourceDependencies = + readJson(workspaceDependenciesPath).dependencies || {}; + const targetPackageJson = readJson(targetPackageJsonPath); + const currentDependencies = { + ...(targetPackageJson.dependencies || {}) + }; + + if (mode === 'clean') { + Object.keys(sourceDependencies).forEach((dependencyName) => { + delete currentDependencies[dependencyName]; + }); + } else { + Object.assign(currentDependencies, sourceDependencies); + } + + targetPackageJson.dependencies = currentDependencies; + writeJson(targetPackageJsonPath, targetPackageJson); + console.log( + `${mode === 'clean' ? 'Cleaned' : 'Synced'} enterprise dependencies in ${targetPackageJsonPath}`, + ); +}; + +const syncTsconfig = () => { + const sourcePaths = + readJson(workspaceTsconfigPath).compilerOptions?.paths || {}; + const targetTsconfig = readJson(targetTsconfigPath); + const currentPaths = { + ...(targetTsconfig.compilerOptions?.paths || {}) + }; + + if (mode === 'clean') { + Object.keys(sourcePaths).forEach((pathKey) => { + delete currentPaths[pathKey]; + }); + } else { + Object.assign(currentPaths, sourcePaths); + } + + targetTsconfig.compilerOptions = targetTsconfig.compilerOptions || {}; + targetTsconfig.compilerOptions.paths = currentPaths; + + writeJson(targetTsconfigPath, targetTsconfig); + console.log( + `${mode === 'clean' ? 'Cleaned' : 'Synced'} enterprise tsconfig paths in ${targetTsconfigPath}`, + ); +}; + +if (mode === 'clean') { + if (strippedContent !== currentContent) { + fs.writeFileSync(targetGlobalPath, strippedContent); + console.log(`Cleaned enterprise plugin block from ${targetGlobalPath}`); + } else { + console.log(`Enterprise plugin block already absent: ${targetGlobalPath}`); + } +} else { + const updatedContent = `${strippedContent.trimEnd()}${blockContent}`; + if (updatedContent !== currentContent) { + fs.writeFileSync(targetGlobalPath, updatedContent); + console.log(`Appended enterprise plugin block to ${targetGlobalPath}`); + } else { + console.log(`Enterprise plugin block already up to date: ${targetGlobalPath}`); + } +} + +syncPackageJson(); +syncTsconfig(); diff --git a/src/app.tsx b/src/app.tsx index c8af9456..6c2d39de 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,6 +1,9 @@ import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user'; import { setAtomStorage } from '@/atoms/utils'; import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings'; +import { getGPUStackPlugin } from '@/plugins'; +import { GPUStackPluginManager } from '@/plugins/manager'; +import { mergeEnterpriseRoutes } from '@/plugins/route-merger'; import { requestConfig } from '@/request-config'; import { queryCurrentUserState, @@ -14,6 +17,7 @@ import { readState, writeState } from '@/utils/localstore/index'; +import '@gpustack/core-ui/style.css'; import { RequestConfig, history } from '@umijs/max'; import { message } from 'antd'; @@ -33,8 +37,18 @@ const checkDefaultPage = async (userInfo: any) => { export async function getInitialState(): Promise<{ fetchUserInfo: () => Promise; currentUser?: Global.UserInfo; + pluginData?: Record; }> { const { location } = history; + const enterprisePlugin = getGPUStackPlugin(); + + // initialize plugins and merge enterprise locales + let pluginData = {}; + try { + pluginData = await GPUStackPluginManager.initialize(); + } catch (error) { + console.error('Failed to initialize plugins:', error); + } const getUpdateCheck = async () => { try { @@ -103,11 +117,13 @@ export async function getInitialState(): Promise<{ checkDefaultPage(userInfo); return { fetchUserInfo, - currentUser: userInfo + currentUser: userInfo, + pluginData }; } return { - fetchUserInfo + fetchUserInfo, + pluginData }; } @@ -115,3 +131,11 @@ export const request: RequestConfig = { baseURL: `/${GPUSTACK_API_BASE_URL}`, ...requestConfig }; + +/** + * 动态修改路由 + * @param routes 路由配置 + */ +export function patchClientRoutes({ routes }: any) { + mergeEnterpriseRoutes(routes); +} diff --git a/src/components/templates/card-list.tsx b/src/components/templates/card-list.tsx index 37974e47..0c79631d 100644 --- a/src/components/templates/card-list.tsx +++ b/src/components/templates/card-list.tsx @@ -1,9 +1,9 @@ import { InfiniteScroller, ResizeContainer, - TemplateCardSkeleton + TemplateCardSkeleton, + useScrollerContext } from '@gpustack/core-ui'; -import { useScrollerContext } from '@gpustack/core-ui/lib/components/infinite-scroller/use-scroller-context'; import { Spin } from 'antd'; import React from 'react'; import styled from 'styled-components'; diff --git a/src/config/theme/constants.ts b/src/config/theme/constants.ts new file mode 100644 index 00000000..71a0aa2c --- /dev/null +++ b/src/config/theme/constants.ts @@ -0,0 +1 @@ +export const COLOR_PRIMARY = '#007BFF'; diff --git a/src/config/theme/dark.ts b/src/config/theme/dark.ts index 52fd4d8f..07680da2 100644 --- a/src/config/theme/dark.ts +++ b/src/config/theme/dark.ts @@ -1,4 +1,4 @@ -import { COLOR_PRIMARY } from './index'; +import { COLOR_PRIMARY } from './constants'; export default { 'root-entry-name': 'variable', diff --git a/src/config/theme/index.ts b/src/config/theme/index.ts index 116f53b9..6351ee48 100644 --- a/src/config/theme/index.ts +++ b/src/config/theme/index.ts @@ -1,10 +1,11 @@ +import { COLOR_PRIMARY } from './constants'; import dark from './dark'; import light from './light'; -export const COLOR_PRIMARY = '#007BFF'; - export default { light, dark, colorPrimary: COLOR_PRIMARY }; + +export { COLOR_PRIMARY }; diff --git a/src/config/theme/light.ts b/src/config/theme/light.ts index 28b08462..3e01a88d 100644 --- a/src/config/theme/light.ts +++ b/src/config/theme/light.ts @@ -1,4 +1,4 @@ -import { COLOR_PRIMARY } from './index'; +import { COLOR_PRIMARY } from './constants'; export default { 'root-entry-name': 'variable', diff --git a/src/hooks/use-user-settings-storage.ts b/src/hooks/use-user-settings-storage.ts new file mode 100644 index 00000000..2048ec23 --- /dev/null +++ b/src/hooks/use-user-settings-storage.ts @@ -0,0 +1,17 @@ +import { userSettingsHelperAtom } from '@/atoms/settings'; +import { getAtomStorage, setAtomStorage } from '@/atoms/utils'; + +export default function useUserSettingsStorage() { + const setStorageUserSettings = (value: Record) => { + setAtomStorage(userSettingsHelperAtom, value || {}); + }; + + const getStorageUserSettings = () => { + return getAtomStorage(userSettingsHelperAtom); + }; + + return { + setStorageUserSettings, + getStorageUserSettings + }; +} diff --git a/src/hooks/use-user-settings.ts b/src/hooks/use-user-settings.ts index ff490b54..71334a31 100644 --- a/src/hooks/use-user-settings.ts +++ b/src/hooks/use-user-settings.ts @@ -1,7 +1,7 @@ -import { userSettingsHelperAtom } from '@/atoms/settings'; -import themeConfig from '@/config/theme'; import { useAtom } from 'jotai'; import { useEffect, useMemo } from 'react'; +import { userSettingsHelperAtom } from '../atoms/settings'; +import themeConfig from '../config/theme'; type Theme = 'light' | 'realDark' | 'auto'; diff --git a/src/layouts/extraRender.tsx b/src/layouts/extraRender.tsx index 1c49de78..a2e64a1e 100644 --- a/src/layouts/extraRender.tsx +++ b/src/layouts/extraRender.tsx @@ -3,6 +3,7 @@ import VersionInfo, { modalConfig } from '@/components/version-info'; import externalLinks from '@/constants/external-links'; import useBodyScroll from '@/hooks/use-body-scroll'; import { logout } from '@/pages/login/apis'; +import { getGPUStackPlugin } from '@/plugins'; import { useModel } from '@@/plugin-model'; import { DiscordOutlined, @@ -105,6 +106,7 @@ const CustomItem = styled.div` export const ExtraContent = (props: { isDarkTheme?: boolean }) => { const { isDarkTheme } = props; + const pluginManager = getGPUStackPlugin(); const { saveScrollHeight, restoreScrollHeight } = useBodyScroll(); const [modal, contextHolder] = Modal.useModal(); const [version] = useAtom(GPUStackVersionAtom); @@ -117,6 +119,7 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => { }; const { initialState } = initialInfo; + console.log('plugin+++++++++', pluginManager); const navigate = useNavigate(); @@ -291,6 +294,18 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => { )} + { }); const [, contextHolder] = Modal.useModal(); const { themeData, setUserSettings, userSettings } = useUserSettings(); + const userSettingsStorage = useUserSettingsStorage(); const [userInfo] = useAtom(userAtom); const [routeCache] = useAtom(routeCacheAtom); const location = useLocation(); @@ -205,7 +208,7 @@ export default (props: any) => { const role = initialState?.currentUser?.is_admin ? 'admin' : 'user'; const [route] = useAccessMarkedRoutes(mapRoutes(newRoutes, role)); - + console.log('route++++++++', route, clientRoutes); patchRoutes({ routes: route.children, initialState: initialInfo.initialState @@ -363,7 +366,13 @@ export default (props: any) => { apiBaseUrl: GPUSTACK_API_BASE_URL, theme: userSettings.theme, iconUrl: '//at.alicdn.com/t/c/font_4613488_8fi68fmt1th.js', - isDarkTheme: userSettings.isDarkTheme + isDarkTheme: userSettings.isDarkTheme, + defaultColorPrimary: COLOR_PRIMARY + }} + hooks={{ + useUserSettings: useUserSettings, + useUserSettingsStorage: () => userSettingsStorage, + useIntl: useIntl }} i18n={intl} locale={{ @@ -371,7 +380,12 @@ export default (props: any) => { setLocale: setLocale }} services={{ - request: request + request: request, + router: { + push: (path: string) => navigate(path), + replace: (path: string) => navigate(path, { replace: true }), + goBack: () => navigate(-1) + } }} localStore={{ readColumnSettings, diff --git a/src/pages/api-keys/hooks/use-keys-columns.tsx b/src/pages/api-keys/hooks/use-keys-columns.tsx index fa38c5eb..f73b6f72 100644 --- a/src/pages/api-keys/hooks/use-keys-columns.tsx +++ b/src/pages/api-keys/hooks/use-keys-columns.tsx @@ -1,7 +1,6 @@ // columns.ts import { tableSorter } from '@/config/settings'; -import { AutoTooltip, DropdownButtons } from '@gpustack/core-ui'; -import icons from '@gpustack/core-ui/lib/components/icon-font/icons'; +import { AutoTooltip, DropdownButtons, icons } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Tag } from 'antd'; import { ColumnsType } from 'antd/lib/table'; diff --git a/src/pages/backends/components/backend-list.tsx b/src/pages/backends/components/backend-list.tsx index aa326ad2..2a246f84 100644 --- a/src/pages/backends/components/backend-list.tsx +++ b/src/pages/backends/components/backend-list.tsx @@ -1,9 +1,9 @@ import { InfiniteScroller, ResizeContainer, - TemplateCardSkeleton + TemplateCardSkeleton, + useScrollerContext } from '@gpustack/core-ui'; -import { useScrollerContext } from '@gpustack/core-ui/lib/components/infinite-scroller/use-scroller-context'; import { Spin } from 'antd'; import React from 'react'; import backendListCss from '../styles/backend-list.less'; diff --git a/src/pages/backends/config/index.ts b/src/pages/backends/config/index.ts index 0c07d3f9..d6e06322 100644 --- a/src/pages/backends/config/index.ts +++ b/src/pages/backends/config/index.ts @@ -7,7 +7,7 @@ import { GPUDriverMap, ManufacturerMap } from '@/pages/resources/config/gpu-driver'; -import icons from '@gpustack/core-ui/lib/components/icon-font/icons'; +import { icons } from '@gpustack/core-ui'; import jsYaml from 'js-yaml'; import { trim } from 'lodash'; diff --git a/src/pages/benchmark/components/row-actions.tsx b/src/pages/benchmark/components/row-actions.tsx index 3cf4ae58..d2edfa69 100644 --- a/src/pages/benchmark/components/row-actions.tsx +++ b/src/pages/benchmark/components/row-actions.tsx @@ -1,6 +1,10 @@ import { DownloadOutlined } from '@ant-design/icons'; -import { DropdownButtons, IconFont, useDownloadLogs } from '@gpustack/core-ui'; -import icons from '@gpustack/core-ui/lib/components/icon-font/icons'; +import { + DropdownButtons, + IconFont, + icons, + useDownloadLogs +} from '@gpustack/core-ui'; import { BENCHMARKS_API } from '../apis'; import { BenchmarkStatusValueMap } from '../config'; import { BenchmarkListItem as ListItem } from '../config/types'; diff --git a/src/pages/benchmark/forms/index.tsx b/src/pages/benchmark/forms/index.tsx index 2b808c6b..186d3f8e 100644 --- a/src/pages/benchmark/forms/index.tsx +++ b/src/pages/benchmark/forms/index.tsx @@ -1,7 +1,11 @@ import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; -import { CollapsePanel, IconFont, ScrollSpyTabs } from '@gpustack/core-ui'; -import { useWrapperContext } from '@gpustack/core-ui/lib/components/column-wrapper/use-wrapper-context'; +import { + CollapsePanel, + IconFont, + ScrollSpyTabs, + useWrapperContext +} from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Form } from 'antd'; import { diff --git a/src/pages/cluster-management/clusters.tsx b/src/pages/cluster-management/clusters.tsx index 7edca203..f19476ac 100644 --- a/src/pages/cluster-management/clusters.tsx +++ b/src/pages/cluster-management/clusters.tsx @@ -10,9 +10,9 @@ import { FilterBar, IconFont, Table as SealTable, + TableOrder, TableProvider } from '@gpustack/core-ui'; -import { TableOrder } from '@gpustack/core-ui/lib/components/table/types'; import { useIntl, useNavigate } from '@umijs/max'; import { useMemoizedFn } from 'ahooks'; import { message } from 'antd'; diff --git a/src/pages/cluster-management/components/cloud-options.tsx b/src/pages/cluster-management/components/cloud-options.tsx index 7d65f475..8d4eec81 100644 --- a/src/pages/cluster-management/components/cloud-options.tsx +++ b/src/pages/cluster-management/components/cloud-options.tsx @@ -1,6 +1,4 @@ -import { DropdownActions } from '@gpustack/core-ui'; -import ListMap from '@gpustack/core-ui/lib/components/dynamic-form/components/list-map'; -import { FieldSchema } from '@gpustack/core-ui/lib/components/dynamic-form/config/types'; +import { DropdownActions, type FieldSchema, ListMap } from '@gpustack/core-ui'; import { PlusOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; diff --git a/src/pages/cluster-management/components/pool-form.tsx b/src/pages/cluster-management/components/pool-form.tsx index 4691dcca..a6c1b777 100644 --- a/src/pages/cluster-management/components/pool-form.tsx +++ b/src/pages/cluster-management/components/pool-form.tsx @@ -14,9 +14,9 @@ import { IconFont, LabelSelector, Select as SealSelect, - useAppUtils + useAppUtils, + type CollapseContainerProps } from '@gpustack/core-ui'; -import { type CollapseContainerProps } from '@gpustack/core-ui/lib/components/collapse-container'; import { useIntl } from '@umijs/max'; import { useMemoizedFn } from 'ahooks'; import { Button, Form } from 'antd'; diff --git a/src/pages/cluster-management/components/volumes-config.tsx b/src/pages/cluster-management/components/volumes-config.tsx index 1e1a62b7..6d4d6f78 100644 --- a/src/pages/cluster-management/components/volumes-config.tsx +++ b/src/pages/cluster-management/components/volumes-config.tsx @@ -1,6 +1,4 @@ -import ListMap from '@gpustack/core-ui/lib/components/dynamic-form/components/list-map'; -import { statusType } from '@gpustack/core-ui/lib/components/dynamic-form/config/types'; -import useValidateFields from '@gpustack/core-ui/lib/components/dynamic-form/hooks/use-validate-fields'; +import { ListMap, type statusType, useValidateFields } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Form } from 'antd'; import React, { forwardRef, useState } from 'react'; diff --git a/src/pages/cluster-management/config/cloud-options-config.ts b/src/pages/cluster-management/config/cloud-options-config.ts index 4cc73e64..f64ef93b 100644 --- a/src/pages/cluster-management/config/cloud-options-config.ts +++ b/src/pages/cluster-management/config/cloud-options-config.ts @@ -1,4 +1,4 @@ -import { FieldSchema } from '@gpustack/core-ui/lib/components/dynamic-form/config/types'; +import { type FieldSchema } from '@gpustack/core-ui'; export const fields = { volumes: { diff --git a/src/pages/cluster-management/config/index.ts b/src/pages/cluster-management/config/index.ts index 47d897f4..b3cac3cb 100644 --- a/src/pages/cluster-management/config/index.ts +++ b/src/pages/cluster-management/config/index.ts @@ -2,7 +2,7 @@ import { StatusMaps } from '@/config'; import { GPUSTACK_API_BASE_URL } from '@/config/settings'; import { StatusType } from '@/config/types'; import { GPUsConfigs } from '@/pages/resources/config/gpu-driver'; -import icons from '@gpustack/core-ui/lib/components/icon-font/icons'; +import { icons } from '@gpustack/core-ui'; export const ClusterStatusValueMap = { Provisioning: 'provisioning', diff --git a/src/pages/cluster-management/config/providers.ts b/src/pages/cluster-management/config/providers.ts index b6ad2272..ad5f51c3 100644 --- a/src/pages/cluster-management/config/providers.ts +++ b/src/pages/cluster-management/config/providers.ts @@ -1,4 +1,4 @@ -import icons from '@gpustack/core-ui/lib/components/icon-font/icons'; +import { icons } from '@gpustack/core-ui'; import React from 'react'; import { ProviderValueMap } from '.'; diff --git a/src/pages/cluster-management/hooks/use-cluster-columns.tsx b/src/pages/cluster-management/hooks/use-cluster-columns.tsx index 34a44b4a..e63beb16 100644 --- a/src/pages/cluster-management/hooks/use-cluster-columns.tsx +++ b/src/pages/cluster-management/hooks/use-cluster-columns.tsx @@ -6,10 +6,10 @@ import { AutoTooltip, DropdownButtons, GrafanaIcon, - StatusTag + StatusTag, + icons, + type TableColumnProps as SealColumnProps } from '@gpustack/core-ui'; -import icons from '@gpustack/core-ui/lib/components/icon-font/icons'; -import { ColumnProps as SealColumnProps } from '@gpustack/core-ui/lib/components/table/types'; import { useIntl } from '@umijs/max'; import { Tooltip } from 'antd'; import dayjs from 'dayjs'; diff --git a/src/pages/cluster-management/hooks/use-pools-columns.tsx b/src/pages/cluster-management/hooks/use-pools-columns.tsx index 0ee0d660..63640d33 100644 --- a/src/pages/cluster-management/hooks/use-pools-columns.tsx +++ b/src/pages/cluster-management/hooks/use-pools-columns.tsx @@ -1,6 +1,9 @@ import { DeleteOutlined, EditOutlined } from '@ant-design/icons'; -import { AutoTooltip, DropdownButtons } from '@gpustack/core-ui'; -import { ColumnProps } from '@gpustack/core-ui/lib/components/table/types'; +import { + AutoTooltip, + DropdownButtons, + type TableColumnProps +} from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import dayjs from 'dayjs'; import _ from 'lodash'; @@ -27,7 +30,7 @@ const actionItems = [ const usePoolsColumns = ( handleSelect: (val: string, record: ListItem) => void, sortOrder?: string[] -): ColumnProps[] => { +): TableColumnProps[] => { const intl = useIntl(); return useMemo(() => { diff --git a/src/pages/llmodels/components/catalog/catalog-list.tsx b/src/pages/llmodels/components/catalog/catalog-list.tsx index 8f0d1fd7..4932177f 100644 --- a/src/pages/llmodels/components/catalog/catalog-list.tsx +++ b/src/pages/llmodels/components/catalog/catalog-list.tsx @@ -1,9 +1,9 @@ import { InfiniteScroller, ResizeContainer, - TemplateCardSkeleton + TemplateCardSkeleton, + useScrollerContext } from '@gpustack/core-ui'; -import { useScrollerContext } from '@gpustack/core-ui/lib/components/infinite-scroller/use-scroller-context'; import { Spin } from 'antd'; import React from 'react'; import styled from 'styled-components'; diff --git a/src/pages/llmodels/components/instance-cells/actions-cell.tsx b/src/pages/llmodels/components/instance-cells/actions-cell.tsx index cf610c61..bdf52137 100644 --- a/src/pages/llmodels/components/instance-cells/actions-cell.tsx +++ b/src/pages/llmodels/components/instance-cells/actions-cell.tsx @@ -2,10 +2,10 @@ import { useBenchmarkTargetInstance } from '@/pages/llmodels/hooks/use-run-bench import { DeleteOutlined, DownloadOutlined } from '@ant-design/icons'; import { DropdownButtons, + type HandlerOptions, IconFont, useDownloadStream } from '@gpustack/core-ui'; -import { HandlerOptions } from '@gpustack/core-ui/lib/hooks/use-chunk-fetch'; import { useIntl } from '@umijs/max'; import { Progress, notification } from 'antd'; import dayjs from 'dayjs'; diff --git a/src/pages/llmodels/components/instance-cells/downloading-status-cell.tsx b/src/pages/llmodels/components/instance-cells/downloading-status-cell.tsx index 348726e7..d6f7c3d0 100644 --- a/src/pages/llmodels/components/instance-cells/downloading-status-cell.tsx +++ b/src/pages/llmodels/components/instance-cells/downloading-status-cell.tsx @@ -1,6 +1,5 @@ import { ListItem as WorkerListItem } from '@/pages/resources/config/types'; -import { SimpleTable, StatusTag } from '@gpustack/core-ui'; -import { type ColumnProps } from '@gpustack/core-ui/lib/components/simple-table'; +import { SimpleTable, StatusTag, type ColumnProps } from '@gpustack/core-ui'; import { Progress, Tooltip } from 'antd'; import _ from 'lodash'; import { InstanceStatusMap, status } from '../../config'; diff --git a/src/pages/llmodels/components/table-list.tsx b/src/pages/llmodels/components/table-list.tsx index 739b78bc..2e083492 100644 --- a/src/pages/llmodels/components/table-list.tsx +++ b/src/pages/llmodels/components/table-list.tsx @@ -18,9 +18,9 @@ import { DropdownActions, DropdownButtons, PageTools, - Table as SealTable + Table as SealTable, + TableOrder } from '@gpustack/core-ui'; -import { TableOrder } from '@gpustack/core-ui/lib/components/table/types'; import { useIntl, useNavigate, useSearchParams } from '@umijs/max'; import { useMemoizedFn, useToggle } from 'ahooks'; import { Button, Space, message } from 'antd'; diff --git a/src/pages/llmodels/config/button-actions.ts b/src/pages/llmodels/config/button-actions.ts index 2b738f15..bb6a59c4 100644 --- a/src/pages/llmodels/config/button-actions.ts +++ b/src/pages/llmodels/config/button-actions.ts @@ -1,5 +1,5 @@ import HotKeys from '@/config/hotkeys'; -import icons from '@gpustack/core-ui/lib/components/icon-font/icons'; +import { icons } from '@gpustack/core-ui'; import React from 'react'; import { modelCategoriesMap, modelSourceMap } from './index'; diff --git a/src/pages/llmodels/deployments.tsx b/src/pages/llmodels/deployments.tsx index 53921b32..31814b77 100644 --- a/src/pages/llmodels/deployments.tsx +++ b/src/pages/llmodels/deployments.tsx @@ -3,8 +3,7 @@ import useSetChunkRequest from '@/hooks/use-chunk-request'; import { usePaginationStatus } from '@/hooks/use-pagination-status'; import { useTableMultiSort } from '@/hooks/use-table-sort'; import useUpdateChunkedList from '@/hooks/use-update-chunk-list'; -import { TableProvider } from '@gpustack/core-ui'; -import { TableOrder } from '@gpustack/core-ui/lib/components/table/types'; +import { TableOrder, TableProvider } from '@gpustack/core-ui'; import { useMemoizedFn } from 'ahooks'; import _ from 'lodash'; import qs from 'query-string'; diff --git a/src/pages/llmodels/forms/index.tsx b/src/pages/llmodels/forms/index.tsx index 73483af2..c53ce331 100644 --- a/src/pages/llmodels/forms/index.tsx +++ b/src/pages/llmodels/forms/index.tsx @@ -1,7 +1,11 @@ import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; -import { CollapsePanel, IconFont, ScrollSpyTabs } from '@gpustack/core-ui'; -import { useWrapperContext } from '@gpustack/core-ui/lib/components/column-wrapper/use-wrapper-context'; +import { + CollapsePanel, + IconFont, + ScrollSpyTabs, + useWrapperContext +} from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Form } from 'antd'; import _ from 'lodash'; diff --git a/src/pages/llmodels/hooks/use-models-columns.tsx b/src/pages/llmodels/hooks/use-models-columns.tsx index da895e0f..b77e9ccd 100644 --- a/src/pages/llmodels/hooks/use-models-columns.tsx +++ b/src/pages/llmodels/hooks/use-models-columns.tsx @@ -3,9 +3,13 @@ import { systemConfigAtom } from '@/atoms/system'; import { OPENAI_COMPATIBLE, tableSorter } from '@/config/settings'; import { TargetStatusValueMap } from '@/pages/model-routes/config'; import { QuestionCircleOutlined } from '@ant-design/icons'; -import { AutoTooltip, DropdownButtons, GrafanaIcon } from '@gpustack/core-ui'; -import icons from '@gpustack/core-ui/lib/components/icon-font/icons'; -import { ColumnProps } from '@gpustack/core-ui/lib/components/table/types'; +import { + AutoTooltip, + DropdownButtons, + GrafanaIcon, + icons, + type TableColumnProps +} from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { useMemoizedFn } from 'ahooks'; import { Tooltip } from 'antd'; @@ -98,7 +102,7 @@ const useModelsColumns = ({ clusterList, sortOrder, targetList -}: ModelsColumnsHookProps & { targetList: any[] }): ColumnProps[] => { +}: ModelsColumnsHookProps & { targetList: any[] }): TableColumnProps[] => { const intl = useIntl(); const systemConfig = useAtomValue(systemConfigAtom); diff --git a/src/pages/llmodels/hooks/use-view-instance-logs.ts b/src/pages/llmodels/hooks/use-view-instance-logs.ts index 161db099..8f67eb8a 100644 --- a/src/pages/llmodels/hooks/use-view-instance-logs.ts +++ b/src/pages/llmodels/hooks/use-view-instance-logs.ts @@ -1,5 +1,5 @@ import useBodyScroll from '@/hooks/use-body-scroll'; -import { PageSize } from '@gpustack/core-ui/lib/components/logs-viewer/config'; +import { PageSize } from '@gpustack/core-ui'; import { useState } from 'react'; import { MODEL_INSTANCE_API } from '../apis'; import { InstanceRealtimeLogStatus } from '../config'; diff --git a/src/pages/maas-provider/config/index.ts b/src/pages/maas-provider/config/index.ts index 34627311..87fe1585 100644 --- a/src/pages/maas-provider/config/index.ts +++ b/src/pages/maas-provider/config/index.ts @@ -1,6 +1,6 @@ import { StatusMaps } from '@/config'; import { StatusType } from '@/config/types'; -import icons from '@gpustack/core-ui/lib/components/icon-font/icons'; +import { icons } from '@gpustack/core-ui'; import { ProviderEnum } from './providers'; export { maasProviderLabelMap, maasProviderOptions } from './providers'; diff --git a/src/pages/maas-provider/forms/index.tsx b/src/pages/maas-provider/forms/index.tsx index aa940d1b..9e4c9ec6 100644 --- a/src/pages/maas-provider/forms/index.tsx +++ b/src/pages/maas-provider/forms/index.tsx @@ -1,10 +1,14 @@ import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import { json2Yaml, yaml2Json } from '@/pages/backends/config'; -import { CollapsePanel, IconFont, ScrollSpyTabs } from '@gpustack/core-ui'; -import { useWrapperContext } from '@gpustack/core-ui/lib/components/column-wrapper/use-wrapper-context'; -import useFinishFailed from '@gpustack/core-ui/lib/components/scroll-spy-tabs/use-finish-failed'; -import useScrollActiveChange from '@gpustack/core-ui/lib/components/scroll-spy-tabs/use-scroll-active-change'; +import { + CollapsePanel, + IconFont, + ScrollSpyTabs, + useFinishFailed, + useScrollActiveChange, + useWrapperContext +} from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Form } from 'antd'; import _ from 'lodash'; diff --git a/src/pages/model-routes/config/index.ts b/src/pages/model-routes/config/index.ts index 5a4f6945..977c2d2b 100644 --- a/src/pages/model-routes/config/index.ts +++ b/src/pages/model-routes/config/index.ts @@ -1,6 +1,6 @@ import { StatusMaps } from '@/config'; import { StatusType } from '@/config/types'; -import icons from '@gpustack/core-ui/lib/components/icon-font/icons'; +import { icons } from '@gpustack/core-ui'; export const TargetStatusValueMap: Record = { Active: 'active', diff --git a/src/pages/model-routes/forms/index.tsx b/src/pages/model-routes/forms/index.tsx index 38076a8c..94f0543c 100644 --- a/src/pages/model-routes/forms/index.tsx +++ b/src/pages/model-routes/forms/index.tsx @@ -1,10 +1,14 @@ import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import { modelCategoriesMap } from '@/pages/llmodels/config'; -import { CollapsePanel, IconFont, ScrollSpyTabs } from '@gpustack/core-ui'; -import { useWrapperContext } from '@gpustack/core-ui/lib/components/column-wrapper/use-wrapper-context'; -import useFinishFailed from '@gpustack/core-ui/lib/components/scroll-spy-tabs/use-finish-failed'; -import useScrollActiveChange from '@gpustack/core-ui/lib/components/scroll-spy-tabs/use-scroll-active-change'; +import { + CollapsePanel, + IconFont, + ScrollSpyTabs, + useFinishFailed, + useScrollActiveChange, + useWrapperContext +} from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Form } from 'antd'; import _ from 'lodash'; diff --git a/src/pages/model-routes/hooks/use-routes-columns.tsx b/src/pages/model-routes/hooks/use-routes-columns.tsx index d1bf3dde..f89c8001 100644 --- a/src/pages/model-routes/hooks/use-routes-columns.tsx +++ b/src/pages/model-routes/hooks/use-routes-columns.tsx @@ -1,8 +1,11 @@ // columns.ts import { tableSorter } from '@/config/settings'; import ModelTag from '@/pages/_components/model-tag'; -import { AutoTooltip, DropdownButtons } from '@gpustack/core-ui'; -import { type ColumnProps } from '@gpustack/core-ui/lib/components/table/types'; +import { + AutoTooltip, + DropdownButtons, + type TableColumnProps +} from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import dayjs from 'dayjs'; import { useMemo } from 'react'; @@ -11,7 +14,7 @@ import { RouteItem } from '../config/types'; const useAccessColumns = ( handleSelect: (val: string, record: RouteItem) => void, onCellClick?: (record: RouteItem, dataIndex: string) => void -): ColumnProps[] => { +): TableColumnProps[] => { const intl = useIntl(); const filterActions = (record: RouteItem) => { diff --git a/src/pages/model-routes/index.tsx b/src/pages/model-routes/index.tsx index 6f63398e..242abc27 100644 --- a/src/pages/model-routes/index.tsx +++ b/src/pages/model-routes/index.tsx @@ -11,9 +11,9 @@ import { FilterBar, IconFont, Table as SealTable, + TableOrder, TableProvider } from '@gpustack/core-ui'; -import { TableOrder } from '@gpustack/core-ui/lib/components/table/types'; import { useIntl } from '@umijs/max'; import { useMemoizedFn } from 'ahooks'; import { message } from 'antd'; diff --git a/src/pages/playground/images/edit.tsx b/src/pages/playground/images/edit.tsx index 7641c087..1821657f 100644 --- a/src/pages/playground/images/edit.tsx +++ b/src/pages/playground/images/edit.tsx @@ -6,9 +6,9 @@ import { AlertInfo, ImageEditor as CanvasImageEditor, IconFont, + processImage, SingleImage } from '@gpustack/core-ui'; -import { processImage } from '@gpustack/core-ui/lib/components/image-editor/extract-image-colors'; import { useIntl } from '@umijs/max'; import { Divider } from 'antd'; import _ from 'lodash'; diff --git a/src/pages/settings/index.tsx b/src/pages/settings/index.tsx new file mode 100644 index 00000000..3b4b8b9b --- /dev/null +++ b/src/pages/settings/index.tsx @@ -0,0 +1,22 @@ +import { getGPUStackPlugin } from '@/plugins'; +import { Result } from 'antd'; +import React from 'react'; + +const SettingsPage: React.FC = () => { + const enterprisePlugin = getGPUStackPlugin(); + const BrandingUI = enterprisePlugin?.components?.BrandingUI; + + if (!BrandingUI) { + return ( + + ); + } + + return ; +}; + +export default SettingsPage; diff --git a/src/pages/users/hooks/use-users-columns.tsx b/src/pages/users/hooks/use-users-columns.tsx index 1f15510c..0c48f57a 100644 --- a/src/pages/users/hooks/use-users-columns.tsx +++ b/src/pages/users/hooks/use-users-columns.tsx @@ -1,7 +1,11 @@ // columns.ts import { tableSorter } from '@/config/settings'; -import { AutoTooltip, DropdownButtons, IconFont } from '@gpustack/core-ui'; -import icons from '@gpustack/core-ui/lib/components/icon-font/icons'; +import { + AutoTooltip, + DropdownButtons, + IconFont, + icons +} from '@gpustack/core-ui'; import { useIntl, useModel } from '@umijs/max'; import { useMemoizedFn } from 'ahooks'; import { Tag } from 'antd'; diff --git a/src/plugins/index.ts b/src/plugins/index.ts new file mode 100644 index 00000000..cdf398ca --- /dev/null +++ b/src/plugins/index.ts @@ -0,0 +1,23 @@ +import { GPUStackPluginManager } from './manager'; +import { AppPlugin } from './types'; + +/** + * Hook to access the enterprise plugin instance + */ +export const getGPUStackPlugin = (): AppPlugin | undefined => { + return GPUStackPluginManager.get('enterprise'); +}; + +/** + * Hook to access a plugin by its name + */ +export const getPlugin = (name: string): AppPlugin | undefined => { + return GPUStackPluginManager.get(name); +}; + +/** + * Hook to check if a plugin is registered + */ +export const hasPlugin = (name: string): boolean => { + return GPUStackPluginManager.has(name); +}; diff --git a/src/plugins/locale-merger.ts b/src/plugins/locale-merger.ts new file mode 100644 index 00000000..5cec3ca7 --- /dev/null +++ b/src/plugins/locale-merger.ts @@ -0,0 +1,34 @@ +import { addLocale } from '@umijs/max'; + +const mergedLocales = new Set(); + +/** + * Merge enterprise plugin locales into the main application + */ +export function mergeEnterpriseLocales(locales: Record = {}) { + if (!locales) { + return; + } + + console.log('Merging enterprise plugin locales:', Object.keys(locales)); + + // Iterate over all locale configurations of the enterprise plugin + Object.entries(locales).forEach(([locale, messages]) => { + try { + if (mergedLocales.has(locale)) { + return; + } + + // Merge the enterprise locale into the main application + addLocale(locale, messages, { + momentLocale: '', + // @ts-ignore + antd: locale + }); + mergedLocales.add(locale); + console.log(`✓ Merged enterprise locale: ${locale}`); + } catch (error) { + console.error(`Failed to merge enterprise locale ${locale}:`, error); + } + }); +} diff --git a/src/plugins/manager.ts b/src/plugins/manager.ts new file mode 100644 index 00000000..a3f53fb4 --- /dev/null +++ b/src/plugins/manager.ts @@ -0,0 +1,81 @@ +import { AppPlugin, IPluginManager } from './types'; + +class PluginManager implements IPluginManager { + private plugins = new Map(); + private initialized = false; + + /** + * register a plugin + */ + register(name: string, plugin: AppPlugin) { + if (this.plugins.has(name)) { + console.warn(`Plugin "${name}" is already registered. Overwriting...`); + } + this.plugins.set(name, plugin); + } + + /** + * get a plugin + */ + get(name: string): AppPlugin | undefined { + return this.plugins.get(name); + } + + /** + * check if a plugin exists + */ + has(name: string): boolean { + return this.plugins.has(name); + } + + /** + * get all plugins + */ + getAll(): Map { + return this.plugins; + } + + /** + * initialize all plugins + */ + async initialize(): Promise> { + if (this.initialized) { + return {}; + } + + const initData: Record = {}; + + for (const [name, plugin] of this.plugins.entries()) { + if (plugin.onAppInit) { + try { + const data = await plugin.onAppInit(); + if (data) { + initData[name] = data; + } + } catch (error) { + console.error(`Failed to initialize plugin "${name}":`, error); + } + } + } + + this.initialized = true; + return initData; + } + + /** + * 通知所有插件应用已就绪 + */ + async notifyReady(): Promise { + for (const [name, plugin] of this.plugins.entries()) { + if (plugin.onAppReady) { + try { + await plugin.onAppReady(); + } catch (error) { + console.error(`Plugin "${name}" onAppReady failed:`, error); + } + } + } + } +} + +export const GPUStackPluginManager = new PluginManager(); diff --git a/src/plugins/optional-empty-module.ts b/src/plugins/optional-empty-module.ts new file mode 100644 index 00000000..594f9c3a --- /dev/null +++ b/src/plugins/optional-empty-module.ts @@ -0,0 +1,2 @@ +// If the enterprise plugin is not installed, imports will be redirected to this empty module to prevent errors. +export default undefined; diff --git a/src/plugins/route-merger.ts b/src/plugins/route-merger.ts new file mode 100644 index 00000000..b9a31d77 --- /dev/null +++ b/src/plugins/route-merger.ts @@ -0,0 +1,57 @@ +import { getGPUStackPlugin } from '@/plugins'; +import type { RouteConfig } from '@/plugins/types'; + +type ClientRoute = RouteConfig & { + id?: string; + children?: ClientRoute[]; + routes?: ClientRoute[]; +}; + +function getLayoutChildren(routes: ClientRoute[]): ClientRoute[] | null { + const rootRoute = routes.find((route) => route.path === '/'); + if (!rootRoute) return null; + + const rootChildren = rootRoute.routes || rootRoute.children; + if (!Array.isArray(rootChildren)) return null; + + const globalLayout = rootChildren.find((route) => + route.id?.includes('global-layout') + ); + if (!globalLayout) return null; + + const layoutChildren = globalLayout.children || globalLayout.routes; + return Array.isArray(layoutChildren) ? layoutChildren : null; +} + +function mergeRoute(targetRoutes: ClientRoute[], pluginRoute: ClientRoute) { + const existsIndex = targetRoutes.findIndex( + (route) => + (pluginRoute.path && route.path === pluginRoute.path) || + (pluginRoute.key && route.key === pluginRoute.key) + ); + + if (existsIndex === -1) { + targetRoutes.push(pluginRoute); + return; + } + + const currentRoute = targetRoutes[existsIndex]; + targetRoutes[existsIndex] = { + ...currentRoute, + ...pluginRoute, + routes: pluginRoute.routes || currentRoute.routes, + children: pluginRoute.children || currentRoute.children + }; +} + +export function mergeEnterpriseRoutes(routes: ClientRoute[]) { + const enterprisePlugin = getGPUStackPlugin(); + if (!enterprisePlugin?.routes?.length) return; + + const layoutChildren = getLayoutChildren(routes); + if (!layoutChildren) return; + + enterprisePlugin.routes.forEach((route) => + mergeRoute(layoutChildren, route as ClientRoute) + ); +} diff --git a/src/plugins/types.ts b/src/plugins/types.ts new file mode 100644 index 00000000..c1ea6066 --- /dev/null +++ b/src/plugins/types.ts @@ -0,0 +1,95 @@ +import { ComponentType } from 'react'; + +/** + * routes + */ +export interface RouteConfig { + path: string; + component?: ComponentType; + routes?: RouteConfig[]; + [key: string]: any; +} + +/** + * login module configuration + */ +export interface LoginPlugin { + shouldUseCustomLogin?: () => boolean; + CustomLoginComponent?: ComponentType; +} + +/** + * branding module configuration + */ +export interface BrandingPlugin { + ConfigPage?: ComponentType; +} + +/** + * localization configuration + */ +export interface LocalesConfig { + [locale: string]: Record; +} + +/** + * application plugin interface + */ +export interface AppPlugin { + /** + * application initialization hook + * called when the application starts, can return initialization data + */ + onAppInit?: () => Promise> | Record; + + /** + * application ready hook + * called when the application is fully loaded + */ + onAppReady?: () => void | Promise; + + /** + * login module + */ + login?: LoginPlugin; + + /** + * branding module + */ + branding?: BrandingPlugin; + + /** + * localization configuration + */ + locales?: LocalesConfig; + + /** + * routes extension + */ + routes?: RouteConfig[]; + + /** + * components extension + */ + components?: Record; + + /** + * Hooks extension + */ + hooks?: Record unknown>; + + /** + * other custom fields for future extensions + */ + [key: string]: any; +} + +/** + * plugin manager interface + */ +export interface IPluginManager { + register(name: string, plugin: AppPlugin): void; + get(name: string): AppPlugin | undefined; + has(name: string): boolean; + getAll(): Map; +} diff --git a/tsconfig.json b/tsconfig.json index 93ab3233..d030004f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,22 +17,8 @@ "paths": { "@/*": ["./src/*"], "@@/*": ["./src/.umi/*"], - "@@test/*": ["./src/.umi-test/*"], - "@gpustack/core-ui": ["../core-ui/src/index.ts"], - "@gpustack/core-ui/*": ["../core-ui/src/*"], - "@gpustack/core-ui/hooks": ["../core-ui/src/lib/hooks"], - "@gpustack/core-ui/utils": ["../core-ui/src/lib/utils"], - "@gpustack/core-ui/types": ["../core-ui/src/lib/types"], - "@gpustack/core-ui/components": ["../core-ui/src/lib/components"], - "@gpustack/core-ui/lib/*": ["../core-ui/src/lib/*"] + "@@test/*": ["./src/.umi-test/*"] } }, - "include": [ - "./**/*.d.ts", - "./**/*.ts", - "./**/*.tsx", - "@gpustack/core-ui/lib/components/logs-viewer/parse-worker.ts", - "@gpustack/core-ui/lib/components/image-editor/invert-worker.ts", - "@gpustack/core-ui/lib/components/image-editor/offscreen-worker.ts" - ] + "include": ["./**/*.d.ts", "./**/*.ts", "./**/*.tsx"] }