Compare commits

...
Author SHA1 Message Date
hibigandgithub-actions[bot] 2a857b93b7 chore: bump @gpustack/core-ui to v1.0.32 2026-06-24 10:50:35 +00:00
jialinandjialin ee42a9d0ed refactor(components): migrate shared components to core-ui and remove unused ones 2026-06-24 14:02:41 +08:00
jialinandjialin 28976dea4b fix(cardpage): duplicate data appears after clicking refresh 2026-06-24 14:02:41 +08:00
jialinandjialin 22e5cc3aa5 fix(usage): display usage tabs base on access permissions 2026-06-24 11:26:40 +08:00
jialinandjialin 3052c0b23e fix(app): run probe resources at two stages 2026-06-24 11:26:40 +08:00
jialinandjialin 1c9cabf7e7 fix(usage): instance type check conditions 2026-06-22 15:25:18 +08:00
jialinandjialin 03842d1741 fix: gpu selector height 2026-06-22 15:25:18 +08:00
jialinandjialin 84304eadb7 fix(playgroundimage): allow to input size 2026-06-18 16:03:35 +08:00
jialinandjialin ab3739219e chore: update vllm, sglang, preset argument options 2026-06-18 16:03:35 +08:00
jialinandjialin 7cf4932f3a fix(deploymentlog): show the checkbox only when container exsiting previous 2026-06-18 11:50:19 +08:00
jialinandjialin 032f1eefef fix(deployments): show a prefix before model name in playground 2026-06-18 11:50:19 +08:00
jialinandjialin e35be94f7c fix(provider): filter bailian do not work 2026-06-18 11:50:19 +08:00
jialinandjialin e76e2ad85a fix(worker): IP cell text wrapping 2026-06-18 11:50:19 +08:00
jialinandjialin cb19cd2476 fix: show cpu only spec info 2026-06-17 20:11:06 +08:00
jialinandjialin c532732ce9 fix: show CPU Only in usage instanc types 2026-06-17 18:29:58 +08:00
jialinandjialin 2bc10dbb2e fix: bailian logo, instance logs params container_name 2026-06-17 16:49:25 +08:00
jialinandjialin b634a03ef9 fix: use alibaba cloud icon for qwen provider 2026-06-17 15:30:43 +08:00
jialinandjialin ac64bb98ad fix: Only display name editable when editing storage and storage type 2026-06-17 12:35:24 +08:00
jialinandjialin aa49539210 fix: select gpu stype when creating cluster from gpu service 2026-06-17 12:35:24 +08:00
jialinandjialin 1f5678f781 fix: update nividia notes 2026-06-17 12:35:24 +08:00
jialinandjialin 7adc089e94 fix: save the default_width, default_height in image size options 2026-06-17 12:35:24 +08:00
jialinandjialin 6e1e9c07ee fix: template name readonly in editting 2026-06-17 11:06:39 +08:00
jialinandjialin 1d55382f3b fix: fetch all model instance after stopping 2026-06-17 11:06:39 +08:00
jialinandjialin e68bb0bc4e fix: export data should contain all tables 2026-06-16 22:28:46 +08:00
jialinandjialin c8e14dade5 fix: remove vendor logo in template list 2026-06-16 22:28:46 +08:00
jialinandjialin e4c1a3f5c2 refactor: usage instance tab, split tables 2026-06-16 22:28:46 +08:00
jialinandjialin cdd6ef9cb3 refactor: use hook for fetch event 2026-06-16 22:28:46 +08:00
jialinandjialin 737d43fd16 refactor: use hook for fetch chart 2026-06-16 22:28:46 +08:00
jialinandjialin 9f5163531c refactor: usage storage tab, split tables 2026-06-16 22:28:46 +08:00
jialinandjialin df5cf0f4e5 chore: nvidia notes in add worker 2026-06-16 18:35:19 +08:00
jialinandjialin e160b64697 fix: do not show billing menu in ent 2026-06-16 18:35:19 +08:00
jialinandjialin 7d36c6ac4a revert: show model name from api directly in apikeys 2026-06-16 15:25:46 +08:00
jialinandjialin 0820edf55f fix: refresh data in card page list 2026-06-16 15:25:46 +08:00
jialinandjialin 12e324893f fix: gpu instance state color 2026-06-16 14:17:43 +08:00
jialinandjialin 4017c4dd33 style: table hover bg, remove description in billing 2026-06-16 14:17:43 +08:00
jialinandjialin 17b65d80e1 fix(style): API info access url overflow 2026-06-16 14:17:43 +08:00
jialinandjialin 47cb3321ab fix(style): forgot password layout 2026-06-16 14:17:43 +08:00
jialinandjialin 547b9f617b fix(style): forget button in login page 2026-06-15 15:48:41 +08:00
jialinandjialin 9e7bd78693 fix: allowed models do not contain the org name 2026-06-15 14:45:27 +08:00
jialinandjialin c53d60b628 fix: remove the depude code in buildTrend 2026-06-14 19:40:19 +08:00
jialinandjialin 9566e0cdb2 fix: queryParams do not be updated timely 2026-06-14 19:21:36 +08:00
jialinandjialin ed3e779d22 fix: do not trigger setInitialized in catch when error occured 2026-06-14 19:14:18 +08:00
jialinandjialin 3e2133753d fix: barchart, piechart flash issue 2026-06-14 19:14:18 +08:00
jialinandjialin 736a853192 chore: add cancel token 2026-06-14 19:14:18 +08:00
jialinandjialin 08ef2ffa9a refactor(usagesummary): fetch data 2026-06-14 19:14:18 +08:00
jialinandjialin 6cf512cccd refactor(usage): split tabs into separate dirs 2026-06-14 19:14:18 +08:00
jialinandjialin 6f56e614f3 chore: instance creationg UE 2026-06-14 19:14:18 +08:00
123 changed files with 3893 additions and 3493 deletions
+1
View File
@@ -346,6 +346,7 @@ const baseRoutes = [
icon: 'icon-billing-outlined',
selectedIcon: 'icon-billing-filled',
defaultIcon: 'icon-billing-outlined',
hideInMenu: process.env.ENABLE_ENTERPRISE === 'true',
// OSS exposes the menu as a teaser for the enterprise billing
// module. The page itself just renders an upsell notice — the real
// billing UI lives in the enterprise plugin and shadows this route
+1 -1
View File
@@ -17,7 +17,7 @@
"@ant-design/pro-components": "3.1.0-0",
"@antv/g6": "^5.0.51",
"@braintree/sanitize-url": "^7.1.1",
"@gpustack/core-ui": "^1.0.27",
"@gpustack/core-ui": "^1.0.32",
"@huggingface/gguf": "^0.1.7",
"@huggingface/hub": "^0.15.1",
"@huggingface/tasks": "^0.11.6",
+117 -117
View File
@@ -24,8 +24,8 @@ importers:
specifier: ^7.1.1
version: 7.1.2
'@gpustack/core-ui':
specifier: ^1.0.27
version: 1.0.27(czdvzceysqw7iv6pct2ucnb23e)
specifier: ^1.0.32
version: 1.0.32(czdvzceysqw7iv6pct2ucnb23e)
'@huggingface/gguf':
specifier: ^0.1.7
version: 0.1.18
@@ -49,7 +49,7 @@ importers:
version: 4.17.24
'@umijs/max':
specifier: ^4.6.15
version: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
version: 4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@xterm/addon-fit':
specifier: ^0.10.0
version: 0.10.0(@xterm/xterm@5.5.0)
@@ -109,7 +109,7 @@ importers:
version: 3.3.0
jotai:
specifier: ^2.8.4
version: 2.20.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@18.3.28)(react@18.3.1)
version: 2.20.0(@babel/core@7.23.6)(@babel/template@7.28.6)(@types/react@18.3.28)(react@18.3.1)
js-yaml:
specifier: ^4.1.0
version: 4.1.1
@@ -205,7 +205,7 @@ importers:
version: 6.4.1(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
umi-presets-pro:
specifier: ^2.0.3
version: 2.0.3(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))
version: 2.0.3(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))
wavesurfer.js:
specifier: ^7.8.8
version: 7.12.6
@@ -233,10 +233,10 @@ importers:
version: 1.0.1
'@umijs/plugins':
specifier: ^4.4.11
version: 4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
version: 4.6.51(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
babel-plugin-named-asset-import:
specifier: ^0.3.8
version: 0.3.8(@babel/core@7.29.0)
version: 0.3.8(@babel/core@7.23.6)
case-sensitive-paths-webpack-plugin:
specifier: ^2.4.0
version: 2.4.0
@@ -1484,24 +1484,24 @@ packages:
resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz}
deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package
'@gpustack/core-ui@1.0.27':
resolution: {integrity: sha512-m3ue0EHFKULla0mpnpxZwn9LVaGKS+HnuzQYSBECQa4vaP8MEEQsR7oFBtG8bhWUWYA9VzP6k7fjozDkP6TymA==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.27.tgz}
'@gpustack/core-ui@1.0.32':
resolution: {integrity: sha512-kGTazoqbK2KyZgOP6gmQaRxTiQVfF2IKLGDXjJq6w6BbmJgALXFJA2v2ROjAbjEVyfTdBzyYeXfPo/JgISpMNw==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.32.tgz}
peerDependencies:
'@ant-design/icons': '>=6.0.0'
'@ant-design/icons': ^6.1.0
'@ant-design/pro-components': 3.1.0-0
'@monaco-editor/react': ^4.6.0
ahooks: '>=3.0.0'
antd: '>=6.0.0'
antd-style: '>=3.0.0'
axios: '>=1.8.0'
echarts: '>=5.0.0'
ahooks: ^3.8.5
antd: ^6.3.3
antd-style: ^3.6.2
axios: ^1.8.2
echarts: ^5.5.1
file-saver: ^2.0.5
monaco-editor: ^0.30.1
monaco-yaml: ^4.0.0
overlayscrollbars-react: ^0.5.6
react: '>=18.0.0'
react-dom: '>=18.0.0'
styled-components: '>=6.0.0'
react: ^18.2.0
react-dom: ^18.2.0
styled-components: ^6.1.15
'@hono/node-server@1.19.14':
resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==, tarball: https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz}
@@ -9257,14 +9257,14 @@ snapshots:
'@radix-ui/popper': 0.0.10
react: 18.3.1
'@alita/plugins@3.5.5(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
'@alita/plugins@3.5.5(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@alita/babel-transform-jsx-class': 0.0.2
'@alita/inspx': 0.0.2(react@18.3.1)
'@alita/request': 3.1.2
'@alita/types': 3.1.2
'@umijs/bundler-utils': 4.4.11
'@umijs/plugins': 4.4.11(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/plugins': 4.4.11(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/utils': 4.4.11
ahooks: 3.9.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
antd-mobile-alita: 2.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -10134,90 +10134,90 @@ snapshots:
dependencies:
'@babel/types': 7.29.0
'@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)':
'@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)':
'@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)':
'@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)':
'@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)':
'@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)':
'@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)':
'@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)':
'@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)':
'@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)':
'@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)':
'@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)':
'@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)':
'@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)':
'@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)':
'@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)':
'@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.29.0)':
'@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
'@babel/core': 7.23.6
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.23.6)
'@babel/helper-plugin-utils': 7.28.6
'@babel/helper-simple-access': 7.27.1
transitivePeerDependencies:
@@ -10808,7 +10808,7 @@ snapshots:
'@formatjs/intl-utils@2.3.0': {}
'@gpustack/core-ui@1.0.27(czdvzceysqw7iv6pct2ucnb23e)':
'@gpustack/core-ui@1.0.32(czdvzceysqw7iv6pct2ucnb23e)':
dependencies:
'@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@ant-design/pro-components': 3.1.0-0(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -12281,11 +12281,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@umijs/bundler-utoopack@4.6.51(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
'@umijs/bundler-utoopack@4.6.51(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
dependencies:
'@umijs/bundler-utils': 4.6.51
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@utoo/pack': 1.4.3(less-loader@11.1.0(less@4.1.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(less@4.1.3)(postcss@8.5.14)(resolve-url-loader@5.0.0)(sass-loader@13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))
'@utoo/pack': 1.4.3(less-loader@11.1.0(less@4.1.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(less@4.1.3)(postcss@8.5.14)(resolve-url-loader@5.0.0)(sass-loader@13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))
compression: 1.8.1
connect-history-api-fallback: 2.0.0
cors: 2.8.6
@@ -12553,14 +12553,14 @@ snapshots:
- supports-color
- typescript
'@umijs/max@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
'@umijs/max@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
dependencies:
'@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3)
'@umijs/plugins': 4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/plugins': 4.6.51(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
antd: 4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
eslint: 8.35.0
stylelint: 14.8.2
umi: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
umi: 4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
transitivePeerDependencies:
- '@babel/core'
- '@rspack/core'
@@ -12642,7 +12642,7 @@ snapshots:
dependencies:
tsx: 3.12.2
'@umijs/plugins@4.4.11(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
'@umijs/plugins@4.4.11(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@ahooksjs/use-request': 2.8.15(react@18.3.1)
'@ant-design/antd-theme-variable': 1.0.0
@@ -12657,7 +12657,7 @@ snapshots:
antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20)
axios: 0.27.2
babel-plugin-import: 1.13.8
babel-plugin-styled-components: 2.1.4(@babel/core@7.29.0)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
babel-plugin-styled-components: 2.1.4(@babel/core@7.23.6)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
dayjs: 1.11.20
dva-core: 2.0.4(redux@4.2.1)
dva-immer: 1.0.2(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
@@ -12687,7 +12687,7 @@ snapshots:
- react-native
- supports-color
'@umijs/plugins@4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
'@umijs/plugins@4.6.51(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@ahooksjs/use-request': 2.8.15(react@18.3.1)
'@ant-design/antd-theme-variable': 1.0.0
@@ -12702,7 +12702,7 @@ snapshots:
antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20)
axios: 0.27.2
babel-plugin-import: 1.13.8
babel-plugin-styled-components: 2.1.4(@babel/core@7.29.0)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
babel-plugin-styled-components: 2.1.4(@babel/core@7.23.6)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
dayjs: 1.11.20
dva-core: 2.0.4(redux@4.2.1)
dva-immer: 1.0.2(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
@@ -12732,7 +12732,7 @@ snapshots:
- react-native
- supports-color
'@umijs/plugins@4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
'@umijs/plugins@4.6.51(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@ahooksjs/use-request': 2.8.15(react@18.3.1)
'@ant-design/antd-theme-variable': 1.0.0
@@ -12747,7 +12747,7 @@ snapshots:
antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20)
axios: 0.27.2
babel-plugin-import: 1.13.8
babel-plugin-styled-components: 2.1.4(@babel/core@7.29.0)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
babel-plugin-styled-components: 2.1.4(@babel/core@7.23.6)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
dayjs: 1.11.20
dva-core: 2.0.4(redux@4.2.1)
dva-immer: 1.0.2(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
@@ -12777,7 +12777,7 @@ snapshots:
- react-native
- supports-color
'@umijs/preset-umi@4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
'@umijs/preset-umi@4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
dependencies:
'@iconify/utils': 2.1.1
'@stagewise/toolbar': 0.6.2
@@ -12787,7 +12787,7 @@ snapshots:
'@umijs/bundler-esbuild': 4.6.51
'@umijs/bundler-mako': 0.11.10(postcss@8.5.14)(sass@1.54.0)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/bundler-utils': 4.6.51
'@umijs/bundler-utoopack': 4.6.51(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/bundler-utoopack': 4.6.51(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/bundler-vite': 4.6.51(@types/node@25.6.2)(lightningcss@1.22.1)(postcss@8.5.14)(rollup@3.30.0)(sass@1.54.0)(terser@5.48.0)
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/core': 4.6.51
@@ -12870,13 +12870,13 @@ snapshots:
react-helmet-async: 1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router-dom: 6.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/request-record@1.1.4(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))':
'@umijs/request-record@1.1.4(umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))':
dependencies:
chokidar: 3.6.0
express: 4.22.1
lodash: 4.18.1
prettier: 2.8.8
umi: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
umi: 4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
transitivePeerDependencies:
- supports-color
@@ -12892,13 +12892,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@umijs/test@4.6.51(@babel/core@7.29.0)':
'@umijs/test@4.6.51(@babel/core@7.23.6)':
dependencies:
'@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.29.0)
'@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.6)
'@jest/types': 27.5.1
'@umijs/bundler-utils': 4.6.51
'@umijs/utils': 4.6.51
babel-jest: 29.7.0(@babel/core@7.29.0)
babel-jest: 29.7.0(@babel/core@7.23.6)
esbuild: 0.21.4
identity-obj-proxy: 3.0.0
isomorphic-unfetch: 4.0.2
@@ -13018,7 +13018,7 @@ snapshots:
'@utoo/pack-win32-x64-msvc@1.4.3':
optional: true
'@utoo/pack@1.4.3(less-loader@11.1.0(less@4.1.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(less@4.1.3)(postcss@8.5.14)(resolve-url-loader@5.0.0)(sass-loader@13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))':
'@utoo/pack@1.4.3(less-loader@11.1.0(less@4.1.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(less@4.1.3)(postcss@8.5.14)(resolve-url-loader@5.0.0)(sass-loader@13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))':
dependencies:
'@babel/code-frame': 7.22.5
'@hono/node-server': 1.19.14(hono@4.12.18)
@@ -13039,7 +13039,7 @@ snapshots:
sass-loader: 13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
semver: 7.8.0
send: 0.17.1
styled-jsx: 5.1.7(@babel/core@7.29.0)(react@18.3.1)
styled-jsx: 5.1.7(@babel/core@7.23.6)(react@18.3.1)
ws: 8.20.0
optionalDependencies:
'@utoo/pack-darwin-arm64': 1.4.3
@@ -13555,13 +13555,13 @@ snapshots:
transitivePeerDependencies:
- debug
babel-jest@29.7.0(@babel/core@7.29.0):
babel-jest@29.7.0(@babel/core@7.23.6):
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@jest/transform': 29.7.0
'@types/babel__core': 7.20.5
babel-plugin-istanbul: 6.1.1
babel-preset-jest: 29.6.3(@babel/core@7.29.0)
babel-preset-jest: 29.6.3(@babel/core@7.23.6)
chalk: 4.1.2
graceful-fs: 4.2.11
slash: 3.0.0
@@ -13601,9 +13601,9 @@ snapshots:
cosmiconfig: 7.1.0
resolve: 1.22.12
babel-plugin-named-asset-import@0.3.8(@babel/core@7.29.0):
babel-plugin-named-asset-import@0.3.8(@babel/core@7.23.6):
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
babel-plugin-react-compiler@0.0.0-experimental-c23de8d-20240515:
dependencies:
@@ -13615,11 +13615,11 @@ snapshots:
zod: 3.25.76
zod-validation-error: 2.1.0(zod@3.25.76)
babel-plugin-styled-components@2.1.4(@babel/core@7.29.0)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
babel-plugin-styled-components@2.1.4(@babel/core@7.23.6)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
dependencies:
'@babel/helper-annotate-as-pure': 7.27.3
'@babel/helper-module-imports': 7.28.6
'@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.23.6)
lodash: 4.18.1
picomatch: 2.3.2
styled-components: 6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -13627,30 +13627,30 @@ snapshots:
- '@babel/core'
- supports-color
babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0):
babel-preset-current-node-syntax@1.2.0(@babel/core@7.23.6):
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0)
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0)
'@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0)
'@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0)
'@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0)
'@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0)
'@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0)
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0)
'@babel/core': 7.23.6
'@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.6)
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.23.6)
'@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.6)
'@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.6)
'@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.23.6)
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.6)
'@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.6)
'@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.6)
'@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.6)
'@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.6)
'@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.6)
'@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.6)
'@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.6)
'@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.6)
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.6)
babel-preset-jest@29.6.3(@babel/core@7.29.0):
babel-preset-jest@29.6.3(@babel/core@7.23.6):
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
babel-plugin-jest-hoist: 29.6.3
babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0)
babel-preset-current-node-syntax: 1.2.0(@babel/core@7.23.6)
babel-runtime-jsx-plus@0.1.5: {}
@@ -16410,9 +16410,9 @@ snapshots:
jiti@2.7.0: {}
jotai@2.20.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@18.3.28)(react@18.3.1):
jotai@2.20.0(@babel/core@7.23.6)(@babel/template@7.28.6)(@types/react@18.3.28)(react@18.3.1):
optionalDependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/template': 7.28.6
'@types/react': 18.3.28
react: 18.3.1
@@ -19805,12 +19805,12 @@ snapshots:
css-to-react-native: 3.2.0
react-dom: 18.3.1(react@18.3.1)
styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1):
styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1):
dependencies:
client-only: 0.0.1
react: 18.3.1
optionalDependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
stylelint-config-recommended@7.0.0(stylelint@14.8.2):
dependencies:
@@ -20164,12 +20164,12 @@ snapshots:
ua-parser-js@0.7.41: {}
umi-presets-pro@2.0.3(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))):
umi-presets-pro@2.0.3(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))):
dependencies:
'@alita/plugins': 3.5.5(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@alita/plugins': 3.5.5(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/max-plugin-openapi': 2.0.3(chokidar@3.6.0)(encoding@0.1.13)(typescript@5.9.3)
'@umijs/plugins': 4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/request-record': 1.1.4(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))
'@umijs/plugins': 4.6.51(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/request-record': 1.1.4(umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))
swagger-ui-dist: 4.19.1
transitivePeerDependencies:
- '@babel/core'
@@ -20193,17 +20193,17 @@ snapshots:
isomorphic-fetch: 2.2.1
qs: 6.15.1
umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)):
umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)):
dependencies:
'@babel/runtime': 7.23.6
'@umijs/bundler-utils': 4.6.51
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/core': 4.6.51
'@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3)
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/renderer-react': 4.6.51(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/server': 4.6.51
'@umijs/test': 4.6.51(@babel/core@7.29.0)
'@umijs/test': 4.6.51(@babel/core@7.23.6)
'@umijs/utils': 4.6.51
prettier-plugin-organize-imports: 3.2.4(prettier@3.8.3)(typescript@5.9.3)
prettier-plugin-packagejson: 2.4.3(prettier@3.8.3)
@@ -20247,17 +20247,17 @@ snapshots:
- webpack-hot-middleware
- webpack-plugin-serve
umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)):
umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)):
dependencies:
'@babel/runtime': 7.23.6
'@umijs/bundler-utils': 4.6.51
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/core': 4.6.51
'@umijs/lint': 4.6.51(eslint@9.39.4(jiti@2.7.0))(stylelint@14.8.2)(typescript@5.9.3)
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/renderer-react': 4.6.51(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/server': 4.6.51
'@umijs/test': 4.6.51(@babel/core@7.29.0)
'@umijs/test': 4.6.51(@babel/core@7.23.6)
'@umijs/utils': 4.6.51
prettier-plugin-organize-imports: 3.2.4(prettier@3.8.3)(typescript@5.9.3)
prettier-plugin-packagejson: 2.4.3(prettier@3.8.3)
+17 -85
View File
@@ -3,9 +3,6 @@ import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
import { setAtomStorage } from '@/atoms/utils';
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
import { COLOR_PRIMARY } from '@/config/theme/constants';
import { queryClusterList } from '@/pages/cluster-management/apis';
import { ProviderValueMap } from '@/pages/cluster-management/config';
import { queryResourceEvents } from '@/pages/usage/apis/resource';
import { getGPUStackPlugin } from '@/plugins';
import { enterprisePluginReady } from '@/plugins/enterprise-ready';
import { GPUStackPluginManager } from '@/plugins/manager';
@@ -17,6 +14,10 @@ import {
} from '@/services/profile/apis';
import { fetchSystemConfig } from '@/services/system/query-system-config';
import { isOnline } from '@/utils';
import {
markInitialStateProbed,
probeAccessFlags
} from '@/utils/access-probes';
import { installTenantFetch } from '@/utils/install-fetch';
import {
IS_FIRST_LOGIN,
@@ -41,80 +42,6 @@ const checkDefaultPage = async (userInfo: any) => {
}
};
// Probes the caller's cluster list once so access predicates can gate
// GPU Service (Kubernetes-only). Cheap (one list request) and never
// blocks login — any failure just falls back to `undefined`, which
// the predicate treats as "unknown / don't restrict beyond role".
// The result is also mirrored into sessionStorage so access extensions
// that run without the initialState argument can read it (e.g. to
// override the admin shortcut in scopes where the menu shouldn't
// show even for admins).
const HAS_K8S_CLUSTER_KEY = 'hasKubernetesCluster';
const probeHasKubernetesCluster = async (): Promise<boolean | undefined> => {
try {
const res = await queryClusterList(
{ page: -1 },
{
skipErrorHandler: true
}
);
const value = (res?.items ?? []).some(
(c) => c?.provider === ProviderValueMap.Kubernetes
);
try {
window.sessionStorage.setItem(HAS_K8S_CLUSTER_KEY, JSON.stringify(value));
} catch {
// sessionStorage may be unavailable (Safari private mode); the
// access predicate already handles a missing value as "unknown".
}
return value;
} catch (error) {
console.error('probeHasKubernetesCluster error', error);
try {
window.sessionStorage.removeItem(HAS_K8S_CLUSTER_KEY);
} catch {
// ignore
}
return undefined;
}
};
// Probes whether the caller has ANY resource-usage events (GPU/CPU instance or
// storage lifecycle). Used alongside the cluster probe so a user who has run
// GPU instances still sees GPU Service / the full Usage page even if they
// currently have no Kubernetes cluster. Mirrored into sessionStorage for the
// access extensions; any failure → undefined ("unknown — don't restrict").
const HAS_RESOURCE_EVENTS_KEY = 'hasResourceEvents';
const probeHasResourceEvents = async (): Promise<boolean | undefined> => {
try {
// No date range = "ever"; scope is clamped to the caller server-side.
const res = await queryResourceEvents(
{ perPage: 1 },
{
skipErrorHandler: true
}
);
const value = (res?.pagination?.total ?? 0) > 0;
try {
window.sessionStorage.setItem(
HAS_RESOURCE_EVENTS_KEY,
JSON.stringify(value)
);
} catch {
// sessionStorage may be unavailable; predicate treats missing as unknown.
}
return value;
} catch (error) {
console.error('probeHasResourceEvents error', error);
try {
window.sessionStorage.removeItem(HAS_RESOURCE_EVENTS_KEY);
} catch {
// ignore
}
return undefined;
}
};
// runtime configuration
export async function getInitialState(): Promise<{
fetchUserInfo: () => Promise<Global.UserInfo>;
@@ -235,19 +162,24 @@ export async function getInitialState(): Promise<{
getAppVersionInfo();
if (![DEFAULT_ENTER_PAGE.login].includes(location.pathname)) {
const [userInfo, hasKubernetesCluster, hasResourceEvents] =
await Promise.all([
fetchUserInfo(),
probeHasKubernetesCluster(),
probeHasResourceEvents()
]);
const [userInfo, accessFlags] = await Promise.all([
fetchUserInfo(),
probeAccessFlags()
]);
// Record that the probes ran for an authenticated user this page load
// (the refresh path) so the layout doesn't re-probe. A failed
// fetch (empty user — e.g. unauthenticated deep link that bounces to
// login) is NOT marked: the user will log in via SPA afterwards and
// the layout becomes responsible for probing.
if (userInfo?.username) {
markInitialStateProbed();
}
checkDefaultPage(userInfo);
return {
fetchUserInfo,
currentUser: userInfo,
pluginData,
hasKubernetesCluster,
hasResourceEvents
...accessFlags
};
}
return {
@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>AlibabaCloud</title><path d="M14.752 4.64h5.274C22.242 4.64 24 6.475 24 8.691V15.8a3.947 3.947 0 01-3.974 3.975h-5.274l1.299-1.835 3.822-1.222c.688-.23 1.146-.918 1.146-1.605v-5.81c0-.687-.458-1.375-1.146-1.605L16.05 6.475l-1.3-1.835zM2.98 15.111c0 .688.46 1.376 1.147 1.606l3.822 1.146 1.3 1.835H3.974A3.947 3.947 0 010 15.723V8.69c0-2.216 1.758-4.05 3.975-4.05h5.273L7.95 6.474 4.127 7.697c-.688.23-1.146.918-1.146 1.606v5.808z" fill="#FF6A00"></path><path d="M16.051 11.213H8.025v1.835h8.026v-1.835z" fill="#FF6A00"></path></svg>

After

Width:  |  Height:  |  Size: 656 B

+1
View File
@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>BaiLian</title><path d="M6.336 8.919v6.162l5.335-3.083L6.337 8.92z" fill="#1C54E3"></path><path d="M21.394 5.288s-.006-.006-.01-.006L17.01 2.754 6.336 8.92l5.335 3.082 9.701-5.6.016-.01a.635.635 0 00.006-1.1v-.003z" fill="#AA9AFF"></path><path d="M21.71 12.465a.62.62 0 00-.316.085s-.006 0-.009.003l-4.375 2.528 5.05 2.915h.006a2.06 2.06 0 00.28-1.04v-3.855a.637.637 0 00-.636-.636z" fill="#00EAD1"></path><path d="M22.06 17.996l-5.05-2.915L6.34 21.242l4.27 2.465s.016.006.022.012a2.102 2.102 0 002.093 0c.006-.003.016-.006.022-.012l8.538-4.93c.003 0 .006-.003.01-.006.321-.183.589-.45.775-.772h-.006l-.004-.003z" fill="#00CEC9"></path><path d="M11.672 11.998l-5.336 3.083-1.444.832-3.605 2.083H1.28c.173.303.416.555.709.738l.078.044.016.01.02.012 4.232 2.442 10.671-6.161-5.335-3.082z" fill="#00EAD1"></path><path d="M12.74.29c-.1-.06-.208-.107-.315-.148-.02-.006-.038-.016-.057-.022a2.121 2.121 0 00-.7-.12c-.233 0-.457.038-.668.11l-.031.01a2.196 2.196 0 00-.372.17L2.068 5.222s-.003 0-.006.003c-.324.183-.592.451-.781.773h.006l5.049 2.918L17.01 2.758 12.74.29z" fill="#7347FF"></path><path d="M1.287 6.001H1.28A2.06 2.06 0 001 7.041v9.915c0 .378.1.735.28 1.043h.007l5.049-2.918V8.919l-5.05-2.918z" fill="#0423DA"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -63,6 +63,7 @@ export const fromClusterCreationAtom = atom(false);
export const clusterSessionAtom = atom<{
firstAddWorker: boolean;
firstAddCluster: boolean;
presetClusterType?: 'model' | 'gpu';
// Provider to preselect when the create flow opens — set by the
// empty-state CTA on feature pages that need a specific provider
// (e.g. GPU Service can only schedule on Kubernetes, so its
+7 -3
View File
@@ -276,7 +276,6 @@ body {
border-radius: var(--table-td-radius);
> td {
background-color: unset !important;
border-bottom: 1px solid var(--ant-color-split);
}
}
@@ -639,10 +638,15 @@ body {
inset: 53px auto auto 0 !important;
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
border-top: none;
max-height: 240px;
min-height: 100px;
.ant-cascader-menus {
align-items: stretch;
}
&.gpu-selector {
.ant-cascader-menu {
max-height: 400px;
height: unset;
}
.ant-cascader-menu:last-child {
flex: auto;
+7 -3
View File
@@ -18,6 +18,7 @@ export function useQueryDataList<
Response = Array<ListItem>
>(option: {
key: string;
manual?: boolean;
responseType?: 'array' | 'object';
fetchList: (
params: Params,
@@ -38,6 +39,7 @@ export function useQueryDataList<
fetchList,
getLabel,
getValue,
manual = true,
responseType = 'array',
errorMsg
} = option;
@@ -70,7 +72,7 @@ export function useQueryDataList<
return responseType === 'array' ? res.items || [] : res;
},
{
manual: true,
manual: manual,
debounceWait: option.debounceWait || 300,
onSuccess: () => {},
onError: (error) => {
@@ -105,16 +107,18 @@ export function useQueryDataList<
export function useQueryData<Detail, Params = any>(option: {
key: string;
delay?: number;
manual?: boolean;
fetchDetail: (params: Params, options?: any) => Promise<Detail>;
getData?: (response: Detail, params?: any) => any;
errorMsg?: string;
}): {
loading: boolean;
detailData: Detail;
manual?: boolean;
cancelRequest: () => void;
fetchData: (params: Params, extra?: any) => Promise<Detail>;
} {
const { key, fetchDetail, getData, errorMsg, delay } = option;
const { key, fetchDetail, getData, errorMsg, delay, manual = true } = option;
const axiosTokenRef = useRef<CancelTokenSource | null>(null);
const [detailData, setDetailData] = useState<Detail>({} as Detail);
@@ -142,7 +146,7 @@ export function useQueryData<Detail, Params = any>(option: {
return res;
},
{
manual: true,
manual: manual,
onSuccess: () => {},
onError: (error) => {
message.error(
+46 -1
View File
@@ -12,6 +12,7 @@ import useUserSettings from '@/hooks/use-user-settings';
import useUserSettingsStorage from '@/hooks/use-user-settings-storage';
import useAddResource from '@/pages/dashboard/hooks/use-add-resource';
import { logout } from '@/pages/login/apis';
import { didInitialStateProbe, probeAccessFlags } from '@/utils/access-probes';
import {
readColumnSettings,
readState,
@@ -41,7 +42,7 @@ import {
import { Button, ConfigProvider, Modal, theme } from 'antd';
import { useAtom } from 'jotai';
import 'overlayscrollbars/overlayscrollbars.css';
import { useMemo, useRef } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import { PageContainerInner } from '../pages/_components/page-box';
import Exception from './Exception';
import './Layout.css';
@@ -155,6 +156,50 @@ export default (props: any) => {
const { initialState, loading, setInitialState } = initialInfo;
const access = useAccess();
const probedForUserRef = useRef<string | null>(null);
// Backfill the access probes (cluster / resource-events) once we're in
// the authenticated shell. `getInitialState` runs only once at app
// boot and can't probe on the login page (no session => 401), so after
// a first login the flags arrive here as `undefined` — which the
// access predicate treats as "don't restrict", flashing GPU Service /
// the full Usage page on until a manual refresh. This layout mounts
// only post-auth (login is `layout:false`) and on every entry, so it's
// the reliable place to resolve them.
//
// The effect is keyed on the user IDENTITY, not on the flag values —
// gating on the flags is what made this fragile (on a refresh
// `getInitialState` commits `currentUser` and the flags in the same
// update, so a flag-gated effect sees them already-known and never
// fires). Whether to actually hit the network is decided by the
// module-scoped `didInitialStateProbe()` marker, which is true only
// when `getInitialState` already probed for an authenticated user this
// page load (the refresh path) — so we skip the duplicate request there
// but still probe on the SPA-login path. Keying on identity also
// re-probes correctly if the signed-in user changes.
const currentUser = initialState?.currentUser;
const username = currentUser?.username;
useEffect(() => {
if (!username || !setInitialState) {
return;
}
if (probedForUserRef.current === username) {
return;
}
probedForUserRef.current = username;
// The refresh path already resolved the flags inside `getInitialState`
// for this user — don't issue a duplicate probe.
if (didInitialStateProbe()) {
return;
}
probeAccessFlags().then((accessFlags) => {
setInitialState((prev: any) => ({
...prev,
...accessFlags
}));
});
}, [username, setInitialState]);
const userConfig = {
title: '',
+1 -1
View File
@@ -25,7 +25,7 @@ export default {
'ai.provider.ollama': 'Ollama',
'ai.provider.openai': 'OpenAI',
'ai.provider.openrouter': 'OpenRouter',
'ai.provider.qwen': 'Qwen',
'ai.provider.qwen': 'Alibaba Cloud Model Studio',
'ai.provider.spark': 'Spark',
'ai.provider.stepfun': 'StepFun',
'ai.provider.together-ai': 'TogetherAI',
+1 -1
View File
@@ -144,7 +144,7 @@ export default {
'clusters.addworker.theadNotes-02':
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
'clusters.addworker.nvidiaNotes':
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.',
'The built-in inference backends in GPUStack require <span class="bold-text">CUDA 12.8+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">570</span> or newer.',
'clusters.volume.title': 'Volume Mounts',
'clusters.volume.name': 'Volume Name',
'clusters.volume.mountPath': 'Container Path',
+1 -1
View File
@@ -25,7 +25,7 @@ export default {
'ai.provider.ollama': 'Ollama',
'ai.provider.openai': 'OpenAI',
'ai.provider.openrouter': 'OpenRouter',
'ai.provider.qwen': 'Qwen',
'ai.provider.qwen': 'Alibaba Cloud Model Studio',
'ai.provider.spark': 'Spark',
'ai.provider.stepfun': 'StepFun',
'ai.provider.together-ai': 'TogetherAI',
+1 -1
View File
@@ -144,7 +144,7 @@ export default {
'clusters.addworker.theadNotes-02':
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
'clusters.addworker.nvidiaNotes':
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.',
'The built-in inference backends in GPUStack require <span class="bold-text">CUDA 12.8+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">570</span> or newer.',
'clusters.volume.title': 'Volume Mounts',
'clusters.volume.name': 'Volume Name',
'clusters.volume.mountPath': 'Container Path',
+1 -1
View File
@@ -25,7 +25,7 @@ export default {
'ai.provider.ollama': 'Ollama',
'ai.provider.openai': 'OpenAI',
'ai.provider.openrouter': 'OpenRouter',
'ai.provider.qwen': 'Qwen',
'ai.provider.qwen': 'Alibaba Cloud Model Studio',
'ai.provider.spark': 'Spark',
'ai.provider.stepfun': 'StepFun',
'ai.provider.together-ai': 'TogetherAI',
+1 -1
View File
@@ -145,7 +145,7 @@ export default {
'clusters.addworker.theadNotes-02':
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
'clusters.addworker.nvidiaNotes':
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.',
'The built-in inference backends in GPUStack require <span class="bold-text">CUDA 12.8+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">570</span> or newer.',
'clusters.volume.title': 'Volume Mounts',
'clusters.volume.name': 'Volume Name',
'clusters.volume.mountPath': 'Container Path',
+1 -1
View File
@@ -25,7 +25,7 @@ export default {
'ai.provider.ollama': 'Ollama',
'ai.provider.openai': 'OpenAI',
'ai.provider.openrouter': 'OpenRouter',
'ai.provider.qwen': 'Qwen',
'ai.provider.qwen': 'Alibaba Cloud Model Studio',
'ai.provider.spark': 'Spark',
'ai.provider.stepfun': 'StepFun',
'ai.provider.together-ai': 'TogetherAI',
+1 -1
View File
@@ -145,7 +145,7 @@ export default {
'clusters.addworker.theadNotes-02':
'T-Head PPU, cihaz enjeksiyonu için Container Device Interface (CDI) kullanır ve CDI oluşturma için <span class="bold-text">/var/run/cdi</span> dizininin kullanılabilir olmasını gerektirir.',
'clusters.addworker.nvidiaNotes':
'GPUStack v2.1\'deki yerleşik çıkarım altyapıları <span class="bold-text">CUDA 12.6+</span> gerektirir. Lütfen NVIDIA sürücü sürümünüzün <span class="bold-text">560</span> veya daha yeni olduğundan emin olun.',
'GPUStack\'teki yerleşik çıkarım altyapıları <span class="bold-text">CUDA 12.8+</span> gerektirir. Lütfen NVIDIA sürücü sürümünüzün <span class="bold-text">570</span> veya daha yeni olduğundan emin olun.',
'clusters.volume.title': 'Volume Mounts',
'clusters.volume.name': 'Volume Name',
'clusters.volume.mountPath': 'Container Path',
+1 -1
View File
@@ -25,7 +25,7 @@ export default {
'ai.provider.ollama': 'Ollama',
'ai.provider.openai': 'OpenAI',
'ai.provider.openrouter': 'OpenRouter',
'ai.provider.qwen': '通义千问',
'ai.provider.qwen': '阿里云百炼',
'ai.provider.spark': '讯飞星火',
'ai.provider.stepfun': '阶跃星辰',
'ai.provider.together-ai': 'Together AI',
+1 -1
View File
@@ -138,7 +138,7 @@ export default {
'clusters.addworker.theadNotes-02':
'平头哥(T-Head)PPU 使用容器设备接口(CDI)进行设备注入,因此需要确保 <span class="bold-text">/var/run/cdi</span> 目录可用以生成 CDI。',
'clusters.addworker.nvidiaNotes':
'GPUStack v2.1 内置推理后端依赖 <span class="bold-text">CUDA 12.6</span> 及以上版本,请确保 NVIDIA 驱动版本为 <span class="bold-text">560</span> 或以上。',
'GPUStack 内置推理后端依赖 <span class="bold-text">CUDA 12.8</span> 及以上版本,请确保 NVIDIA 驱动版本为 <span class="bold-text">570</span> 或以上。',
'clusters.volume.title': '卷挂载',
'clusters.volume.name': '卷名称',
'clusters.volume.mountPath': '容器内路径',
+63 -9
View File
@@ -1,9 +1,15 @@
import useCoolColors from '@/hooks/use-cool-colors';
import { Chart } from '@gpustack/core-ui';
import { formatLargeNumber } from '@gpustack/core-ui/utils';
import { Empty, theme } from 'antd';
import { Empty, Spin, theme } from 'antd';
import _ from 'lodash';
import React, { useEffect, useMemo, useRef } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react';
export interface BarSeriesItem {
name: string;
@@ -17,6 +23,7 @@ export interface BarChartProps {
xAxisData: string[];
height: number | string;
width?: number | string;
loading?: boolean;
legendData?: { name: string; icon?: string }[];
labelFormatter?: (val: any, index?: number) => string;
tooltipValueFormatter?: (val: any) => string;
@@ -38,6 +45,7 @@ const BarChart: React.FC<BarChartProps> = (props) => {
height,
width,
legendData,
loading,
labelFormatter,
tooltipValueFormatter,
title,
@@ -48,6 +56,27 @@ const BarChart: React.FC<BarChartProps> = (props) => {
const chartRef = useRef<{ chart: any } | null>(null);
const generateCoolColors = useCoolColors();
// ECharts reads the DOM width at init time; with a "100%" width it can pick
// up a stale/tiny value while the flex child's layout is still resolving,
// rendering every bar squeezed at the left edge until its internal (throttled)
// ResizeObserver corrects it ~100ms later — a visible blue-sliver flash. We
// measure the container ourselves via a callback ref (which fires during
// commit, before paint) and feed ECharts an explicit pixel width, so the very
// first render is already correct. No gating, so the chart mounts with no
// extra delay; the ResizeObserver keeps the width in sync afterwards.
const resizeObserverRef = useRef<ResizeObserver | null>(null);
const [measuredWidth, setMeasuredWidth] = useState(0);
const measureRef = useCallback((node: HTMLDivElement | null) => {
resizeObserverRef.current?.disconnect();
if (!node) return;
setMeasuredWidth(node.clientWidth);
resizeObserverRef.current = new ResizeObserver(() => {
setMeasuredWidth(node.clientWidth);
});
resizeObserverRef.current.observe(node);
}, []);
useEffect(() => () => resizeObserverRef.current?.disconnect(), []);
const dynamicColors = useMemo(
() => generateCoolColors(seriesData.length),
[seriesData.length, generateCoolColors]
@@ -277,18 +306,43 @@ const BarChart: React.FC<BarChartProps> = (props) => {
justifyContent: 'center'
}}
>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
{loading ? (
<Spin size="middle" />
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</div>
);
}
return (
<Chart
ref={chartRef as any}
options={options as any}
height={height}
width={width || '100%'}
/>
<div
ref={measureRef}
style={{ width: width || '100%', height, position: 'relative' }}
>
<Chart
ref={chartRef as any}
options={options as any}
height={height}
width={measuredWidth || width || '100%'}
/>
{loading && (
<div
style={{
position: 'absolute',
inset: 20,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--ant-color-bg-container)',
opacity: 0.6,
pointerEvents: 'none'
}}
>
<Spin size="middle" />
</div>
)}
</div>
);
};
@@ -1,68 +0,0 @@
import { IconFont } from '@gpustack/core-ui';
import { Collapse, CollapseProps } from 'antd';
import React from 'react';
import styled from 'styled-components';
const CollapseInner = styled(Collapse)`
.ant-collapse-header {
display: flex;
align-items: center;
margin-bottom: 10px !important;
padding-inline: 5px !important;
padding-block: 8px !important;
border-radius: var(--border-radius-base) !important;
font-size: 14px !important;
font-weight: 600 !important;
&:hover {
background-color: var(--ant-color-fill-tertiary) !important;
}
}
.ant-collapse-body {
padding-inline: 0 !important;
padding-block: 0 !important;
}
.ant-collapse-header-text {
display: flex;
align-items: center;
height: 24px;
}
`;
const CollapsePanel: React.FC<{
items: CollapseProps['items'];
activeKey: string | string[];
accordion?: boolean;
defaultActiveKey?: string | string[];
onChange?: (key: string | string[]) => void;
styles?: Record<string, React.CSSProperties>;
}> = ({ items, activeKey, accordion, defaultActiveKey, onChange, styles }) => {
return (
<CollapseInner
expandIconPlacement="start"
bordered={false}
ghost
accordion={accordion}
activeKey={activeKey}
defaultActiveKey={defaultActiveKey}
onChange={onChange}
destroyOnHidden={false}
styles={{
...styles,
header: {
backgroundColor: 'var(--ant-collapse-header-bg)'
}
}}
expandIcon={({ isActive }) => (
<IconFont
type="icon-down"
rotate={isActive ? 0 : -90}
style={{ fontSize: '14px' }}
></IconFont>
)}
items={items}
></CollapseInner>
);
};
export default CollapsePanel;
-250
View File
@@ -1,250 +0,0 @@
import {
readColumnSettings,
writeColumnSettings
} from '@/utils/localstore/index';
import { SettingOutlined } from '@ant-design/icons';
import { OverlayScroller } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Button, Checkbox, Col, Popover, Row, Tooltip } from 'antd';
import React, { useEffect } from 'react';
import styled from 'styled-components';
const Container = styled.div`
padding: 8px 12px;
padding-right: 4px;
.title {
font-weight: 500;
margin-bottom: 12px;
}
.btn-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
}
.buttons {
display: flex;
gap: 8px;
justify-content: flex-end;
}
`;
const Title = styled.div`
font-weight: 500;
margin-bottom: 8px;
margin-top: 4px;
`;
const LabelWrapper = styled.span`
color: var(--ant-color-text-secondary);
> span {
display: flex;
align-items: center;
gap: 4px;
color: var(--ant-color-text-secondary);
.sub-title {
display: none;
}
}
`;
const ColumnSettings: React.FC<{
width?: number;
fixedColumns?: string[];
tableName: string;
contentHeight: number;
columns: {
title: React.ReactNode;
dataIndex?: string;
children?: { title: React.ReactNode; dataIndex?: string }[];
}[];
selectedColumns?: string[];
defaultSelectedColumns?: string[];
grouped?: boolean;
onReset?: () => void;
onChange?: (selectedColumns: string[]) => void;
}> = (props) => {
const intl = useIntl();
const {
tableName,
contentHeight,
width = 420,
columns,
selectedColumns,
defaultSelectedColumns,
grouped,
onReset,
onChange,
fixedColumns
} = props;
const [open, setOpen] = React.useState(false);
const [innerSelectedColumns, setInnerSelectedColumns] = React.useState<
string[]
>(defaultSelectedColumns || []);
React.useEffect(() => {
if (open) {
setInnerSelectedColumns(selectedColumns ?? defaultSelectedColumns ?? []);
}
}, [open, selectedColumns, defaultSelectedColumns]);
const handleToggle = () => {
setOpen(!open);
};
const handleDraftChange = (columns: string[]) => {
setInnerSelectedColumns(columns);
};
const handleSelectAll = () => {
if (grouped) {
const allCols: string[] = [];
columns.forEach((group) => {
group.children?.forEach((col) => {
if (col.dataIndex) {
allCols.push(col.dataIndex);
}
});
});
handleDraftChange(allCols);
} else {
const allCols = columns
.map((col) => col.dataIndex)
.filter((dataIndex): dataIndex is string => Boolean(dataIndex));
handleDraftChange(allCols);
}
};
const handleConfirm = () => {
setOpen(false);
writeColumnSettings(tableName, innerSelectedColumns);
onChange?.(innerSelectedColumns);
};
const handleReset = () => {
const resetCols = defaultSelectedColumns ?? [];
writeColumnSettings(tableName, resetCols);
onChange?.(resetCols);
setInnerSelectedColumns(resetCols);
onReset?.();
};
const handleOpenChange = (isOpen: boolean) => {
setOpen(isOpen);
};
useEffect(() => {
const initColumns = async () => {
const stored = await readColumnSettings(tableName);
if (stored && stored.length > 0) {
setInnerSelectedColumns(stored);
onChange?.(stored);
} else {
setInnerSelectedColumns(defaultSelectedColumns || []);
}
};
initColumns();
}, []);
const contentRender = () => {
return (
<Container>
{!grouped && (
<div className="title">
{intl.formatMessage({ id: 'benchmark.table.columnSettings' })}
</div>
)}
<OverlayScroller
maxHeight={contentHeight}
styles={{
wrapper: {
paddingInlineStart: 0
}
}}
>
<Checkbox.Group
value={innerSelectedColumns}
onChange={handleDraftChange}
>
<>
{grouped ? (
columns.map((row, index) => (
<div key={index}>
<Title>{row.title}</Title>
<Row>
{row.children?.map((col) => (
<Col key={col.dataIndex} span={12}>
<Checkbox
disabled={fixedColumns?.includes(
col.dataIndex || ''
)}
value={col.dataIndex}
style={{ marginBottom: 8 }}
>
<LabelWrapper>{col.title}</LabelWrapper>
</Checkbox>
</Col>
))}
</Row>
</div>
))
) : (
<Row>
{columns.map((col) => (
<Col key={col.dataIndex} span={12}>
<Checkbox
disabled={fixedColumns?.includes(col.dataIndex || '')}
value={col.dataIndex}
style={{ marginBottom: 8 }}
>
<LabelWrapper>{col.title}</LabelWrapper>
</Checkbox>
</Col>
))}
</Row>
)}
</>
</Checkbox.Group>
</OverlayScroller>
<div className="btn-wrapper">
<Button size="middle" onClick={handleReset}>
{intl.formatMessage({ id: 'common.button.resetdefault' })}
</Button>
<div className="buttons">
<Button size="middle" type="primary" onClick={handleSelectAll}>
{intl.formatMessage({ id: 'common.checkbox.all' })}
</Button>
<Button size="middle" type="primary" onClick={handleConfirm}>
{intl.formatMessage({ id: 'common.button.save' })}
</Button>
</div>
</div>
</Container>
);
};
return (
<Popover
open={open}
onOpenChange={handleOpenChange}
trigger={'click'}
arrow={false}
placement="bottomRight"
content={contentRender()}
styles={{
root: {
width: width
}
}}
>
<Tooltip
title={intl.formatMessage({ id: 'benchmark.table.columnSettings' })}
>
<Button onClick={handleToggle} icon={<SettingOutlined />}></Button>
</Tooltip>
</Popover>
);
};
export default ColumnSettings;
@@ -1,99 +0,0 @@
import { useOverlayScroller } from '@gpustack/core-ui';
import React, { useCallback } from 'react';
import styled from 'styled-components';
import { WrapperContext } from './use-wrapper-context';
const Wrapper = styled.div`
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
width: 100%;
`;
const ContentWrapper = styled.div`
flex: 1;
position: relative;
overflow-y: auto;
`;
const Footer = styled.div`
padding-block: 0;
background-color: var(--ant-color-bg-elevated);
`;
interface ColumnWrapperProps {
children: React.ReactNode;
footer?: React.ReactNode;
maxHeight?: string | number;
paddingBottom?: number;
styles?: {
wrapper?: React.CSSProperties;
container?: React.CSSProperties;
};
}
const ColumnWrapper: React.FC<ColumnWrapperProps> = ({
children,
footer,
maxHeight,
styles = {}
}) => {
const scroller = React.useRef<any>(null);
const footerRef = React.useRef<HTMLDivElement>(null);
const contentRef = React.useRef<HTMLDivElement>(null);
const {
initialize,
instance,
scrollEventElement,
scrollToBottom,
scrollToTarget,
getScrollElementScrollableHeight
} = useOverlayScroller({
options: {
scrollbars: {
autoHide: 'move'
}
}
});
React.useEffect(() => {
if (scroller.current) {
initialize(scroller.current);
}
}, []);
const setContentPaddingBottom = useCallback((padding: number) => {
contentRef.current!.style.paddingBottom = `${padding}px`;
}, []);
return (
<WrapperContext.Provider
value={{
scroller: scroller,
osInstance: instance,
scrollEventElement,
getScrollElementScrollableHeight,
scrollToBottom,
scrollToTarget,
setSScrollContentPaddingBottom: setContentPaddingBottom
}}
>
<Wrapper style={{ height: maxHeight || '100%', ...styles.wrapper }}>
<ContentWrapper
ref={scroller}
style={{
padding: '16px 24px',
...styles.container
}}
>
<div ref={contentRef}>{children}</div>
</ContentWrapper>
{footer && <Footer ref={footerRef}>{footer}</Footer>}
</Wrapper>
</WrapperContext.Provider>
);
};
export default ColumnWrapper;
@@ -1,28 +0,0 @@
import { createContext, useContext } from 'react';
interface WrapperContextProps {
osInstance?: any;
scroller?: any;
scrollEventElement?: any;
scrollToBottom?: () => void;
scrollToTop?: () => void;
getScrollElementScrollableHeight?: () => {
scrollHeight: number;
scrollTop: number;
};
scrollToTarget?: (target: any, offset?: number) => void;
getScrollElement?: () => HTMLElement | null;
setSScrollContentPaddingBottom?: (padding: number) => void;
}
export const WrapperContext = createContext<WrapperContextProps>(
{} as WrapperContextProps
);
export const useWrapperContext = () => {
const context = useContext(WrapperContext);
if (!context) {
throw new Error('useWrapperContext must be used within a WrapperProvider');
}
return context;
};
-155
View File
@@ -1,155 +0,0 @@
import { CloseOutlined } from '@ant-design/icons';
import { OverlayScroller } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Button, Form } from 'antd';
import classNames from 'classnames';
import React, { forwardRef, useImperativeHandle } from 'react';
import filterFormCss from '../styles/filter-form.less';
const FilterForm: React.FC<
React.PropsWithChildren & {
ref?: any;
width?: number;
contentHeight?: number | string;
initialValues?: any;
hasFilters?: boolean;
open?: boolean;
onClear?: () => void;
onClose?: () => void;
onValuesChange?: (ChangeValues: any, allValues: any) => void;
styles?: {
container?: React.CSSProperties;
wrapper?: React.CSSProperties;
};
}
> = forwardRef(
(
{
children,
open,
width = 300,
contentHeight = 400,
initialValues = {},
styles,
onClose,
onClear,
onValuesChange
},
ref
) => {
const intl = useIntl();
const [form] = Form.useForm();
const handleOnReset = () => {
form.resetFields(Object.keys(initialValues));
form.resetFields();
onValuesChange?.({}, form.getFieldsValue());
};
const handleOnClose = () => {
onClose?.();
};
const handleOnValuesChange = (changedValues: any, allValues: any) => {
onValuesChange?.(changedValues, allValues);
};
const handleOnClear = () => {
handleOnReset();
};
const filtersCount = Object.values(form.getFieldsValue()).filter(
(value) => value !== undefined && value !== null && value !== ''
).length;
const renderFooter = () => {
return (
<div className={filterFormCss['btn-wrapper']}>
<Button size="middle" onClick={handleOnReset}>
{intl.formatMessage({ id: 'common.button.reset' })}
</Button>
<div className={filterFormCss.buttons}>
<Button size="middle" type="primary" onClick={handleOnClose}>
{intl.formatMessage({ id: 'common.button.close' })}
</Button>
</div>
</div>
);
};
useImperativeHandle(ref, () => ({
form,
reset: handleOnReset,
getValues: () => form.getFieldsValue(),
setValues: (values: any) => form.setFieldsValue(values)
}));
return (
<div
className={classNames(filterFormCss.wrapper, {
[filterFormCss.show]: open
})}
style={{
width: open ? width : 0,
height: contentHeight,
...styles?.wrapper
}}
>
<div
style={{ width: width, ...styles?.container }}
className={filterFormCss.container}
>
<div className={filterFormCss.title}>
<span
style={{
fontWeight: 500,
color: 'var(--ant-color-text-tertiary)'
}}
>
{intl.formatMessage({ id: 'common.filter.label' })}
</span>
<Button
size="small"
icon={<CloseOutlined />}
onClick={handleOnClose}
type="text"
style={{
color: 'var(--ant-color-text-secondary)'
}}
></Button>
</div>
<OverlayScroller
maxHeight={contentHeight}
styles={{
wrapper: {
paddingInline: 8
}
}}
>
<Form
onValuesChange={handleOnValuesChange}
initialValues={initialValues}
form={form}
layout="vertical"
styles={{
label: {
lineHeight: 1,
height: 'auto',
marginBottom: 8,
fontWeight: 500
},
content: {
minHeight: 0
}
}}
>
{children}
</Form>
</OverlayScroller>
</div>
</div>
);
}
);
export default FilterForm;
-90
View File
@@ -1,90 +0,0 @@
import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui';
import type { DrawerProps } from 'antd';
import { Tag } from 'antd';
import React from 'react';
const ModalFooterStyle = {
padding: '16px 24px 8px',
display: 'flex',
justifyContent: 'flex-end'
};
type AddModalProps = {
title: React.ReactNode;
open: boolean;
onCancel?: () => void;
children?: React.ReactNode;
onSubmit?: () => void;
width?: number | string;
footer?: React.ReactNode;
subTitle?: React.ReactNode;
push?: DrawerProps['push'];
};
const FormDrawer: React.FC<AddModalProps> = ({
title,
open,
onCancel,
onSubmit,
children,
width = 600,
subTitle,
footer,
push
}) => {
return (
<GSDrawer
title={
<>
{title}
{subTitle && (
<Tag
variant="outlined"
style={{
fontSize: 12,
fontWeight: 400,
marginLeft: 8,
borderRadius: 4,
borderColor: 'var(--ant-color-border-secondary)',
color: 'var(--ant-color-text-secondary)'
}}
>
{subTitle}
</Tag>
)}
</>
}
open={open}
onClose={onCancel}
destroyOnHidden={true}
closeIcon={false}
mask={{
closable: false
}}
keyboard={false}
push={push}
styles={{
wrapper: { width }
}}
footer={false}
>
<ColumnWrapper
styles={{
container: { paddingBlock: 0 }
}}
footer={
footer ?? (
<ModalFooter
onOk={onSubmit}
onCancel={onCancel}
style={ModalFooterStyle}
></ModalFooter>
)
}
>
{children}
</ColumnWrapper>
</GSDrawer>
);
};
export default FormDrawer;
@@ -1,81 +0,0 @@
.mask {
position: fixed;
inset: 0;
z-index: 1004;
height: 100%;
background-color: var(--ant-color-bg-mask);
opacity: 0;
transition: opacity var(--ant-motion-duration-slow)
var(--ant-motion-ease-in-out);
}
.maskOpen {
opacity: 1;
}
.overlay {
position: fixed;
top: 0;
right: 0;
bottom: auto;
left: auto;
z-index: 1005;
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
background: var(--ant-color-bg-elevated);
border-radius: var(--ant-border-radius-lg) 0 0 var(--ant-border-radius-lg);
box-shadow:
-6px 0 16px 0 rgb(0 0 0 / 8%),
-3px 0 6px -4px rgb(0 0 0 / 12%),
-9px 0 28px 8px rgb(0 0 0 / 5%);
transform: translateX(100%);
transition: transform var(--ant-motion-duration-slow)
var(--ant-motion-ease-in-out);
}
.overlayOpen {
transform: translateX(0);
}
.header {
display: flex;
align-items: center;
gap: 12px;
min-height: 56px;
padding: var(--ant-padding) var(--ant-padding-lg);
border-bottom: 1px solid var(--ant-color-split);
color: var(--ant-color-text);
font-size: 14px;
font-weight: 600;
line-height: 24px;
}
.title {
display: flex;
min-width: 0;
align-items: center;
}
.subTitle {
margin-left: 8px;
border-color: var(--ant-color-border-secondary);
border-radius: 4px;
color: var(--ant-color-text-secondary);
font-size: 12px;
font-weight: 400;
}
.content {
padding-block: 16px;
flex: 1;
height: calc(100vh - 57px);
overflow-y: auto;
}
.footer {
display: flex;
justify-content: flex-end;
padding: 16px 24px 8px;
}
-122
View File
@@ -1,122 +0,0 @@
import { ColumnWrapper, IconFont } from '@gpustack/core-ui';
import { Button, Tag } from 'antd';
import classNames from 'classnames';
import React from 'react';
import { createPortal } from 'react-dom';
import styles from './form-overlay-view.module.less';
type FormOverlayViewProps = {
title: React.ReactNode;
open: boolean;
onCancel?: () => void;
children?: React.ReactNode;
onSubmit?: () => void;
footer?: React.ReactNode;
subTitle?: React.ReactNode;
width?: number | string;
className?: string;
style?: React.CSSProperties;
maskClosable?: boolean;
getContainer?: () => HTMLElement | null | undefined;
};
const FormOverlayView: React.FC<FormOverlayViewProps> = ({
title,
open,
onCancel,
onSubmit,
children,
subTitle,
footer,
width = 600,
className,
style,
maskClosable = false,
getContainer
}) => {
const [container, setContainer] = React.useState<HTMLElement | null>(null);
const [mounted, setMounted] = React.useState(false);
const [active, setActive] = React.useState(false);
React.useEffect(() => {
if (open) {
setContainer(getContainer?.() ?? null);
setMounted(true);
return;
}
setActive(false);
}, [getContainer, open]);
React.useEffect(() => {
if (!mounted || !container) {
return;
}
const id = requestAnimationFrame(() => setActive(true));
return () => cancelAnimationFrame(id);
}, [mounted, container]);
const handleTransitionEnd = (e: React.TransitionEvent<HTMLDivElement>) => {
if (e.target !== e.currentTarget || e.propertyName !== 'transform') {
return;
}
if (!open && !active) {
setMounted(false);
setContainer(null);
}
};
if (!mounted || !container) {
return null;
}
return createPortal(
<>
<div
className={classNames(styles.mask, { [styles.maskOpen]: active })}
onClick={maskClosable ? onCancel : undefined}
/>
<div
className={classNames(
styles.overlay,
{ [styles.overlayOpen]: active },
className
)}
style={{ width, ...style }}
role="dialog"
aria-modal="true"
onTransitionEnd={handleTransitionEnd}
>
<div className={styles.header}>
<Button
type="text"
size="small"
style={{ fontWeight: 600, fontSize: 16 }}
icon={<IconFont type="icon-down2" rotate={90} />}
onClick={onCancel}
/>
<div className={styles.title}>
{title}
{subTitle && (
<Tag variant="outlined" className={styles.subTitle}>
{subTitle}
</Tag>
)}
</div>
</div>
<ColumnWrapper
styles={{
container: { paddingBlock: 16 }
}}
footer={footer}
>
{children}
</ColumnWrapper>
</div>
</>,
container
);
};
export default FormOverlayView;
File diff suppressed because one or more lines are too long
-26
View File
@@ -1,26 +0,0 @@
import { update } from 'jdenticon';
import React, { useEffect, useRef } from 'react';
interface IdenticonProps {
value: string;
size: number;
}
const Identicon: React.FC<IdenticonProps> = ({
value = 'test',
size = 28
}: IdenticonProps) => {
const icon = useRef<any>(null);
useEffect(() => {
update(icon.current, value);
}, [value]);
return (
<>
<svg data-jdenticon-value={value} height={size} ref={icon} width={size} />
</>
);
};
export default Identicon;
@@ -1,68 +0,0 @@
import {
digitReg,
lowercaseReg,
specialCharacterReg,
uppercaseReg
} from '@/config';
import { CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Space } from 'antd';
const PasswordValidate: React.FC<{ value: string }> = ({ value = '' }) => {
const intl = useIntl();
const renderIcon = ({ valid, text }: { valid: boolean; text: string }) => {
return (
<>
{valid ? (
<CheckCircleFilled style={{ color: 'green' }} />
) : (
<CloseCircleFilled style={{ color: 'red' }} />
)}
<span
className="m-l-5"
style={{ color: 'var(--ant-color-text-description)' }}
>
{text}
</span>
</>
);
};
return (
<Space direction="vertical" style={{ paddingTop: '10px' }}>
<span>
{renderIcon({
valid: uppercaseReg.test(value),
text: intl.formatMessage({ id: 'users.password.uppcase' })
})}
</span>
<span>
{renderIcon({
valid: lowercaseReg.test(value),
text: intl.formatMessage({ id: 'users.password.lowercase' })
})}
</span>
<span>
{renderIcon({
valid: digitReg.test(value),
text: intl.formatMessage({ id: 'users.password.number' })
})}
</span>
<span>
{renderIcon({
valid: value.length >= 6 && value.length <= 12,
text: intl.formatMessage({ id: 'users.password.length' })
})}
</span>
<span>
{renderIcon({
valid: specialCharacterReg.test(value),
text: intl.formatMessage({ id: 'users.password.special' })
})}
</span>
</Space>
);
};
export default PasswordValidate;
+29 -3
View File
@@ -2,7 +2,13 @@ import useCoolColors from '@/hooks/use-cool-colors';
import { Chart } from '@gpustack/core-ui';
import { formatLargeNumber } from '@gpustack/core-ui/utils';
import { Empty, Spin, theme } from 'antd';
import React, { useMemo, useRef } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react';
export interface PieChartItem {
name: string;
@@ -37,6 +43,26 @@ const PieChart: React.FC<PieChartProps> = ({
const chartRef = useRef<{ chart: any } | null>(null);
const generateCoolColors = useCoolColors();
// ECharts reads the DOM width at init time; with a "100%" width it can pick
// up a stale/tiny value while layout is still resolving, briefly rendering
// the donut undersized before its internal (throttled) ResizeObserver
// corrects it — a visible flash. We measure the container via a callback ref
// (fires during commit, before paint) and feed ECharts an explicit pixel
// width so the first render is already correct. The ResizeObserver keeps it
// in sync afterwards.
const resizeObserverRef = useRef<ResizeObserver | null>(null);
const [measuredWidth, setMeasuredWidth] = useState(0);
const measureRef = useCallback((node: HTMLDivElement | null) => {
resizeObserverRef.current?.disconnect();
if (!node) return;
setMeasuredWidth(node.clientWidth);
resizeObserverRef.current = new ResizeObserver(() => {
setMeasuredWidth(node.clientWidth);
});
resizeObserverRef.current.observe(node);
}, []);
useEffect(() => () => resizeObserverRef.current?.disconnect(), []);
const colors = useMemo(() => {
const generatedColors = generateCoolColors(data.length + colorOffset);
return generatedColors.slice(colorOffset);
@@ -142,12 +168,12 @@ const PieChart: React.FC<PieChartProps> = ({
const displayTotal = total ?? data.reduce((sum, item) => sum + item.value, 0);
return (
<div style={{ width, height, position: 'relative' }}>
<div ref={measureRef} style={{ width, height, position: 'relative' }}>
<Chart
ref={chartRef as any}
options={options as any}
height={height}
width={width}
width={measuredWidth || width}
/>
<div
style={{
@@ -1,60 +0,0 @@
import { AutoTooltip } from '@gpustack/core-ui';
import React from 'react';
import pillButtonCss from './styles.less';
type Option = {
label: string;
value: string | number | null;
icon?: React.ReactNode;
};
type Props = {
value?: string | number;
onChange?: (value: string | number | undefined | null) => void;
options: Option[];
disabled?: boolean;
variant?: 'filled' | 'outlined' | 'solid';
};
const PillButtonGroup: React.FC<Props> = ({
value,
onChange,
options,
disabled,
variant = 'outlined'
}) => {
return (
<div className={pillButtonCss['wrapper']}>
{options.map((item) => {
const active = value === item.value;
return (
<AutoTooltip
ghost
title={item.label}
key={item.value}
maxWidth={'100%'}
>
<span
className={`${pillButtonCss['pill-item']} ${active ? pillButtonCss['active'] : ''}`}
key={item.value}
color="default"
onClick={() => {
if (item.value !== value) {
onChange?.(item.value);
}
if (item.value === value) {
onChange?.(undefined);
}
}}
>
{item.icon}
{item.label}
</span>
</AutoTooltip>
);
})}
</div>
);
};
export default PillButtonGroup;
@@ -1,31 +0,0 @@
.pill-item {
height: 28px;
line-height: 28px;
display: flex;
align-items: center;
gap: 8px;
justify-content: center;
border-radius: var(--ant-border-radius);
background-color: var(--ant-color-fill-tertiary);
color: var(--ant-color-text-secondary);
padding: 0 8px;
cursor: pointer;
transition: all 0.3s;
&.active {
background-color: var(--ant-color-fill);
color: var(--ant-color-text);
font-weight: 500;
}
&:not(.active):hover {
background-color: var(--ant-color-fill-secondary);
}
}
.wrapper {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 8px;
align-items: center;
}
@@ -1,115 +0,0 @@
import { SegmentLine } from '@gpustack/core-ui';
import { useMemoizedFn } from 'ahooks';
import _ from 'lodash';
import React, { forwardRef, useImperativeHandle } from 'react';
import styled from 'styled-components';
import useFieldScroll from './use-field-scroll';
const SegmentedHeader = styled.div<{ $top?: number }>`
position: sticky;
top: ${(props) => props.$top || 0}px;
z-index: 10;
margin-bottom: 16px;
border-bottom: 1px solid var(--ant-color-split);
background-color: var(--ant-color-bg-elevated);
`;
/**
* ScrollSpyTabs component
* defaultTarget: The default active target tab
* segmentedTop: { // Mostlty, It's always a constants.
* top: number; // The top offset for the sticky header
* offsetTop: number; // The offset top for the target
* }
* getScrollElementScrollableHeight: function to get the scrollable height of the scroll element
* segmentOptions.field: The target data-field={segmentOptions.field} to scroll to
* activeKey: The current active keys for collapsible sections
* setActiveKey: The function to set active keys for collapsible sections
*/
interface ScrollSpyTabsProps {
ref?: any;
children?: React.ReactNode;
defaultTarget?: string;
segmentedTop: {
top: number;
offsetTop: number;
};
activeKey: string[];
setActiveKey: (keys: string[]) => void;
getScrollElementScrollableHeight?: () => {
scrollHeight: number;
scrollTop: number;
};
segmentOptions: {
label: string;
value: string;
icon?: React.ReactNode;
field: string;
}[];
}
const ScrollSpyTabs: React.FC<ScrollSpyTabsProps> = forwardRef(
(
{
getScrollElementScrollableHeight,
segmentedTop,
segmentOptions,
defaultTarget,
activeKey,
setActiveKey,
children
},
ref
) => {
const [target, setTarget] = React.useState<string>(
defaultTarget || segmentOptions[0]?.value || ''
);
const { scrollToSegment, holderHeight } = useFieldScroll({
activeKey,
setActiveKey,
segmentOptions,
segmentedTop: segmentedTop,
getScrollElementScrollableHeight: getScrollElementScrollableHeight
});
const throttleScrollToSegment = useMemoizedFn(
_.throttle(
async (val: string) => {
setTarget(val);
scrollToSegment(val, { offsetTop: segmentedTop.offsetTop });
},
500,
{ trailing: true }
)
);
const handleTargetChange = async (val: any) => {
throttleScrollToSegment(val);
};
useImperativeHandle(ref, () => ({
handleTargetChange
}));
return (
<div>
{segmentOptions.length > 0 && (
<SegmentedHeader $top={segmentedTop.top}>
<SegmentLine
theme={'light'}
defaultValue={target}
value={target}
onChange={handleTargetChange}
options={segmentOptions}
/>
</SegmentedHeader>
)}
{children}
<div className="holder" style={{ height: holderHeight }}></div>
</div>
);
}
);
export default ScrollSpyTabs;
@@ -1,118 +0,0 @@
import { useMemoizedFn } from 'ahooks';
import { useCallback, useRef, useState } from 'react';
interface ScrollOptions {
wait?: number;
behavior?: 'smooth' | 'auto';
block?: 'start' | 'end' | 'center';
offsetTop?: number;
}
export default function useScrollAfterExpand({
activeKey,
setActiveKey,
segmentOptions,
defaultWait = 300,
segmentedTop = { top: 0, offsetTop: 96 },
getScrollElementScrollableHeight
}: {
activeKey: string[];
setActiveKey: (keys: string[]) => void;
segmentOptions: { value: string; field: string }[];
getScrollElementScrollableHeight?: () => {
scrollHeight: number;
scrollTop: number;
};
defaultWait?: number;
segmentedTop: {
top: number; // The top offset for the sticky header
offsetTop: number; // The offset top for the target
};
}) {
const [holderHeight, setHolderHeight] = useState<number>(0);
const boxHeightRef = useRef<number>(0);
const scrollToElement = useCallback(
(
el: HTMLElement,
{ behavior = 'smooth', offsetTop = 0 }: ScrollOptions = {}
) => {
// find the nearest scrollable parent
const scrollParent = (() => {
let node: HTMLElement | null = el;
while (node) {
const { overflowY } = getComputedStyle(node);
if (overflowY === 'auto' || overflowY === 'scroll') return node;
node = node.parentElement;
}
return document.scrollingElement || document.documentElement;
})();
const parentRect = scrollParent.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
const top =
elRect.top - parentRect.top + scrollParent.scrollTop - offsetTop;
scrollParent.scrollTo({ top, behavior });
},
[]
);
/**
* due to the scrollheight changes after expanding the segment and including the holder height.
*
*/
const scrollToSegment = useMemoizedFn(
async (val: string, options?: ScrollOptions) => {
if (!activeKey.includes(val)) {
setActiveKey([...activeKey, val]);
await new Promise((r) => {
setTimeout(r, options?.wait ?? defaultWait);
});
}
const current = segmentOptions.find((item) => item.value === val);
if (!current?.field) return;
await new Promise(requestAnimationFrame);
const el: HTMLElement | null = document.querySelector(
`[data-field="${current.field}"]`
) as HTMLElement | null;
const targetRectTop = el?.getBoundingClientRect().top || 0;
const scroller = getScrollElementScrollableHeight?.() || {
scrollHeight: 0,
scrollTop: 0
};
// remaining scroll height
const remainingScrollHeight = scroller.scrollHeight - scroller.scrollTop;
// total distance from the top of the scroller to the target element
const offsetDistance =
targetRectTop - segmentedTop.offsetTop - segmentedTop.top;
let boxHeight = 0;
// verify boxHeight is correct, if setting the boxHeight causes the element to be hidden, use the previous boxHeight
if (offsetDistance <= 0) {
boxHeight = boxHeightRef.current;
} else {
boxHeight =
offsetDistance - remainingScrollHeight + boxHeightRef.current;
}
// update boxHeightRef
boxHeightRef.current = boxHeight;
setHolderHeight(boxHeight);
await new Promise(requestAnimationFrame);
if (el) scrollToElement(el, options);
}
);
return { scrollToSegment, holderHeight };
}
@@ -1,44 +0,0 @@
interface FinishFailedOptions {
requiredFields: {
[tab: string]: {
sort: number;
fields: string[];
};
};
onTargetChange: (key: string) => void;
updateActiveKey: (key: string[]) => void;
}
const useFinishFailed = (options: FinishFailedOptions) => {
const { requiredFields, onTargetChange, updateActiveKey } = options;
const handleOnFinishFailed = (errorInfo: any) => {
const { errorFields } = errorInfo;
console.log('Finish failed:', errorInfo);
if (errorFields && errorFields.length > 0) {
const collapseKeys: { sort: number; key: string }[] = [];
const names = errorFields.map((item: any) => item.name[0]);
Object.entries(requiredFields).forEach(([tab, { fields, sort }]) => {
const hasError = fields.some((field: string) => names.includes(field));
if (hasError) {
collapseKeys.push({ sort, key: tab });
}
});
if (collapseKeys.length > 0) {
const keys = collapseKeys
.sort((a, b) => a.sort - b.sort)
.map((item) => item.key);
updateActiveKey(keys);
onTargetChange(collapseKeys[0].key);
}
}
};
return {
handleOnFinishFailed
};
};
export default useFinishFailed;
@@ -1,40 +0,0 @@
import { useState } from 'react';
const useScrollActiveChange = (options: {
initalActiveKeys: string[];
initialCollapseKeys?: string[];
}) => {
const [activeKey, setActiveKey] = useState<string[]>(
options.initalActiveKeys
);
const [collapseKeys, setCollapseKeys] = useState<string[]>(
options.initialCollapseKeys || options.initalActiveKeys
);
const handleActiveChange = (key: string[]) => {
setActiveKey(key);
setCollapseKeys(key);
};
const handleOnCollapseChange = (keys: string | string[]) => {
const keysArray = Array.isArray(keys) ? keys : [keys];
setActiveKey(keysArray);
setCollapseKeys(keysArray);
};
const updateActiveKey = (keys: string[]) => {
setActiveKey((prev: string[]) => [...new Set([...prev, ...keys])]);
setCollapseKeys((prev) => [...new Set([...prev, ...keys])]);
};
return {
activeKey,
collapseKeys,
setCollapseKeys,
handleActiveChange,
handleOnCollapseChange,
updateActiveKey
};
};
export default useScrollActiveChange;
@@ -1,36 +0,0 @@
import { CloseCircleFilled } from '@ant-design/icons';
import { Button } from 'antd';
import styled from 'styled-components';
const StyledButton = styled(Button)`
background-color: transparent !important;
padding: 0;
.anticon {
color: var(--ant-color-text-quaternary);
font-size: 12px !important;
transition: color 0.3s ease;
}
&:hover {
.anticon {
color: var(--ant-color-text-tertiary);
}
}
`;
interface SmallCloseButtonProps {
onClick?: () => void;
}
const SmallCloseButton: React.FC<SmallCloseButtonProps> = ({ onClick }) => {
return (
<StyledButton
icon={<CloseCircleFilled />}
shape="circle"
type="text"
onClick={onClick}
size="small"
></StyledButton>
);
};
export default SmallCloseButton;
@@ -1,41 +0,0 @@
.wrapper {
flex-shrink: 0;
overflow: hidden;
width: 0;
transition: all var(--ant-motion-duration-slow) var(--ant-motion-ease-in-out);
border-color: var(--ant-color-split);
&.show {
width: 232px;
border-right: 1px solid var(--ant-color-split);
}
}
.container {
padding: 0 16px;
.title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
margin-top: 8px;
padding-inline: 8px;
font-weight: 500;
color: var(--ant-color-text-tertiary);
}
.btn-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 24px;
padding-right: 8px;
}
.buttons {
display: flex;
gap: 8px;
justify-content: flex-end;
}
}
-109
View File
@@ -1,109 +0,0 @@
import { MoreOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Transfer, TransferProps } from 'antd';
import styled from 'styled-components';
type TransferKey = string | number | bigint;
const TransferWrap = styled.div`
.ant-transfer-section {
width: 100%;
height: 300px;
.anticon-more {
display: none;
}
.ant-input-outlined {
height: 32px;
padding-block: 4px;
border-radius: 4px;
}
}
.ant-transfer-actions {
margin: 0 16px;
gap: 12px;
.ant-btn-icon-only {
width: 32px;
height: 32px;
border-radius: 50%;
}
}
.ant-transfer-list-content {
&::-webkit-scrollbar {
width: var(--scrollbar-size);
}
&::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: 4px;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
&:hover {
&::-webkit-scrollbar-thumb {
background-color: var(--color-scrollbar-thumb);
border-radius: 4px;
}
}
.ant-transfer-list-content-item {
&:hover {
background-color: var(--ant-control-item-bg-hover);
}
&.ant-transfer-list-content-item-checked {
background-color: unset;
&:hover {
background-color: var(--ant-control-item-bg-hover);
}
}
}
}
.ant-pagination {
justify-content: center;
}
`;
interface TransferInnerProps extends TransferProps {
total?: number;
perPage?: number;
onPageChange?: (page: number, perPage?: number) => void;
dataSource?: Array<{ key: TransferKey; title: string }>;
targetKeys?: TransferKey[];
}
const TransferInner: React.FC<TransferInnerProps> = (props) => {
const intl = useIntl();
const renderAllLabels = (info: {
selectedCount: number;
totalCount: number;
}) => {
if (info.selectedCount) {
return (
<span style={{ color: 'var(--ant-color-text-secondary)' }}>
{intl.formatMessage(
{ id: 'common.select.count' },
{ count: info.selectedCount }
)}
</span>
);
}
return null;
};
return (
<TransferWrap>
<Transfer
{...props}
selectAllLabels={
props.selectAllLabels || [renderAllLabels, renderAllLabels]
}
selectionsIcon={
<MoreOutlined style={{ fontSize: 14, marginBottom: 3 }} />
}
></Transfer>
</TransferWrap>
);
};
export default TransferInner;
+1 -1
View File
@@ -3,6 +3,7 @@ import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import {
AlertBlockInfo,
ColumnWrapper,
GSDrawer,
IconFont,
ModalFooter,
@@ -13,7 +14,6 @@ import { Tabs } from 'antd';
import _ from 'lodash';
import React, { useEffect, useId, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
import ColumnWrapper from '../../_components/column-wrapper';
import {
BackendSourceValueMap,
builtInBackendFields,
+5 -1
View File
@@ -237,6 +237,10 @@ const BackendList = () => {
});
});
const handleRefresh = () => {
fetchData({ query: { ...queryParams, page: 1 } });
};
return (
<PageBox>
<FilterBar
@@ -251,7 +255,7 @@ const BackendList = () => {
selectHolder={intl.formatMessage({ id: 'backend.filter.source' })}
buttonText={intl.formatMessage({ id: 'backend.button.add' })}
handleClickPrimary={handleAddBackend}
handleSearch={handleSearch}
handleSearch={handleRefresh}
handleSelectChange={handleFilterBySource}
handleInputChange={handleNameChange}
rowSelection={rowSelection}
@@ -124,6 +124,23 @@ const Instance: React.FC = () => {
</Flex>
)
},
{
key: '2',
label: intl.formatMessage({ id: 'models.form.env' }),
children: (
<Flex gap={8} wrap="wrap">
{instanceData?.env
? Object.entries(instanceData?.env || {}).map(
([key, value], index: number) => (
<Tag key={index} style={{ margin: 0 }}>
{`${key}=${value}`}
</Tag>
)
)
: '-'}
</Flex>
)
},
{
key: '4',
label: intl.formatMessage({
@@ -159,23 +176,6 @@ const Instance: React.FC = () => {
)}
</Flex>
)
},
{
key: '2',
label: intl.formatMessage({ id: 'models.form.env' }),
children: (
<Flex gap={8} wrap="wrap">
{instanceData?.env
? Object.entries(instanceData?.env || {}).map(
([key, value], index: number) => (
<Tag key={index} style={{ margin: 0 }}>
{`${key}=${value}`}
</Tag>
)
)
: '-'}
</Flex>
)
}
];
}, [detailData]);
@@ -17,6 +17,9 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
const logsViewerRef = React.useRef<any>(null);
const requestRef = React.useRef<any>(null);
const contentRef = React.useRef<any>(null);
const [params, setParams] = React.useState<any>({
follow: true
});
const handleCancel = useCallback(() => {
logsViewerRef.current?.abort();
@@ -94,9 +97,7 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
tail={undefined}
enableScorllLoad={true}
isDownloading={false}
params={{
follow: true
}}
params={params}
></LogsViewer>
</div>
</Modal>
+1 -1
View File
@@ -59,7 +59,7 @@ const Billing: React.FC = () => {
</li>
))}
</ul>
<div style={{ textAlign: 'center' }}>
<div style={{ textAlign: 'center', marginTop: 56 }}>
<Button
type="primary"
href="https://gpustack.ai/enterprise"
@@ -59,9 +59,16 @@ const ClusterCreate: React.FC<{
// empty-state CTAs that already know which kind of cluster the user
// is heading for (e.g. GPU Service's "Add a Kubernetes Cluster").
providerHint?: string;
presetClusterType?: 'model' | 'gpu';
setCurrentTitle?: (title: string) => void;
onClose?: () => void;
}> = ({ onClose, action, providerHint, setCurrentTitle }) => {
}> = ({
onClose,
action,
providerHint,
presetClusterType,
setCurrentTitle
}) => {
const stepList = useStepList();
const [systemConfigState] = useAtom(systemConfigAtom);
const intl = useIntl();
@@ -377,6 +384,7 @@ const ClusterCreate: React.FC<{
)}
<StepsContext.Provider
value={{
presetClusterType: presetClusterType,
formValues: formValues,
systemConfig: systemConfigState
}}
@@ -6,10 +6,10 @@ import ClusterCreate from './cluster-create';
interface ClusterModalProps {
open: boolean;
title: string;
// When set, ClusterCreate preselects this provider and skips the
// provider-catalog step. Used by feature pages (e.g. GPU Service)
// whose empty state already implies which kind of cluster is needed.
providerHint?: string;
pendingProviderHint?: {
providerHint?: string;
presetClusterType?: 'model' | 'gpu';
};
onClose: () => void;
}
@@ -17,7 +17,7 @@ const ClusterModal: React.FC<ClusterModalProps> = ({
open,
onClose,
title,
providerHint
pendingProviderHint
}) => {
const [currentTitle, setCurrentTitle] = React.useState<string>(title);
const handleCancel = () => {
@@ -46,7 +46,8 @@ const ClusterModal: React.FC<ClusterModalProps> = ({
<ClusterCreate
onClose={handleCancel}
action={PageAction.CREATE}
providerHint={providerHint}
providerHint={pendingProviderHint?.providerHint}
presetClusterType={pendingProviderHint?.presetClusterType}
setCurrentTitle={setCurrentTitle}
></ClusterCreate>
</GSDrawer>
+16 -6
View File
@@ -321,13 +321,20 @@ const Clusters: React.FC = () => {
// the session atom is cleared right after we open the modal, but
// ClusterCreate mounts a tick later and needs the value to skip the
// provider-catalog step. Cache it locally and clear on close.
const [pendingProviderHint, setPendingProviderHint] = useState<
string | undefined
>(undefined);
const [pendingProviderHint, setPendingProviderHint] = useState<{
providerHint?: string;
presetClusterType?: 'model' | 'gpu';
}>({
providerHint: undefined,
presetClusterType: undefined
});
useEffect(() => {
if (clusterSession?.firstAddCluster && dataSource.loadend) {
setPendingProviderHint(clusterSession.providerHint);
setPendingProviderHint({
providerHint: clusterSession.providerHint,
presetClusterType: clusterSession.presetClusterType
});
openClusterModal();
// reset session
setClusterSession(null);
@@ -335,7 +342,10 @@ const Clusters: React.FC = () => {
}, [clusterSession, dataSource.loadend]);
const handleClusterModalClose = () => {
setPendingProviderHint(undefined);
setPendingProviderHint({
providerHint: undefined,
presetClusterType: undefined
});
closeClusterModal();
};
@@ -464,7 +474,7 @@ const Clusters: React.FC = () => {
id: 'menu.resources.clusterCreate'
})}
open={clusterModalStatus.open}
providerHint={pendingProviderHint}
pendingProviderHint={pendingProviderHint}
onClose={handleClusterModalClose}
></ClusterModal>
{AddWorkerModal}
@@ -6,6 +6,7 @@ import { Form } from 'antd';
import _ from 'lodash';
import React, { useEffect, useId, useMemo } from 'react';
import styled from 'styled-components';
import { useStepsContext } from '../config/steps-context';
import { ClusterListItem as ListItem } from '../config/types';
import ImageCredential from './image-credential';
import K8SVolumeMount from './k8s-volume-mount';
@@ -175,6 +176,7 @@ const RadioDot = styled.span<{ $active: boolean }>`
export const ClusterTypeSelector: React.FC = () => {
const intl = useIntl();
const form = Form.useFormInstance();
const { presetClusterType } = useStepsContext();
const labelId = useId();
const gpuInstanceOptions = Form.useWatch(GPU_INSTANCE_OPTIONS_PATH, {
form,
@@ -211,6 +213,12 @@ export const ClusterTypeSelector: React.FC = () => {
}
];
useEffect(() => {
if (presetClusterType) {
handleSelect(presetClusterType);
}
}, [presetClusterType]);
return (
<ClusterTypeWrap>
<ClusterTypeLabel id={labelId}>
@@ -1,13 +1,15 @@
import { createContext, useContext } from 'react';
export interface StepsContextProps {
presetClusterType?: 'model' | 'gpu';
formValues: Record<string, any>;
systemConfig?: Record<string, any>;
}
export const StepsContext = createContext<StepsContextProps>({
formValues: {},
systemConfig: {}
systemConfig: {},
presetClusterType: undefined
});
export const useStepsContext = () => useContext(StepsContext);
@@ -14,7 +14,7 @@ export const useQueryClusterList = (options?: { useStateData?: boolean }) => {
const { useStateData = true } = options || {};
const axiosTokenRef = useRef<CancelTokenSource | null>(null);
const [dataList, setDataList] = useState<
Array<Partial<ClusterListItem> & { label: string; value: number }>
Array<ClusterListItem & { label: string; value: number }>
>([]);
const {
@@ -1,7 +1,6 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
import useUserDirectory from '@/pages/gpu-service/hooks/use-user-directory';
import Separator from '@/pages/llmodels/components/separator';
import { getGPUStackPlugin } from '@/plugins';
@@ -13,7 +12,7 @@ import {
ModalFooter
} from '@gpustack/core-ui';
import { useIntl, useModel } from '@umijs/max';
import { Empty, Input, Typography } from 'antd';
import { Input, Typography } from 'antd';
import _ from 'lodash';
import { useEffect, useMemo, useRef, useState } from 'react';
import { ListItem as TemplateItem } from '../../templates/config/types';
@@ -31,6 +30,12 @@ type AddModalProps = {
open: boolean;
width?: number | string;
realAction?: string;
clusterList?: Array<{
label: string;
value: number;
id: number;
owner_principal_id?: number;
}>;
onOk: (values: FormData) => void;
data?: ListItem | null;
onCancel: () => void;
@@ -78,6 +83,7 @@ const AddModal: React.FC<AddModalProps> = ({
data,
onCancel,
width,
clusterList = [],
realAction
}) => {
const intl = useIntl();
@@ -100,16 +106,18 @@ const AddModal: React.FC<AddModalProps> = ({
const [instanceKeyword, setInstanceKeyword] = useState('');
const [templateKeyword, setTemplateKeyword] = useState('');
const { loading, guard, run, release } = useSubmitLock();
const initializedRef = useRef(false);
const [initialized, setInitialized] = useState(false);
const {
detailData: instanceTypeList,
loading: instanceTypesLoading,
fetchData
} = useQueryInstanceTypes();
const { detailData: templatesData, fetchData: fetchTemplates } =
useQueryTemplates();
const { clusterList, fetchClusterList } = useQueryClusterList();
const {
detailData: templatesData,
loading: templateLoading,
fetchData: fetchTemplates
} = useQueryTemplates();
// Set by the create-scope picker (admin "All" view) via onScopeChange.
// undefined = no picker (org context) → no client-side scoping.
const [scopeOrgId, setScopeOrgId] = useState<number | null | undefined>(
@@ -125,18 +133,13 @@ const AddModal: React.FC<AddModalProps> = ({
// fetched list client-side, so it doesn't rely on the request scope.
const filterTypesByOwner = (
types: InstanceTypeItem[],
clusters: Array<{
id?: number;
value?: number;
owner_principal_id?: number;
}>,
orgId?: number | null
): InstanceTypeItem[] => {
if (orgId == null) return types;
const owned = new Set(
(clusters || [])
(clusterList || [])
.filter((c) => c.owner_principal_id === orgId)
.map((c) => c.id ?? c.value)
.map((c) => c.id || c.value)
);
return types
.map((it) => ({
@@ -157,7 +160,7 @@ const AddModal: React.FC<AddModalProps> = ({
};
const ownedInstanceTypes = useMemo(
() => filterTypesByOwner(instanceTypeList, clusterList as any, scopeOrgId),
() => filterTypesByOwner(instanceTypeList, scopeOrgId),
[instanceTypeList, clusterList, scopeOrgId]
);
@@ -279,11 +282,6 @@ const AddModal: React.FC<AddModalProps> = ({
const applyAutoSelection = (
instanceTypes: InstanceTypeItem[],
templates: TemplateItem[],
clusters?: Array<{
id?: number;
value?: number;
owner_principal_id?: number;
}>,
orgId?: number | null
) => {
// On edit / view, surface the persisted selection in the card list.
@@ -303,7 +301,7 @@ const AddModal: React.FC<AddModalProps> = ({
}
// Scope to clusters the chosen org owns (admin "All" view).
const owned = filterTypesByOwner(instanceTypes, clusters || [], orgId);
const owned = filterTypesByOwner(instanceTypes, orgId);
// On create, auto-select the first available instance type (clears the
// selection when the chosen org has none).
@@ -314,26 +312,22 @@ const AddModal: React.FC<AddModalProps> = ({
// The query hook cancels any in-flight request on each new call, so when
// this runs twice in quick succession (drawer open, then the scope
// picker settling on its default) the latest scope's result wins.
const loadCreateResources = (orgId?: number | null) => {
const loadCreateResources = async (orgId?: number | null) => {
const session = ++sessionRef.current;
try {
Promise.all([
const [instanceResItems, templatesRes] = await Promise.all([
fetchData({ page: -1 }),
fetchTemplates({ page: -1 }),
fetchClusterList({ page: -1 })
]).then(([instanceResItems, templatesRes, clusters]) => {
if (sessionRef.current !== session) return;
applyAutoSelection(
instanceResItems || [],
templatesRes?.items || [],
(Array.isArray(clusters) ? clusters : (clusters as any)?.items) || [],
orgId
);
initializedRef.current = true;
});
fetchTemplates({ page: -1 })
]);
if (sessionRef.current !== session) return;
applyAutoSelection(
instanceResItems || [],
templatesRes?.items || [],
orgId
);
setInitialized(true);
} catch (error) {
} finally {
initializedRef.current = true;
setInitialized(true);
}
};
@@ -352,13 +346,13 @@ const AddModal: React.FC<AddModalProps> = ({
// owner-scoped auto-selection re-fills them from the new org, or leaves
// them empty (blocking submit) when the chosen org has no clusters.
clearSelection();
initializedRef.current = false;
setInitialized(false);
loadCreateResources(orgId);
};
useEffect(() => {
if (!open) {
initializedRef.current = false;
setInitialized(false);
sessionRef.current += 1;
setInstanceTypeSelection({
instanceType: undefined,
@@ -619,15 +613,12 @@ const AddModal: React.FC<AddModalProps> = ({
onChange={(e) => setTemplateKeyword(e.target.value)}
/>
</div>
{filteredTemplates.length > 0 && initializedRef.current ? (
<TemplateSelector
value={templateId}
groups={templateGroups}
onChange={handleTemplateChange}
/>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
<TemplateSelector
value={templateId}
loading={templateLoading || !initialized}
groups={templateGroups}
onChange={handleTemplateChange}
/>
</div>
</ColumnWrapper>
<Separator></Separator>
@@ -1,17 +1,10 @@
import FileSkeleton from '@/pages/llmodels/components/model-source/file-skeleton';
import { FileSkeletonRows } from '@/pages/llmodels/components/model-source/file-skeleton';
import { TemplateCard } from '@gpustack/core-ui';
import { Empty, Spin } from 'antd';
import { Empty, Flex, Spin } from 'antd';
import _ from 'lodash';
import styled from 'styled-components';
import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types';
import InstanceTypeItem from './instance-type-item';
const TypeGrid = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
`;
interface InstanceTypeListProps {
value?: string;
onChange?: (item: InstanceTypeItemModel) => void;
@@ -34,11 +27,11 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
if (loading) {
return (
<Spin spinning size="middle">
<TypeGrid style={{ minHeight: 200 }}>
<Flex orientation="vertical" gap={16} style={{ minHeight: 200 }}>
{_.times(6, (index: number) => (
<FileSkeleton key={index} counts={3} itemHeight={106} />
<FileSkeletonRows key={index} counts={2} itemHeight={106} />
))}
</TypeGrid>
</Flex>
</Spin>
);
}
@@ -46,7 +39,7 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
}
return (
<TypeGrid>
<Flex orientation="vertical" gap={16}>
{dataList.map((item) => {
const name = item.name;
return (
@@ -64,7 +57,7 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
</TemplateCard>
);
})}
</TypeGrid>
</Flex>
);
};
@@ -70,9 +70,9 @@ export const status: Record<string, StatusType> = {
[InstanceStatusValueMap.NotReady]: StatusMaps.error,
[InstanceStatusValueMap.Ready]: StatusMaps.success,
[InstanceStatusValueMap.Starting]: StatusMaps.transitioning,
[InstanceStatusValueMap.Deleting]: StatusMaps.error,
[InstanceStatusValueMap.Stopping]: StatusMaps.error,
[InstanceStatusValueMap.Stopped]: StatusMaps.warning,
[InstanceStatusValueMap.Deleting]: StatusMaps.warning,
[InstanceStatusValueMap.Stopping]: StatusMaps.transitioning,
[InstanceStatusValueMap.Stopped]: StatusMaps.inactive,
[InstanceStatusValueMap.CreateFailed]: StatusMaps.error,
[InstanceStatusValueMap.SSHPublicKeyCreateFailed]: StatusMaps.error,
[InstanceStatusValueMap.PersistentVolumeTypeCreateFailed]: StatusMaps.error,
+25 -14
View File
@@ -129,6 +129,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
// exists/changes when a platform admin retargets the form. Watch it
// so the parent can re-scope offerings (see onScopeChange).
const scopeOrgId = Form.useWatch('organization_id', form);
const ports = Form.useWatch(['spec', 'ports'], form) || [];
const scopeInitRef = useRef(true);
// Keep the latest callback in a ref so the scope-change effect can call it
// without depending on its identity (parent may pass a new fn each render).
@@ -151,17 +152,31 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
: [TABKeysMap.INSTANCE_TYPE, TABKeysMap.TEMPLATE, TABKeysMap.STORAGE]
});
const hasSSHPort = useMemo(
() =>
ports.some(
(item: any) => item?.protocol === 'TCP' && item?.port === SSH_PORT
),
[ports]
);
const isGPUType = useMemo(() => {
const spec = parseJsonSafe(description || '{}', {} as any)?.spec;
console.log('derived spec from description', spec);
return spec?.acceleratable;
}, [description]);
useEffect(() => {
if (open) {
fetchSSHData({ page: 1, perPage: 100 });
const initSSHKeys = async () => {
// await 200 ms
await new Promise((resolve) => {
setTimeout(resolve, 200);
});
fetchSSHData({ page: -1 });
};
initSSHKeys();
}
}, [open, action]);
}, [open]);
// Skip the first run (initial mount value); thereafter notify the
// parent whenever the chosen create scope changes so it can reload
@@ -174,16 +189,6 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
onScopeChangeRef.current?.(scopeOrgId);
}, [scopeOrgId]);
const ports = Form.useWatch(['spec', 'ports'], form) || [];
const hasSSHPort = useMemo(
() =>
ports.some(
(item: any) => item?.protocol === 'TCP' && item?.port === SSH_PORT
),
[ports]
);
useEffect(() => {
if (
hasSSHPort &&
@@ -554,7 +559,13 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
image: '',
imagePullPolicy: DefaultImagePullPolicy,
command: [],
ports: [],
ports: [
{
protocol: 'TCP',
port: SSH_PORT,
name: 'SSH'
}
],
env: [],
volumeMount: '',
resources: {
@@ -1,6 +1,5 @@
import { PageAction } from '@/config';
import FormOverlayView from '@/pages/_components/form-overlay-view';
import { ModalFooter } from '@gpustack/core-ui';
import { ModalFooter, SubDrawer } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { useRef, useState } from 'react';
import { FormData as PublicKeyFormData } from '../../public-keys/config/types';
@@ -45,7 +44,7 @@ const PublicKeyOverlay: React.FC<PublicKeyOverlayProps> = ({
};
return (
<FormOverlayView
<SubDrawer
title={intl.formatMessage({ id: 'gpuservice.publicKey.add' })}
open={open}
width={drawerWidth}
@@ -70,7 +69,7 @@ const PublicKeyOverlay: React.FC<PublicKeyOverlayProps> = ({
open={open}
onFinish={handleFinish}
/>
</FormOverlayView>
</SubDrawer>
);
};
@@ -1,8 +1,7 @@
import { PageAction } from '@/config';
import FormOverlayView from '@/pages/_components/form-overlay-view';
import { FormContext } from '@/pages/gpu-service/storage/config/form-context';
import useQueryStorageClass from '@/pages/gpu-service/storage/services/use-query-storage-class';
import { ModalFooter } from '@gpustack/core-ui';
import { ModalFooter, SubDrawer } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { useEffect, useRef, useState } from 'react';
import { FormData as StorageFormData } from '../../storage/config/types';
@@ -52,7 +51,7 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
};
return (
<FormOverlayView
<SubDrawer
title={intl.formatMessage({ id: 'gpuservice.storage.add' })}
open={open}
width={drawerWidth}
@@ -79,7 +78,7 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
onFinish={handleFinish}
/>
</FormContext.Provider>
</FormOverlayView>
</SubDrawer>
);
};
@@ -36,7 +36,13 @@ const StorageVolume = ({
const [overlayOpen, setOverlayOpen] = useState(false);
useEffect(() => {
fetchStorage({ page: 1, perPage: 100 });
const initStorage = async () => {
await new Promise((resolve) => {
setTimeout(resolve, 200);
});
fetchStorage({ page: -1 });
};
initStorage();
}, []);
const storageOptions = useMemo(
@@ -1,15 +1,12 @@
import { FileSkeletonRows } from '@/pages/llmodels/components/model-source/file-skeleton';
import { AutoTooltip, IconFont, TemplateCard } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Empty, Flex, Spin } from 'antd';
import _ from 'lodash';
import { Fragment } from 'react';
import styled from 'styled-components';
import { ListItem as TemplateItem } from '../../templates/config/types';
const TemplateGrid = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
`;
const GroupTitle = styled.div`
font-size: 12px;
font-weight: 500;
@@ -49,6 +46,12 @@ const TemplateContent = styled.div`
}
`;
const TypeGrid = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
`;
export interface TemplateGroup {
key: string;
label: React.ReactNode;
@@ -57,6 +60,7 @@ export interface TemplateGroup {
interface TemplateSelectorProps {
value?: number;
loading?: boolean;
onChange?: (value: number, item: TemplateItem) => void;
groups?: TemplateGroup[];
}
@@ -64,6 +68,7 @@ interface TemplateSelectorProps {
const TemplateSelector: React.FC<TemplateSelectorProps> = ({
value,
onChange,
loading,
groups = []
}) => {
const intl = useIntl();
@@ -73,6 +78,22 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
onChange?.(item.id, item);
};
if (loading) {
return (
<Spin spinning size="middle">
<Flex orientation="vertical" gap={16} style={{ minHeight: 200 }}>
{_.times(6, (index: number) => (
<FileSkeletonRows key={index} counts={2} itemHeight={106} />
))}
</Flex>
</Spin>
);
}
if (!groups.length) {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />;
}
const renderItem = (item: TemplateItem) => (
<TemplateCard
key={item.id}
@@ -120,14 +141,14 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
const showGroupTitles = groups.length > 1;
return (
<TemplateGrid>
<Flex orientation="vertical" gap={16}>
{groups.map((group) => (
<Fragment key={group.key}>
{showGroupTitles && <GroupTitle>{group.label}</GroupTitle>}
{group.items.map(renderItem)}
</Fragment>
))}
</TemplateGrid>
</Flex>
);
};
+5 -2
View File
@@ -51,6 +51,7 @@ const GPUService: React.FC = () => {
setClusterSession({
firstAddWorker: false,
firstAddCluster: true,
presetClusterType: 'gpu',
providerHint: ProviderValueMap.Kubernetes
});
navigate('/resources/clusters/list');
@@ -111,7 +112,7 @@ const GPUService: React.FC = () => {
useEffect(() => {
fetchClusterList({ page: -1 });
(async () => {
const fetchPVCapacities = async () => {
try {
const res = await queryGPUServiceStorage({ page: -1 } as any);
const map: Record<string, string> = {};
@@ -124,7 +125,8 @@ const GPUService: React.FC = () => {
} catch {
// best-effort; the popover falls back to the PV name
}
})();
};
fetchPVCapacities();
}, []);
const hasK8sCluster = useMemo(
@@ -387,6 +389,7 @@ const GPUService: React.FC = () => {
data={openInstanceModalStatus.currentData}
width={openInstanceModalStatus.width}
realAction={openInstanceModalStatus.realAction}
clusterList={clusterList}
onCancel={closeInstanceModal}
onOk={handleModalOk}
/>
@@ -85,6 +85,9 @@ const formatResources = (
};
};
// Spec popover categories, in render order.
type SpecCategory = 'gpu' | 'cpu' | 'ram' | 'disk';
export const renderInstanceType = (
record: ListItem,
options: {
@@ -92,22 +95,31 @@ export const renderInstanceType = (
// name → capacity (e.g. "20Gi") for referenced persistent volumes, so the
// Disk → Persistent row can show the size instead of just the PV name.
pvCapacityByName?: Record<string, string>;
// Limit the spec popover to these categories (default: all). The Instance
// Types breakdown only wants CPU + RAM, for example.
categories?: SpecCategory[];
// Override the primary label (default: derived "<product> x <count>" /
// "CPU Only"). The Instance Types breakdown keeps its plain product name.
title?: string;
}
) => {
const { intl, pvCapacityByName } = options;
const { intl, pvCapacityByName, categories } = options;
const description =
parseJsonSafe<any>(record?.description || '{}', {}).spec || {};
const resources = formatResources({ spec: description }, record);
const accelerator = record.spec?.resources?.accelerator;
const title = description.acceleratable
? `${description.product} x ${accelerator}`
: 'CPU Only';
const title =
options.title ??
(description.acceleratable
? `${description.product} x ${accelerator}`
: 'CPU Only');
const volume = (record.spec as any)?.volume;
// Spec popover grouped by category (GPU / CPU / Memory / Disk), mirroring
// the Deployments instance info icon: dark tooltip, per-category icon,
// instance name as the title. Rows with no value are dropped.
type Section = {
key: SpecCategory;
icon: string;
name: string;
rows: [string | null, string | undefined][];
@@ -115,6 +127,7 @@ export const renderInstanceType = (
const sections: Section[] = [];
if (description.acceleratable) {
sections.push({
key: 'gpu',
icon: 'icon-gpu',
name: 'GPU',
rows: [
@@ -134,16 +147,19 @@ export const renderInstanceType = (
});
}
sections.push({
key: 'cpu',
icon: 'icon-cpu',
name: 'CPU',
rows: [[null, resources.cpu]]
});
sections.push({
key: 'ram',
icon: 'icon-ram-02',
name: intl.formatMessage({ id: 'gpuservice.instance.ram' }),
rows: [[null, resources.ram]]
});
sections.push({
key: 'disk',
icon: 'icon-hard-disk',
name: intl.formatMessage({ id: 'gpuservice.instance.disk' }),
rows: [
@@ -170,8 +186,16 @@ export const renderInstanceType = (
]
});
const visibleSections = categories
? sections.filter((s) => categories.includes(s.key))
: sections;
return (
<InstanceTypeCell title={title} name={record.name} sections={sections} />
<InstanceTypeCell
title={title}
name={record.name}
sections={visibleSections}
/>
);
};
@@ -1,8 +1,7 @@
import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { ModalFooter } from '@gpustack/core-ui';
import { FormDrawer, ModalFooter } from '@gpustack/core-ui';
import { useRef } from 'react';
import FormDrawer from '../../../_components/form-drawer';
import { FormData, ListItem } from '../config/types';
import GPUServicePublicKeyForm from '../forms';
@@ -1,8 +1,7 @@
import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { ModalFooter } from '@gpustack/core-ui';
import { FormDrawer, ModalFooter } from '@gpustack/core-ui';
import { useRef } from 'react';
import FormDrawer from '../../../_components/form-drawer';
import { FormData, ListItem } from '../config/types';
import GPUServiceStorageTypeForm from '../forms';
@@ -7,6 +7,7 @@ import { FormData } from '../config/types';
const NFSForm = ({ action }: { action: string }) => {
const intl = useIntl();
const { getRuleMessage } = useAppUtils();
const disabled = action === PageAction.EDIT;
return (
<>
@@ -24,6 +25,7 @@ const NFSForm = ({ action }: { action: string }) => {
>
<CInput.Input
required
disabled={disabled}
description={intl.formatMessage({
id: 'gpuservice.storageType.nfs.server.tips'
})}
@@ -43,6 +45,7 @@ const NFSForm = ({ action }: { action: string }) => {
>
<CInput.Input
required
disabled={disabled}
description={intl.formatMessage({
id: 'gpuservice.storageType.nfs.share.tips'
})}
@@ -51,6 +54,7 @@ const NFSForm = ({ action }: { action: string }) => {
</Form.Item>
<Form.Item<FormData> name={['spec', 'nfs', 'subDirectory']}>
<CInput.Input
disabled={disabled}
label={intl.formatMessage({
id: 'gpuservice.storageType.nfs.subDirectory'
})}
@@ -58,6 +62,7 @@ const NFSForm = ({ action }: { action: string }) => {
</Form.Item>
<Form.Item<FormData> name={['spec', 'nfs', 'mountPermissions']}>
<CInput.Input
disabled={disabled}
placeholder="0755,0777,..."
description={intl.formatMessage({
id: 'gpuservice.storageType.nfs.mountPermissions.tips'
@@ -69,7 +74,7 @@ const NFSForm = ({ action }: { action: string }) => {
</Form.Item>
<Form.Item<FormData> name={['spec', 'nfs', 'mountOptions']}>
<ListInput
disabled={action === PageAction.EDIT}
disabled={disabled}
label={intl.formatMessage({
id: 'gpuservice.storageType.mountOptions'
})}
@@ -14,6 +14,7 @@ const S3Form = ({ action }: { action: string }) => {
const intl = useIntl();
const form = Form.useFormInstance<FormData>();
const { getRuleMessage } = useAppUtils();
const disabled = action === PageAction.EDIT;
const handleEndpointBlur = (e: any) => {
const value: string = e.target.value;
@@ -51,6 +52,7 @@ const S3Form = ({ action }: { action: string }) => {
>
<CInput.Input
required
disabled={disabled}
label={intl.formatMessage({
id: 'gpuservice.storageType.s3.endpoint'
})}
@@ -65,6 +67,7 @@ const S3Form = ({ action }: { action: string }) => {
<div style={{ flex: 1 }}>
<Form.Item<FormData> name={['spec', 's3', 'region']}>
<CInput.Input
disabled={disabled}
label={intl.formatMessage({
id: 'gpuservice.storageType.s3.region'
})}
@@ -86,6 +89,7 @@ const S3Form = ({ action }: { action: string }) => {
>
<CInput.Input
required
disabled={disabled}
description={
<Flex orientation="vertical" gap={4} align="start">
<span>
@@ -111,6 +115,7 @@ const S3Form = ({ action }: { action: string }) => {
</Flex>
<Form.Item<FormData> name={['spec', 's3', 'accessKey']}>
<CInput.Input
disabled={disabled}
label={intl.formatMessage({
id: 'gpuservice.storageType.s3.accessKey'
})}
@@ -118,6 +123,7 @@ const S3Form = ({ action }: { action: string }) => {
</Form.Item>
<Form.Item<FormData> name={['spec', 's3', 'secretKey']}>
<CInput.Password
disabled={disabled}
label={intl.formatMessage({
id: 'gpuservice.storageType.s3.secretKey'
})}
@@ -129,6 +135,7 @@ const S3Form = ({ action }: { action: string }) => {
style={{ marginBottom: 12 }}
>
<CheckboxField
disabled={disabled}
description={intl.formatMessage({
id: 'gpuservice.storageType.s3.insecure.tips'
})}
@@ -139,7 +146,7 @@ const S3Form = ({ action }: { action: string }) => {
</Form.Item>
<Form.Item<FormData> name={['spec', 's3', 'mountOptions']}>
<ListInput
disabled={action === PageAction.EDIT}
disabled={disabled}
label={intl.formatMessage({
id: 'gpuservice.storageType.mountOptions'
})}
@@ -1,8 +1,7 @@
import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { ModalFooter } from '@gpustack/core-ui';
import { FormDrawer, ModalFooter } from '@gpustack/core-ui';
import { useRef } from 'react';
import FormDrawer from '../../../_components/form-drawer';
import { FormContext } from '../config/form-context';
import { FormData, ListItem } from '../config/types';
import GPUServiceStorageForm from '../forms';
@@ -1,8 +1,7 @@
import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { ModalFooter } from '@gpustack/core-ui';
import { FormDrawer, ModalFooter } from '@gpustack/core-ui';
import { useRef } from 'react';
import FormDrawer from '../../../_components/form-drawer';
import { FormData, ListItem } from '../config/types';
import GPUServiceTemplateForm from '../forms';
@@ -36,7 +36,6 @@ import { ListItem } from '../config/types';
const imageLogoMap = {
vllm: vllmLogo,
sglang: sgLangLogo,
cann: ascendLogo,
jupyter: jupyterLogo,
pytorch: pytorchBlackLogo,
tensorflow: tensorflowkLogo,
@@ -1,5 +1,5 @@
import PluginExtraFields from '@/components/plugin-extra-fields';
import { validateLabelNameRegxFor63 } from '@/config';
import { PageAction, validateLabelNameRegxFor63 } from '@/config';
import { PageActionType } from '@/config/types';
import {
Input as CInput,
@@ -86,7 +86,7 @@ const Basic: React.FC<BasicProps> = ({
]}
>
<CInput.Input
disabled={disabled}
disabled={disabled || action === PageAction.EDIT}
label={intl.formatMessage({ id: 'common.table.name' })}
required
/>
+5 -1
View File
@@ -129,6 +129,10 @@ const GPUServiceTemplates: React.FC = () => {
});
});
const handleRefresh = () => {
fetchData({ query: { ...queryParams, page: 1 } });
};
return (
<PageBox>
<FilterBar
@@ -145,7 +149,7 @@ const GPUServiceTemplates: React.FC = () => {
})}
buttonText={intl.formatMessage({ id: 'gpuservice.template.add' })}
handleClickPrimary={handleAddTemplate}
handleSearch={handleSearch}
handleSearch={handleRefresh}
handleSelectChange={handleFilterByManufacturer}
handleInputChange={handleNameChange}
rowSelection={rowSelection}
+5 -1
View File
@@ -27,7 +27,7 @@ const Catalog: React.FC = () => {
const {
dataSource,
queryParams,
handleSearch,
fetchData,
handleQueryChange,
loadMore,
handleNameChange
@@ -111,6 +111,10 @@ const Catalog: React.FC = () => {
navigate('/models/deployments');
};
const handleSearch = () => {
fetchData({ query: { ...queryParams, page: 1 } });
};
useEffect(() => {
if (dataSource.loadend) {
const getCatalogSource = async () => {
@@ -15,7 +15,7 @@ import {
ScrollerModal
} from '@gpustack/core-ui';
import { useIntl, useNavigate } from '@umijs/max';
import { Button, Tag } from 'antd';
import { Button, Flex, Tag } from 'antd';
import _ from 'lodash';
import { useEffect, useMemo } from 'react';
import styled from 'styled-components';
@@ -79,6 +79,7 @@ const Tips = styled.div`
const APITAG = styled(Tag)`
border-radius: 12px;
margin: 0;
width: fit-content;
margin-left: 8px;
`;
@@ -149,7 +150,7 @@ const ApiAccessInfo = ({ open, data, onClose }: ApiAccessInfoProps) => {
{intl.formatMessage({ id: 'models.table.button.apiAccessInfo' })}
</span>
}
width={600}
width={660}
destroyOnHidden
closable={true}
mask={{
@@ -164,10 +165,15 @@ const ApiAccessInfo = ({ open, data, onClose }: ApiAccessInfoProps) => {
<span className="label">
{intl.formatMessage({ id: 'models.table.apiAccessInfo.endpoint' })}
</span>
<span className="value">
<AutoTooltip ghost maxWidth={data.generic_proxy ? 400 : 240}>
{endPoint}
</AutoTooltip>
<Flex align="center" wrap>
<span
className="value"
style={{ display: 'flex', flex: 1, minWidth: 0 }}
>
<AutoTooltip ghost maxWidth={'100%'}>
{endPoint}
</AutoTooltip>
</span>
{!data.generic_proxy && (
<>
<APITAG color="geekblue">
@@ -188,7 +194,7 @@ const ApiAccessInfo = ({ open, data, onClose }: ApiAccessInfoProps) => {
)}
</>
)}
</span>
</Flex>
<span className="copy-btn">
<CopyButton text={endPoint} type="link" size="small"></CopyButton>
</span>
@@ -197,32 +203,36 @@ const ApiAccessInfo = ({ open, data, onClose }: ApiAccessInfoProps) => {
id: 'models.table.apiAccessInfo.modelName'
})}
</span>
<span className="value">
<AutoTooltip ghost maxWidth={300}>
{data.name}
</AutoTooltip>
</span>
<Flex align="center">
<span className="value">
<AutoTooltip ghost maxWidth={300}>
{data.name}
</AutoTooltip>
</span>
</Flex>
<span className="copy-btn">
<CopyButton text={data.name} type="link" size="small"></CopyButton>
</span>
<span className="label">
{intl.formatMessage({ id: 'models.table.apiAccessInfo.apikey' })}
</span>
<span className="value">
<CreateButton
type="link"
size="small"
onClick={() => navigate('/access-control/api-keys')}
>
{intl.formatMessage({
id: 'models.table.apiAccessInfo.gotoCreate'
})}
<IconFont
type="icon-external-link"
className="font-size-14"
></IconFont>
</CreateButton>
</span>
<Flex align="center">
<span className="value">
<CreateButton
type="link"
size="small"
onClick={() => navigate('/access-control/api-keys')}
>
{intl.formatMessage({
id: 'models.table.apiAccessInfo.gotoCreate'
})}
<IconFont
type="icon-external-link"
className="font-size-14"
></IconFont>
</CreateButton>
</span>
</Flex>
</ApiAccessInfoWrapper>
{data.generic_proxy && (
<div style={{ marginTop: 12 }}>{GenericProxyCommandCode}</div>
@@ -2,14 +2,13 @@ import { getRequestId } from '@/atoms/models';
import { PageActionType } from '@/config/types';
import useDeferredRequest from '@/hooks/use-deferred-request';
import { ClusterStatusValueMap } from '@/pages/cluster-management/config';
import { GSDrawer, ModalFooter } from '@gpustack/core-ui';
import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { Button } from 'antd';
import _ from 'lodash';
import { FC, useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
import ColumnWrapper from '../../../_components/column-wrapper';
import {
defaultFormValues,
DeployFormKeyMap,
@@ -1,10 +1,9 @@
import { ProviderValueMap } from '@/pages/cluster-management/config';
import { GSDrawer, ModalFooter } from '@gpustack/core-ui';
import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { debounce } from 'lodash';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import ColumnWrapper from '../../../_components/column-wrapper';
import { modelSourceMap } from '../../config';
import { FormData } from '../../config/types';
import HFModelFile from '../model-source/hf-model-file';
+3 -3
View File
@@ -1,7 +1,6 @@
import { convertFileSize } from '@/utils';
import { AutoTooltip } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import _ from 'lodash';
import React from 'react';
import styled from 'styled-components';
import '../style/gpu-card.less';
@@ -24,6 +23,7 @@ const ItemInfo = styled.div`
display: flex;
flex-wrap: wrap;
white-space: break-spaces;
font-size: 13px;
`;
const Description = styled.div`
@@ -76,7 +76,7 @@ const GPUCard: React.FC<{
/ {convertFileSize(data?.memory?.total || 0)}
</span>
</ItemInfo>
<ItemInfo>
{/* <ItemInfo>
<span>
{intl.formatMessage({ id: 'resources.table.gpuutilization' })}
:{' '}
@@ -85,7 +85,7 @@ const GPUCard: React.FC<{
? _.round(data?.memory?.utilization_rate || 0, 2)
: _.round(data.memory?.allocated / data.memory?.total, 2) * 100}
%
</ItemInfo>
</ItemInfo> */}
</>
)
}
@@ -31,4 +31,24 @@ const FileSkeleton: React.FC<{ counts: number; itemHeight?: number }> = ({
);
};
export const FileSkeletonRows: React.FC<{
counts: number;
itemHeight?: number;
}> = ({ counts = 2, itemHeight }) => {
return (
<Wrapper orientation="vertical" style={{ height: itemHeight || 'auto' }}>
<Skeleton paragraph={{ rows: 1, width: '100%' }} title={false}></Skeleton>
<Flex orientation="vertical" gap={12}>
<Skeleton
paragraph={{
rows: counts,
width: Array.from({ length: counts }, () => '60%')
}}
title={false}
></Skeleton>
</Flex>
</Wrapper>
);
};
export default FileSkeleton;
@@ -55,6 +55,12 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
);
}, [countOptions, showPrevious]);
const hasPreviousContainer = useMemo(() => {
return countOptions?.some((option) =>
option.children?.some((child) => child.previous)
);
}, [countOptions]);
const handleCancel = useCallback(() => {
logsViewerRef.current?.abort();
onCancel();
@@ -114,7 +120,7 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
watch: !option.previous,
previous: option.previous,
worker_id: option.worker_id,
container: option.container
container_name: option.container
});
}
setSelectedContainer(option?.parentValue || null);
@@ -186,15 +192,15 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
{intl.formatMessage({ id: 'common.button.viewlog' })}
</span>
<span className="flex-center gap-8" style={{ height: 32 }}>
{showCascader && (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 16,
marginRight: 8
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 16,
marginRight: 8
}}
>
{hasPreviousContainer && (
<Checkbox onChange={handleOnChecked} checked={showPrevious}>
<Tooltip
title={
@@ -219,6 +225,9 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
<QuestionCircleOutlined style={{ marginLeft: 4 }} />
</Tooltip>
</Checkbox>
)}
{showCascader && (
<BaseSelect
prefix={
<span
@@ -240,9 +249,8 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
onChange={handleContainerChange}
style={{ width: 360 }}
></BaseSelect>
</div>
)}
)}
</div>
<Button
type="text"
color="default"
+1 -1
View File
@@ -455,7 +455,7 @@ export const getBackendParamsTips = (backend: string) => {
return {
backend: 'SGLang',
releases: '',
link: 'https://docs.sglang.ai/advanced_features/server_arguments.html',
link: 'https://docs.sglang.io/docs/advanced_features/server_arguments',
version: 'v0.5.4'
};
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+16 -1
View File
@@ -278,6 +278,21 @@ const Models = forwardRef((props, ref) => {
fetchData({
loadingVal: false
});
// re-align the instance cache with the backend as a backstop, in case a
// DELETE watch event for terminated instances was missed
getAllModelInstances();
});
// proactively drop a model's instances from the cache when it is stopped, so
// a stale (terminating) instance can't linger and show up alongside the new
// one after an immediate restart
const handleStop = useMemoizedFn(async (modelIds: number[]) => {
const idSet = new Set(modelIds);
cacheInsDataListRef.current = cacheInsDataListRef.current.filter(
(item) => !idSet.has(item.model_id)
);
setModelInstances(cacheInsDataListRef.current);
handleSearchBySilent();
});
const handleSearch = useMemoizedFn(async (params?: any) => {
@@ -456,7 +471,7 @@ const Models = forwardRef((props, ref) => {
handleOnToggleExpandAll={createModelsInstanceChunkRequest}
onViewLogs={cancelRequestsOnPageInactive}
onCancelViewLogs={handleOnCancelViewLogs}
onStop={handleSearchBySilent}
onStop={handleStop}
onStart={handleSearchBySilent}
onTableSort={handleOnSortChange}
onFilterChange={handleOnFilterChange}
+8 -1
View File
@@ -175,7 +175,7 @@ const ScheduleTypeForm: React.FC = () => {
<SealCascader
required
showSearch
expandTrigger="hover"
expandTrigger="click"
multiple={
form.getFieldValue('backend') !== backendOptionsMap.voxBox
}
@@ -184,6 +184,13 @@ const ScheduleTypeForm: React.FC = () => {
root: 'cascader-popup-wrapper gpu-selector'
}
}}
styles={{
popup: {
listItem: {
maxWidth: 'unset'
}
}
}}
maxTagCount={1}
label={intl.formatMessage({ id: 'models.form.gpuselector' })}
options={gpuOptions}
+7 -1
View File
@@ -145,6 +145,12 @@ const UserModels: React.FC = () => {
return result;
}, [dataSource.dataList]);
const handleRefresh = () => {
fetchData({
query: { ...queryParams, page: 1 }
});
};
return (
<PageBox>
<PageTools
@@ -192,7 +198,7 @@ const UserModels: React.FC = () => {
type="text"
style={{ color: 'var(--ant-color-text-tertiary)' }}
icon={<SyncOutlined></SyncOutlined>}
onClick={handleSearch}
onClick={handleRefresh}
></Button>
</Space>
}
+61 -53
View File
@@ -29,21 +29,12 @@ const useStyles = createStyles(({ token, css }) => {
}
}
`,
password: css`
.ant-input-suffix {
display: flex;
flex-direction: row-reverse;
.reset-link {
padding: 0;
font-size: 12px;
color: ${token.colorTextSecondary};
}
.ant-divider {
margin-left: 2px;
}
.reset-link:hover {
color: var(--ant-color-link-hover);
}
passwordWrapper: css`
position: relative;
.forgot-password {
position: absolute;
right: 0;
top: 74px;
}
`
};
@@ -97,6 +88,12 @@ const LocalUserForm: React.FC<LocalUserFormProps> = (props) => {
// enterprise cover layout (272px inner).
layout="vertical"
requiredMark={false}
styles={{
label: {
width: '100%',
lineHeight: '24px'
}
}}
style={{ width: '360px', maxWidth: '100%', margin: '0 auto' }}
onFinish={handleLogin}
>
@@ -122,54 +119,65 @@ const LocalUserForm: React.FC<LocalUserFormProps> = (props) => {
autoComplete="username"
prefix={<UserOutlined />}
style={{ height: 44 }}
placeholder={intl.formatMessage({
id: 'common.login.username.holder'
})}
/>
</Form.Item>
<Form.Item
style={{
marginBottom: 20
}}
name="password"
label={intl.formatMessage({ id: 'common.form.password' })}
rules={[
{
required: true,
message: intl.formatMessage(
{ id: 'common.form.rule.input' },
{
name: intl.formatMessage({ id: 'common.form.password' })
}
)
}
]}
>
<Input.Password
className={styles.password}
autoComplete="current-password"
prefix={<LockOutlined />}
style={{ height: 44 }}
suffix={
<Flex align="center" gap={0}>
<Button
type="link"
size="small"
className="reset-link"
href={externalLinks.resetPassword}
target="_blank"
>
{intl.formatMessage({ id: 'common.button.forgotpassword' })}
</Button>
<Divider orientation="vertical" />
<div className={`${styles.passwordWrapper} password-wrapper`}>
<Form.Item
style={{
marginBottom: 20
}}
name="password"
label={
<Flex
align="center"
justify="space-between"
style={{ width: '100%' }}
>
<span>{intl.formatMessage({ id: 'common.form.password' })}</span>
</Flex>
}
/>
</Form.Item>
rules={[
{
required: true,
message: intl.formatMessage(
{ id: 'common.form.rule.input' },
{
name: intl.formatMessage({ id: 'common.form.password' })
}
)
}
]}
>
<Input.Password
autoComplete="current-password"
prefix={<LockOutlined />}
style={{ height: 44 }}
placeholder={intl.formatMessage({
id: 'common.login.password.holder'
})}
/>
</Form.Item>
<Button
type="link"
size="small"
className="forgot-password"
style={{ fontSize: 12, padding: 0 }}
href={externalLinks.resetPassword}
target="_blank"
>
{intl.formatMessage({ id: 'common.button.forgotpassword' })}
</Button>
</div>
<Button
htmlType="submit"
type="primary"
block
loading={props.loading}
icon={<IconFont type="icon-login" />}
style={{ height: '44px', fontSize: '14px', marginTop: 16 }}
style={{ height: '44px', fontSize: '14px', marginTop: 20 }}
>
{intl.formatMessage({ id: 'common.button.login' })}
</Button>
+1 -1
View File
@@ -139,7 +139,7 @@ const PasswordForm: React.FC = () => {
htmlType="submit"
type="primary"
block
style={{ height: '44px', fontSize: '14px', marginTop: 16 }}
style={{ height: '44px', fontSize: '14px', marginTop: 8 }}
>
{intl.formatMessage({ id: 'common.button.submit' })}
</Button>
+4
View File
@@ -74,6 +74,10 @@ const Box = styled.div`
min-height: 100vh;
.ant-form-item-label {
line-height: 1;
padding-bottom: 4px;
}
.ant-form-item-label > label:after {
display: none;
}
.ant-form-item-label > label {
height: auto !important;
@@ -3,6 +3,7 @@ import ai360 from '@/assets/providers-logo/ai360.svg';
import azure from '@/assets/providers-logo/azureai.svg';
import baichuan from '@/assets/providers-logo/baichuan.svg';
import baiduCloud from '@/assets/providers-logo/baiducloud.svg';
import qwen from '@/assets/providers-logo/bailian.svg';
import bedrock from '@/assets/providers-logo/bedrock.svg';
import claude from '@/assets/providers-logo/claude.svg';
import cloudflare from '@/assets/providers-logo/cloudflare.svg';
@@ -26,7 +27,6 @@ import triton from '@/assets/providers-logo/nvidia.svg';
import ollama from '@/assets/providers-logo/ollama.svg';
import openai from '@/assets/providers-logo/openai.svg';
import openrouter from '@/assets/providers-logo/openrouter.svg';
import qwen from '@/assets/providers-logo/qwen.svg';
import spark from '@/assets/providers-logo/spark.svg';
import stepfun from '@/assets/providers-logo/stepfun.svg';
import togetherai from '@/assets/providers-logo/together.svg';
@@ -70,12 +70,17 @@ export const ProviderI18nKeyMap: Record<ProviderEnum, string> = {
[ProviderEnum.ZHIPUAI]: 'ai.provider.zhipuai'
};
export const ProviderDescriptionMap: Record<ProviderEnum, string> = {
[ProviderEnum.QWEN]: 'qwen, bailian, ali,alicloud,百炼, 阿里, 阿里云百炼'
} as Record<ProviderEnum, string>;
// generate provider list: {label: string;value:string}[]
export const maasProviderOptions = Object.entries(ProviderEnum).map(
([key, value]) => ({
locale: true,
label: ProviderI18nKeyMap[value],
value: value,
description: ProviderDescriptionMap[value] || '',
key: value
})
);
+21 -2
View File
@@ -12,7 +12,9 @@ import ProviderLogo from '../components/provider-logo';
import { useFormContext } from '../config/form-context';
import { maasProviderOptions } from '../config/providers';
import { FormData } from '../config/types';
import providerTypeStyles from '../styles/provider-type.less';
import ProviderConfigs from './provider-configs';
const Basic: React.FC<{
onAPIKeyBlur?: (e: any) => void;
}> = ({ onAPIKeyBlur }) => {
@@ -32,12 +34,28 @@ const Basic: React.FC<{
};
const filterOption = (input: string, option?: any) => {
console.log('filterOption input, option: ', input, option);
return (
option?.label?.toLowerCase().includes(input.toLowerCase()) ||
option?.value?.toLowerCase().includes(input.toLowerCase())
option?.value?.toLowerCase().includes(input.toLowerCase()) ||
option?.description?.toLowerCase().includes(input.toLowerCase())
);
};
const renderLogoPrefix = () => {
const hasProvider = maasProviderOptions.some(
(option) => option.value === providerType
);
if (hasProvider) {
return (
<div style={{ display: 'flex', alignItems: 'end', height: '100%' }}>
<ProviderLogo provider={providerType as string} />
</div>
);
}
return null;
};
return (
<>
<Form.Item<FormData>
@@ -73,9 +91,10 @@ const Basic: React.FC<{
filterOption: filterOption
}}
required
className={providerTypeStyles.providerType}
prefix={renderLogoPrefix()}
options={maasProviderOptions}
optionRender={optionRender}
labelRender={optionRender}
label={intl.formatMessage({
id: 'common.table.type'
})}
@@ -0,0 +1,11 @@
.providerType {
:global {
.ant-select-prefix {
height: 100%;
display: flex;
}
}
:global(input.ant-select-input) {
padding-top: 6px !important;
}
}
@@ -1,13 +1,20 @@
import { getCurrentOrg, getOrgById } from '@/atoms/user';
import { getGPUStackPlugin } from '@/plugins';
import { useNavigate } from '@umijs/max';
import { modelCategoriesMap } from '../../llmodels/config';
import { categoryToPathMap } from '../../llmodels/config/button-actions';
const useOpenPlayground = () => {
const navigate = useNavigate();
const plugin = getGPUStackPlugin();
const generateModelName = (row: any) => {
// if has plugin, it means is enterpise edition, we need to generate the model name with org name prefix, otherwise, we just use the model name
if (!plugin) {
return row.name;
}
const org = getOrgById(row.owner_principal_id) ?? getCurrentOrg();
// The platform Org is always named ``default`` (backend constant
// ``PLATFORM_PRINCIPAL_NAME``); its models are reported by
// ``/v1/models`` without a prefix. Match by name too, not just the
@@ -27,6 +34,7 @@ const useOpenPlayground = () => {
// ``owner_principal_id`` (an org principal id); fall back to the Org the
// caller is currently acting under for the admin "All" view, where the
// row's owner still resolves via the platform-wide org cache.
const rawModel = generateModelName(row);
const modelName = encodeURIComponent(rawModel);
+1 -1
View File
@@ -59,7 +59,7 @@ const Organizations: React.FC = () => {
</li>
))}
</ul>
<div style={{ textAlign: 'center' }}>
<div style={{ textAlign: 'center', marginTop: 56 }}>
<Button
type="primary"
href="https://gpustack.ai/enterprise"
+2 -2
View File
@@ -392,7 +392,7 @@ export const CustomSizeConfig: ParamsSchema[] = [
max: 1024,
step: 64,
labelWidth: 314,
inputnumber: false
inputnumber: true
},
rules: [
{
@@ -413,7 +413,7 @@ export const CustomSizeConfig: ParamsSchema[] = [
max: 1024,
step: 64,
labelWidth: 314,
inputnumber: false
inputnumber: true
},
rules: [
{
+26 -14
View File
@@ -75,10 +75,32 @@ export const useInitImageMeta = (
...advancedFieldsDefaultValus
});
const ensureSizeOption = (
options: SizeOption[],
width?: number,
height?: number
) => {
if (
width &&
height &&
!options.find((item) => item.width === width && item.height === height)
) {
options.push({
width,
height,
label: `${width}x${height}`,
value: `${width}x${height}`
});
}
};
const getNewImageSizeOptions = (metaData: any) => {
const { max_height, max_width } = metaData || {};
const { max_height, max_width, default_width, default_height } =
metaData || {};
if (!max_height || !max_width) {
return imageSizeList;
const newImageSizeOptions = [...imageSizeList];
ensureSizeOption(newImageSizeOptions, default_width, default_height);
return newImageSizeOptions;
}
const newImageSizeOptions = imageSizeList.filter((item) => {
return (
@@ -86,18 +108,8 @@ export const useInitImageMeta = (
item.value === 'custom'
);
});
if (
!newImageSizeOptions.find(
(item) => item.width === max_width && item.height === max_height
)
) {
newImageSizeOptions.push({
width: max_width,
height: max_height,
label: `${max_width}x${max_height}`,
value: `${max_width}x${max_height}`
});
}
ensureSizeOption(newImageSizeOptions, max_width, max_height);
ensureSizeOption(newImageSizeOptions, default_width, default_height);
return newImageSizeOptions;
};
+1 -4
View File
@@ -437,8 +437,5 @@ export const AddWorkerDockerNotes: Record<string, string[]> = {
[GPUDriverMap.ILUVATAR]: ['clusters.addworker.corexNotes'],
[GPUDriverMap.CAMBRICON]: ['clusters.addworker.cambriconNotes'],
[GPUDriverMap.METAX]: ['clusters.addworker.metaxNotes'],
[GPUDriverMap.THEAD]: [
'clusters.addworker.theadNotes',
'clusters.addworker.theadNotes-02'
]
[GPUDriverMap.THEAD]: ['clusters.addworker.theadNotes-02']
};
@@ -264,11 +264,15 @@ const useWorkerColumns = ({
) {
return (
<span className={workerCss.ipWrapper}>
<span className="item">
<span className={workerCss.item}>
<span className="text-primary">{record.ip}</span>
<span className="label">{`(${intl.formatMessage({ id: 'clusters.table.ip.internal' })})`}</span>
<span
className={workerCss.label}
>{`(${intl.formatMessage({ id: 'clusters.table.ip.internal' })})`}</span>
<span className="text-primary">{record.advertise_address}</span>
<span className="label">{`(${intl.formatMessage({ id: 'clusters.table.ip.external' })})`}</span>
<span
className={workerCss.label}
>{`(${intl.formatMessage({ id: 'clusters.table.ip.external' })})`}</span>
</span>
</span>
);
+47 -26
View File
@@ -366,32 +366,45 @@ function toServerRequest(data: ResourceBreakdownRequest) {
async function _breakdown(
url: string,
data: ResourceBreakdownRequest
data: ResourceBreakdownRequest,
options?: {
token?: any;
}
): Promise<ResourceBreakdownResponse> {
const { body, groupBy } = toServerRequest(data);
const res = await request<ServerBreakdownResponse>(url, {
data: body,
method: 'POST'
method: 'POST',
cancelToken: options?.token
});
return flattenResponse(groupBy, res);
}
export async function queryResourceBreakdown(
data: ResourceBreakdownRequest
data: ResourceBreakdownRequest,
options?: {
token?: any;
}
): Promise<ResourceBreakdownResponse> {
return _breakdown(URL.RESOURCE_BREAKDOWN, data);
return _breakdown(URL.RESOURCE_BREAKDOWN, data, options);
}
export async function queryGpuInstancesBreakdown(
data: ResourceBreakdownRequest
data: ResourceBreakdownRequest,
options?: {
token?: any;
}
): Promise<ResourceBreakdownResponse> {
return _breakdown(URL.GPU_BREAKDOWN, data);
return _breakdown(URL.GPU_BREAKDOWN, data, options);
}
export async function queryStorageBreakdown(
data: ResourceBreakdownRequest
data: ResourceBreakdownRequest,
options?: {
token?: any;
}
): Promise<ResourceBreakdownResponse> {
return _breakdown(URL.STORAGE_BREAKDOWN, data);
return _breakdown(URL.STORAGE_BREAKDOWN, data, options);
}
export async function queryResourceEvents(
@@ -406,7 +419,7 @@ export async function queryResourceEvents(
page?: number;
perPage?: number;
},
options?: { skipErrorHandler?: boolean }
options?: { skipErrorHandler?: boolean; token?: any }
): Promise<ResourceEventsResponse> {
const creatorIds = data.filters?.creator_ids;
return request<ResourceEventsResponse>(URL.EVENTS, {
@@ -426,7 +439,8 @@ export async function queryResourceEvents(
perPage: data.perPage ?? 50
},
method: 'GET',
skipErrorHandler: options?.skipErrorHandler
skipErrorHandler: options?.skipErrorHandler,
cancelToken: options?.token
});
}
@@ -456,12 +470,15 @@ export async function queryResourceFilterMeta(
};
}
export async function queryUsageSummary(params: {
start_date: string;
end_date: string;
scope?: 'self' | 'all';
creator_ids?: number[];
}): Promise<UsageSummaryResponse> {
export async function queryUsageSummary(
params: {
start_date: string;
end_date: string;
scope?: 'self' | 'all';
creator_ids?: number[];
},
options?: { token?: any }
): Promise<UsageSummaryResponse> {
const { creator_ids, ...rest } = params;
const res = await request<{
total_tokens: number;
@@ -478,7 +495,8 @@ export async function queryUsageSummary(params: {
scope: params.scope ?? 'all',
...(creator_ids?.length ? { creator_ids: creator_ids.join(',') } : {})
},
method: 'GET'
method: 'GET',
cancelToken: options?.token
});
// Resource Distribution donut — by GPU type, using GPU-Hours (a single,
@@ -486,15 +504,18 @@ export async function queryUsageSummary(params: {
// instance type. (A true cross-resource split needs a common unit.)
let distribution: SummaryResourceDistributionItem[] = [];
try {
const byType = await queryGpuInstancesBreakdown({
start_date: params.start_date,
end_date: params.end_date,
scope: params.scope ?? 'all',
group_by: ['gpu_type'],
...(creator_ids?.length ? { filters: { creator_ids } } : {}),
page: 1,
perPage: 100
});
const byType = await queryGpuInstancesBreakdown(
{
start_date: params.start_date,
end_date: params.end_date,
scope: params.scope ?? 'all',
group_by: ['gpu_type'],
...(creator_ids?.length ? { filters: { creator_ids } } : {}),
page: 1,
perPage: 100
},
{ token: options?.token }
);
const total = byType.items.reduce((s, i) => s + (i.gpu_hours || 0), 0);
distribution = byType.items
.filter((i) => (i.gpu_hours || 0) > 0)
@@ -40,6 +40,7 @@ interface MetricOption {
}
interface MetricChartCardProps {
loading?: boolean;
metric: string;
metricOptions: MetricOption[];
granularity: string;
@@ -55,6 +56,7 @@ interface MetricChartCardProps {
}
const MetricChartCard: React.FC<MetricChartCardProps> = ({
loading,
metric,
metricOptions,
granularity,
@@ -129,6 +131,7 @@ const MetricChartCard: React.FC<MetricChartCardProps> = ({
/>
</ControlsWrapper>
<BarChart
loading={loading}
seriesData={seriesData}
xAxisData={xAxisData}
height={280}

Some files were not shown because too many files have changed in this diff Show More