refactor: gpu service API alignment

This commit is contained in:
jialin
2026-05-25 22:42:02 +08:00
committed by jialin
parent 81c7e2ee61
commit 88ab927755
90 changed files with 3247 additions and 2023 deletions
+10 -1
View File
@@ -213,10 +213,19 @@ const baseRoutes = [
name: 'storage', name: 'storage',
path: '/gpu-service/storage', path: '/gpu-service/storage',
key: 'gpuServiceStorage', key: 'gpuServiceStorage',
icon: 'icon-database-outlined',
selectedIcon: 'icon-database-filled',
defaultIcon: 'icon-database-outlined',
component: './gpu-service/storage'
},
{
name: 'storageTypes',
path: '/gpu-service/storage-types',
key: 'gpuServiceStorageTypes',
icon: 'icon-storage-outlined', icon: 'icon-storage-outlined',
selectedIcon: 'icon-storage-filled', selectedIcon: 'icon-storage-filled',
defaultIcon: 'icon-storage-outlined', defaultIcon: 'icon-storage-outlined',
component: './gpu-service/storage' component: './gpu-service/storage-types'
}, },
{ {
name: 'publicKeys', name: 'publicKeys',
+1 -1
View File
@@ -17,7 +17,7 @@
"@ant-design/pro-components": "3.1.0-0", "@ant-design/pro-components": "3.1.0-0",
"@antv/g6": "^5.0.51", "@antv/g6": "^5.0.51",
"@braintree/sanitize-url": "^7.1.1", "@braintree/sanitize-url": "^7.1.1",
"@gpustack/core-ui": "^1.0.14", "@gpustack/core-ui": "^1.0.16",
"@huggingface/gguf": "^0.1.7", "@huggingface/gguf": "^0.1.7",
"@huggingface/hub": "^0.15.1", "@huggingface/hub": "^0.15.1",
"@huggingface/tasks": "^0.11.6", "@huggingface/tasks": "^0.11.6",
+79 -38
View File
@@ -24,8 +24,8 @@ importers:
specifier: ^7.1.1 specifier: ^7.1.1
version: 7.1.2 version: 7.1.2
'@gpustack/core-ui': '@gpustack/core-ui':
specifier: ^1.0.14 specifier: ^1.0.16
version: 1.0.14(czdvzceysqw7iv6pct2ucnb23e) version: 1.0.16(czdvzceysqw7iv6pct2ucnb23e)
'@huggingface/gguf': '@huggingface/gguf':
specifier: ^0.1.7 specifier: ^0.1.7
version: 0.1.18 version: 0.1.18
@@ -49,7 +49,7 @@ importers:
version: 4.17.24 version: 4.17.24
'@umijs/max': '@umijs/max':
specifier: ^4.6.15 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.47.1)(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.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))
'@xterm/addon-fit': '@xterm/addon-fit':
specifier: ^0.10.0 specifier: ^0.10.0
version: 0.10.0(@xterm/xterm@5.5.0) version: 0.10.0(@xterm/xterm@5.5.0)
@@ -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) 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: umi-presets-pro:
specifier: ^2.0.3 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.47.1)(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.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)))
wavesurfer.js: wavesurfer.js:
specifier: ^7.8.8 specifier: ^7.8.8
version: 7.12.6 version: 7.12.6
@@ -1484,8 +1484,8 @@ packages:
resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz} 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 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.14': '@gpustack/core-ui@1.0.16':
resolution: {integrity: sha512-nmkwYt4YA815uzaXc/9KZ4dGe0bW8FkD9yFXL8xoFXPH5gjGg8DGGXDvWJ4UqG3IIPjY6whwYm61sMxgfK192Q==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.14.tgz} resolution: {integrity: sha512-wFKDv7X0FXRmAmZPt4WKYV0+aGHcx82/3EZSJkrut/49XQd5XdrqKaimGtlFWFCNEWqsouwzs4uqaC7JFqfOkQ==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.16.tgz}
peerDependencies: peerDependencies:
'@ant-design/icons': '>=6.0.0' '@ant-design/icons': '>=6.0.0'
'@ant-design/pro-components': 3.1.0-0 '@ant-design/pro-components': 3.1.0-0
@@ -2463,6 +2463,9 @@ packages:
'@types/node@25.6.2': '@types/node@25.6.2':
resolution: {integrity: sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==, tarball: https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz} resolution: {integrity: sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==, tarball: https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz}
'@types/node@25.9.1':
resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==, tarball: https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz}
'@types/normalize-package-data@2.4.4': '@types/normalize-package-data@2.4.4':
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==, tarball: https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz} resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==, tarball: https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz}
@@ -2492,6 +2495,9 @@ packages:
'@types/react@18.3.28': '@types/react@18.3.28':
resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==, tarball: https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz} resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==, tarball: https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz}
'@types/react@18.3.29':
resolution: {integrity: sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==, tarball: https://registry.npmjs.org/@types/react/-/react-18.3.29.tgz}
'@types/resolve@1.20.6': '@types/resolve@1.20.6':
resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==, tarball: https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz} resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==, tarball: https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz}
@@ -4402,6 +4408,10 @@ packages:
resolution: {integrity: sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ==, tarball: https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.2.tgz} resolution: {integrity: sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ==, tarball: https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.2.tgz}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
enhanced-resolve@5.22.0:
resolution: {integrity: sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==, tarball: https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz}
engines: {node: '>=10.13.0'}
enhanced-resolve@5.9.3: enhanced-resolve@5.9.3:
resolution: {integrity: sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==, tarball: https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz} resolution: {integrity: sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==, tarball: https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
@@ -8605,6 +8615,11 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
terser@5.48.0:
resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==, tarball: https://registry.npmjs.org/terser/-/terser-5.48.0.tgz}
engines: {node: '>=10'}
hasBin: true
test-exclude@6.0.0: test-exclude@6.0.0:
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==, tarball: https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz} resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==, tarball: https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -8818,6 +8833,9 @@ packages:
undici-types@7.19.2: undici-types@7.19.2:
resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==, tarball: https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz} resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==, tarball: https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz}
undici-types@7.24.6:
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==, tarball: https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz}
unfetch@5.0.0: unfetch@5.0.0:
resolution: {integrity: sha512-3xM2c89siXg0nHvlmYsQ2zkLASvVMBisZm5lF3gFDqfF2xonNStDJyMpvaOBe0a1Edxmqrf2E0HBdmy9QyZaeg==, tarball: https://registry.npmjs.org/unfetch/-/unfetch-5.0.0.tgz} resolution: {integrity: sha512-3xM2c89siXg0nHvlmYsQ2zkLASvVMBisZm5lF3gFDqfF2xonNStDJyMpvaOBe0a1Edxmqrf2E0HBdmy9QyZaeg==, tarball: https://registry.npmjs.org/unfetch/-/unfetch-5.0.0.tgz}
@@ -9050,8 +9068,8 @@ packages:
engines: {node: '>= 10.13.0'} engines: {node: '>= 10.13.0'}
hasBin: true hasBin: true
webpack-sources@3.4.1: webpack-sources@3.5.0:
resolution: {integrity: sha512-eACpxRN02yaawnt+uUNIF7Qje6A9zArxBbcAJjK1PK3S9Ycg5jIuJ8pW4q8EMnwNZCEGltcjkRx1QzOxOkKD8A==, tarball: https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.1.tgz} resolution: {integrity: sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==, tarball: https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
webpack@5.106.2: webpack@5.106.2:
@@ -10790,7 +10808,7 @@ snapshots:
'@formatjs/intl-utils@2.3.0': {} '@formatjs/intl-utils@2.3.0': {}
'@gpustack/core-ui@1.0.14(czdvzceysqw7iv6pct2ucnb23e)': '@gpustack/core-ui@1.0.16(czdvzceysqw7iv6pct2ucnb23e)':
dependencies: dependencies:
'@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@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) '@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)
@@ -11811,7 +11829,7 @@ snapshots:
'@types/history@5.0.0': '@types/history@5.0.0':
dependencies: dependencies:
history: 5.3.0 history: 4.10.1
'@types/hoist-non-react-statics@3.3.7(@types/react@18.3.28)': '@types/hoist-non-react-statics@3.3.7(@types/react@18.3.28)':
dependencies: dependencies:
@@ -11865,6 +11883,10 @@ snapshots:
dependencies: dependencies:
undici-types: 7.19.2 undici-types: 7.19.2
'@types/node@25.9.1':
dependencies:
undici-types: 7.24.6
'@types/normalize-package-data@2.4.4': {} '@types/normalize-package-data@2.4.4': {}
'@types/parse-json@4.0.2': {} '@types/parse-json@4.0.2': {}
@@ -11885,26 +11907,31 @@ snapshots:
'@types/react-router-dom@4.3.5': '@types/react-router-dom@4.3.5':
dependencies: dependencies:
'@types/history': 5.0.0 '@types/history': 5.0.0
'@types/react': 18.3.28 '@types/react': 18.3.29
'@types/react-router': 5.1.20 '@types/react-router': 5.1.20
'@types/react-router-redux@5.0.27': '@types/react-router-redux@5.0.27':
dependencies: dependencies:
'@types/history': 4.7.11 '@types/history': 4.7.11
'@types/react': 18.3.28 '@types/react': 18.3.29
'@types/react-router': 5.1.20 '@types/react-router': 5.1.20
redux: 4.2.1 redux: 3.7.2
'@types/react-router@5.1.20': '@types/react-router@5.1.20':
dependencies: dependencies:
'@types/history': 4.7.11 '@types/history': 4.7.11
'@types/react': 18.3.28 '@types/react': 18.3.29
'@types/react@18.3.28': '@types/react@18.3.28':
dependencies: dependencies:
'@types/prop-types': 15.7.15 '@types/prop-types': 15.7.15
csstype: 3.2.3 csstype: 3.2.3
'@types/react@18.3.29':
dependencies:
'@types/prop-types': 15.7.15
csstype: 3.2.3
'@types/resolve@1.20.6': {} '@types/resolve@1.20.6': {}
'@types/semver@7.7.1': {} '@types/semver@7.7.1': {}
@@ -12287,18 +12314,18 @@ snapshots:
- webpack-hot-middleware - webpack-hot-middleware
- webpack-plugin-serve - webpack-plugin-serve
'@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.47.1)': '@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)':
dependencies: dependencies:
'@svgr/core': 6.5.1 '@svgr/core': 6.5.1
'@umijs/bundler-utils': 4.6.51 '@umijs/bundler-utils': 4.6.51
'@umijs/utils': 4.6.51 '@umijs/utils': 4.6.51
'@vitejs/plugin-react': 4.0.0(vite@4.5.2(@types/node@25.6.2)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.47.1)) '@vitejs/plugin-react': 4.0.0(vite@4.5.2(@types/node@25.6.2)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.48.0))
core-js: 3.34.0 core-js: 3.34.0
less: 4.1.3 less: 4.1.3
postcss-preset-env: 7.5.0(postcss@8.5.14) postcss-preset-env: 7.5.0(postcss@8.5.14)
rollup-plugin-visualizer: 5.9.0(rollup@3.30.0) rollup-plugin-visualizer: 5.9.0(rollup@3.30.0)
systemjs: 6.15.1 systemjs: 6.15.1
vite: 4.5.2(@types/node@25.6.2)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.47.1) vite: 4.5.2(@types/node@25.6.2)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.48.0)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node' - '@types/node'
- lightningcss - lightningcss
@@ -12526,14 +12553,14 @@ snapshots:
- supports-color - supports-color
- typescript - 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.47.1)(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.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))':
dependencies: dependencies:
'@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3) '@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.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)
antd: 4.24.16(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 eslint: 8.35.0
stylelint: 14.8.2 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.47.1)(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.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))
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
- '@rspack/core' - '@rspack/core'
@@ -12750,7 +12777,7 @@ snapshots:
- react-native - react-native
- supports-color - 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.47.1)(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.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))':
dependencies: dependencies:
'@iconify/utils': 2.1.1 '@iconify/utils': 2.1.1
'@stagewise/toolbar': 0.6.2 '@stagewise/toolbar': 0.6.2
@@ -12761,7 +12788,7 @@ snapshots:
'@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-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-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.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-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.47.1) '@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/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/core': 4.6.51
'@umijs/did-you-know': 1.0.4 '@umijs/did-you-know': 1.0.4
@@ -12843,13 +12870,13 @@ snapshots:
react-helmet-async: 1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) 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) 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.47.1)(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.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)))':
dependencies: dependencies:
chokidar: 3.6.0 chokidar: 3.6.0
express: 4.22.1 express: 4.22.1
lodash: 4.18.1 lodash: 4.18.1
prettier: 2.8.8 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.47.1)(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.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))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -13027,13 +13054,13 @@ snapshots:
- supports-color - supports-color
- utf-8-validate - utf-8-validate
'@vitejs/plugin-react@4.0.0(vite@4.5.2(@types/node@25.6.2)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.47.1))': '@vitejs/plugin-react@4.0.0(vite@4.5.2(@types/node@25.6.2)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.48.0))':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.29.0
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0)
react-refresh: 0.14.2 react-refresh: 0.14.2
vite: 4.5.2(@types/node@25.6.2)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.47.1) vite: 4.5.2(@types/node@25.6.2)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.48.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -14645,6 +14672,11 @@ snapshots:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
tapable: 2.3.3 tapable: 2.3.3
enhanced-resolve@5.22.0:
dependencies:
graceful-fs: 4.2.11
tapable: 2.3.3
enhanced-resolve@5.9.3: enhanced-resolve@5.9.3:
dependencies: dependencies:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
@@ -16358,7 +16390,7 @@ snapshots:
jest-worker@27.5.1: jest-worker@27.5.1:
dependencies: dependencies:
'@types/node': 25.6.2 '@types/node': 25.9.1
merge-stream: 2.0.0 merge-stream: 2.0.0
supports-color: 8.1.1 supports-color: 8.1.1
@@ -19928,7 +19960,7 @@ snapshots:
'@jridgewell/trace-mapping': 0.3.31 '@jridgewell/trace-mapping': 0.3.31
jest-worker: 27.5.1 jest-worker: 27.5.1
schema-utils: 4.3.3 schema-utils: 4.3.3
terser: 5.47.1 terser: 5.48.0
webpack: 5.106.2(lightningcss@1.22.1)(postcss@8.5.14) webpack: 5.106.2(lightningcss@1.22.1)(postcss@8.5.14)
optionalDependencies: optionalDependencies:
lightningcss: 1.22.1 lightningcss: 1.22.1
@@ -19941,6 +19973,13 @@ snapshots:
commander: 2.20.3 commander: 2.20.3
source-map-support: 0.5.21 source-map-support: 0.5.21
terser@5.48.0:
dependencies:
'@jridgewell/source-map': 0.3.11
acorn: 8.16.0
commander: 2.20.3
source-map-support: 0.5.21
test-exclude@6.0.0: test-exclude@6.0.0:
dependencies: dependencies:
'@istanbuljs/schema': 0.1.6 '@istanbuljs/schema': 0.1.6
@@ -20125,12 +20164,12 @@ snapshots:
ua-parser-js@0.7.41: {} 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.47.1)(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.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))):
dependencies: 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.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/max-plugin-openapi': 2.0.3(chokidar@3.6.0)(encoding@0.1.13)(typescript@5.9.3) '@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/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.47.1)(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.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)))
swagger-ui-dist: 4.19.1 swagger-ui-dist: 4.19.1
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
@@ -20154,14 +20193,14 @@ snapshots:
isomorphic-fetch: 2.2.1 isomorphic-fetch: 2.2.1
qs: 6.15.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.47.1)(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.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)):
dependencies: dependencies:
'@babel/runtime': 7.23.6 '@babel/runtime': 7.23.6
'@umijs/bundler-utils': 4.6.51 '@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/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/core': 4.6.51
'@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3) '@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.47.1)(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.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/renderer-react': 4.6.51(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@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/server': 4.6.51
'@umijs/test': 4.6.51(@babel/core@7.29.0) '@umijs/test': 4.6.51(@babel/core@7.29.0)
@@ -20208,14 +20247,14 @@ snapshots:
- webpack-hot-middleware - webpack-hot-middleware
- webpack-plugin-serve - 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.47.1)(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.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)):
dependencies: dependencies:
'@babel/runtime': 7.23.6 '@babel/runtime': 7.23.6
'@umijs/bundler-utils': 4.6.51 '@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/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/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/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.47.1)(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.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/renderer-react': 4.6.51(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@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/server': 4.6.51
'@umijs/test': 4.6.51(@babel/core@7.29.0) '@umijs/test': 4.6.51(@babel/core@7.29.0)
@@ -20273,6 +20312,8 @@ snapshots:
undici-types@7.19.2: {} undici-types@7.19.2: {}
undici-types@7.24.6: {}
unfetch@5.0.0: {} unfetch@5.0.0: {}
unified@11.0.5: unified@11.0.5:
@@ -20457,7 +20498,7 @@ snapshots:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
vfile-message: 4.0.3 vfile-message: 4.0.3
vite@4.5.2(@types/node@25.6.2)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.47.1): vite@4.5.2(@types/node@25.6.2)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.48.0):
dependencies: dependencies:
esbuild: 0.18.20 esbuild: 0.18.20
postcss: 8.5.14 postcss: 8.5.14
@@ -20468,7 +20509,7 @@ snapshots:
less: 4.1.3 less: 4.1.3
lightningcss: 1.22.1 lightningcss: 1.22.1
sass: 1.54.0 sass: 1.54.0
terser: 5.47.1 terser: 5.48.0
vm-browserify@1.1.2: {} vm-browserify@1.1.2: {}
@@ -20530,7 +20571,7 @@ snapshots:
- bufferutil - bufferutil
- utf-8-validate - utf-8-validate
webpack-sources@3.4.1: {} webpack-sources@3.5.0: {}
webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14): webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14):
dependencies: dependencies:
@@ -20544,7 +20585,7 @@ snapshots:
acorn-import-phases: 1.0.4(acorn@8.16.0) acorn-import-phases: 1.0.4(acorn@8.16.0)
browserslist: 4.28.2 browserslist: 4.28.2
chrome-trace-event: 1.0.4 chrome-trace-event: 1.0.4
enhanced-resolve: 5.21.2 enhanced-resolve: 5.22.0
es-module-lexer: 2.1.0 es-module-lexer: 2.1.0
eslint-scope: 5.1.1 eslint-scope: 5.1.1
events: 3.3.0 events: 3.3.0
@@ -20557,7 +20598,7 @@ snapshots:
tapable: 2.3.3 tapable: 2.3.3
terser-webpack-plugin: 5.6.0(lightningcss@1.22.1)(postcss@8.5.14)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)) terser-webpack-plugin: 5.6.0(lightningcss@1.22.1)(postcss@8.5.14)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
watchpack: 2.5.1 watchpack: 2.5.1
webpack-sources: 3.4.1 webpack-sources: 3.5.0
transitivePeerDependencies: transitivePeerDependencies:
- '@minify-html/node' - '@minify-html/node'
- '@swc/core' - '@swc/core'
+2 -4
View File
@@ -66,9 +66,7 @@ const NO_CONTAINER_PAGES = [
'clusterCreate', 'clusterCreate',
'benchmarkDetail', 'benchmarkDetail',
'deployment', 'deployment',
'video', 'video'
'instances',
'storage'
]; ];
const CHECK_RESOURCE_PATH = [ const CHECK_RESOURCE_PATH = [
@@ -358,7 +356,7 @@ export default (props: any) => {
config={{ config={{
apiBaseUrl: GPUSTACK_API_BASE_URL, apiBaseUrl: GPUSTACK_API_BASE_URL,
theme: userSettings.theme, theme: userSettings.theme,
iconUrl: '//at.alicdn.com/t/c/font_4613488_13sztx8tdru.js', iconUrl: '//at.alicdn.com/t/c/font_4613488_jmdepcs90im.js',
isDarkTheme: userSettings.isDarkTheme, isDarkTheme: userSettings.isDarkTheme,
defaultColorPrimary: COLOR_PRIMARY defaultColorPrimary: COLOR_PRIMARY
}} }}
+2
View File
@@ -58,6 +58,7 @@ export default {
'common.table.createTime': 'Created', 'common.table.createTime': 'Created',
'common.table.updateTime': 'Updated', 'common.table.updateTime': 'Updated',
'common.table.description': 'Description', 'common.table.description': 'Description',
'common.table.displayName': 'Display Name',
'common.table.name': 'Name', 'common.table.name': 'Name',
'common.table.status': 'Status', 'common.table.status': 'Status',
'common.table.name.list': '{type} Name', 'common.table.name.list': '{type} Name',
@@ -283,5 +284,6 @@ export default {
'common.image.limit.width': 'Image width must be {width}.', 'common.image.limit.width': 'Image width must be {width}.',
'common.image.limit.height': 'Image height must be {height}.', 'common.image.limit.height': 'Image height must be {height}.',
'common.remaining': 'Remaining {count}', 'common.remaining': 'Remaining {count}',
'common.max': 'Maximum {count}',
'common.validate.group': 'Please complete the {group} configuration' 'common.validate.group': 'Please complete the {group} configuration'
}; };
+34
View File
@@ -39,6 +39,35 @@ export default {
'gpuservice.template.card.mount': 'Mount', 'gpuservice.template.card.mount': 'Mount',
'gpuservice.template.card.resources': 'Resources', 'gpuservice.template.card.resources': 'Resources',
'gpuservice.template.card.ports': 'Ports', 'gpuservice.template.card.ports': 'Ports',
'gpuservice.storageType': 'Storage Type',
'gpuservice.storageType.add': 'Add Storage Type',
'gpuservice.storageType.edit': 'Edit Storage Type',
'gpuservice.storageType.filter.name': 'Search by name',
'gpuservice.storageType.kind': 'Type',
'gpuservice.storageType.mountOptions': 'Mount Options',
'gpuservice.storageType.nfs.server': 'NFS Server',
'gpuservice.storageType.nfs.share': 'Share Path',
'gpuservice.storageType.nfs.subDirectory': 'Sub Directory',
'gpuservice.storageType.nfs.subDirectory.tips':
'If empty, a subdirectory named after the persistent volume will be created. If set, a directory with the persistent volume name will be created beneath this subdirectory.',
'gpuservice.storageType.nfs.mountPermissions': 'Mount Permissions',
'gpuservice.storageType.nfs.mountPermissions.tips':
'Inherit the file permissions from the NFS server.',
'gpuservice.storageType.s3.endpoint': 'Endpoint',
'gpuservice.storageType.s3.endpoint.rule': 'Must start with http or https',
'gpuservice.storageType.s3.region': 'Region',
'gpuservice.storageType.s3.bucket': 'Bucket',
'gpuservice.storageType.s3.bucket.tips':
'If empty, a new bucket named after the persistent volume will be created. If set, a subdirectory with the persistent volume name will be created inside this bucket.',
'gpuservice.storageType.s3.accessKey': 'Access Key',
'gpuservice.storageType.s3.secretKey': 'Secret Key',
'gpuservice.storageType.s3.insecure': 'Skip TLS/SSL certificate verification',
'gpuservice.storageType.s3.insecure.tips':
'When enabled, the S3 server certificate is not validated. Use this for internal testing or self-signed certificates; enable with caution in production.',
'gpuservice.publicKey': 'SSH Public Key',
'gpuservice.publicKey.add': 'Add SSH Public Key',
'gpuservice.publicKey.edit': 'Edit SSH Public Key',
'gpuservice.publicKey.filter.name': 'Search by name',
'gpuservice.publicKey.label': 'SSH Public Key', 'gpuservice.publicKey.label': 'SSH Public Key',
'gpuservice.instance.ssh.enable': 'Enable SSH Access', 'gpuservice.instance.ssh.enable': 'Enable SSH Access',
'gpuservice.instance.ssh.assignKey': 'Assign SSH Public Key', 'gpuservice.instance.ssh.assignKey': 'Assign SSH Public Key',
@@ -108,6 +137,11 @@ export default {
'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.', 'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.',
'gpuservice.storage.persistentVolume.required': 'gpuservice.storage.persistentVolume.required':
'Please select a persistent volume', 'Please select a persistent volume',
'gpuservice.storage.persistentVolume.capacity': 'Capacity (GB)',
'gpuservice.storage.persistentVolume.capacity.required':
'Please enter capacity',
'gpuservice.storage.persistentVolume.releaseWithInstance':
'Release with instance',
'gpuservice.storage.tempCapacity': 'Capacity (GB)', 'gpuservice.storage.tempCapacity': 'Capacity (GB)',
'gpuservice.storage.tempCapacity.required': 'gpuservice.storage.tempCapacity.required':
'Please enter the temporary storage capacity', 'Please enter the temporary storage capacity',
+1
View File
@@ -45,5 +45,6 @@ export default {
'menu.gpuService.instances': 'GPU Instances', 'menu.gpuService.instances': 'GPU Instances',
'menu.gpuService.templates': 'Instance Templates', 'menu.gpuService.templates': 'Instance Templates',
'menu.gpuService.storage': 'Storage', 'menu.gpuService.storage': 'Storage',
'menu.gpuService.storageTypes': 'Storage Types',
'menu.gpuService.publicKeys': 'SSH Public Keys' 'menu.gpuService.publicKeys': 'SSH Public Keys'
}; };
+4
View File
@@ -65,6 +65,10 @@ export default {
'noresult.gpuservice.storage.title': 'No Storage', 'noresult.gpuservice.storage.title': 'No Storage',
'noresult.gpuservice.storage.subTitle': 'No storage has been added yet.', 'noresult.gpuservice.storage.subTitle': 'No storage has been added yet.',
'noresult.gpuservice.storage.nofound': 'No matching storage found.', 'noresult.gpuservice.storage.nofound': 'No matching storage found.',
'noresult.gpuservice.storageType.title': 'No Storage Types',
'noresult.gpuservice.storageType.subTitle':
'No storage types have been added yet.',
'noresult.gpuservice.storageType.nofound': 'No matching storage types found.',
'noresult.gpuservice.sshkey.title': 'No SSH Public Keys', 'noresult.gpuservice.sshkey.title': 'No SSH Public Keys',
'noresult.gpuservice.sshkey.subTitle': 'noresult.gpuservice.sshkey.subTitle':
'No SSH public keys have been added yet.', 'No SSH public keys have been added yet.',
+2
View File
@@ -58,6 +58,7 @@ export default {
'common.table.createTime': '作成日時', 'common.table.createTime': '作成日時',
'common.table.updateTime': '更新日時', 'common.table.updateTime': '更新日時',
'common.table.description': '説明', 'common.table.description': '説明',
'common.table.displayName': '表示名',
'common.table.name': '名前', 'common.table.name': '名前',
'common.table.status': 'ステータス', 'common.table.status': 'ステータス',
'common.table.name.list': '{type} 名称', 'common.table.name.list': '{type} 名称',
@@ -283,6 +284,7 @@ export default {
'common.image.limit.width': 'Image width must be {width}.', 'common.image.limit.width': 'Image width must be {width}.',
'common.image.limit.height': 'Image height must be {height}.', 'common.image.limit.height': 'Image height must be {height}.',
'common.remaining': '残り {count}', 'common.remaining': '残り {count}',
'common.max': 'Maximum {count}',
'common.validate.group': 'Please complete the {group} configuration' 'common.validate.group': 'Please complete the {group} configuration'
}; };
+35
View File
@@ -37,6 +37,36 @@ export default {
'gpuservice.template.card.mount': 'マウント', 'gpuservice.template.card.mount': 'マウント',
'gpuservice.template.card.resources': 'リソース', 'gpuservice.template.card.resources': 'リソース',
'gpuservice.template.card.ports': 'ポート', 'gpuservice.template.card.ports': 'ポート',
'gpuservice.storageType': 'ストレージタイプ',
'gpuservice.storageType.add': 'ストレージタイプを追加',
'gpuservice.storageType.edit': 'ストレージタイプを編集',
'gpuservice.storageType.filter.name': '名前で検索',
'gpuservice.storageType.kind': '種別',
'gpuservice.storageType.mountOptions': 'マウントオプション',
'gpuservice.storageType.nfs.server': 'NFS サーバー',
'gpuservice.storageType.nfs.share': '共有パス',
'gpuservice.storageType.nfs.subDirectory': 'サブディレクトリ',
'gpuservice.storageType.nfs.subDirectory.tips':
'空の場合、永続ボリューム名のサブディレクトリが作成されます。設定されている場合、このサブディレクトリ配下に永続ボリューム名のディレクトリが作成されます。',
'gpuservice.storageType.nfs.mountPermissions': 'マウント権限',
'gpuservice.storageType.nfs.mountPermissions.tips':
'NFS サーバー上のファイル権限を継承します。',
'gpuservice.storageType.s3.endpoint': 'エンドポイント',
'gpuservice.storageType.s3.endpoint.rule':
'http または https で始まる必要があります',
'gpuservice.storageType.s3.region': 'リージョン',
'gpuservice.storageType.s3.bucket': 'バケット',
'gpuservice.storageType.s3.bucket.tips':
'空の場合、永続ボリューム名で新しいバケットが作成されます。設定されている場合、このバケット配下に永続ボリューム名のサブディレクトリが作成されます。',
'gpuservice.storageType.s3.accessKey': 'アクセスキー',
'gpuservice.storageType.s3.secretKey': 'シークレットキー',
'gpuservice.storageType.s3.insecure': 'TLS/SSL 証明書の検証をスキップ',
'gpuservice.storageType.s3.insecure.tips':
'有効にすると S3 サーバーの証明書検証を無視します。社内テストや自己署名証明書の利用時に適しており、本番環境では慎重に有効化してください。',
'gpuservice.publicKey': 'SSH 公開鍵',
'gpuservice.publicKey.add': 'SSH 公開鍵を追加',
'gpuservice.publicKey.edit': 'SSH 公開鍵を編集',
'gpuservice.publicKey.filter.name': '名前で検索',
'gpuservice.publicKey.label': 'SSH 公開鍵', 'gpuservice.publicKey.label': 'SSH 公開鍵',
'gpuservice.instance.ssh.enable': 'SSH アクセスを有効化', 'gpuservice.instance.ssh.enable': 'SSH アクセスを有効化',
'gpuservice.instance.ssh.assignKey': 'SSH 公開鍵を割り当て', 'gpuservice.instance.ssh.assignKey': 'SSH 公開鍵を割り当て',
@@ -102,6 +132,11 @@ export default {
'gpuservice.storage.persistentVolume': '永続ボリューム', 'gpuservice.storage.persistentVolume': '永続ボリューム',
'gpuservice.storage.persistentVolume.required': 'gpuservice.storage.persistentVolume.required':
'永続ボリュームを選択してください', '永続ボリュームを選択してください',
'gpuservice.storage.persistentVolume.capacity': '容量 (GB)',
'gpuservice.storage.persistentVolume.capacity.required':
'容量を入力してください',
'gpuservice.storage.persistentVolume.releaseWithInstance':
'インスタンスと共に解放',
'gpuservice.storage.tempCapacity': '容量 (GB)', 'gpuservice.storage.tempCapacity': '容量 (GB)',
'gpuservice.storage.tempCapacity.required': 'gpuservice.storage.tempCapacity.required':
'一時ストレージ容量を入力してください', '一時ストレージ容量を入力してください',
+1
View File
@@ -45,6 +45,7 @@ export default {
'menu.gpuService.instances': 'GPU Instances', 'menu.gpuService.instances': 'GPU Instances',
'menu.gpuService.templates': 'Instance Templates', 'menu.gpuService.templates': 'Instance Templates',
'menu.gpuService.storage': 'Storage', 'menu.gpuService.storage': 'Storage',
'menu.gpuService.storageTypes': 'ストレージタイプ',
'menu.gpuService.publicKeys': 'SSH Public Keys' 'menu.gpuService.publicKeys': 'SSH Public Keys'
}; };
+5
View File
@@ -67,6 +67,11 @@ export default {
'noresult.gpuservice.storage.subTitle': 'noresult.gpuservice.storage.subTitle':
'ストレージはまだ追加されていません。', 'ストレージはまだ追加されていません。',
'noresult.gpuservice.storage.nofound': '一致するストレージが見つかりません。', 'noresult.gpuservice.storage.nofound': '一致するストレージが見つかりません。',
'noresult.gpuservice.storageType.title': 'ストレージタイプなし',
'noresult.gpuservice.storageType.subTitle':
'ストレージタイプはまだ追加されていません。',
'noresult.gpuservice.storageType.nofound':
'一致するストレージタイプが見つかりません。',
'noresult.gpuservice.sshkey.title': 'SSH 公開鍵なし', 'noresult.gpuservice.sshkey.title': 'SSH 公開鍵なし',
'noresult.gpuservice.sshkey.subTitle': 'SSH 公開鍵はまだ追加されていません。', 'noresult.gpuservice.sshkey.subTitle': 'SSH 公開鍵はまだ追加されていません。',
'noresult.gpuservice.sshkey.nofound': '一致する SSH 公開鍵が見つかりません。' 'noresult.gpuservice.sshkey.nofound': '一致する SSH 公開鍵が見つかりません。'
+2
View File
@@ -58,6 +58,7 @@ export default {
'common.table.createTime': 'Создано', 'common.table.createTime': 'Создано',
'common.table.updateTime': 'Обновлено', 'common.table.updateTime': 'Обновлено',
'common.table.description': 'Описание', 'common.table.description': 'Описание',
'common.table.displayName': 'Отображаемое имя',
'common.table.name': 'Название', 'common.table.name': 'Название',
'common.table.status': 'Статус', 'common.table.status': 'Статус',
'common.table.name.list': 'Название {type}', 'common.table.name.list': 'Название {type}',
@@ -282,6 +283,7 @@ export default {
'common.image.limit.width': 'Image width must be {width}.', 'common.image.limit.width': 'Image width must be {width}.',
'common.image.limit.height': 'Image height must be {height}.', 'common.image.limit.height': 'Image height must be {height}.',
'common.remaining': 'Остаток {count}', 'common.remaining': 'Остаток {count}',
'common.max': 'Maximum {count}',
'common.validate.group': 'Please complete the {group} configuration' 'common.validate.group': 'Please complete the {group} configuration'
}; };
+34
View File
@@ -40,6 +40,36 @@ export default {
'gpuservice.template.card.mount': 'Монтирование', 'gpuservice.template.card.mount': 'Монтирование',
'gpuservice.template.card.resources': 'Ресурсы', 'gpuservice.template.card.resources': 'Ресурсы',
'gpuservice.template.card.ports': 'Порты', 'gpuservice.template.card.ports': 'Порты',
'gpuservice.storageType': 'Тип хранилища',
'gpuservice.storageType.add': 'Добавить тип хранилища',
'gpuservice.storageType.edit': 'Изменить тип хранилища',
'gpuservice.storageType.filter.name': 'Поиск по имени',
'gpuservice.storageType.kind': 'Тип',
'gpuservice.storageType.mountOptions': 'Параметры монтирования',
'gpuservice.storageType.nfs.server': 'Сервер NFS',
'gpuservice.storageType.nfs.share': 'Путь общего ресурса',
'gpuservice.storageType.nfs.subDirectory': 'Подкаталог',
'gpuservice.storageType.nfs.subDirectory.tips':
'Если поле пустое, будет создан подкаталог с именем постоянного тома. Если задано, под этим подкаталогом будет создан каталог с именем постоянного тома.',
'gpuservice.storageType.nfs.mountPermissions': 'Права монтирования',
'gpuservice.storageType.nfs.mountPermissions.tips':
'Наследует права файлов с NFS-сервера.',
'gpuservice.storageType.s3.endpoint': 'Endpoint',
'gpuservice.storageType.s3.endpoint.rule':
'Должен начинаться с http или https',
'gpuservice.storageType.s3.region': 'Регион',
'gpuservice.storageType.s3.bucket': 'Бакет',
'gpuservice.storageType.s3.bucket.tips':
'Если поле пустое, будет создан новый бакет с именем постоянного тома. Если задано, в этом бакете будет создан подкаталог с именем постоянного тома.',
'gpuservice.storageType.s3.accessKey': 'Access Key',
'gpuservice.storageType.s3.secretKey': 'Secret Key',
'gpuservice.storageType.s3.insecure': 'Пропустить проверку сертификата TLS/SSL',
'gpuservice.storageType.s3.insecure.tips':
'Если включено, сертификат сервера S3 не проверяется. Подходит для внутреннего тестирования или самоподписанных сертификатов; в производственной среде включайте с осторожностью.',
'gpuservice.publicKey': 'Открытый ключ SSH',
'gpuservice.publicKey.add': 'Добавить открытый ключ SSH',
'gpuservice.publicKey.edit': 'Изменить открытый ключ SSH',
'gpuservice.publicKey.filter.name': 'Поиск по имени',
'gpuservice.publicKey.label': 'Открытый ключ SSH', 'gpuservice.publicKey.label': 'Открытый ключ SSH',
'gpuservice.instance.ssh.enable': 'Включить SSH-доступ', 'gpuservice.instance.ssh.enable': 'Включить SSH-доступ',
'gpuservice.instance.ssh.assignKey': 'Назначить открытый ключ SSH', 'gpuservice.instance.ssh.assignKey': 'Назначить открытый ключ SSH',
@@ -102,6 +132,10 @@ export default {
'gpuservice.storage.temporary': 'Временное', 'gpuservice.storage.temporary': 'Временное',
'gpuservice.storage.persistentVolume': 'Постоянный том', 'gpuservice.storage.persistentVolume': 'Постоянный том',
'gpuservice.storage.persistentVolume.required': 'Выберите постоянный том', 'gpuservice.storage.persistentVolume.required': 'Выберите постоянный том',
'gpuservice.storage.persistentVolume.capacity': 'Ёмкость (ГБ)',
'gpuservice.storage.persistentVolume.capacity.required': 'Введите ёмкость',
'gpuservice.storage.persistentVolume.releaseWithInstance':
'Освобождать вместе с экземпляром',
'gpuservice.storage.tempCapacity': 'Объём (ГБ)', 'gpuservice.storage.tempCapacity': 'Объём (ГБ)',
'gpuservice.storage.tempCapacity.required': 'gpuservice.storage.tempCapacity.required':
'Введите объём временного хранилища', 'Введите объём временного хранилища',
+1
View File
@@ -45,6 +45,7 @@ export default {
'menu.gpuService.instances': 'GPU Instances', 'menu.gpuService.instances': 'GPU Instances',
'menu.gpuService.templates': 'Instance Templates', 'menu.gpuService.templates': 'Instance Templates',
'menu.gpuService.storage': 'Storage', 'menu.gpuService.storage': 'Storage',
'menu.gpuService.storageTypes': 'Типы хранилищ',
'menu.gpuService.publicKeys': 'SSH Public Keys' 'menu.gpuService.publicKeys': 'SSH Public Keys'
}; };
+5
View File
@@ -66,6 +66,11 @@ export default {
'noresult.gpuservice.storage.title': 'Нет хранилищ', 'noresult.gpuservice.storage.title': 'Нет хранилищ',
'noresult.gpuservice.storage.subTitle': 'Хранилища ещё не добавлены.', 'noresult.gpuservice.storage.subTitle': 'Хранилища ещё не добавлены.',
'noresult.gpuservice.storage.nofound': 'Подходящие хранилища не найдены.', 'noresult.gpuservice.storage.nofound': 'Подходящие хранилища не найдены.',
'noresult.gpuservice.storageType.title': 'Нет типов хранилищ',
'noresult.gpuservice.storageType.subTitle':
'Типы хранилищ ещё не добавлены.',
'noresult.gpuservice.storageType.nofound':
'Подходящие типы хранилищ не найдены.',
'noresult.gpuservice.sshkey.title': 'Нет открытых ключей SSH', 'noresult.gpuservice.sshkey.title': 'Нет открытых ключей SSH',
'noresult.gpuservice.sshkey.subTitle': 'Открытые ключи SSH ещё не добавлены.', 'noresult.gpuservice.sshkey.subTitle': 'Открытые ключи SSH ещё не добавлены.',
'noresult.gpuservice.sshkey.nofound': 'noresult.gpuservice.sshkey.nofound':
+2
View File
@@ -58,6 +58,7 @@ export default {
'common.table.createTime': 'Oluşturulma', 'common.table.createTime': 'Oluşturulma',
'common.table.updateTime': 'Güncellenme', 'common.table.updateTime': 'Güncellenme',
'common.table.description': 'Açıklama', 'common.table.description': 'Açıklama',
'common.table.displayName': 'Görünen Ad',
'common.table.name': 'Ad', 'common.table.name': 'Ad',
'common.table.status': 'Durum', 'common.table.status': 'Durum',
'common.table.name.list': '{type} Adı', 'common.table.name.list': '{type} Adı',
@@ -286,5 +287,6 @@ export default {
'common.image.limit.width': 'Image width must be {width}.', 'common.image.limit.width': 'Image width must be {width}.',
'common.image.limit.height': 'Image height must be {height}.', 'common.image.limit.height': 'Image height must be {height}.',
'common.remaining': 'Kalan {count}', 'common.remaining': 'Kalan {count}',
'common.max': 'Maksimum {count}',
'common.validate.group': 'Please complete the {group} configuration' 'common.validate.group': 'Please complete the {group} configuration'
}; };
+35
View File
@@ -39,6 +39,36 @@ export default {
'gpuservice.template.card.mount': 'Bağlama', 'gpuservice.template.card.mount': 'Bağlama',
'gpuservice.template.card.resources': 'Kaynaklar', 'gpuservice.template.card.resources': 'Kaynaklar',
'gpuservice.template.card.ports': 'Bağlantı Noktaları', 'gpuservice.template.card.ports': 'Bağlantı Noktaları',
'gpuservice.storageType': 'Depolama Türü',
'gpuservice.storageType.add': 'Depolama Türü Ekle',
'gpuservice.storageType.edit': 'Depolama Türünü Düzenle',
'gpuservice.storageType.filter.name': 'Ada göre ara',
'gpuservice.storageType.kind': 'Tür',
'gpuservice.storageType.mountOptions': 'Bağlama Seçenekleri',
'gpuservice.storageType.nfs.server': 'NFS Sunucusu',
'gpuservice.storageType.nfs.share': 'Paylaşım Yolu',
'gpuservice.storageType.nfs.subDirectory': 'Alt Dizin',
'gpuservice.storageType.nfs.subDirectory.tips':
'Boş bırakılırsa kalıcı hacim adıyla bir alt dizin oluşturulur. Doldurulursa bu alt dizinin altında kalıcı hacim adıyla bir dizin oluşturulur.',
'gpuservice.storageType.nfs.mountPermissions': 'Bağlama İzinleri',
'gpuservice.storageType.nfs.mountPermissions.tips':
'NFS sunucusundaki dosya izinleri devralınır.',
'gpuservice.storageType.s3.endpoint': 'Endpoint',
'gpuservice.storageType.s3.endpoint.rule':
'http veya https ile başlamalıdır',
'gpuservice.storageType.s3.region': 'Bölge',
'gpuservice.storageType.s3.bucket': 'Kova',
'gpuservice.storageType.s3.bucket.tips':
'Boş bırakılırsa kalıcı hacim adıyla yeni bir kova oluşturulur. Doldurulursa bu kova içinde kalıcı hacim adıyla bir alt dizin oluşturulur.',
'gpuservice.storageType.s3.accessKey': 'Access Key',
'gpuservice.storageType.s3.secretKey': 'Secret Key',
'gpuservice.storageType.s3.insecure': 'TLS/SSL sertifika doğrulamasını atla',
'gpuservice.storageType.s3.insecure.tips':
'Etkinleştirildiğinde S3 sunucu sertifikası doğrulanmaz. İç ağ testleri veya kendinden imzalı sertifikalar için uygundur; üretim ortamında dikkatli kullanın.',
'gpuservice.publicKey': 'SSH Açık Anahtarı',
'gpuservice.publicKey.add': 'SSH Açık Anahtarı Ekle',
'gpuservice.publicKey.edit': 'SSH Açık Anahtarını Düzenle',
'gpuservice.publicKey.filter.name': 'Ada göre ara',
'gpuservice.publicKey.label': 'SSH Açık Anahtarı', 'gpuservice.publicKey.label': 'SSH Açık Anahtarı',
'gpuservice.instance.ssh.enable': 'SSH Erişimini Etkinleştir', 'gpuservice.instance.ssh.enable': 'SSH Erişimini Etkinleştir',
'gpuservice.instance.ssh.assignKey': 'SSH Açık Anahtarı Ata', 'gpuservice.instance.ssh.assignKey': 'SSH Açık Anahtarı Ata',
@@ -103,6 +133,11 @@ export default {
'gpuservice.storage.persistentVolume': 'Kalıcı Hacim', 'gpuservice.storage.persistentVolume': 'Kalıcı Hacim',
'gpuservice.storage.persistentVolume.required': 'gpuservice.storage.persistentVolume.required':
'Lütfen bir kalıcı hacim seçin', 'Lütfen bir kalıcı hacim seçin',
'gpuservice.storage.persistentVolume.capacity': 'Kapasite (GB)',
'gpuservice.storage.persistentVolume.capacity.required':
'Lütfen kapasiteyi girin',
'gpuservice.storage.persistentVolume.releaseWithInstance':
'Örnekle birlikte serbest bırak',
'gpuservice.storage.tempCapacity': 'Kapasite (GB)', 'gpuservice.storage.tempCapacity': 'Kapasite (GB)',
'gpuservice.storage.tempCapacity.required': 'gpuservice.storage.tempCapacity.required':
'Lütfen geçici depolama kapasitesini girin', 'Lütfen geçici depolama kapasitesini girin',
+1
View File
@@ -45,6 +45,7 @@ export default {
'menu.gpuService.instances': 'GPU Instances', 'menu.gpuService.instances': 'GPU Instances',
'menu.gpuService.templates': 'Instance Templates', 'menu.gpuService.templates': 'Instance Templates',
'menu.gpuService.storage': 'Storage', 'menu.gpuService.storage': 'Storage',
'menu.gpuService.storageTypes': 'Depolama Türleri',
'menu.gpuService.publicKeys': 'SSH Public Keys' 'menu.gpuService.publicKeys': 'SSH Public Keys'
}; };
+4
View File
@@ -64,6 +64,10 @@ export default {
'noresult.gpuservice.storage.title': 'Depolama Yok', 'noresult.gpuservice.storage.title': 'Depolama Yok',
'noresult.gpuservice.storage.subTitle': 'Henüz depolama eklenmedi.', 'noresult.gpuservice.storage.subTitle': 'Henüz depolama eklenmedi.',
'noresult.gpuservice.storage.nofound': 'Eşleşen depolama bulunamadı.', 'noresult.gpuservice.storage.nofound': 'Eşleşen depolama bulunamadı.',
'noresult.gpuservice.storageType.title': 'Depolama Türü Yok',
'noresult.gpuservice.storageType.subTitle': 'Henüz depolama türü eklenmedi.',
'noresult.gpuservice.storageType.nofound':
'Eşleşen depolama türü bulunamadı.',
'noresult.gpuservice.sshkey.title': 'SSH Açık Anahtarı Yok', 'noresult.gpuservice.sshkey.title': 'SSH Açık Anahtarı Yok',
'noresult.gpuservice.sshkey.subTitle': 'Henüz SSH açık anahtarı eklenmedi.', 'noresult.gpuservice.sshkey.subTitle': 'Henüz SSH açık anahtarı eklenmedi.',
'noresult.gpuservice.sshkey.nofound': 'Eşleşen SSH açık anahtarı bulunamadı.' 'noresult.gpuservice.sshkey.nofound': 'Eşleşen SSH açık anahtarı bulunamadı.'
+2
View File
@@ -56,6 +56,7 @@ export default {
'common.table.createTime': '创建时间', 'common.table.createTime': '创建时间',
'common.table.updateTime': '更新时间', 'common.table.updateTime': '更新时间',
'common.table.description': '描述', 'common.table.description': '描述',
'common.table.displayName': '显示名称',
'common.table.name': '名称', 'common.table.name': '名称',
'common.table.status': '状态', 'common.table.status': '状态',
'common.table.name.list': '{type}名称', 'common.table.name.list': '{type}名称',
@@ -276,5 +277,6 @@ export default {
'common.image.limit.width': '图片宽度须为{width}。', 'common.image.limit.width': '图片宽度须为{width}。',
'common.image.limit.height': '图片高度须为{height}。', 'common.image.limit.height': '图片高度须为{height}。',
'common.remaining': '剩余 {count}', 'common.remaining': '剩余 {count}',
'common.max': '最大 {count}',
'common.validate.group': '请填写完整的{group}配置' 'common.validate.group': '请填写完整的{group}配置'
}; };
+34 -2
View File
@@ -15,8 +15,8 @@ export default {
'gpuservice.template.mountPath': '挂载路径', 'gpuservice.template.mountPath': '挂载路径',
'gpuservice.template.containerDisk': '容器磁盘(GB)', 'gpuservice.template.containerDisk': '容器磁盘(GB)',
'gpuservice.template.memory': '内存(GB)', 'gpuservice.template.memory': '内存(GB)',
'gpuservice.instance.containerDisk.remaining': '容器磁盘(剩余{count}GB)', 'gpuservice.instance.containerDisk.remaining': '容器磁盘(最大{count}GB)',
'gpuservice.instance.memory.remaining': '内存(剩余{count}GB)', 'gpuservice.instance.memory.remaining': '内存(最大{count}GB)',
'gpuservice.template.displayName': '显示名称', 'gpuservice.template.displayName': '显示名称',
'gpuservice.template.displayName.max': '显示名称不能超过 63 个字符', 'gpuservice.template.displayName.max': '显示名称不能超过 63 个字符',
'gpuservice.template.ports': '端口', 'gpuservice.template.ports': '端口',
@@ -34,6 +34,35 @@ export default {
'gpuservice.template.card.mount': '挂载', 'gpuservice.template.card.mount': '挂载',
'gpuservice.template.card.resources': '资源', 'gpuservice.template.card.resources': '资源',
'gpuservice.template.card.ports': '端口', 'gpuservice.template.card.ports': '端口',
'gpuservice.storageType': '存储类型',
'gpuservice.storageType.add': '添加存储类型',
'gpuservice.storageType.edit': '编辑存储类型',
'gpuservice.storageType.filter.name': '按名称搜索',
'gpuservice.storageType.kind': '类型',
'gpuservice.storageType.mountOptions': '挂载参数',
'gpuservice.storageType.nfs.server': 'NFS 服务器',
'gpuservice.storageType.nfs.share': '共享路径',
'gpuservice.storageType.nfs.subDirectory': '子目录',
'gpuservice.storageType.nfs.subDirectory.tips':
'如果为空,会以持久卷的卷名创建一个子目录;如果不为空,会在同名子目录下,以持久卷的卷名创建一个孙目录。',
'gpuservice.storageType.nfs.mountPermissions': '挂载权限',
'gpuservice.storageType.nfs.mountPermissions.tips':
'沿用 NFS 服务器文件的权限。',
'gpuservice.storageType.s3.endpoint': 'Endpoint',
'gpuservice.storageType.s3.endpoint.rule': '必须以 http 或 https 开头',
'gpuservice.storageType.s3.region': '区域',
'gpuservice.storageType.s3.bucket': '存储桶',
'gpuservice.storageType.s3.bucket.tips':
'如果为空,会以持久卷的卷名创建一个新桶;如果不为空,会在同名桶下,以持久卷的卷名创建一个子目录。',
'gpuservice.storageType.s3.accessKey': 'Access Key',
'gpuservice.storageType.s3.secretKey': 'Secret Key',
'gpuservice.storageType.s3.insecure': '跳过 TLS/SSL 证书验证',
'gpuservice.storageType.s3.insecure.tips':
'开启后将忽略 S3 服务端的证书安全检查。适用于内网测试或使用自签名证书的场景,生产环境请谨慎勾选。',
'gpuservice.publicKey': 'SSH 公钥',
'gpuservice.publicKey.add': '添加 SSH 公钥',
'gpuservice.publicKey.edit': '编辑 SSH 公钥',
'gpuservice.publicKey.filter.name': '按名称搜索',
'gpuservice.publicKey.label': 'SSH 公钥', 'gpuservice.publicKey.label': 'SSH 公钥',
'gpuservice.instance.ssh.enable': '启用 SSH 访问', 'gpuservice.instance.ssh.enable': '启用 SSH 访问',
'gpuservice.instance.ssh.assignKey': '分配 SSH 公钥', 'gpuservice.instance.ssh.assignKey': '分配 SSH 公钥',
@@ -98,6 +127,9 @@ export default {
'gpuservice.storage.persistentVolume.tips': 'gpuservice.storage.persistentVolume.tips':
'数据在实例重启后仍会保留,仅在实例被终止时删除。无法被其它实例共享。', '数据在实例重启后仍会保留,仅在实例被终止时删除。无法被其它实例共享。',
'gpuservice.storage.persistentVolume.required': '请选择持久卷', 'gpuservice.storage.persistentVolume.required': '请选择持久卷',
'gpuservice.storage.persistentVolume.capacity': '容量(GB',
'gpuservice.storage.persistentVolume.capacity.required': '请输入容量',
'gpuservice.storage.persistentVolume.releaseWithInstance': '随实例释放',
'gpuservice.storage.tempCapacity': '容量(GB', 'gpuservice.storage.tempCapacity': '容量(GB',
'gpuservice.storage.tempCapacity.required': '请输入临时存储容量', 'gpuservice.storage.tempCapacity.required': '请输入临时存储容量',
'gpuservice.form.rule.name': 'gpuservice.form.rule.name':
+1
View File
@@ -45,5 +45,6 @@ export default {
'menu.gpuService.instances': 'GPU 实例', 'menu.gpuService.instances': 'GPU 实例',
'menu.gpuService.templates': '实例模板', 'menu.gpuService.templates': '实例模板',
'menu.gpuService.storage': '存储', 'menu.gpuService.storage': '存储',
'menu.gpuService.storageTypes': '存储类型',
'menu.gpuService.publicKeys': 'SSH 公钥' 'menu.gpuService.publicKeys': 'SSH 公钥'
}; };
+3
View File
@@ -60,6 +60,9 @@ export default {
'noresult.gpuservice.storage.title': '暂无存储', 'noresult.gpuservice.storage.title': '暂无存储',
'noresult.gpuservice.storage.subTitle': '尚未添加任何存储。', 'noresult.gpuservice.storage.subTitle': '尚未添加任何存储。',
'noresult.gpuservice.storage.nofound': '未找到匹配的存储', 'noresult.gpuservice.storage.nofound': '未找到匹配的存储',
'noresult.gpuservice.storageType.title': '暂无存储类型',
'noresult.gpuservice.storageType.subTitle': '尚未添加任何存储类型。',
'noresult.gpuservice.storageType.nofound': '未找到匹配的存储类型',
'noresult.gpuservice.sshkey.title': '暂无 SSH 公钥', 'noresult.gpuservice.sshkey.title': '暂无 SSH 公钥',
'noresult.gpuservice.sshkey.subTitle': '尚未添加任何 SSH 公钥。', 'noresult.gpuservice.sshkey.subTitle': '尚未添加任何 SSH 公钥。',
'noresult.gpuservice.sshkey.nofound': '未找到匹配的 SSH 公钥' 'noresult.gpuservice.sshkey.nofound': '未找到匹配的 SSH 公钥'
@@ -20,11 +20,10 @@ interface NumberSelectionProps {
input?: React.CSSProperties; input?: React.CSSProperties;
}; };
labelExtra?: React.ReactNode; labelExtra?: React.ReactNode;
maxCount?: number;
onChange?: (value: number) => void; onChange?: (value: number) => void;
} }
const PRESET_ITEMS = [1, 2, 4, 8];
const NumberSelection: React.FC<NumberSelectionProps> = ({ const NumberSelection: React.FC<NumberSelectionProps> = ({
id, id,
step = 1, step = 1,
@@ -36,21 +35,26 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
required, required,
labelExtra, labelExtra,
className, className,
maxCount = 8,
style, style,
onChange onChange
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const showCustomInput = max > 8;
const showCustomInput = max > maxCount;
const presetItems = Array.from(
{ length: Math.max(0, maxCount) },
(_, i) => i + 1
);
const computedItems = showCustomInput const computedItems = showCustomInput
? PRESET_ITEMS.filter((n) => n >= min && n <= max) ? presetItems.filter((n) => n >= min && n <= max)
: Array.from( : Array.from(
{ length: Math.max(0, Math.floor((max - min) / step) + 1) }, { length: Math.max(0, Math.floor((max - min) / step) + 1) },
(_, i) => min + i * step (_, i) => min + i * step
); );
const items = computedItems.length === 0 ? [0] : computedItems; const items = computedItems.length === 0 ? [0] : computedItems;
const [inputValue, setInputValue] = useState<number | null>(() => const [inputValue, setInputValue] = useState<number | null>(() =>
value !== undefined && value !== null && !PRESET_ITEMS.includes(value) value !== undefined && value !== null && !presetItems.includes(value)
? value ? value
: null : null
); );
@@ -58,7 +62,7 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
useEffect(() => { useEffect(() => {
if (value === undefined || value === null) { if (value === undefined || value === null) {
setInputValue(null); setInputValue(null);
} else if (PRESET_ITEMS.includes(value)) { } else if (presetItems.includes(value)) {
setInputValue(null); setInputValue(null);
} else { } else {
setInputValue(value); setInputValue(value);
@@ -83,7 +87,6 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
} }
onChange?.(inputValue); onChange?.(inputValue);
}; };
console.log('value+++', value, 'inputValue', items, inputValue);
return ( return (
<div <div
id={id} id={id}
@@ -137,7 +140,7 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
})} })}
> >
<InputNumber <InputNumber
controls controls={false}
variant="borderless" variant="borderless"
min={min} min={min}
max={max} max={max}
@@ -146,7 +149,7 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
value={inputValue || undefined} value={inputValue || undefined}
style={{ style={{
fontSize: 14, fontSize: 14,
width: 140, width: 60,
height: 32, height: 32,
backgroundColor: 'var(--ant-color-bg-container)', backgroundColor: 'var(--ant-color-bg-container)',
...style ...style
@@ -0,0 +1,32 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { Input as CInput } from '@gpustack/core-ui';
import { Form } from 'antd';
import type { NamePath } from 'antd/es/form/interface';
import { useAtomValue } from 'jotai';
import { useEffect } from 'react';
interface OwnerPrincipalIdFieldProps {
name?: NamePath;
}
const OwnerPrincipalIdField: React.FC<OwnerPrincipalIdFieldProps> = ({
name = 'owner_principal_id'
}) => {
const currentCluster = useAtomValue(currentClusterAtom);
const ownerPrincipalId = currentCluster?.owner_principal_id;
const form = Form.useFormInstance();
useEffect(() => {
if (ownerPrincipalId != null) {
form.setFieldValue(name, ownerPrincipalId);
}
}, [ownerPrincipalId, name, form]);
return (
<Form.Item name={name} hidden>
<CInput.Input />
</Form.Item>
);
};
export default OwnerPrincipalIdField;
+43 -199
View File
@@ -2,6 +2,7 @@ import { request } from '@umijs/max';
import _ from 'lodash'; import _ from 'lodash';
import { omitPathParams } from '../../utils'; import { omitPathParams } from '../../utils';
import { import {
FormData,
InstanceEvents, InstanceEvents,
InstanceLog, InstanceLog,
InstanceLogQueryParams, InstanceLogQueryParams,
@@ -9,176 +10,89 @@ import {
ListItem ListItem
} from '../config/types'; } from '../config/types';
export const GPU_SERVICE_INSTANCES_API = (params: { export const GPU_SERVICE_INSTANCES_API = '/gpu-instances';
namespace: string;
clusterID?: number;
}) =>
`/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instances`;
export const GPU_SERVICE_INSTANCES_TYPE_API = (params: { export const GPU_SERVICE_INSTANCES_TYPE_API = '/gpu-instance-types';
clusterID?: number;
}) => { // View logs / events still go through the K8s proxy until the /v2
return `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/instancetypes`; // /gpu-instances API exposes equivalents. clusterID and namespace come
}; // per-row from `row.clusterId` and `row.status.namespace`.
export const GPU_SERVICE_INSTANCES_LOG_API = (params: { export const GPU_SERVICE_INSTANCES_LOG_API = (params: {
namespace: string; namespace: string;
name: string; name: string;
clusterID?: number; clusterID?: number;
}) => { }) =>
return `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instances/${params.name}/log`; `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instances/${params.name}/log`;
};
export const GPU_SERVICE_INSTANCES_EVENTS_API = (params: { export const GPU_SERVICE_INSTANCES_EVENTS_API = (params: {
namespace: string; namespace: string;
name: string; name: string;
clusterID?: number; clusterID?: number;
}) => { }) =>
return `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instances/${params.name}/events`; `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instances/${params.name}/events`;
};
export const GPU_SERVICE_INSTANCE_PV_EVENTS_API = (params: { export const GPU_SERVICE_INSTANCE_PV_EVENTS_API = (params: {
namespace: string; namespace: string;
name: string; name: string;
clusterID?: number; clusterID?: number;
}) => { }) =>
return `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instancepersistentvolumes/${params.name}/events`; `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instancepersistentvolumes/${params.name}/events`;
};
// TODO: replace placeholder once start/stop endpoints are finalized
export const GPU_SERVICE_INSTANCE_START_API = (params: {
namespace: string;
name: string;
clusterID?: number;
}) => {
return `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instances/${params.name}/start`;
};
// TODO: replace placeholder once start/stop endpoints are finalized
export const GPU_SERVICE_INSTANCE_STOP_API = (params: {
namespace: string;
name: string;
clusterID?: number;
}) => {
return `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instances/${params.name}/stop`;
};
// =========== Instances =========== // =========== Instances ===========
export async function queryGPUServiceInstances( export async function queryGPUServiceInstances(
params: Global.K8sSearchParams & { params: Global.SearchParams,
namespace: string;
clusterID?: number;
},
options?: any options?: any
) { ) {
if (!params.clusterID) { return request<Global.PageResponse<ListItem>>(GPU_SERVICE_INSTANCES_API, {
return;
}
return request<Global.K8sPageResponse<ListItem>>(
GPU_SERVICE_INSTANCES_API({
namespace: params.namespace,
clusterID: params.clusterID
}),
{
method: 'GET', method: 'GET',
params: omitPathParams(params), params,
cancelToken: options?.token cancelToken: options?.token
} });
);
} }
export async function createGPUServiceInstance( export async function createGPUServiceInstance(params: { data: FormData }) {
params: { return request<ListItem>(GPU_SERVICE_INSTANCES_API, {
namespace: string;
clusterID?: number;
data: Global.K8sCommonData;
},
option?: any
) {
if (!params.clusterID) {
return;
}
return request<ListItem>(
GPU_SERVICE_INSTANCES_API({
namespace: params.namespace,
clusterID: params.clusterID
}),
{
method: 'POST', method: 'POST',
data: params.data, data: params.data
cancelToken: option?.token });
}
);
} }
export async function updateGPUServiceInstance( export async function updateGPUServiceInstance(params: {
params: {
namespace: string;
clusterID?: number;
id: number; id: number;
data: Global.K8sCommonData; data: Partial<FormData>;
}, }) {
option?: any return request<ListItem>(`${GPU_SERVICE_INSTANCES_API}/${params.id}`, {
) {
if (!params.clusterID) {
return;
}
return request<ListItem>(
`${GPU_SERVICE_INSTANCES_API({
namespace: params.namespace,
clusterID: params.clusterID
})}/${params.data?.metadata?.name}`,
{
method: 'PUT', method: 'PUT',
data: params.data, data: params.data
cancelToken: option?.token });
}
);
} }
export async function deleteGPUServiceInstance( export async function deleteGPUServiceInstance(id: number) {
params: { return request(`${GPU_SERVICE_INSTANCES_API}/${id}`, {
namespace: string; method: 'DELETE'
clusterID?: number; });
id: number;
},
option?: any
) {
if (!params.clusterID) {
return;
}
return request(
`${GPU_SERVICE_INSTANCES_API({
namespace: params.namespace,
clusterID: params.clusterID
})}/${params.id}`,
{
method: 'DELETE',
cancelToken: option?.token
}
);
} }
// =========== Instance Types =========== // =========== Instance Types ===========
export async function queryGPUServiceInstanceTypes( export async function queryGPUServiceInstanceTypes(
params: Global.K8sSearchParams & { clusterID?: number }, params: Global.SearchParams = { page: 1, perPage: 100 },
options?: any options?: any
) { ) {
if (!params.clusterID) { return request<Global.PageResponse<InstanceTypeItem>>(
return; GPU_SERVICE_INSTANCES_TYPE_API,
}
return request<Global.K8sPageResponse<InstanceTypeItem>>(
GPU_SERVICE_INSTANCES_TYPE_API({ clusterID: params.clusterID }),
{ {
method: 'GET', method: 'GET',
params: omitPathParams(params), params,
cancelToken: options?.token cancelToken: options?.token
} }
); );
} }
// =========== Logs / Events (K8s proxy) ===========
export async function queryGPUServiceInstanceEvents( export async function queryGPUServiceInstanceEvents(
params: { params: {
namespace: string; namespace: string;
@@ -192,11 +106,11 @@ export async function queryGPUServiceInstanceEvents(
return; return;
} }
return request<InstanceEvents>( return request<InstanceEvents>(
`${GPU_SERVICE_INSTANCES_EVENTS_API({ GPU_SERVICE_INSTANCES_EVENTS_API({
namespace: params.namespace, namespace: params.namespace,
clusterID: params.clusterID, clusterID: params.clusterID,
name: params.name name: params.name
})}`, }),
{ {
method: 'GET', method: 'GET',
params: omitPathParams(_.omit(params, ['name'])), params: omitPathParams(_.omit(params, ['name'])),
@@ -217,11 +131,11 @@ export async function queryGPUServiceInstancePVEvents(
return; return;
} }
return request<InstanceEvents>( return request<InstanceEvents>(
`${GPU_SERVICE_INSTANCE_PV_EVENTS_API({ GPU_SERVICE_INSTANCE_PV_EVENTS_API({
namespace: params.namespace, namespace: params.namespace,
clusterID: params.clusterID, clusterID: params.clusterID,
name: params.name name: params.name
})}`, }),
{ {
method: 'GET', method: 'GET',
params: omitPathParams(_.omit(params, ['name'])), params: omitPathParams(_.omit(params, ['name'])),
@@ -230,56 +144,6 @@ export async function queryGPUServiceInstancePVEvents(
); );
} }
// TODO: replace placeholder once start/stop endpoints are finalized
export async function startGPUServiceInstance(
params: {
namespace: string;
name: string;
clusterID?: number;
},
option?: any
) {
if (!params.clusterID) {
return;
}
return request(
GPU_SERVICE_INSTANCE_START_API({
namespace: params.namespace,
clusterID: params.clusterID,
name: params.name
}),
{
method: 'POST',
cancelToken: option?.token
}
);
}
// TODO: replace placeholder once start/stop endpoints are finalized
export async function stopGPUServiceInstance(
params: {
namespace: string;
name: string;
clusterID?: number;
},
option?: any
) {
if (!params.clusterID) {
return;
}
return request(
GPU_SERVICE_INSTANCE_STOP_API({
namespace: params.namespace,
clusterID: params.clusterID,
name: params.name
}),
{
method: 'POST',
cancelToken: option?.token
}
);
}
export async function queryGPUServiceInstanceLog( export async function queryGPUServiceInstanceLog(
params: InstanceLogQueryParams & { params: InstanceLogQueryParams & {
namespace: string; namespace: string;
@@ -292,11 +156,11 @@ export async function queryGPUServiceInstanceLog(
return; return;
} }
return request<InstanceLog>( return request<InstanceLog>(
`${GPU_SERVICE_INSTANCES_LOG_API({ GPU_SERVICE_INSTANCES_LOG_API({
namespace: params.namespace, namespace: params.namespace,
clusterID: params.clusterID, clusterID: params.clusterID,
name: params.name name: params.name
})}`, }),
{ {
method: 'GET', method: 'GET',
params: omitPathParams(_.omit(params, ['name'])), params: omitPathParams(_.omit(params, ['name'])),
@@ -304,23 +168,3 @@ export async function queryGPUServiceInstanceLog(
} }
); );
} }
export async function queryGPUServiceInstanceTypeItems(
params: {
name: string;
clusterID?: number;
},
options?: any
) {
if (!params.clusterID) {
return;
}
return request<InstanceTypeItem>(
`${GPU_SERVICE_INSTANCES_TYPE_API({ clusterID: params.clusterID })}/${params.name}`,
{
method: 'GET',
params: omitPathParams(params),
cancelToken: options?.token
}
);
}
@@ -101,7 +101,8 @@ const AddModal: React.FC<AddModalProps> = ({
const instanceTypeList = detailData?.items || []; const instanceTypeList = detailData?.items || [];
const templateList = templatesData?.items || []; const templateList = templatesData?.items || [];
const readonly = action === PageAction.VIEW; // const readonly = action === PageAction.VIEW;
const readonly = false;
const isRecreate = realAction === PageAction.CREATE; const isRecreate = realAction === PageAction.CREATE;
const showResourceSelectors = action === PageAction.CREATE || isRecreate; const showResourceSelectors = action === PageAction.CREATE || isRecreate;
const shouldAutoSelectResource = action === PageAction.CREATE && !isRecreate; const shouldAutoSelectResource = action === PageAction.CREATE && !isRecreate;
@@ -119,27 +120,33 @@ const AddModal: React.FC<AddModalProps> = ({
instanceType: InstanceTypeItem, instanceType: InstanceTypeItem,
template: TemplateItem | undefined template: TemplateItem | undefined
) => { ) => {
const name = instanceType.metadata?.name;
const manufacturer = instanceType.spec?.manufacturer; const manufacturer = instanceType.spec?.manufacturer;
setInstanceTypeSelection({ instanceType: name, manufacturer }); setInstanceTypeSelection({
instanceType: instanceType.name,
manufacturer
});
setTemplateId(template?.id); setTemplateId(template?.id);
const currentSpec = form.current?.getFieldsValue()?.spec || {};
const updatedSpec = {
...currentSpec,
type: name,
resources: { ...(currentSpec.resources || {}), accelerator: '1' }
};
if (template) { if (template) {
const currentSpec = form.current?.getFieldsValue()?.spec || {};
form.current?.setFieldsValue({ form.current?.setFieldsValue({
manufacturer: template.manufacturer, manufacturer: template.manufacturer,
description: JSON.stringify(
{
name: instanceType.name,
spec: { spec: {
...updatedSpec, ...instanceType.spec
}
},
null,
2
),
spec: {
...currentSpec,
...template.spec, ...template.spec,
resources: { resources: {
...(updatedSpec.resources || {}), ...(currentSpec.resources || {}),
...(template.spec?.resources || {}) ...(template.spec?.resources || {})
} }
} }
@@ -147,16 +154,46 @@ const AddModal: React.FC<AddModalProps> = ({
} else { } else {
form.current?.setFieldsValue({ form.current?.setFieldsValue({
manufacturer: undefined, manufacturer: undefined,
spec: updatedSpec description: JSON.stringify(instanceType.spec, null, 2)
}); });
} }
form.current?.applyInstanceType?.(instanceType);
};
const findAggregateOf = (
candidateName: string | undefined,
clusterId: number | null | undefined,
instanceTypes: InstanceTypeItem[]
): InstanceTypeItem | undefined => {
if (!candidateName) return undefined;
return instanceTypes.find((item) =>
(item.status?.acceleratorTiers ?? []).some((tier) =>
(tier.candidates ?? []).some(
(c) => c.name === candidateName && Number(c.cluster) === clusterId
)
)
);
}; };
const applyAutoSelection = ( const applyAutoSelection = (
instanceTypes: InstanceTypeItem[], instanceTypes: InstanceTypeItem[],
templates: TemplateItem[] templates: TemplateItem[]
) => { ) => {
if (!shouldAutoSelectResource) return; // On edit / view, surface the persisted selection in the card list.
if (!shouldAutoSelectResource) {
const aggregate = findAggregateOf(
data?.spec?.type,
data?.clusterId,
instanceTypes
);
if (aggregate) {
setInstanceTypeSelection({
instanceType: aggregate.name,
manufacturer: aggregate.spec?.manufacturer
});
}
return;
}
if (!instanceTypes.length) return; if (!instanceTypes.length) return;
const first = instanceTypes[0]; const first = instanceTypes[0];
@@ -178,18 +215,21 @@ const AddModal: React.FC<AddModalProps> = ({
return; return;
} }
if (action === PageAction.CREATE) {
const session = ++sessionRef.current; const session = ++sessionRef.current;
Promise.all([fetchData({}), fetchTemplates({ page: -1 })]).then( Promise.all([
([instanceRes, templatesRes]) => { fetchData({ page: 1, perPage: 100 }),
fetchTemplates({ page: -1 })
]).then(([instanceRes, templatesRes]) => {
if (sessionRef.current !== session) return; if (sessionRef.current !== session) return;
applyAutoSelection(instanceRes?.items || [], templatesRes?.items || []); applyAutoSelection(instanceRes?.items || [], templatesRes?.items || []);
});
} }
); }, [open, shouldAutoSelectResource, action]);
}, [open, shouldAutoSelectResource]);
// filter instance types // filter instance types
const filteredInstanceTypes = instanceTypeList.filter((item) => const filteredInstanceTypes = instanceTypeList.filter((item) =>
matchKeyword([item.metadata?.name], instanceKeyword) matchKeyword([item.name], instanceKeyword)
); );
// filter templates based on selection and keyword // filter templates based on selection and keyword
@@ -370,7 +410,7 @@ const AddModal: React.FC<AddModalProps> = ({
} }
> >
<> <>
{action !== PageAction.VIEW && ( {action !== PageAction.EDIT && (
<ColTitle> <ColTitle>
{intl.formatMessage({ id: 'common.title.config' })} {intl.formatMessage({ id: 'common.title.config' })}
</ColTitle> </ColTitle>
@@ -1,16 +1,12 @@
import { AutoTooltip, IconFont, StatusTag, ThemeTag } from '@gpustack/core-ui'; import { AutoTooltip, IconFont, ThemeTag } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Flex } from 'antd'; import { Flex } from 'antd';
import styled from 'styled-components'; import styled from 'styled-components';
import { manufactureColorMap } from '../../templates/config'; import { manufactureColorMap } from '../../templates/config';
import { import { convertKiToGi } from '../config';
convertKiToGi,
InstanceTypePhaseLabelMap,
InstanceTypePhaseStatus
} from '../config';
import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types'; import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types';
const toDisplayUnit = (value?: string) => const toDisplayUnit = (value?: string | null) =>
value ? value.replace(/Gi$/, 'GB').replace(/Ti$/, 'TB') : value; value ? value.replace(/Gi$/, 'GB').replace(/Ti$/, 'TB') : value;
const Title = styled.div` const Title = styled.div`
@@ -60,24 +56,48 @@ interface InstanceTypeItemProps {
showStatus?: boolean; showStatus?: boolean;
} }
const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ const MetaItem: React.FC<{
item, icon: string;
showStatus = true label: string;
}) => { value?: string | null;
const intl = useIntl(); showDot?: boolean;
const name = item.metadata?.name; show?: boolean;
const acceleratable = item.spec?.acceleratable; }> = ({ icon, label, value, showDot = true, show = true }) => {
if (!show) return null;
return (
<>
{showDot && <span className="dot"></span>}
<span className="meta-item">
<IconFont type={icon} className="meta-icon" />
<Flex align="center" gap={4}>
<span>{label}</span>
<span>{value || '-'}</span>
</Flex>
</span>
</>
);
};
const manufacturerKey = item.spec?.manufacturer; const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
const intl = useIntl();
const name = item.name;
const specData = item.spec || {};
const acceleratable = specData.acceleratable;
const manufacturerKey = specData.manufacturer || (acceleratable ? '' : 'cpu');
const manufacturer = manufacturerKey?.toUpperCase(); const manufacturer = manufacturerKey?.toUpperCase();
const manufacturerColor = const manufacturerColor =
(manufacturerKey && manufactureColorMap[manufacturerKey]) ?? 'purple'; (manufacturerKey && manufactureColorMap[manufacturerKey]) ?? 'purple';
// resource remaining status
const remainingData = item.status?.remaining || {};
const renderName = () => { const renderName = () => {
const product = item.spec?.product; const product = specData.product;
const cpuCapacity = remainingData.cpu;
const displayName = const displayName =
product || product ||
(!acceleratable ? `${item.status?.cpu?.capacity} vCPUs` : name); (!acceleratable && cpuCapacity ? `${cpuCapacity} vCPUs` : name);
return displayName; return displayName;
}; };
@@ -85,8 +105,8 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({
<> <>
<Title> <Title>
<Flex gap={8} align="center"> <Flex gap={8} align="center">
<AutoTooltip ghost minWidth={20} maxWidth={180}> <AutoTooltip ghost minWidth={20} maxWidth={200}>
{item.spec ? renderName() : '-'} {renderName() || '-'}
</AutoTooltip> </AutoTooltip>
{acceleratable && manufacturer && ( {acceleratable && manufacturer && (
<span <span
@@ -95,99 +115,59 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({
fontWeight: 400 fontWeight: 400
}} }}
> >
<ThemeTag color={manufacturerColor}>{manufacturer}</ThemeTag> <ThemeTag color={manufacturerColor} disabled={false}>
{manufacturer}
</ThemeTag>
</span> </span>
)} )}
</Flex> </Flex>
{showStatus && (
<Flex gap={8} align="center">
{item.status?.phase && (
<StatusTag
statusValue={{
status:
InstanceTypePhaseStatus[item.status.phase] ?? 'inactive',
text:
InstanceTypePhaseLabelMap[item.status.phase] ??
item.status.phase,
message: ''
}}
/>
)}
</Flex>
)}
</Title> </Title>
<Meta> <Meta>
<span style={{ display: 'flex', height: 15 }}> <span style={{ display: 'flex', height: 15 }}>
{acceleratable && ( {acceleratable && (
<span className="meta-row"> <span className="meta-row">
<span className="meta-item"> <MetaItem
<IconFont type="icon-gpu1" className="meta-icon" /> showDot={false}
<Flex align="center" gap={4}> icon="icon-gpu1"
<span> label={intl.formatMessage({ id: 'gpuservice.instance.memory' })}
{intl.formatMessage({ id: 'gpuservice.instance.memory' })} value={
</span> toDisplayUnit(convertKiToGi(specData?.memory ?? undefined)) ??
<span> '-'
{toDisplayUnit(convertKiToGi(item.spec?.memory)) ?? '-'} }
</span> ></MetaItem>
</Flex> <MetaItem
</span> show={!!specData?.sliced}
{item.spec?.sliced && ( icon="icon-sliced"
<> label={intl.formatMessage({
<span className="dot"></span>
<span className="meta-item">
<IconFont type="icon-sliced" className="meta-icon" />
<Flex align="center" gap={4}>
<span>
{intl.formatMessage({
id: 'gpuservice.instance.sliced' id: 'gpuservice.instance.sliced'
})} })}
</span> value={specData?.sliced}
<span>{item.spec?.sliced ?? '-'}</span> ></MetaItem>
</Flex> <MetaItem
</span> show={!!remainingData.accelerator}
</> icon="icon-database"
)} label={intl.formatMessage({
{item.status?.accelerator?.remaining && (
<>
<span className="dot"></span>
<span className="meta-item">
<IconFont type="icon-database" className="meta-icon" />
<Flex align="center" gap={4}>
<span>
{intl.formatMessage({
id: 'gpuservice.instance.stock' id: 'gpuservice.instance.stock'
})} })}
</span> value={remainingData.accelerator}
<span>{item.status?.accelerator?.remaining ?? '-'}</span> ></MetaItem>
</Flex>
</span>
</>
)}
</span> </span>
)} )}
</span> </span>
<span className="meta-row"> <span className="meta-row">
<span className="meta-item"> <MetaItem
<IconFont type="icon-ram-02" className="meta-icon" /> showDot={false}
<Flex align="center" gap={4}> icon="icon-ram-02"
<span> label={intl.formatMessage({ id: 'gpuservice.instance.ram' })}
{intl.formatMessage({ id: 'gpuservice.instance.ram' })} value={toDisplayUnit(convertKiToGi(remainingData.ram)) ?? '-'}
</span> ></MetaItem>
<span>{toDisplayUnit(item.status?.ram?.capacity) ?? '-'}</span> <MetaItem
</Flex> show={acceleratable}
</span> showDot={true}
{acceleratable && ( icon="icon-cpu"
<> label="vCPU"
<span className="dot"></span> value={remainingData.cpu ?? '-'}
<span className="meta-item"> ></MetaItem>
<IconFont type="icon-cpu" className="meta-icon" />
<Flex align="center" gap={4}>
<span>vCPU</span>
<span>{item.status?.cpu?.capacity ?? '-'}</span>
</Flex>
</span>
</>
)}
</span> </span>
</Meta> </Meta>
</> </>
@@ -3,7 +3,7 @@ import { TemplateCard } from '@gpustack/core-ui';
import { Empty, Spin } from 'antd'; import { Empty, Spin } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import styled from 'styled-components'; import styled from 'styled-components';
import { InstanceTypePhaseValueMap } from '../config'; import { getAcceleratorMax } from '../config';
import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types'; import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types';
import InstanceTypeItem from './instance-type-item'; import InstanceTypeItem from './instance-type-item';
@@ -20,8 +20,12 @@ interface InstanceTypeListProps {
loading?: boolean; loading?: boolean;
} }
const isAvailable = (item: InstanceTypeItemModel) => // CPU types always have stock (no accelerator). Accelerated types are
item.status?.phase === InstanceTypePhaseValueMap.Active; // considered available when at least one tier offers onceMaxRequest > 0.
const isAvailable = (item: InstanceTypeItemModel) => {
if (!item.spec?.acceleratable) return true;
return getAcceleratorMax(item.status?.acceleratorTiers) > 0;
};
const InstanceTypeList: React.FC<InstanceTypeListProps> = ({ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
value, value,
@@ -53,7 +57,7 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
<TypeGrid> <TypeGrid>
{dataList.map((item) => { {dataList.map((item) => {
const disabled = !isAvailable(item); const disabled = !isAvailable(item);
const name = item.metadata?.name; const name = item.name;
return ( return (
<TemplateCard <TemplateCard
key={name} key={name}
@@ -31,7 +31,7 @@ const eventTypeStatus: Record<string, 'success' | 'warning' | 'error'> = {
const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => { const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
const intl = useIntl(); const intl = useIntl();
const { open, onCancel, name } = props || {}; const { open, onCancel, name, namespace, clusterID } = props || {};
const [activeKey, setActiveKey] = useState('instance'); const [activeKey, setActiveKey] = useState('instance');
const { const {
@@ -52,9 +52,9 @@ const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
const volumeEvents = volumeEventsData?.items ?? []; const volumeEvents = volumeEventsData?.items ?? [];
const refreshAll = () => { const refreshAll = () => {
if (!name) return; if (!name || !namespace || !clusterID) return;
fetchInstanceEvents({ name }); fetchInstanceEvents({ name, namespace, clusterID });
fetchVolumeEvents({ name }); fetchVolumeEvents({ name, namespace, clusterID });
}; };
useEffect(() => { useEffect(() => {
@@ -0,0 +1,14 @@
import { PageActionType } from '@/config/types';
import { createContext } from 'react';
export interface FormContextValue {
action: PageActionType;
currentData: any;
}
export const FormContext = createContext<FormContextValue>({
action: 'create',
currentData: null
});
export const FormContextProvider = FormContext.Provider;
+78 -77
View File
@@ -3,11 +3,6 @@ import { StatusType } from '@/config/types';
import { IconFont, icons } from '@gpustack/core-ui'; import { IconFont, icons } from '@gpustack/core-ui';
import _ from 'lodash'; import _ from 'lodash';
import React from 'react'; import React from 'react';
import {
InstanceTypeItem,
InstanceTypeResource,
InstanceTypeStatus
} from './types';
export const InstanceStatusValueMap = { export const InstanceStatusValueMap = {
Scheduling: 'Scheduling', Scheduling: 'Scheduling',
@@ -45,13 +40,22 @@ export const status: Record<string, StatusType> = {
[InstanceStatusValueMap.Ready]: StatusMaps.success [InstanceStatusValueMap.Ready]: StatusMaps.success
}; };
// Lifecycle actions (logs/events/start/stop) require K8s proxy endpoints that
// the new /v2/gpu-instances API does not yet expose; keep them visible but
// disabled until backend support lands.
export const rowActionList = [ export const rowActionList = [
{ {
label: 'common.button.view', label: 'common.button.edit',
key: 'view', key: 'edit',
locale: true, locale: true,
icon: icons.DetailInfo icon: icons.EditOutlined
}, },
// {
// label: 'common.button.view',
// key: 'view',
// locale: true,
// icon: icons.DetailInfo
// },
{ {
label: 'common.button.viewlog', label: 'common.button.viewlog',
key: 'viewlog', key: 'viewlog',
@@ -64,30 +68,30 @@ export const rowActionList = [
locale: true, locale: true,
icon: icons.ProfileOutlined icon: icons.ProfileOutlined
}, },
{
label: 'common.button.start',
key: 'start',
locale: true,
icon: icons.Play
},
{
label: 'common.button.stop',
key: 'stop',
locale: true,
icon: icons.Stop
},
// { // {
// label: 'common.button.edit', // label: 'common.button.start',
// key: 'edit', // key: 'start',
// locale: true, // locale: true,
// icon: icons.EditOutlined // icon: icons.Play,
// props: {
// disabled: true
// }
// },
// {
// label: 'common.button.stop',
// key: 'stop',
// locale: true,
// icon: icons.Stop,
// props: {
// disabled: true
// }
// },
// {
// label: 'common.button.recreate',
// key: 'recreate',
// locale: true,
// icon: icons.RetweetOutlined
// }, // },
{
label: 'common.button.recreate',
key: 'recreate',
locale: true,
icon: icons.RetweetOutlined
},
{ {
label: 'common.button.delete', label: 'common.button.delete',
key: 'delete', key: 'delete',
@@ -103,12 +107,18 @@ export const batchActionList = [
{ {
label: 'common.button.start', label: 'common.button.start',
key: 'start', key: 'start',
icon: icons.Play icon: icons.Play,
props: {
disabled: true
}
}, },
{ {
label: 'common.button.stop', label: 'common.button.stop',
key: 'stop', key: 'stop',
icon: icons.Stop icon: icons.Stop,
props: {
disabled: true
}
}, },
{ {
label: 'common.button.delete', label: 'common.button.delete',
@@ -121,8 +131,6 @@ export const batchActionList = [
]; ];
export const InstanceTypePhaseValueMap = { export const InstanceTypePhaseValueMap = {
// Available: 'Available',
// Unavailable: 'Unavailable'
PreParing: 'Preparing', PreParing: 'Preparing',
Inactive: 'Inactive', Inactive: 'Inactive',
Active: 'Active' Active: 'Active'
@@ -141,10 +149,12 @@ export const InstanceTypePhaseStatus: Record<string, StatusType> = {
}; };
export const StorageModeValueMap = { export const StorageModeValueMap = {
Existing: 'existing', Temporary: 'temporary',
Temporary: 'temporary' Persistent: 'persistent'
}; };
export const DEFAULT_PV_CAPACITY_GB = 20;
// Constant SSH public key resource name used when SSH is enabled // Constant SSH public key resource name used when SSH is enabled
export const DEFAULT_SSH_PUBLIC_KEY_NAME = 'default'; export const DEFAULT_SSH_PUBLIC_KEY_NAME = 'default';
@@ -163,49 +173,40 @@ export const convertKiToGi = (value?: string): string | undefined => {
return `${_.round(Number(num) / GI_DIVISOR[unit], 2)}Gi`; return `${_.round(Number(num) / GI_DIVISOR[unit], 2)}Gi`;
}; };
export const transformInstanceTypeResource = ( const parseQuantity = (value?: string | null): number => {
resource?: InstanceTypeResource if (!value) return 0;
): InstanceTypeResource | undefined => { const match = /^(-?\d+(?:\.\d+)?)/.exec(String(value));
if (!resource) return resource; return match ? Number(match[1]) || 0 : 0;
return {
...resource,
capacity: convertKiToGi(
resource.capacity
) as InstanceTypeResource['capacity'],
onceMaxRequest: convertKiToGi(
resource.onceMaxRequest
) as InstanceTypeResource['onceMaxRequest'],
remaining: convertKiToGi(
resource.remaining
) as InstanceTypeResource['remaining']
};
}; };
export const transformInstanceType = ( // Returns the slider max for the accelerator count: the largest
item: InstanceTypeItem // tier.onceMaxRequest across all acceleratorTiers (not from candidates).
): InstanceTypeItem => { export const getAcceleratorMax = (
if (!item?.status) return item; tiers?: { onceMaxRequest: string }[] | null
const status = item.status; ) => {
return { if (!tiers?.length) return 0;
...item, return tiers.reduce((acc, tier) => {
spec: { const n = parseQuantity(tier.onceMaxRequest);
...item.spec, return n > acc ? n : acc;
manufacturer: !item.spec?.acceleratable ? 'cpu' : item.spec.manufacturer }, 0);
},
status: {
...status,
accelerator: transformInstanceTypeResource(
status.accelerator
) as InstanceTypeStatus['accelerator'],
cpu: transformInstanceTypeResource(
status.cpu
) as InstanceTypeStatus['cpu'],
localStorage: transformInstanceTypeResource(
status.localStorage
) as InstanceTypeStatus['localStorage'],
ram: transformInstanceTypeResource(
status.ram
) as InstanceTypeStatus['ram']
}
}; };
// Picks the candidate (cluster + type name) that should fulfill a requested
// accelerator count: the first candidate of the smallest tier whose
// onceMaxRequest is >= the requested count.
export const pickCandidateForAccelerator = <
C extends { cluster: string; name: string }
>(
tiers:
| { onceMaxRequest: string; candidates?: C[] | null }[]
| undefined
| null,
count: number
): C | null => {
if (!tiers?.length) return null;
const sorted = [...tiers].sort(
(a, b) => parseQuantity(a.onceMaxRequest) - parseQuantity(b.onceMaxRequest)
);
const tier = sorted.find((t) => parseQuantity(t.onceMaxRequest) >= count);
return tier?.candidates?.[0] ?? null;
}; };
+86 -112
View File
@@ -2,16 +2,16 @@ export type ImagePullPolicy = 'Always' | 'IfNotPresent' | 'Never';
// instance form data // instance form data
export interface FormData { export interface FormData {
metadata: {
name: string; name: string;
namespace: string; clusterId?: number | null;
}; owner_principal_id?: number | null;
displayName?: string | null;
description?: string | null;
enable_ssh?: boolean; enable_ssh?: boolean;
spec: { spec: {
type: string; type: string;
image: string; image: string;
imagePullPolicy: ImagePullPolicy; imagePullPolicy: ImagePullPolicy;
displayName: string;
command: string[]; command: string[];
ports: { ports: {
port: number; port: number;
@@ -27,9 +27,11 @@ export interface FormData {
cpu?: string; cpu?: string;
ram?: string; ram?: string;
localStorage?: string; localStorage?: string;
accelerator?: string; // Stored in the form as a number so NumberSelection's strict
// equality picks up the active item. Serialized to a string at the
// API boundary in handleFinish.
accelerator?: number | string;
}; };
description: string;
volume: { volume: {
ephemeral?: { ephemeral?: {
capacity?: string; capacity?: string;
@@ -37,162 +39,136 @@ export interface FormData {
persistent?: { persistent?: {
name: string; name: string;
}; };
persistentTemplate?: {
name?: string;
spec: {
type: string;
capacity: string;
}; };
sshPublicKey: { releaseWithInstance?: boolean;
name: string;
}; };
}; };
sshPublicKeys?: { name: string }[];
};
} }
export type InstanceServicePortProtocol = 'TCP' | 'UDP'; export type InstanceServicePortProtocol = 'TCP' | 'UDP';
export interface InstanceStatus { export interface InstanceIP {
hostIPs?: {
ip: string; ip: string;
}[]; }
phase?: string;
phaseMessage?: string; export interface InstanceServicePort {
podIPs?: {
ip: string;
}[];
ports?: {
port: number; port: number;
name: string; name: string;
nodePort?: number; nodePort?: number;
protocol?: InstanceServicePortProtocol; protocol?: InstanceServicePortProtocol;
}[]; }
export interface InstanceStatus {
namespace?: string;
phase?: string;
phaseMessage?: string;
hostIPs?: InstanceIP[];
podIPs?: InstanceIP[];
ports?: InstanceServicePort[];
} }
// instance list item // instance list item
export interface ListItem extends Omit<FormData, 'metadata'> { export interface ListItem extends FormData {
id: number; id: number;
status?: InstanceStatus; created_at: string;
metadata: { updated_at: string;
name: string; deleted_at?: string | null;
namespace?: string; status?: InstanceStatus | null;
uid: string;
resourceVersion: string;
creationTimestamp: string;
annotations?: Record<string, string>;
managedFields?: {
manager: string;
operation: string;
apiVersion: string;
time: string;
fieldsType: string;
fieldsV1: Record<string, any>;
}[];
};
} }
type Quality = // =========== Instance Types ===========
| `${number}`
| `${number}Ki`
| `${number}Mi`
| `${number}Gi`
| `${number}Ti`;
export interface InstanceTypeResource { export interface InstanceTypeResource {
capacity?: Quality; onceMaxRequest: string;
onceMaxRequest?: Quality; remaining: string;
remaining?: Quality; capacity: string;
} }
export interface InstanceTypeStatus { export interface InstanceTypeCandidate {
cluster: string;
name: string;
accelerator: InstanceTypeResource; accelerator: InstanceTypeResource;
cpu: InstanceTypeResource; cpu: InstanceTypeResource;
localStorage: InstanceTypeResource;
phase: string;
phaseMessage?: string;
ram: InstanceTypeResource; ram: InstanceTypeResource;
localStorage: InstanceTypeResource;
} }
export interface InstanceTypeMetadata { export interface InstanceTypeTier {
name: string; onceMaxRequest: string;
namespace?: string; candidates?: InstanceTypeCandidate[] | null;
uid?: string; }
resourceVersion?: string;
creationTimestamp?: string; export interface InstanceTypeRemainingResource {
generateName?: string; accelerator?: string | null;
generation?: number; cpu: string;
finalizers?: string[]; ram: string;
localStorage: string;
} }
export interface InstanceTypeSpec { export interface InstanceTypeSpec {
acceleratable: boolean;
group: string; group: string;
computeCapability?: string; acceleratable: boolean;
family?: string; manufacturer: string;
manufacturer?: string; product?: string | null;
memory?: string; memory?: string | null;
product?: string; family?: string | null;
sliced?: number; computeCapability?: string | null;
sliced?: string | null;
}
export interface InstanceTypeStatus {
remaining: InstanceTypeRemainingResource;
acceleratorTiers?: InstanceTypeTier[] | null;
} }
export interface InstanceTypeItem { export interface InstanceTypeItem {
apiVersion: string; name: string;
kind: string;
id: number;
metadata: InstanceTypeMetadata;
spec: InstanceTypeSpec; spec: InstanceTypeSpec;
status: InstanceTypeStatus; status: InstanceTypeStatus;
} }
export interface InstanceEventObjectReference { // =========== Events / logs (placeholder) ===========
apiVersion?: string; // The /v2/gpu-instances API does not yet expose log/event endpoints. These
fieldPath?: string; // shapes are retained as minimal placeholders so disabled UI continues to
kind?: string; // compile until backend support lands.
name?: string;
namespace?: string;
resourceVersion?: string;
uid?: string;
}
export interface InstanceEventSource {
component?: string;
host?: string;
}
export interface InstanceEventSeries {
count?: number;
lastObservedTime?: string;
}
export interface InstanceEventItem { export interface InstanceEventItem {
action?: string; type?: string;
apiVersion?: string; reason?: string;
message?: string;
count?: number; count?: number;
eventTime?: string; eventTime?: string;
firstTimestamp?: string; firstTimestamp?: string;
involvedObject: InstanceEventObjectReference;
kind?: string;
lastTimestamp?: string; lastTimestamp?: string;
message?: string;
metadata: {
name?: string;
namespace?: string;
uid?: string;
resourceVersion?: string;
creationTimestamp?: string;
};
reason?: string;
related?: InstanceEventObjectReference;
reportingComponent?: string; reportingComponent?: string;
reportingInstance?: string; reportingInstance?: string;
series?: InstanceEventSeries; source?: {
source?: InstanceEventSource; component?: string;
type?: string; host?: string;
};
series?: {
count?: number;
lastObservedTime?: string;
};
metadata?: {
name?: string;
namespace?: string;
};
} }
export interface InstanceEvents { export interface InstanceEvents {
apiVersion?: string;
kind?: string;
metadata?: {
resourceVersion?: string;
};
items: InstanceEventItem[]; items: InstanceEventItem[];
} }
export type InstanceLog = string;
export interface InstanceLogQueryParams { export interface InstanceLogQueryParams {
follow?: boolean; follow?: boolean;
limitBytes?: number; limitBytes?: number;
@@ -201,5 +177,3 @@ export interface InstanceLogQueryParams {
timestamps?: boolean; timestamps?: boolean;
pretty?: string; pretty?: string;
} }
export type InstanceLog = string;
@@ -3,6 +3,7 @@ import { PageActionType } from '@/config/types';
import { Input as CInput } from '@gpustack/core-ui'; import { Input as CInput } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import OwnerPrincipalIdField from '../../../_components/owner-principal-id-field';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
import formStyles from '../styles/instances.module.less'; import formStyles from '../styles/instances.module.less';
@@ -16,9 +17,10 @@ const Basic = ({
const intl = useIntl(); const intl = useIntl();
return ( return (
<> <>
<OwnerPrincipalIdField name="owner_principal_id" />
<Form.Item<FormData> <Form.Item<FormData>
data-field="name" data-field="name"
name={['metadata', 'name']} name="name"
rules={[ rules={[
{ {
required: true, required: true,
@@ -38,10 +40,17 @@ const Basic = ({
required required
/> />
</Form.Item> </Form.Item>
<Form.Item<FormData> name="displayName">
<CInput.Input
trim={false}
disabled={false}
label={intl.formatMessage({ id: 'common.table.displayName' })}
/>
</Form.Item>
<div className={formStyles.command}> <div className={formStyles.command}>
<Form.Item<FormData> name={['spec', 'description']}> <Form.Item<FormData> name="description" hidden>
<CInput.TextArea <CInput.TextArea
disabled={disabled} disabled={false}
label={intl.formatMessage({ id: 'common.table.description' })} label={intl.formatMessage({ id: 'common.table.description' })}
scaleSize scaleSize
/> />
+184 -49
View File
@@ -4,6 +4,7 @@ import { PageActionType } from '@/config/types';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { import {
CheckboxField, CheckboxField,
Input as CInput,
CollapsePanel, CollapsePanel,
IconFont, IconFont,
MultipleSelect, MultipleSelect,
@@ -21,13 +22,15 @@ import {
useEffect, useEffect,
useImperativeHandle, useImperativeHandle,
useMemo, useMemo,
useRef useRef,
useState
} from 'react'; } from 'react';
import useGetSshkey from '../../public-keys/services/use-get-sshkey'; import useQuerySshkeys from '../../public-keys/services/use-query-sshkeys';
import { DefaultImagePullPolicy } from '../../templates/config'; import { DefaultImagePullPolicy } from '../../templates/config';
import TemplateBasicForm, { import TemplateBasicForm, {
BasicResourceMax BasicResourceMax
} from '../../templates/forms/basic'; } from '../../templates/forms/basic';
import { pickCandidateForAccelerator } from '../config';
import { FormData, InstanceTypeItem, ListItem } from '../config/types'; import { FormData, InstanceTypeItem, ListItem } from '../config/types';
import instanceStyles from '../styles/instances.module.less'; import instanceStyles from '../styles/instances.module.less';
import Basic from './basic'; import Basic from './basic';
@@ -53,7 +56,7 @@ interface InstanceFormProps {
onFinish: (values: FormData) => Promise<void>; onFinish: (values: FormData) => Promise<void>;
} }
const parseQuantityToNumber = (value?: string): number | null => { const parseQuantityToNumber = (value?: string | null): number | null => {
if (!value) return null; if (!value) return null;
const match = /^(-?\d+(?:\.\d+)?)/.exec(String(value)); const match = /^(-?\d+(?:\.\d+)?)/.exec(String(value));
if (!match) return null; if (!match) return null;
@@ -61,6 +64,27 @@ const parseQuantityToNumber = (value?: string): number | null => {
return Number.isFinite(num) && num > 0 ? num : null; return Number.isFinite(num) && num > 0 ? num : null;
}; };
// Converts a K8s-style quantity ("24314504Ki", "2Ti", "10Gi", or a bare
// number assumed to be Gi) to an integer GiB. memory and localStorage
// inputs are displayed in GB, so the max bound and the "Remaining {count}
// GB" label need the converted value.
const GI_FROM_UNIT: Record<string, number> = {
Ki: 1 / (1024 * 1024),
Mi: 1 / 1024,
Gi: 1,
Ti: 1024
};
const parseQuantityToGi = (value?: string | null): number | null => {
if (!value) return null;
const match = /^(-?\d+(?:\.\d+)?)(Ki|Mi|Gi|Ti)?$/.exec(String(value));
if (!match) return null;
const num = Number(match[1]);
if (!Number.isFinite(num) || num <= 0) return null;
const factor = match[2] ? GI_FROM_UNIT[match[2]] : 1;
return Math.floor(num * factor);
};
const TABKeysMap = { const TABKeysMap = {
BASIC: 'basic', BASIC: 'basic',
INSTANCE_TYPE: 'instanceType', INSTANCE_TYPE: 'instanceType',
@@ -71,7 +95,7 @@ const TABKeysMap = {
const requiredFields = { const requiredFields = {
[TABKeysMap.BASIC]: { [TABKeysMap.BASIC]: {
sort: 1, sort: 1,
fields: ['metadata.name'] fields: ['name']
}, },
[TABKeysMap.INSTANCE_TYPE]: { [TABKeysMap.INSTANCE_TYPE]: {
sort: 2, sort: 2,
@@ -95,7 +119,6 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
currentData, currentData,
disabled, disabled,
open, open,
namespace = 'default',
instanceTypeList = [], instanceTypeList = [],
onFinish onFinish
} = props; } = props;
@@ -108,7 +131,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
const formAction = const formAction =
realAction === PageAction.CREATE ? PageAction.CREATE : action; realAction === PageAction.CREATE ? PageAction.CREATE : action;
const sshEnabled = Form.useWatch('enable_ssh', form); const sshEnabled = Form.useWatch('enable_ssh', form);
const { detailData: sshKeyData, fetchData: fetchSSHData } = useGetSshkey(); const { sshkeyOptions, fetchData: fetchSSHData } = useQuerySshkeys();
const { getScrollElementScrollableHeight } = useWrapperContext(); const { getScrollElementScrollableHeight } = useWrapperContext();
const { const {
activeKey, activeKey,
@@ -127,25 +150,25 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
useEffect(() => { useEffect(() => {
if (open) { if (open) {
fetchSSHData({}); fetchSSHData({ page: 1, perPage: 100 });
} }
}, [open, action]); }, [open, action]);
const handleSSHEnableChange = (e: any) => { const ports = Form.useWatch(['spec', 'ports'], form) || [];
if (!e?.target?.checked) {
return; const hasSSHPort = useMemo(
} () =>
const currentPorts = form.getFieldValue(['spec', 'ports']) || []; ports.some(
const hasSSHPort = currentPorts.some(
(item: any) => item?.protocol === 'TCP' && item?.port === SSH_PORT (item: any) => item?.protocol === 'TCP' && item?.port === SSH_PORT
),
[ports]
); );
if (!hasSSHPort) {
form.setFieldValue( useEffect(() => {
['spec', 'ports'], if (hasSSHPort && !form.getFieldValue('enable_ssh')) {
[...currentPorts, { protocol: 'TCP', port: SSH_PORT, name: 'SSH' }] form.setFieldValue('enable_ssh', true);
);
} }
}; }, [hasSSHPort, form]);
const segmentOptions = useMemo( const segmentOptions = useMemo(
() => [ () => [
@@ -183,25 +206,50 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
[intl] [intl]
); );
const ports = Form.useWatch(['spec', 'ports'], form) || []; const [selectedInstanceType, setSelectedInstanceType] = useState<
InstanceTypeItem | undefined
>(undefined);
const [onceMaxRequest, setOnceMaxRequest] = useState<BasicResourceMax>({
cpu: null,
memory: null,
localStorage: null
});
const selectedTypeName = Form.useWatch(['spec', 'type'], form) as const resolveAndApply = (
| string instanceType: InstanceTypeItem | undefined,
| undefined; count: number,
options: { writeAccelerator?: boolean } = {}
const onceMaxRequest = useMemo<BasicResourceMax>(() => { ) => {
const selected = instanceTypeList.find( if (!instanceType) {
(item) => (item.metadata?.name || '') === selectedTypeName setSelectedInstanceType(undefined);
setOnceMaxRequest({ cpu: null, memory: null, localStorage: null });
return;
}
const candidate = pickCandidateForAccelerator(
instanceType.status?.acceleratorTiers,
count
); );
const status = selected?.status; setSelectedInstanceType(instanceType);
return { setOnceMaxRequest({
cpu: parseQuantityToNumber(status?.cpu?.onceMaxRequest), cpu: parseQuantityToNumber(candidate?.cpu?.onceMaxRequest),
memory: parseQuantityToNumber(status?.ram?.onceMaxRequest), memory: parseQuantityToGi(candidate?.ram?.onceMaxRequest),
localStorage: parseQuantityToNumber( localStorage: parseQuantityToGi(candidate?.localStorage?.onceMaxRequest)
status?.localStorage?.onceMaxRequest });
)
form.setFieldsValue({
clusterId: candidate?.cluster ? Number(candidate.cluster) : null,
spec: {
type: candidate?.name || '',
...(options.writeAccelerator
? { resources: { accelerator: count } }
: {})
} as any
});
};
const handleAcceleratorChange = (count: number) => {
resolveAndApply(selectedInstanceType, count);
}; };
}, [instanceTypeList, selectedTypeName]);
const onTargetChange = (key: string) => { const onTargetChange = (key: string) => {
scrollTabsRef.current?.handleTargetChange(key); scrollTabsRef.current?.handleTargetChange(key);
@@ -235,34 +283,110 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
action === PageAction.VIEW || action === PageAction.VIEW ||
realAction === PageAction.CREATE) realAction === PageAction.CREATE)
) { ) {
// The server returns `accelerator` as a string; coerce to a number
// so NumberSelection's strict equality picks up the active item.
const persistedAccelerator = currentData.spec?.resources?.accelerator;
const acceleratorAsNumber =
persistedAccelerator != null && persistedAccelerator !== ''
? Number(persistedAccelerator)
: undefined;
form.setFieldsValue({ form.setFieldsValue({
metadata: { name: currentData.name,
name: currentData.metadata?.name, displayName: currentData.displayName,
namespace: currentData.metadata?.namespace description: currentData.description,
}, clusterId: currentData.clusterId,
spec: { spec: {
...currentData.spec, ...currentData.spec,
imagePullPolicy: imagePullPolicy:
currentData.spec?.imagePullPolicy || DefaultImagePullPolicy currentData.spec?.imagePullPolicy || DefaultImagePullPolicy,
resources: {
...currentData.spec?.resources,
accelerator: acceleratorAsNumber
}, },
enable_ssh: !!currentData.spec?.sshPublicKey?.name sshPublicKeys:
currentData.spec?.sshPublicKeys?.map((k) => k.name) ?? []
} as any,
enable_ssh: !!currentData.spec?.sshPublicKeys?.length
}); });
const candidateName = currentData.spec?.type;
const clusterId = currentData.clusterId;
const aggregate = instanceTypeList.find((item) =>
(item.status?.acceleratorTiers ?? []).some((tier) =>
(tier.candidates ?? []).some(
(c) => c.name === candidateName && Number(c.cluster) === clusterId
)
)
);
if (aggregate) {
const count =
Number(currentData.spec?.resources?.accelerator) ||
(aggregate.spec?.acceleratable ? 1 : 0);
// No accelerator rewrite — the form already has the persisted value.
resolveAndApply(aggregate, count);
}
return; return;
} }
}, [action, currentData, form, open, namespace, realAction]); }, [action, currentData, form, open, realAction, instanceTypeList]);
const handleFinish = async (values: InstanceFormValues) => { const handleFinish = async (values: InstanceFormValues) => {
const selectedKeys = (values.spec as any)?.sshPublicKeys as
| string[]
| undefined;
const volume = values.spec?.volume ?? {};
const normalizedVolume = volume.persistentTemplate
? {
persistentTemplate: {
...volume.persistentTemplate,
name: volume.persistentTemplate.name || values.name || ''
}
}
: volume.persistent
? { persistent: volume.persistent }
: volume.ephemeral
? { ephemeral: volume.ephemeral }
: {};
// `spec.type` and `clusterId` are kept in sync with the resolved
// candidate via `resolveAndApply`, so submission is a straight pass
// -through. The API expects `accelerator` as a string per the schema.
const acceleratorValue = values.spec?.resources?.accelerator;
const normalizedResources = {
...values.spec?.resources,
accelerator:
acceleratorValue != null && acceleratorValue !== ''
? String(acceleratorValue)
: undefined
};
const submittedPorts = [...(values.spec?.ports ?? [])];
const submittedHasSSHPort = submittedPorts.some(
(item: any) => item?.protocol === 'TCP' && item?.port === SSH_PORT
);
if (values.enable_ssh && !submittedHasSSHPort) {
submittedPorts.push({
protocol: 'TCP',
port: SSH_PORT,
name: 'SSH'
});
}
await onFinish({ await onFinish({
...values, ...values,
spec: { spec: {
...values.spec, ...values.spec,
sshPublicKey: { name: sshKeyData?.name } ports: submittedPorts,
resources: normalizedResources,
volume: normalizedVolume,
sshPublicKeys: values.enable_ssh
? (selectedKeys ?? []).map((name) => ({ name }))
: []
} }
}); });
}; };
const segmentedTop = const segmentedTop =
action === PageAction.VIEW action === PageAction.EDIT
? { ? {
top: 0, top: 0,
offsetTop: 100 offsetTop: 100
@@ -282,7 +406,11 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
setFieldsValue: (values: Partial<InstanceFormValues>) => { setFieldsValue: (values: Partial<InstanceFormValues>) => {
form.setFieldsValue(values as any); form.setFieldsValue(values as any);
}, },
getFieldsValue: () => form.getFieldsValue() getFieldsValue: () => form.getFieldsValue(),
applyInstanceType: (instanceType: InstanceTypeItem) => {
const count = instanceType.spec?.acceleratable ? 1 : 0;
resolveAndApply(instanceType, count, { writeAccelerator: true });
}
})); }));
const handleAddSSHKey = () => { const handleAddSSHKey = () => {
@@ -319,7 +447,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
env: [], env: [],
volumeMount: '', volumeMount: '',
resources: { resources: {
accelerator: '1' accelerator: 1
}, },
volume: { volume: {
ephemeral: { ephemeral: {
@@ -334,6 +462,11 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
}} }}
> >
<Basic action={formAction} disabled={disabled} /> <Basic action={formAction} disabled={disabled} />
{/* Hidden form field: the candidate.cluster resolved from the
selected aggregate + accelerator count. */}
<Form.Item name="clusterId" hidden>
<CInput.Input />
</Form.Item>
<CollapsePanel <CollapsePanel
activeKey={collapseKeys} activeKey={collapseKeys}
accordion={false} accordion={false}
@@ -349,6 +482,9 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
<InstanceTypeFormItem <InstanceTypeFormItem
action={formAction} action={formAction}
disabled={disabled} disabled={disabled}
selectedInstanceType={selectedInstanceType}
currentData={currentData as any}
onGPUCountChange={handleAcceleratorChange}
/> />
) )
}, },
@@ -392,13 +528,12 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
label={intl.formatMessage({ label={intl.formatMessage({
id: 'gpuservice.instance.ssh.enable' id: 'gpuservice.instance.ssh.enable'
})} })}
onChange={handleSSHEnableChange}
></CheckboxField> ></CheckboxField>
</Form.Item> </Form.Item>
{sshEnabled && ( {sshEnabled && (
<div className={instanceStyles.sshkeySelection}> <div className={instanceStyles.sshkeySelection}>
<Form.Item<FormData> <Form.Item<FormData>
name={['spec', 'sshPublicKey', 'name']} name={['spec', 'sshPublicKeys']}
style={{ style={{
marginBottom: 12 marginBottom: 12
}} }}
@@ -425,7 +560,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
width: '100%' width: '100%'
} }
}} }}
options={[]} options={sshkeyOptions}
notFoundContent={ notFoundContent={
<Empty <Empty
image={Empty.PRESENTED_IMAGE_SIMPLE} image={Empty.PRESENTED_IMAGE_SIMPLE}
@@ -2,16 +2,17 @@ import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import NumberSelection from '@/pages/_components/number-selection'; import NumberSelection from '@/pages/_components/number-selection';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Alert, Form } from 'antd'; import { Alert, Flex, Form } from 'antd';
import { useEffect, useMemo } from 'react'; import _ from 'lodash';
import { useMemo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import InstanceTypeItem from '../components/instance-type-item'; import InstanceTypeItem from '../components/instance-type-item';
import { InstanceTypePhaseValueMap } from '../config'; import { getAcceleratorMax } from '../config';
import { import {
FormData, FormData,
InstanceTypeItem as InstanceTypeItemModel InstanceTypeItem as InstanceTypeItemModel,
ListItem
} from '../config/types'; } from '../config/types';
import useQueryInstanceTypes from '../services/use-query-instance-types';
const FieldBlock = styled.div` const FieldBlock = styled.div`
margin-bottom: 24px; margin-bottom: 24px;
@@ -25,24 +26,22 @@ const SelectedCard = styled.div`
`; `;
interface InstanceTypePickerProps { interface InstanceTypePickerProps {
value?: string; selectedInstanceType?: InstanceTypeItemModel;
dataList: InstanceTypeItemModel[];
} }
const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({ const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
value, selectedInstanceType
dataList
}) => { }) => {
const selected = useMemo(() => { const intl = useIntl();
return (
dataList.find((item) => (item.metadata?.name || '') === value) ||
({} as InstanceTypeItemModel)
);
}, [value, dataList]);
return ( return (
<SelectedCard> <SelectedCard>
<InstanceTypeItem item={selected} showStatus={false} /> {selectedInstanceType ? (
<InstanceTypeItem item={selectedInstanceType} showStatus={false} />
) : (
<span style={{ color: 'var(--ant-color-text-tertiary)' }}>
{intl.formatMessage({ id: 'gpuservice.instance.type.required' })}
</span>
)}
</SelectedCard> </SelectedCard>
); );
}; };
@@ -50,56 +49,62 @@ const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
interface InstanceTypeFormItemProps { interface InstanceTypeFormItemProps {
action: PageActionType; action: PageActionType;
disabled?: boolean; disabled?: boolean;
currentData?: ListItem;
selectedInstanceType?: InstanceTypeItemModel;
onGPUCountChange?: (value: number) => void;
} }
const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
action, action,
disabled disabled,
currentData,
selectedInstanceType,
onGPUCountChange
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const form = Form.useFormInstance<FormData>();
const typeName = Form.useWatch(['spec', 'type'], form) as string | undefined;
const { detailData, fetchData } = useQueryInstanceTypes(); const maxGpuCount =
action === PageAction.EDIT
? _.toNumber(currentData?.spec?.resources?.accelerator) || 0
: getAcceleratorMax(selectedInstanceType?.status?.acceleratorTiers);
useEffect(() => { const handleOnGPUCountChange = (value: number) => {
fetchData({}); onGPUCountChange?.(value);
}, [fetchData]); };
const dataList = detailData?.items || []; const showGPUCount = useMemo(() => {
if (action === PageAction.EDIT) {
return _.toNumber(currentData?.spec?.resources?.accelerator) > 0;
}
return selectedInstanceType?.spec?.acceleratable;
}, [selectedInstanceType, action]);
const selectedInstanceType = useMemo(() => { const selectedInstanceData = useMemo(() => {
return dataList.find((item) => (item.metadata?.name || '') === typeName); if (action === PageAction.EDIT) {
}, [dataList, typeName]); return JSON.parse(currentData?.description || '{}') || {};
}
return selectedInstanceType;
}, [selectedInstanceType, action, currentData]);
const maxGpuCount = useMemo(() => { const renderInstanceType = () => {
const onceMaxRequest = const description = JSON.parse(currentData?.description || '{}').spec || {};
selectedInstanceType?.status?.accelerator?.onceMaxRequest; return (
const num = onceMaxRequest ? Number(onceMaxRequest) : 0; <SelectedCard>
return num; <Flex align="flex-start" orientation="vertical">
}, [selectedInstanceType]); <span
style={{
useEffect(() => { color: 'var(--ant-color-text)',
if (!typeName && dataList.length > 0) { fontWeight: 400
const defaultType = dataList.find( }}
(item) => item.status?.phase === InstanceTypePhaseValueMap.Active >
{description.manufacturer
? `${description.product} x ${currentData?.spec?.resources?.accelerator}`
: ''}
</span>
</Flex>
</SelectedCard>
); );
if (defaultType) { };
form.setFieldValue(['spec', 'type'], defaultType.metadata?.name || '');
}
return;
}
const currentAccelerator =
Number(form.getFieldValue(['spec', 'resources', 'accelerator'])) || 1;
if (currentAccelerator > maxGpuCount) {
form.setFieldValue(['spec', 'resources', 'accelerator'], maxGpuCount);
}
if (currentAccelerator < 1) {
form.setFieldValue(['spec', 'resources', 'accelerator'], 1);
}
}, [form, typeName, maxGpuCount, dataList]);
return ( return (
<div data-field="instanceType"> <div data-field="instanceType">
@@ -115,17 +120,22 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
} }
]} ]}
> >
<InstanceTypePicker dataList={dataList} value={typeName} /> {action === PageAction.CREATE && (
<InstanceTypePicker selectedInstanceType={selectedInstanceType} />
)}
{action === PageAction.EDIT && renderInstanceType()}
</Form.Item> </Form.Item>
</FieldBlock> </FieldBlock>
{selectedInstanceType?.spec?.acceleratable && ( {showGPUCount && (
<Form.Item <Form.Item<FormData>
name={['spec', 'resources', 'accelerator']} name={['spec', 'resources', 'accelerator']}
hidden={action === PageAction.EDIT}
rules={[ rules={[
{ {
required: true, required: true,
validator: (_, value) => { validator: (_, value) => {
if (value > maxGpuCount) { const num = Number(value);
if (num > maxGpuCount) {
return Promise.reject( return Promise.reject(
new Error( new Error(
intl.formatMessage( intl.formatMessage(
@@ -135,16 +145,12 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
) )
); );
} }
if (value < 1) { if (num < 1) {
return Promise.reject( return Promise.reject(
new Error( new Error(
intl.formatMessage( intl.formatMessage(
{ { id: 'gpuservice.instance.gpuCount.min' },
id: 'gpuservice.instance.gpuCount.min' { count: 1 }
},
{
count: 1
}
) )
) )
); );
@@ -156,12 +162,14 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
> >
<NumberSelection <NumberSelection
min={1} min={1}
onChange={handleOnGPUCountChange}
max={maxGpuCount} max={maxGpuCount}
step={1} step={1}
required required
disabled={disabled || action === PageAction.EDIT} disabled={disabled || action === PageAction.EDIT}
labelExtra={ labelExtra={
!maxGpuCount && ( !maxGpuCount &&
action !== PageAction.EDIT && (
<Alert <Alert
showIcon showIcon
type="warning" type="warning"
@@ -8,14 +8,12 @@ import GPUServiceStorageForm from '../../storage/forms';
interface StorageOverlayProps { interface StorageOverlayProps {
open: boolean; open: boolean;
namespace?: string;
onCancel: () => void; onCancel: () => void;
onSubmit: (values: StorageFormData) => Promise<void> | void; onSubmit: (values: StorageFormData) => Promise<void> | void;
} }
const StorageOverlay: React.FC<StorageOverlayProps> = ({ const StorageOverlay: React.FC<StorageOverlayProps> = ({
open, open,
namespace,
onCancel, onCancel,
onSubmit onSubmit
}) => { }) => {
@@ -66,7 +64,6 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
ref={formRef} ref={formRef}
action={PageAction.CREATE} action={PageAction.CREATE}
open={open} open={open}
namespace={namespace}
onFinish={handleFinish} onFinish={handleFinish}
/> />
</FormOverlayView> </FormOverlayView>
@@ -1,31 +1,36 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { getCurrentOrgNamespace } from '@/atoms/user';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { import {
Input as CInput, Input as CInput,
InputNumber as CInputNumber, InputNumber as CInputNumber,
CheckboxField,
LabelInfo, LabelInfo,
Select, Select,
useAppUtils useAppUtils
} from '@gpustack/core-ui'; } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Flex, Form, Radio } from 'antd'; import { Button, Flex, Form, Radio } from 'antd';
import { useAtomValue } from 'jotai';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import { FormData as StorageFormData } from '../../storage/config/types'; import { FormData as StorageFormData } from '../../storage/config/types';
import useCreateStorage from '../../storage/services/use-create-storage'; import useCreateStorage from '../../storage/services/use-create-storage';
import useQueryStorage from '../../storage/services/use-query-storage'; import useQueryStorage from '../../storage/services/use-query-storage';
import { StorageModeValueMap } from '../config'; import useQueryStorageTypes from '../../storage-types/services/use-query-storage-types';
import { DEFAULT_PV_CAPACITY_GB, StorageModeValueMap } from '../config';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
import StorageOverlay from './storage-overlay'; import StorageOverlay from './storage-overlay';
const FieldBlock = styled.div`
margin-bottom: 24px;
`;
const DEFAULT_TEMP_CAPACITY_GB = 50; const DEFAULT_TEMP_CAPACITY_GB = 50;
const detectMode = (volume?: FormData['spec']['volume']) => {
if (volume?.persistentTemplate) {
return { mode: StorageModeValueMap.Persistent, releaseWithInstance: true };
}
if (volume?.persistent) {
return { mode: StorageModeValueMap.Persistent, releaseWithInstance: false };
}
return { mode: StorageModeValueMap.Temporary, releaseWithInstance: false };
};
const StorageVolume = ({ const StorageVolume = ({
disabled, disabled,
action action
@@ -35,50 +40,86 @@ const StorageVolume = ({
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const { getRuleMessage } = useAppUtils(); const { getRuleMessage } = useAppUtils();
const { fetchData: createStorage } = useCreateStorage(); const form = Form.useFormInstance<FormData>();
const { detailData: storageData, fetchData: fetchStorage } = const currentVolume = Form.useWatch(['spec', 'volume'], form);
useQueryStorage();
const [overlayOpen, setOverlayOpen] = useState(false);
const [storageMode, setStorageMode] = useState<string>( const [storageMode, setStorageMode] = useState<string>(
StorageModeValueMap.Temporary StorageModeValueMap.Temporary
); );
const [releaseWithInstance, setReleaseWithInstance] = useState<boolean>(false);
const form = Form.useFormInstance<FormData>();
const currentCluster = useAtomValue(currentClusterAtom);
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
useEffect(() => { useEffect(() => {
fetchStorage({}); if (!currentVolume) return;
const detected = detectMode(currentVolume);
setStorageMode(detected.mode);
setReleaseWithInstance(detected.releaseWithInstance);
}, [currentVolume]);
const { fetchData: createStorage } = useCreateStorage();
const { detailData: storageData, fetchData: fetchStorage } =
useQueryStorage();
const { detailData: storageTypesData, fetchData: fetchStorageTypes } =
useQueryStorageTypes();
const [overlayOpen, setOverlayOpen] = useState(false);
useEffect(() => {
fetchStorage({ page: 1, perPage: 100 });
fetchStorageTypes({ page: 1, perPage: 100 });
}, []); }, []);
const storageOptions = useMemo( const storageOptions = useMemo(
() => () =>
(storageData?.items || []).map((item) => ({ (storageData?.items || []).map((item) => ({
label: `${item.metadata?.name} / ${item.spec?.capacity ?? '-'}`, label: `${item.name} / ${item.spec?.capacity ?? '-'}`,
value: item.metadata?.name value: item.name
})), })),
[storageData] [storageData]
); );
const handleModeChange = (mode: string) => { const storageTypeOptions = useMemo(
console.log('selected storage mode', mode); () =>
if (mode === StorageModeValueMap.Existing) { (storageTypesData?.items || []).map((item) => ({
form.setFieldValue(['spec', 'volume'], { persistent: { name: '' } }); label: item.displayName || item.name,
} else { value: item.name
})),
[storageTypesData]
);
const applyMode = (mode: string, release: boolean) => {
if (mode === StorageModeValueMap.Temporary) {
form.setFieldValue(['spec', 'volume'], { form.setFieldValue(['spec', 'volume'], {
ephemeral: { capacity: `${DEFAULT_TEMP_CAPACITY_GB}Gi` } ephemeral: { capacity: `${DEFAULT_TEMP_CAPACITY_GB}Gi` }
}); });
return;
} }
if (release) {
form.setFieldValue(['spec', 'volume'], {
persistentTemplate: {
spec: { type: '', capacity: `${DEFAULT_PV_CAPACITY_GB}Gi` },
releaseWithInstance: true
}
});
return;
}
form.setFieldValue(['spec', 'volume'], { persistent: { name: '' } });
};
const handleModeChange = (mode: string) => {
setStorageMode(mode); setStorageMode(mode);
applyMode(mode, releaseWithInstance);
};
const handleReleaseChange = (e: any) => {
const next = !!e?.target?.checked;
setReleaseWithInstance(next);
applyMode(storageMode, next);
}; };
const handleCreateStorage = async (values: StorageFormData) => { const handleCreateStorage = async (values: StorageFormData) => {
try { try {
await createStorage({ data: values }); await createStorage({ data: values });
await fetchStorage({}); await fetchStorage({ page: 1, perPage: 100 });
form.setFieldValue(['spec', 'volume'], { form.setFieldValue(['spec', 'volume'], {
persistent: { name: values.metadata.name } persistent: { name: values.name }
}); });
setOverlayOpen(false); setOverlayOpen(false);
} catch (error) { } catch (error) {
@@ -89,13 +130,6 @@ const StorageVolume = ({
return ( return (
<> <>
<div data-field="storage"></div> <div data-field="storage"></div>
<Flex
style={{
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 6
}}
>
<Radio.Group <Radio.Group
disabled={disabled} disabled={disabled}
style={{ marginBottom: 12 }} style={{ marginBottom: 12 }}
@@ -134,45 +168,10 @@ const StorageVolume = ({
} }
/> />
), ),
value: StorageModeValueMap.Existing value: StorageModeValueMap.Persistent
} }
]} ]}
/> />
{action === PageAction.CREATE &&
storageMode === StorageModeValueMap.Existing && (
<Button
type="link"
size="small"
style={{ marginBottom: 6 }}
onClick={() => setOverlayOpen(true)}
>
{intl.formatMessage({ id: 'gpuservice.storage.add' })}
</Button>
)}
</Flex>
{storageMode === StorageModeValueMap.Existing && (
<Form.Item
name={['spec', 'volume', 'persistent', 'name']}
rules={[
{
required: true,
message: intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.required'
})
}
]}
>
<Select
disabled={disabled}
label={intl.formatMessage({
id: 'gpuservice.storage.persistentVolume'
})}
required
options={storageOptions}
/>
</Form.Item>
)}
{storageMode === StorageModeValueMap.Temporary && ( {storageMode === StorageModeValueMap.Temporary && (
<Form.Item <Form.Item
@@ -203,6 +202,112 @@ const StorageVolume = ({
/> />
</Form.Item> </Form.Item>
)} )}
{storageMode === StorageModeValueMap.Persistent && (
<>
<Flex
style={{
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12
}}
>
<CheckboxField
disabled={disabled}
checked={releaseWithInstance}
onChange={handleReleaseChange}
label={intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.releaseWithInstance'
})}
/>
{action === PageAction.CREATE && !releaseWithInstance && (
<Button
type="link"
size="small"
onClick={() => setOverlayOpen(true)}
>
{intl.formatMessage({ id: 'gpuservice.storage.add' })}
</Button>
)}
</Flex>
{releaseWithInstance ? (
<>
<Form.Item
name={['spec', 'volume', 'persistentTemplate', 'spec', 'type']}
rules={[
{
required: true,
message: getRuleMessage('select', 'gpuservice.storageType')
}
]}
>
<Select
required
disabled={disabled}
label={intl.formatMessage({ id: 'gpuservice.storageType' })}
options={storageTypeOptions}
/>
</Form.Item>
<Form.Item
name={[
'spec',
'volume',
'persistentTemplate',
'spec',
'capacity'
]}
getValueProps={(val) => ({
value: val
? Number(String(val).replace(/Gi$/i, '')) || undefined
: undefined
})}
normalize={(val) => (val ? `${val}Gi` : '')}
rules={[
{
required: true,
message: intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.capacity.required'
})
}
]}
>
<CInputNumber
required
disabled={disabled}
min={1}
precision={0}
label={intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.capacity'
})}
/>
</Form.Item>
</>
) : (
<Form.Item
name={['spec', 'volume', 'persistent', 'name']}
rules={[
{
required: true,
message: intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.required'
})
}
]}
>
<Select
disabled={disabled}
label={intl.formatMessage({
id: 'gpuservice.storage.persistentVolume'
})}
required
options={storageOptions}
/>
</Form.Item>
)}
</>
)}
<Form.Item<FormData> <Form.Item<FormData>
name={['spec', 'volumeMount']} name={['spec', 'volumeMount']}
rules={[ rules={[
@@ -226,7 +331,6 @@ const StorageVolume = ({
<StorageOverlay <StorageOverlay
open={overlayOpen} open={overlayOpen}
namespace={namespace}
onCancel={() => setOverlayOpen(false)} onCancel={() => setOverlayOpen(false)}
onSubmit={handleCreateStorage} onSubmit={handleCreateStorage}
/> />
@@ -34,7 +34,7 @@ type ConnectEntry =
const getConnectEntries = (record: ListItem): ConnectEntry[] => { const getConnectEntries = (record: ListItem): ConnectEntry[] => {
const ip = record.status?.hostIPs?.[0]?.ip; const ip = record.status?.hostIPs?.[0]?.ip;
const ports = record.status?.ports || []; const ports = record.status?.ports || [];
const hasSshKey = !!record.spec?.sshPublicKey?.name; const hasSshKey = !!record.spec?.sshPublicKeys?.length;
const configPorts = record.spec?.ports || []; const configPorts = record.spec?.ports || [];
if (!ip) { if (!ip) {
@@ -66,6 +66,27 @@ const getConnectEntries = (record: ListItem): ConnectEntry[] => {
}); });
}; };
const renderInstanceType = (record: ListItem) => {
const description = JSON.parse(record?.description || '{}').spec || {};
return (
<Flex align="flex-start" orientation="vertical">
<span
style={{
color: 'var(--ant-color-text)',
fontWeight: 400
}}
>
{description.manufacturer
? `${description.product} x ${record.spec?.resources?.accelerator}`
: ''}
</span>
<span className="text-tertiary">
{`${record.spec.resources?.cpu}C / RAM: ${record.spec.resources?.ram?.replace('Gi', 'GB')} / Disk: ${record.spec.resources?.localStorage?.replace('Gi', 'GB')}`}{' '}
</span>
</Flex>
);
};
interface ColumnsHookProps { interface ColumnsHookProps {
handleSelect: (val: string, record: ListItem) => void; handleSelect: (val: string, record: ListItem) => void;
sortOrder: string[]; sortOrder: string[];
@@ -80,15 +101,15 @@ const useInstancesColumns = ({
return [ return [
{ {
title: intl.formatMessage({ id: 'common.table.name' }), title: intl.formatMessage({ id: 'common.table.name' }),
dataIndex: ['metadata', 'name'], dataIndex: 'name',
key: 'name', key: 'name',
sorter: false, sorter: false,
ellipsis: { ellipsis: {
showTitle: false showTitle: false
}, },
render: (text: string) => ( render: (text: string, record: ListItem) => (
<AutoTooltip ghost style={{ maxWidth: 360 }}> <AutoTooltip ghost style={{ maxWidth: 360 }}>
<span className="text-primary">{text}</span> <span className="text-primary">{text || record.displayName}</span>
</AutoTooltip> </AutoTooltip>
) )
}, },
@@ -198,40 +219,19 @@ const useInstancesColumns = ({
ellipsis: { ellipsis: {
showTitle: false showTitle: false
}, },
width: 230, render: (_text: string, record: ListItem) => (
render: (text: string) => (
<AutoTooltip <AutoTooltip
ghost ghost
style={{ maxWidth: 360, color: 'var(--ant-color-text) !important' }} style={{ maxWidth: 360, color: 'var(--ant-color-text) !important' }}
> >
<Flex align="center"> {renderInstanceType(record)}
<span
style={{
fontWeight: 400
}}
>
NVIDIA A100 (x2)
</span>
<span
className="text-secondary"
style={{
display: 'flex',
width: 4,
height: 4,
borderRadius: '50%',
backgroundColor: 'var(--ant-color-text-secondary)',
margin: '0 8px'
}}
></span>
<span className="text-tertiary">4C / 16GB</span>
</Flex>
</AutoTooltip> </AutoTooltip>
) )
}, },
{ {
title: intl.formatMessage({ id: 'common.table.createTime' }), title: intl.formatMessage({ id: 'common.table.createTime' }),
dataIndex: ['metadata', 'creationTimestamp'], dataIndex: 'created_at',
key: 'creationTimestamp', key: 'created_at',
sorter: false, sorter: false,
ellipsis: { ellipsis: {
showTitle: false showTitle: false
@@ -1,14 +1,9 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { getCurrentOrgNamespace } from '@/atoms/user';
import { useAtomValue } from 'jotai';
import { useState } from 'react'; import { useState } from 'react';
import { ListItem } from '../config/types'; import { ListItem } from '../config/types';
// View events still hits the K8s proxy. clusterID and namespace are taken
// from the row itself (`row.clusterId`, `row.status.namespace`).
const useViewEvents = () => { const useViewEvents = () => {
const currentCluster = useAtomValue(currentClusterAtom);
const clusterID = currentCluster?.id;
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
const [openModalStatus, setOpenModalStatus] = useState<{ const [openModalStatus, setOpenModalStatus] = useState<{
open: boolean; open: boolean;
name: string; name: string;
@@ -22,13 +17,11 @@ const useViewEvents = () => {
}); });
const openModal = (row?: ListItem) => { const openModal = (row?: ListItem) => {
const name = row?.metadata?.name || '';
const rowNamespace = row?.metadata?.namespace || namespace;
setOpenModalStatus({ setOpenModalStatus({
open: true, open: true,
name, name: row?.name || '',
namespace: rowNamespace, namespace: row?.status?.namespace || '',
clusterID clusterID: row?.clusterId ?? undefined
}); });
}; };
@@ -1,15 +1,11 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { getCurrentOrgNamespace } from '@/atoms/user';
import { useAtomValue } from 'jotai';
import { useState } from 'react'; import { useState } from 'react';
import { GPU_SERVICE_INSTANCES_LOG_API } from '../apis'; import { GPU_SERVICE_INSTANCES_LOG_API } from '../apis';
import { ListItem } from '../config/types'; import { ListItem } from '../config/types';
// View logs still hits the K8s proxy. clusterID and namespace are taken
// from the row itself (`row.clusterId`, `row.status.namespace`) since the
// page no longer has a global cluster selector.
const useViewLogs = () => { const useViewLogs = () => {
const currentCluster = useAtomValue(currentClusterAtom);
const clusterID = currentCluster?.id;
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
const [openModalStatus, setOpenModalStatus] = useState<{ const [openModalStatus, setOpenModalStatus] = useState<{
open: boolean; open: boolean;
url: string; url: string;
@@ -23,17 +19,14 @@ const useViewLogs = () => {
}); });
const openModal = (row?: ListItem) => { const openModal = (row?: ListItem) => {
const name = row?.metadata?.name; const name = row?.name;
const rowNamespace = row?.metadata?.namespace || namespace; const namespace = row?.status?.namespace;
const clusterID = row?.clusterId ?? undefined;
setOpenModalStatus({ setOpenModalStatus({
open: true, open: true,
url: url:
name && clusterID name && namespace && clusterID
? `${GPU_SERVICE_INSTANCES_LOG_API({ ? GPU_SERVICE_INSTANCES_LOG_API({ namespace, clusterID, name })
namespace: rowNamespace,
clusterID,
name
})}`
: '', : '',
tail: 1000, tail: 1000,
status: row?.status?.phase || undefined status: row?.status?.phase || undefined
+28 -301
View File
@@ -1,14 +1,8 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { getCurrentOrgNamespace } from '@/atoms/user';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings'; import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
import useTableFetch from '@/hooks/use-table-fetch'; import useTableFetch from '@/hooks/use-table-fetch';
import { ProviderValueMap } from '@/pages/cluster-management/config';
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
import { handleBatchRequest } from '@/utils';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { import {
BaseSelect,
DeleteModal, DeleteModal,
DropdownButtons, DropdownButtons,
FilterBar, FilterBar,
@@ -17,26 +11,13 @@ import {
} from '@gpustack/core-ui'; } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { import { Button, ConfigProvider, message, Modal, Space, Table } from 'antd';
Button,
ConfigProvider,
Divider,
Flex,
message,
Modal,
Space,
Table
} from 'antd';
import { useAtom } from 'jotai';
import _ from 'lodash'; import _ from 'lodash';
import { useCallback, useEffect, useMemo } from 'react'; import PageBox from '../../_components/page-box';
import { PageContainerInner } from '../../_components/page-box';
import { import {
deleteGPUServiceInstance, deleteGPUServiceInstance,
GPU_SERVICE_INSTANCES_API, GPU_SERVICE_INSTANCES_API,
queryGPUServiceInstances, queryGPUServiceInstances
startGPUServiceInstance,
stopGPUServiceInstance
} from './apis'; } from './apis';
import AddModal from './components/add-modal'; import AddModal from './components/add-modal';
import ViewEventsModal from './components/view-events-modal'; import ViewEventsModal from './components/view-events-modal';
@@ -52,72 +33,7 @@ import useUpdateInstance from './services/use-update-instance';
const GPUService: React.FC = () => { const GPUService: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const [modal, modalContextHolder] = Modal.useModal(); const [, modalContextHolder] = Modal.useModal();
const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom);
const clusterID = currentCluster?.id;
// In admin "All" view there's no Org context, so the helper falls
// back to the selected cluster's owner Org name.
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
const deleteInstance = useCallback(
(id: number) => deleteGPUServiceInstance({ namespace, clusterID, id }),
[namespace, clusterID]
);
const startInstance = useCallback(
(row: ListItem) =>
startGPUServiceInstance({
namespace: row.metadata?.namespace || namespace,
clusterID,
name: row.metadata?.name as string
}),
[namespace, clusterID]
);
const stopInstance = useCallback(
(row: ListItem) =>
stopGPUServiceInstance({
namespace: row.metadata?.namespace || namespace,
clusterID,
name: row.metadata?.name as string
}),
[namespace, clusterID]
);
const fetchInstances = useMemoizedFn(
async (
params: any,
options?: any
): Promise<Global.PageResponse<ListItem>> => {
const effectiveClusterID = params.cluster_id ?? clusterID;
if (!effectiveClusterID) {
return {
items: [],
pagination: {
total: 0,
totalPage: 0,
page: 1,
perPage: params.perPage || 10
}
} as Global.PageResponse<ListItem>;
}
const res = await queryGPUServiceInstances(
{ ...params, namespace, clusterID: effectiveClusterID },
options
);
const total = res?.items?.length ?? 0;
const perPage = params.perPage || 10;
return {
items: res?.items ?? [],
pagination: {
total,
totalPage: Math.ceil(total / perPage),
page: params.page || 1,
perPage
}
};
}
);
const { const {
dataSource, dataSource,
@@ -130,16 +46,14 @@ const GPUService: React.FC = () => {
fetchData, fetchData,
handlePageChange, handlePageChange,
handleTableChange, handleTableChange,
handleQueryChange,
handleSearch, handleSearch,
handleNameChange handleNameChange
} = useTableFetch<ListItem>({ } = useTableFetch<ListItem>({
key: PaginationKey.Instances, key: PaginationKey.Instances,
fetchAPI: fetchInstances, fetchAPI: queryGPUServiceInstances,
deleteAPI: deleteInstance, deleteAPI: deleteGPUServiceInstance,
watch: false, watch: true,
polling: true, API: GPU_SERVICE_INSTANCES_API,
API: GPU_SERVICE_INSTANCES_API({ namespace, clusterID }),
contentForDelete: intl.formatMessage({ id: 'gpuservice.instance' }) contentForDelete: intl.formatMessage({ id: 'gpuservice.instance' })
}); });
@@ -160,55 +74,11 @@ const GPUService: React.FC = () => {
closeViewEventsModal, closeViewEventsModal,
openViewEventsModalStatus openViewEventsModalStatus
} = useViewEvents(); } = useViewEvents();
const {
fetchClusterList,
cancelRequest: cancelClusterRequest,
clusterList
} = useQueryClusterList();
const k8sClusterList = useMemo(
() =>
clusterList.filter(
(item) => item.provider === ProviderValueMap.Kubernetes
),
[clusterList]
);
useEffect(() => {
fetchClusterList({ page: -1 }).then((clusters) => {
const k8sClusters = clusters.filter(
(item: any) => item.provider === ProviderValueMap.Kubernetes
);
if (k8sClusters.length === 0) {
return;
}
const storedCluster = k8sClusters.find(
(item: any) => item.id === currentCluster?.id
);
const targetCluster = storedCluster ?? k8sClusters[0];
if (!storedCluster) {
setCurrentCluster({
...targetCluster,
label: targetCluster.name,
value: targetCluster.id
});
}
handleQueryChange({
cluster_id: targetCluster.id,
page: 1
});
});
return () => {
cancelClusterRequest();
};
}, []);
const handleModalOk = async (data: FormData) => { const handleModalOk = async (data: FormData) => {
try { try {
if (openInstanceModalStatus.realAction === PageAction.CREATE) { if (openInstanceModalStatus.realAction === PageAction.CREATE) {
await deleteInstance( await deleteGPUServiceInstance(openInstanceModalStatus.currentData!.id);
openInstanceModalStatus.currentData!.metadata?.name as any
);
await new Promise((resolve) => { await new Promise((resolve) => {
setTimeout(resolve, 500); setTimeout(resolve, 500);
}); });
@@ -226,161 +96,34 @@ const GPUService: React.FC = () => {
closeInstanceModal(); closeInstanceModal();
message.success(intl.formatMessage({ id: 'common.message.success' })); message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) { } catch (error) {
message.error(intl.formatMessage({ id: 'common.message.fail' })); // ignore
} }
}; };
const handleRecreate = useMemoizedFn((row: ListItem) => {
modalRef.current?.show({
title: intl.formatMessage({
id: 'gpuservice.instance.recreate.confirm.title'
}),
content: 'gpuservice.instance.recreate.confirm.content',
okText: 'common.button.recreate',
operation: 'gpuservice.instance.recreate.confirm.content',
name: row.metadata?.name,
onOk: async () => {
try {
await deleteInstance(row.metadata?.name as any);
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
await createInstance({
data: {
metadata: { name: row.metadata.name, namespace },
spec: row.spec
} as FormData
});
message.success(intl.formatMessage({ id: 'common.message.success' }));
handleSearch();
} catch (error) {
message.error(intl.formatMessage({ id: 'common.message.fail' }));
}
}
});
});
const handleStart = useMemoizedFn(async (row: ListItem) => {
modalRef.current?.show({
title: intl.formatMessage({ id: 'common.title.start.confirm' }),
content: 'gpuservice.instance',
okText: 'common.button.start',
operation: 'common.start.single.confirm',
name: row.metadata?.name,
async onOk() {
try {
await startInstance(row);
message.success(intl.formatMessage({ id: 'common.message.success' }));
fetchData();
} catch (error) {
// ignore
}
}
});
});
const handleStop = useMemoizedFn(async (row: ListItem) => {
modalRef.current?.show({
title: intl.formatMessage({ id: 'common.title.stop.confirm' }),
content: 'gpuservice.instance',
okText: 'common.button.stop',
operation: 'common.stop.single.confirm',
name: row.metadata?.name,
async onOk() {
try {
await stopInstance(row);
message.success(intl.formatMessage({ id: 'common.message.success' }));
fetchData();
} catch (error) {
// ignore
}
}
});
});
const handleStartBatch = useMemoizedFn(() => {
modalRef.current?.show({
title: intl.formatMessage({ id: 'common.title.start.confirm' }),
content: 'gpuservice.instance',
okText: 'common.button.start',
operation: 'common.start.confirm',
selection: true,
async onOk() {
const res = await handleBatchRequest(
rowSelection.selectedRows as ListItem[],
startInstance
);
fetchData();
return res;
}
});
});
const handleStopBatch = useMemoizedFn(() => {
modalRef.current?.show({
title: intl.formatMessage({ id: 'common.title.stop.confirm' }),
content: 'gpuservice.instance',
okText: 'common.button.stop',
operation: 'common.stop.confirm',
selection: true,
async onOk() {
const res = await handleBatchRequest(
rowSelection.selectedRows as ListItem[],
stopInstance
);
fetchData();
return res;
}
});
});
const handleSelect = useMemoizedFn((val: string, row: ListItem) => { const handleSelect = useMemoizedFn((val: string, row: ListItem) => {
if (val === 'view') { if (val === 'view') {
openViewInstanceModal(row); openViewInstanceModal(row);
} else if (val === 'edit') { } else if (val === 'edit') {
openEditInstanceModal(row); openEditInstanceModal(row);
} else if (val === 'delete') { } else if (val === 'delete') {
handleDelete({ handleDelete({ ...row, name: row.name });
...row,
name: row.metadata?.name,
id: row.metadata?.name as any
});
} else if (val === 'recreate') { } else if (val === 'recreate') {
// Keep handleRecreate above for the old confirm-modal flow. The current
// UX opens the editable form and recreates after submit.
openRecreateInstanceModal(row); openRecreateInstanceModal(row);
} else if (val === 'viewlog') { } else if (val === 'viewlog') {
openViewLogsModal(row); openViewLogsModal(row);
} else if (val === 'viewevent') { } else if (val === 'viewevent') {
openViewEventsModal(row); openViewEventsModal(row);
} else if (val === 'start') {
handleStart(row);
} else if (val === 'stop') {
handleStop(row);
} }
// start / stop are disabled until backend endpoints exist
}); });
const handleBatchActionSelect = useMemoizedFn((val: string) => { const handleBatchActionSelect = useMemoizedFn((val: string) => {
if (val === 'start') { if (val === 'delete') {
handleStartBatch();
} else if (val === 'stop') {
handleStopBatch();
} else if (val === 'delete') {
handleDeleteBatch(); handleDeleteBatch();
} }
// start / stop are disabled until backend endpoints exist
}); });
const handleClusterChange = (value: number) => {
const cluster = k8sClusterList.find((item) => item.value === value);
if (cluster) {
setCurrentCluster(cluster);
}
handleQueryChange({
cluster_id: value,
page: 1
});
};
const renderEmpty = (type?: string) => { const renderEmpty = (type?: string) => {
if (type !== 'Table') return; if (type !== 'Table') return;
return ( return (
@@ -389,7 +132,7 @@ const GPUService: React.FC = () => {
loadend={dataSource.loadend} loadend={dataSource.loadend}
dataSource={dataSource.dataList} dataSource={dataSource.dataList}
image={<IconFont type="icon-cloud-outlined" />} image={<IconFont type="icon-cloud-outlined" />}
filters={_.pick(queryParams, ['search', 'manufacturer'])} filters={_.pick(queryParams, ['search'])}
noFoundText={intl.formatMessage({ noFoundText={intl.formatMessage({
id: 'noresult.gpuservice.instance.nofound' id: 'noresult.gpuservice.instance.nofound'
})} })}
@@ -411,37 +154,13 @@ const GPUService: React.FC = () => {
}); });
return ( return (
<PageContainerInner <>
leftContent={ <PageBox>
<Flex align="center">
<span>{intl.formatMessage({ id: 'menu.gpuService.instances' })}</span>
<Divider
orientation="vertical"
style={{
marginLeft: 16
}}
/>
<BaseSelect
variant="borderless"
value={queryParams.cluster_id}
options={k8sClusterList}
onChange={handleClusterChange}
style={{ minWidth: 120, fontWeight: 500 }}
></BaseSelect>
</Flex>
}
>
<FilterBar <FilterBar
marginBottom={22} marginBottom={22}
marginTop={30} marginTop={30}
showSelect={false} showSelect={false}
selectOptions={k8sClusterList}
select={{ showSearch: true }}
selectHolder={intl.formatMessage({
id: 'gpuservice.instance.filter.cluster'
})}
handleSearch={handleSearch} handleSearch={handleSearch}
handleSelectChange={handleClusterChange}
handleInputChange={handleNameChange} handleInputChange={handleNameChange}
rowSelection={rowSelection} rowSelection={rowSelection}
widths={{ input: 300 }} widths={{ input: 300 }}
@@ -480,11 +199,19 @@ const GPUService: React.FC = () => {
}} }}
sortDirections={TABLE_SORT_DIRECTIONS} sortDirections={TABLE_SORT_DIRECTIONS}
showSorterTooltip={false} showSorterTooltip={false}
rowKey={(record) => record.metadata.name} rowKey={(record) => record.id}
onChange={handleTableChange} onChange={handleTableChange}
pagination={false} pagination={{
showSizeChanger: true,
pageSize: queryParams.perPage,
current: queryParams.page,
total: dataSource.total,
hideOnSinglePage: queryParams.perPage === 10,
onChange: handlePageChange
}}
/> />
</ConfigProvider> </ConfigProvider>
</PageBox>
<AddModal <AddModal
open={openInstanceModalStatus.open} open={openInstanceModalStatus.open}
action={openInstanceModalStatus.action} action={openInstanceModalStatus.action}
@@ -510,7 +237,7 @@ const GPUService: React.FC = () => {
/> />
<DeleteModal ref={modalRef} /> <DeleteModal ref={modalRef} />
{modalContextHolder} {modalContextHolder}
</PageContainerInner> </>
); );
}; };
@@ -1,9 +1,5 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { getCurrentOrgNamespace } from '@/atoms/user';
import { useQueryData } from '@gpustack/core-ui'; import { useQueryData } from '@gpustack/core-ui';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { apiVersion, KindMapping } from '../../constants';
import { createGPUServiceInstance } from '../apis'; import { createGPUServiceInstance } from '../apis';
import { FormData, ListItem } from '../config/types'; import { FormData, ListItem } from '../config/types';
@@ -12,31 +8,10 @@ interface CreateInstanceParams {
} }
export default function useCreateInstance() { export default function useCreateInstance() {
const currentCluster = useAtomValue(currentClusterAtom);
const clusterID = currentCluster?.id;
// Admin "All" view has no Org context — fall back to the
// selected cluster's owner Org name for the K8s namespace.
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
const fetchDetail = useCallback( const fetchDetail = useCallback(
(params: CreateInstanceParams, option?: any) => (params: CreateInstanceParams) =>
createGPUServiceInstance( createGPUServiceInstance({ data: params.data }),
{ []
namespace,
clusterID,
data: {
apiVersion,
kind: KindMapping.instance,
metadata: {
name: params.data.metadata.name,
namespace
},
spec: params.data.spec
}
},
option
),
[namespace, clusterID]
); );
const { detailData, loading, cancelRequest, fetchData } = useQueryData< const { detailData, loading, cancelRequest, fetchData } = useQueryData<
@@ -1,33 +1,20 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { getCurrentOrgNamespace } from '@/atoms/user';
import { useQueryData } from '@gpustack/core-ui'; import { useQueryData } from '@gpustack/core-ui';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { queryGPUServiceInstanceEvents } from '../apis'; import { queryGPUServiceInstanceEvents } from '../apis';
import { InstanceEvents } from '../config/types'; import { InstanceEvents } from '../config/types';
interface QueryInstanceEventsParams { interface QueryInstanceEventsParams {
name: string; name: string;
namespace: string;
clusterID?: number;
pretty?: string; pretty?: string;
} }
export default function useQueryInstanceEvents() { export default function useQueryInstanceEvents() {
const currentCluster = useAtomValue(currentClusterAtom);
const clusterID = currentCluster?.id;
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
const fetchDetail = useCallback( const fetchDetail = useCallback(
(params: QueryInstanceEventsParams, options?: any) => (params: QueryInstanceEventsParams, options?: any) =>
queryGPUServiceInstanceEvents( queryGPUServiceInstanceEvents(params, options),
{ []
namespace,
clusterID,
name: params.name,
pretty: params.pretty
},
options
),
[namespace, clusterID]
); );
const { detailData, loading, cancelRequest, fetchData } = useQueryData< const { detailData, loading, cancelRequest, fetchData } = useQueryData<
@@ -1,31 +1,19 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { getCurrentOrgNamespace } from '@/atoms/user';
import { useQueryData } from '@gpustack/core-ui'; import { useQueryData } from '@gpustack/core-ui';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { queryGPUServiceInstanceLog } from '../apis'; import { queryGPUServiceInstanceLog } from '../apis';
import { InstanceLog, InstanceLogQueryParams } from '../config/types'; import { InstanceLog, InstanceLogQueryParams } from '../config/types';
interface QueryInstanceLogParams extends InstanceLogQueryParams { interface QueryInstanceLogParams extends InstanceLogQueryParams {
name: string; name: string;
namespace: string;
clusterID?: number;
} }
export default function useQueryInstanceLog() { export default function useQueryInstanceLog() {
const currentCluster = useAtomValue(currentClusterAtom);
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
const clusterID = currentCluster?.id;
const fetchDetail = useCallback( const fetchDetail = useCallback(
(params: QueryInstanceLogParams, options?: any) => (params: QueryInstanceLogParams, options?: any) =>
queryGPUServiceInstanceLog( queryGPUServiceInstanceLog(params, options),
{ []
...params,
namespace,
clusterID
},
options
),
[namespace, clusterID]
); );
const { detailData, loading, cancelRequest, fetchData } = useQueryData< const { detailData, loading, cancelRequest, fetchData } = useQueryData<
@@ -1,32 +1,18 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { useQueryData } from '@gpustack/core-ui'; import { useQueryData } from '@gpustack/core-ui';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { queryGPUServiceInstanceTypes } from '../apis'; import { queryGPUServiceInstanceTypes } from '../apis';
import { transformInstanceType } from '../config';
import { InstanceTypeItem } from '../config/types'; import { InstanceTypeItem } from '../config/types';
export default function useQueryInstanceTypes() { export default function useQueryInstanceTypes() {
const currentCluster = useAtomValue(currentClusterAtom);
const clusterID = currentCluster?.id;
const fetchDetail = useCallback( const fetchDetail = useCallback(
async (params: Global.K8sSearchParams = {}, options?: any) => { (params: Global.SearchParams = { page: 1, perPage: 100 }, options?: any) =>
const res = await queryGPUServiceInstanceTypes( queryGPUServiceInstanceTypes(params, options),
{ ...params, clusterID }, []
options
);
return {
...res,
items: (res?.items || []).map(transformInstanceType)
};
},
[clusterID]
); );
const { detailData, loading, cancelRequest, fetchData } = useQueryData< const { detailData, loading, cancelRequest, fetchData } = useQueryData<
Global.K8sPageResponse<InstanceTypeItem>, Global.PageResponse<InstanceTypeItem>,
Global.K8sSearchParams Global.SearchParams
>({ >({
fetchDetail, fetchDetail,
key: 'instanceTypes' key: 'instanceTypes'
@@ -1,32 +1,19 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { getCurrentOrgNamespace } from '@/atoms/user';
import { useQueryData } from '@gpustack/core-ui'; import { useQueryData } from '@gpustack/core-ui';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { queryGPUServiceInstancePVEvents } from '../apis'; import { queryGPUServiceInstancePVEvents } from '../apis';
import { InstanceEvents } from '../config/types'; import { InstanceEvents } from '../config/types';
interface QueryVolumeEventsParams { interface QueryVolumeEventsParams {
name: string; name: string;
pretty?: string; namespace: string;
clusterID?: number;
} }
export default function useQueryVolumeEvents() { export default function useQueryVolumeEvents() {
const currentCluster = useAtomValue(currentClusterAtom);
const clusterID = currentCluster?.id;
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
const fetchDetail = useCallback( const fetchDetail = useCallback(
(params: QueryVolumeEventsParams, options?: any) => (params: QueryVolumeEventsParams, options?: any) =>
queryGPUServiceInstancePVEvents( queryGPUServiceInstancePVEvents(params, options),
{ []
namespace,
clusterID,
name: params.name
},
options
),
[namespace, clusterID]
); );
const { detailData, loading, cancelRequest, fetchData } = useQueryData< const { detailData, loading, cancelRequest, fetchData } = useQueryData<
@@ -1,44 +1,18 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { getCurrentOrgNamespace } from '@/atoms/user';
import { useQueryData } from '@gpustack/core-ui'; import { useQueryData } from '@gpustack/core-ui';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { apiVersion, KindMapping } from '../../constants';
import { updateGPUServiceInstance } from '../apis'; import { updateGPUServiceInstance } from '../apis';
import { FormData, ListItem } from '../config/types'; import { FormData, ListItem } from '../config/types';
interface UpdateInstanceParams { interface UpdateInstanceParams {
id: number; id: number;
data: FormData; data: Partial<FormData>;
} }
export default function useUpdateInstance() { export default function useUpdateInstance() {
const currentCluster = useAtomValue(currentClusterAtom);
const clusterID = currentCluster?.id;
// Admin "All" view has no Org context — fall back to the
// selected cluster's owner Org name for the K8s namespace.
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
const fetchDetail = useCallback( const fetchDetail = useCallback(
(params: UpdateInstanceParams, option?: any) => (params: UpdateInstanceParams) =>
updateGPUServiceInstance( updateGPUServiceInstance({ id: params.id, data: params.data }),
{ []
namespace,
clusterID,
id: params.id,
data: {
apiVersion,
kind: KindMapping.instance,
metadata: {
name: params.data.metadata.name,
namespace
},
spec: params.data.spec
}
},
option
),
[namespace, clusterID]
); );
const { detailData, loading, cancelRequest, fetchData } = useQueryData< const { detailData, loading, cancelRequest, fetchData } = useQueryData<
@@ -1,10 +1,13 @@
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import { FormData, ListItem } from '../types'; import { FormData, ListItem } from '../config/types';
export const GPU_SERVICE_PUBLIC_KEY_API = '/gpu-instance-ssh-public-keys/data'; export const GPU_SERVICE_PUBLIC_KEY_API = '/gpu-instance-ssh-public-keys';
export async function queryGPUServicePublicKeys(params: {}, options?: any) { export async function queryGPUServicePublicKeys(
return request<ListItem>(GPU_SERVICE_PUBLIC_KEY_API, { params: Global.SearchParams,
options?: any
) {
return request<Global.PageResponse<ListItem>>(GPU_SERVICE_PUBLIC_KEY_API, {
method: 'GET', method: 'GET',
params, params,
cancelToken: options?.token cancelToken: options?.token
@@ -18,8 +21,11 @@ export async function createGPUServicePublicKey(params: { data: FormData }) {
}); });
} }
export async function updateGPUServicePublicKey(params: { data: FormData }) { export async function updateGPUServicePublicKey(params: {
return request<ListItem>(`${GPU_SERVICE_PUBLIC_KEY_API}`, { id: number;
data: Omit<FormData, 'name'>;
}) {
return request<ListItem>(`${GPU_SERVICE_PUBLIC_KEY_API}/${params.id}`, {
method: 'PUT', method: 'PUT',
data: params.data data: params.data
}); });
@@ -0,0 +1,70 @@
import { PageActionType } from '@/config/types';
import { 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';
type AddPublicKeyModalProps = {
title: string;
action: PageActionType;
open: boolean;
onOk: (values: FormData) => void;
data?: ListItem | null;
onCancel: () => void;
};
const AddPublicKeyModal: React.FC<AddPublicKeyModalProps> = ({
title,
action,
open,
onOk,
data,
onCancel
}) => {
const form = useRef<any>(null);
const handleSubmit = () => {
form.current?.submit();
};
const handleCancel = () => {
form.current?.resetFields();
onCancel();
};
const onFinish = async (values: FormData) => {
onOk({ ...values });
};
return (
<FormDrawer
title={title}
open={open}
onCancel={handleCancel}
onSubmit={handleSubmit}
width={600}
footer={
<ModalFooter
onOk={handleSubmit}
onCancel={handleCancel}
style={{
padding: '16px 24px 8px',
display: 'flex',
justifyContent: 'flex-end'
}}
/>
}
>
<GPUServicePublicKeyForm
ref={form}
action={action}
currentData={data}
onFinish={onFinish}
open={open}
/>
</FormDrawer>
);
};
export default AddPublicKeyModal;
@@ -0,0 +1,23 @@
export interface FormData {
name: string;
owner_principal_id?: number | null;
displayName?: string | null;
description?: string | null;
spec: {
data: string;
};
}
export interface ListItem {
id: number;
created_at: string;
updated_at: string;
deleted_at?: string | null;
owner_principal_id?: number | null;
name?: string;
displayName?: string | null;
description?: string | null;
spec: {
data: string;
};
}
@@ -0,0 +1,69 @@
import { PageAction, validateLabelNameRegxFor63 } from '@/config';
import { Input as CInput, Textarea, useAppUtils } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import OwnerPrincipalIdField from '../../../_components/owner-principal-id-field';
import { FormData } from '../config/types';
const Basic = ({ action }: { action: string }) => {
const intl = useIntl();
const { getRuleMessage } = useAppUtils();
return (
<>
<OwnerPrincipalIdField name="owner_principal_id" />
<Form.Item<FormData>
name="name"
rules={[
{
required: true,
message: getRuleMessage('input', 'common.table.name')
},
{
pattern: validateLabelNameRegxFor63,
message: intl.formatMessage({ id: 'gpuservice.form.rule.name' })
}
]}
>
<CInput.Input
disabled={action === PageAction.EDIT}
label={intl.formatMessage({ id: 'common.table.name' })}
required
/>
</Form.Item>
<Form.Item<FormData> name="displayName">
<CInput.Input
label={intl.formatMessage({ id: 'common.table.displayName' })}
/>
</Form.Item>
<Form.Item<FormData> name="description">
<Textarea
scaleSize={true}
label={intl.formatMessage({ id: 'common.table.description' })}
/>
</Form.Item>
<Form.Item<FormData>
name={['spec', 'data']}
rules={[
{
required: true,
message: getRuleMessage('input', 'gpuservice.publicKey.label')
}
]}
>
<Textarea
required
alwaysFocus
label={intl.formatMessage({ id: 'gpuservice.publicKey.label' })}
placeholder={intl.formatMessage({
id: 'gpuservice.publicKey.placeholder'
})}
trim={false}
autoSize={{ minRows: 6, maxRows: 12 }}
/>
</Form.Item>
</>
);
};
export default Basic;
@@ -0,0 +1,62 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { Form } from 'antd';
import { forwardRef, useEffect, useImperativeHandle } from 'react';
import { FormData, ListItem } from '../config/types';
import Basic from './basic';
interface PublicKeyFormProps {
ref?: any;
open: boolean;
action: PageActionType;
currentData?: ListItem | null;
onFinish: (values: FormData) => Promise<void>;
}
const GPUServicePublicKeyForm: React.FC<PublicKeyFormProps> = forwardRef(
(props, ref) => {
const { action, currentData, open, onFinish } = props;
const [form] = Form.useForm<FormData>();
useEffect(() => {
if (!open) {
form.resetFields();
return;
}
if (action === PageAction.EDIT && currentData) {
form.setFieldsValue({
name: currentData.name as string,
displayName: currentData.displayName,
description: currentData.description,
spec: {
data: currentData.spec?.data ?? ''
}
});
}
}, [action, currentData, form, open]);
useImperativeHandle(ref, () => ({
submit: () => {
form.submit();
},
resetFields: () => {
form.resetFields();
}
}));
return (
<Form
name="gpuServicePublicKeyForm"
form={form}
onFinish={onFinish}
preserve={false}
initialValues={{}}
>
<Basic action={action} />
</Form>
);
}
);
export default GPUServicePublicKeyForm;
@@ -0,0 +1,52 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import useBodyScroll from '@/hooks/use-body-scroll';
import { useState } from 'react';
import { ListItem } from '../config/types';
const useCreatePublicKeyModal = () => {
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const [openModalStatus, setOpenModalStatus] = useState<{
open: boolean;
action: PageActionType;
currentData?: ListItem | null;
title: string;
}>({
open: false,
action: PageAction.CREATE,
currentData: null,
title: ''
});
const openModal = (
action: PageActionType,
title: string,
row?: ListItem | null
) => {
setOpenModalStatus({
open: true,
title,
action,
currentData: row ?? null
});
saveScrollHeight();
};
const closeModal = () => {
setOpenModalStatus({
open: false,
action: PageAction.CREATE,
currentData: null,
title: ''
});
restoreScrollHeight();
};
return {
openPublicKeyModalStatus: openModalStatus,
openPublicKeyModal: openModal,
closePublicKeyModal: closeModal
};
};
export default useCreatePublicKeyModal;
@@ -0,0 +1,95 @@
import { AutoTooltip, DropdownButtons, icons } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import type { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs';
import { useMemo } from 'react';
import { ListItem } from '../config/types';
const rowActionList = [
{
label: 'common.button.edit',
key: 'edit',
locale: true,
icon: icons.EditOutlined
},
{
label: 'common.button.delete',
key: 'delete',
locale: true,
icon: icons.DeleteOutlined,
props: {
danger: true
}
}
];
interface ColumnsHookProps {
handleSelect: (val: string, record: ListItem) => void;
sortOrder: string[];
}
const usePublicKeyColumns = ({
handleSelect,
sortOrder
}: ColumnsHookProps): ColumnsType<ListItem> => {
const intl = useIntl();
return useMemo(() => {
return [
{
title: intl.formatMessage({ id: 'common.table.name' }),
dataIndex: 'name',
key: 'name',
sorter: false,
ellipsis: {
showTitle: false
},
render: (text: string, record: ListItem) => (
<AutoTooltip ghost style={{ maxWidth: 360 }}>
<span className="text-primary">{record.displayName || text}</span>
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'common.table.description' }),
dataIndex: 'description',
key: 'description',
sorter: false,
ellipsis: {
showTitle: false
},
render: (text: string) => (
<AutoTooltip ghost style={{ maxWidth: 360 }}>
{text || '-'}
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'common.table.createTime' }),
dataIndex: 'created_at',
key: 'created_at',
sorter: false,
ellipsis: {
showTitle: false
},
render: (text: string) => (
<AutoTooltip ghost>
{text ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '-'}
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'common.table.operation' }),
key: 'operation',
dataIndex: 'operation',
render: (_text, record) => (
<DropdownButtons
items={rowActionList}
onSelect={(val) => handleSelect(val, record)}
/>
)
}
];
}, [handleSelect, sortOrder, intl]);
};
export default usePublicKeyColumns;
+149 -68
View File
@@ -1,94 +1,175 @@
import { Input as CInput, Textarea, useAppUtils } from '@gpustack/core-ui'; import { PageAction } from '@/config';
import { TABLE_SORT_DIRECTIONS } from '@/config/settings';
import useTableFetch from '@/hooks/use-table-fetch';
import { DeleteModal, FilterBar, IconFont, NoResult } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Form, message } from 'antd'; import { useMemoizedFn } from 'ahooks';
import React, { useEffect } from 'react'; import { ConfigProvider, message, Table } from 'antd';
import _ from 'lodash';
import PageBox from '../../_components/page-box'; import PageBox from '../../_components/page-box';
import useGetSshkey from './services/use-get-sshkey'; import {
deleteGPUServicePublicKey,
GPU_SERVICE_PUBLIC_KEY_API,
queryGPUServicePublicKeys
} from './apis';
import AddPublicKeyModal from './components/add-public-key-modal';
import { FormData, ListItem } from './config/types';
import useCreatePublicKeyModal from './hooks/use-create-public-key-modal';
import usePublicKeyColumns from './hooks/use-public-key-columns';
import useCreateSshkey from './services/use-create-sshkey';
import useUpdateSshkey from './services/use-update-sshkey'; import useUpdateSshkey from './services/use-update-sshkey';
import { FormData } from './types';
const GPUServicePublicKeys: React.FC = () => { const GPUServicePublicKeys: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const [form] = Form.useForm<FormData>();
const { getRuleMessage } = useAppUtils();
const { fetchData: fetchSshkey } = useGetSshkey();
const { fetchData: updateSshkey, loading: updating } = useUpdateSshkey();
useEffect(() => { const {
fetchSshkey({}).then((res) => { dataSource,
const data = res?.spec?.data || ''; rowSelection,
form.setFieldsValue({ queryParams,
name: res?.name, sortOrder,
spec: { modalRef,
data handleDelete,
} handleDeleteBatch,
fetchData,
handlePageChange,
handleTableChange,
handleSearch,
handleNameChange
} = useTableFetch<ListItem>({
key: 'PublicKeys',
fetchAPI: queryGPUServicePublicKeys,
deleteAPI: deleteGPUServicePublicKey,
watch: false,
polling: false,
API: GPU_SERVICE_PUBLIC_KEY_API,
contentForDelete: intl.formatMessage({ id: 'gpuservice.publicKey' })
}); });
});
}, []);
const handleSave = async () => { const { fetchData: createSshkey } = useCreateSshkey();
form.submit(); const { fetchData: updateSshkey } = useUpdateSshkey();
const { openPublicKeyModalStatus, openPublicKeyModal, closePublicKeyModal } =
useCreatePublicKeyModal();
const handleAdd = () => {
openPublicKeyModal(
PageAction.CREATE,
intl.formatMessage({ id: 'gpuservice.publicKey.add' })
);
}; };
const onFinish = async (values: FormData) => { const handleEdit = (row: ListItem) => {
openPublicKeyModal(
PageAction.EDIT,
intl.formatMessage({ id: 'gpuservice.publicKey.edit' }),
row
);
};
const handleModalOk = async (data: FormData) => {
try { try {
if (openPublicKeyModalStatus.action === PageAction.EDIT) {
await updateSshkey({ await updateSshkey({
data: values id: openPublicKeyModalStatus.currentData!.id,
data: data
}); });
} else {
await createSshkey({ data });
}
fetchData();
closePublicKeyModal();
message.success(intl.formatMessage({ id: 'common.message.success' })); message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) { } catch (error) {
// ignore // it's handled in interceptor
} }
}; };
const handleSelect = useMemoizedFn((val: string, row: ListItem) => {
if (val === 'edit') {
handleEdit(row);
} else if (val === 'delete') {
handleDelete({ ...row, name: row.name as string });
}
});
const renderEmpty = (type?: string) => {
if (type !== 'Table') return;
return ( return (
<PageBox> <NoResult
<Form loading={dataSource.loading}
style={{ width: 700 }} loadend={dataSource.loadend}
name="gpuServicePublicKeyForm" dataSource={dataSource.dataList}
form={form} image={<IconFont type="icon-storage-outlined" />}
onFinish={onFinish} filters={_.pick(queryParams, ['search'])}
initialValues={{ noFoundText={intl.formatMessage({
name: 'SSHPublicKey', id: 'noresult.gpuservice.sshkey.nofound'
spec: {
data: ''
}
}}
>
<Form.Item<FormData> name="name" hidden>
<CInput.Input />
</Form.Item>
<Form.Item<FormData>
name={['spec', 'data']}
rules={[
{
required: true,
message: getRuleMessage('input', 'gpuservice.publicKey.label')
}
]}
>
<Textarea
required
label={intl.formatMessage({ id: 'gpuservice.publicKey.label' })}
placeholder={intl.formatMessage({
id: 'gpuservice.publicKey.placeholder'
})} })}
trim={false} title={intl.formatMessage({ id: 'noresult.gpuservice.sshkey.title' })}
alwaysFocus subTitle={intl.formatMessage({
autoSize={{ minRows: 8, maxRows: 16 }} id: 'noresult.gpuservice.sshkey.subTitle'
style={{ width: 700, minHeight: 186 }} })}
onClick={handleAdd}
buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
/> />
</Form.Item> );
<Button };
type="primary"
loading={updating} const columns = usePublicKeyColumns({
style={{ width: 120, marginTop: 24 }} handleSelect,
onClick={handleSave} sortOrder
> });
{intl.formatMessage({ id: 'common.button.save' })}
</Button> return (
</Form> <>
<PageBox>
<FilterBar
marginBottom={22}
marginTop={30}
showSelect={false}
inputHolder={intl.formatMessage({
id: 'gpuservice.publicKey.filter.name'
})}
buttonText={intl.formatMessage({ id: 'gpuservice.publicKey.add' })}
handleSearch={handleSearch}
handleDeleteByBatch={handleDeleteBatch}
handleClickPrimary={handleAdd}
handleInputChange={handleNameChange}
rowSelection={rowSelection}
widths={{ input: 300 }}
/>
<ConfigProvider renderEmpty={renderEmpty}>
<Table
columns={columns}
dataSource={dataSource.dataList}
rowSelection={rowSelection}
loading={{
spinning: dataSource.loading,
size: 'middle'
}}
sortDirections={TABLE_SORT_DIRECTIONS}
showSorterTooltip={false}
rowKey={(record) => record.id}
onChange={handleTableChange}
pagination={{
showSizeChanger: true,
pageSize: queryParams.perPage,
current: queryParams.page,
total: dataSource.total,
hideOnSinglePage: queryParams.perPage === 10,
onChange: handlePageChange
}}
/>
</ConfigProvider>
</PageBox> </PageBox>
<AddPublicKeyModal
open={openPublicKeyModalStatus.open}
action={openPublicKeyModalStatus.action}
title={openPublicKeyModalStatus.title}
data={openPublicKeyModalStatus.currentData}
onCancel={closePublicKeyModal}
onOk={handleModalOk}
/>
<DeleteModal ref={modalRef} />
</>
); );
}; };
@@ -1,7 +1,7 @@
import { useQueryData } from '@gpustack/core-ui'; import { useQueryData } from '@gpustack/core-ui';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { createGPUServicePublicKey } from '../apis'; import { createGPUServicePublicKey } from '../apis';
import { FormData, ListItem } from '../types'; import { FormData, ListItem } from '../config/types';
interface CreateSshkeyParams { interface CreateSshkeyParams {
data: FormData; data: FormData;
@@ -1,24 +0,0 @@
import { useQueryData } from '@gpustack/core-ui';
import { useCallback } from 'react';
import { queryGPUServicePublicKeys } from '../apis';
import { ListItem } from '../types';
export default function useGetSshkey() {
const fetchDetail = useCallback(
(params: {}, options?: any) => queryGPUServicePublicKeys(params, options),
[]
);
const { detailData, loading, cancelRequest, fetchData } =
useQueryData<ListItem>({
fetchDetail,
key: 'sshkey'
});
return {
detailData,
loading,
cancelRequest,
fetchData
};
}
@@ -0,0 +1,27 @@
import { useQueryData } from '@gpustack/core-ui';
import { useCallback } from 'react';
import { queryGPUServicePublicKeys } from '../apis';
import { ListItem } from '../config/types';
export default function useQueryPublicKeys() {
const fetchDetail = useCallback(
(params: Global.SearchParams = { page: 1, perPage: 100 }, options?: any) =>
queryGPUServicePublicKeys(params, options),
[]
);
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
Global.PageResponse<ListItem>,
Global.SearchParams
>({
fetchDetail,
key: 'publicKeys'
});
return {
detailData,
loading,
cancelRequest,
fetchData
};
}
@@ -0,0 +1,29 @@
import { useQueryDataList } from '@gpustack/core-ui';
import { useCallback } from 'react';
import { queryGPUServicePublicKeys } from '../apis';
import { ListItem } from '../config/types';
export default function useQuerySshkeys() {
const fetchList = useCallback(
(params: Global.SearchParams = { page: 1, perPage: 100 }, options?: any) =>
queryGPUServicePublicKeys(params, options),
[]
);
const { dataList, loading, cancelRequest, fetchData } = useQueryDataList<
ListItem,
Global.SearchParams
>({
key: 'sshkeyOptions',
fetchList,
getLabel: (item) => item?.name as string,
getValue: (item) => item?.name as string
});
return {
sshkeyOptions: dataList as Array<{ label: string; value: string }>,
loading,
cancelRequest,
fetchData
};
}
@@ -1,16 +1,17 @@
import { useQueryData } from '@gpustack/core-ui'; import { useQueryData } from '@gpustack/core-ui';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { updateGPUServicePublicKey } from '../apis'; import { updateGPUServicePublicKey } from '../apis';
import { FormData, ListItem } from '../types'; import { FormData, ListItem } from '../config/types';
interface UpdateSshkeyParams { interface UpdateSshkeyParams {
data: FormData; id: number;
data: Omit<FormData, 'name'>;
} }
export default function useUpdateSshkey() { export default function useUpdateSshkey() {
const fetchDetail = useCallback( const fetchDetail = useCallback(
(params: UpdateSshkeyParams) => (params: UpdateSshkeyParams) =>
updateGPUServicePublicKey({ data: params.data }), updateGPUServicePublicKey({ id: params.id, data: params.data }),
[] []
); );
@@ -1,13 +0,0 @@
export interface FormData {
name: string;
description: string;
spec: {
data: string;
};
}
export interface ListItem extends FormData {
id: number;
created_at?: string;
updated_at?: string;
}
@@ -0,0 +1,41 @@
import { request } from '@umijs/max';
import { FormData, ListItem } from '../config/types';
export const GPU_SERVICE_STORAGE_TYPE_API =
'/gpu-instance-persistent-volume-types';
export async function queryGPUServiceStorageTypes(
params: Global.SearchParams,
options?: any
) {
return request<Global.PageResponse<ListItem>>(GPU_SERVICE_STORAGE_TYPE_API, {
method: 'GET',
params,
cancelToken: options?.token
});
}
export async function createGPUServiceStorageType(params: {
data: Omit<FormData, 'type'>;
}) {
return request<ListItem>(GPU_SERVICE_STORAGE_TYPE_API, {
method: 'POST',
data: params.data
});
}
export async function updateGPUServiceStorageType(params: {
id: number;
data: Omit<FormData, 'type' | 'name'>;
}) {
return request<ListItem>(`${GPU_SERVICE_STORAGE_TYPE_API}/${params.id}`, {
method: 'PUT',
data: params.data
});
}
export async function deleteGPUServiceStorageType(id: number) {
return request(`${GPU_SERVICE_STORAGE_TYPE_API}/${id}`, {
method: 'DELETE'
});
}
@@ -0,0 +1,70 @@
import { PageActionType } from '@/config/types';
import { 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';
type AddStorageTypeModalProps = {
title: string;
action: PageActionType;
open: boolean;
onOk: (values: FormData) => void;
data?: ListItem | null;
onCancel: () => void;
};
const AddStorageTypeModal: React.FC<AddStorageTypeModalProps> = ({
title,
action,
open,
onOk,
data,
onCancel
}) => {
const form = useRef<any>(null);
const handleSubmit = () => {
form.current?.submit();
};
const handleCancel = () => {
form.current?.resetFields();
onCancel();
};
const onFinish = async (values: FormData) => {
onOk({ ...values });
};
return (
<FormDrawer
title={title}
open={open}
onCancel={handleCancel}
onSubmit={handleSubmit}
width={640}
footer={
<ModalFooter
onOk={handleSubmit}
onCancel={handleCancel}
style={{
padding: '16px 24px 8px',
display: 'flex',
justifyContent: 'flex-end'
}}
/>
}
>
<GPUServiceStorageTypeForm
ref={form}
action={action}
currentData={data}
onFinish={onFinish}
open={open}
/>
</FormDrawer>
);
};
export default AddStorageTypeModal;
@@ -0,0 +1,54 @@
import { icons } from '@gpustack/core-ui';
import { StorageTypeKind } from './types';
export const StorageTypeKindValueMap: Record<string, StorageTypeKind> = {
NFS: 'nfs',
S3: 's3'
};
export const StorageTypeKindLabelMap: Record<string, string> = {
[StorageTypeKindValueMap.NFS]: 'NFS',
[StorageTypeKindValueMap.S3]: 'S3'
};
export const StorageTypeKindOptions = [
{ label: 'NFS', value: StorageTypeKindValueMap.NFS },
{ label: 'S3', value: StorageTypeKindValueMap.S3 }
];
export const DefaultNFSMountOptions = [
'hard',
'vers=4',
'rsize=1048576',
'wsize=1048576',
'noatime',
'nodiratime'
];
export const DefaultS3MountOptions = [
'--no-checksum',
'--memory-limit=4000',
'--max-flushers=32',
'--max-parallel-parts=32',
'--part-sizes=25',
'--list-type=2',
'--no-specials'
];
export const rowActionList = [
{
label: 'common.button.edit',
key: 'edit',
locale: true,
icon: icons.EditOutlined
},
{
label: 'common.button.delete',
key: 'delete',
locale: true,
icon: icons.DeleteOutlined,
props: {
danger: true
}
}
];
@@ -0,0 +1,46 @@
export type StorageTypeKind = 'nfs' | 's3';
export interface StorageTypeNFS {
server: string;
share: string;
subDirectory?: string | null;
mountPermissions?: string | null;
mountOptions?: string[] | null;
}
export interface StorageTypeS3 {
endpoint: string;
region?: string | null;
insecure?: boolean | null;
accessKey?: string | null;
secretKey?: string | null;
bucket?: string | null;
mountOptions?: string[] | null;
}
export interface FormData {
name: string;
owner_principal_id?: number | null;
displayName?: string | null;
description?: string | null;
type: StorageTypeKind;
spec: {
nfs?: StorageTypeNFS | null;
s3?: StorageTypeS3 | null;
};
}
export interface ListItem {
id: number;
created_at: string;
updated_at: string;
deleted_at?: string | null;
owner_principal_id?: number | null;
displayName?: string | null;
description?: string | null;
name: string;
spec: {
nfs?: StorageTypeNFS | null;
s3?: Omit<StorageTypeS3, 'secretKey'> | null;
};
}
@@ -0,0 +1,73 @@
import { PageAction, validateLabelNameRegxFor63 } from '@/config';
import {
Input as CInput,
Select as SealSelect,
Textarea,
useAppUtils
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import OwnerPrincipalIdField from '../../../_components/owner-principal-id-field';
import { StorageTypeKindOptions } from '../config';
import { FormData } from '../config/types';
const Basic = ({ action }: { action: string }) => {
const intl = useIntl();
const { getRuleMessage } = useAppUtils();
return (
<>
<OwnerPrincipalIdField name="owner_principal_id" />
<Form.Item<FormData>
name="name"
rules={[
{
required: true,
message: getRuleMessage('input', 'common.table.name')
},
{
pattern: validateLabelNameRegxFor63,
message: intl.formatMessage({ id: 'gpuservice.form.rule.name' })
}
]}
>
<CInput.Input
disabled={action === PageAction.EDIT}
label={intl.formatMessage({ id: 'common.table.name' })}
required
/>
</Form.Item>
<Form.Item<FormData> name="displayName">
<CInput.Input
trim={false}
label={intl.formatMessage({ id: 'common.table.displayName' })}
/>
</Form.Item>
<Form.Item<FormData> name="description">
<Textarea
scaleSize={true}
trim={false}
label={intl.formatMessage({ id: 'common.table.description' })}
/>
</Form.Item>
<Form.Item<FormData>
name="type"
rules={[
{
required: true,
message: getRuleMessage('select', 'gpuservice.storageType.kind')
}
]}
>
<SealSelect
disabled={action === PageAction.EDIT}
label={intl.formatMessage({ id: 'gpuservice.storageType.kind' })}
required
options={StorageTypeKindOptions}
/>
</Form.Item>
</>
);
};
export default Basic;
@@ -0,0 +1,129 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { Form } from 'antd';
import { forwardRef, useEffect, useImperativeHandle } from 'react';
import {
DefaultNFSMountOptions,
DefaultS3MountOptions,
StorageTypeKindValueMap
} from '../config';
import { FormData, ListItem, StorageTypeKind } from '../config/types';
import Basic from './basic';
import NFSForm from './nfs';
import S3Form from './s3';
interface StorageTypeFormProps {
ref?: any;
open: boolean;
action: PageActionType;
currentData?: ListItem | null;
onFinish: (values: FormData) => Promise<void>;
}
const detectKind = (item?: ListItem | null): StorageTypeKind => {
if (item?.spec?.s3) return StorageTypeKindValueMap.S3;
return StorageTypeKindValueMap.NFS;
};
const GPUServiceStorageTypeForm: React.FC<StorageTypeFormProps> = forwardRef(
(props, ref) => {
const { action, currentData, open, onFinish } = props;
const [form] = Form.useForm<FormData>();
const kind = Form.useWatch('type', form);
useEffect(() => {
if (!open) {
form.resetFields();
return;
}
if (action === PageAction.EDIT && currentData) {
const detected = detectKind(currentData);
form.setFieldsValue({
name: currentData.name,
displayName: currentData.displayName,
description: currentData.description,
type: detected,
spec: {
nfs: currentData.spec?.nfs ?? null,
s3: (currentData.spec?.s3 ?? null) as any
}
});
return;
}
form.setFieldsValue({
type: StorageTypeKindValueMap.NFS,
spec: {
nfs: {
server: '',
share: '',
mountOptions: DefaultNFSMountOptions
},
s3: null
}
});
}, [action, currentData, form, open]);
useEffect(() => {
if (!open || action === PageAction.EDIT) return;
if (kind === StorageTypeKindValueMap.S3) {
const existing = form.getFieldValue(['spec', 's3']);
if (!existing) {
form.setFieldValue(['spec', 's3'], {
endpoint: '',
insecure: false,
mountOptions: DefaultS3MountOptions
});
}
form.setFieldValue(['spec', 'nfs'], null);
} else if (kind === StorageTypeKindValueMap.NFS) {
const existing = form.getFieldValue(['spec', 'nfs']);
if (!existing) {
form.setFieldValue(['spec', 'nfs'], {
server: '',
share: '',
mountOptions: DefaultNFSMountOptions
});
}
form.setFieldValue(['spec', 's3'], null);
}
}, [kind, open, action, form]);
useImperativeHandle(ref, () => ({
submit: () => {
form.submit();
},
resetFields: () => {
form.resetFields();
}
}));
const handleFinish = async (values: FormData) => {
const isNFS = values.type === StorageTypeKindValueMap.NFS;
await onFinish({
...values,
spec: {
nfs: isNFS ? (values.spec?.nfs ?? null) : null,
s3: !isNFS ? (values.spec?.s3 ?? null) : null
}
});
};
return (
<Form
name="gpuServiceStorageTypeForm"
form={form}
onFinish={handleFinish}
preserve={false}
initialValues={{}}
>
<Basic action={action} />
{kind === StorageTypeKindValueMap.NFS && <NFSForm action={action} />}
{kind === StorageTypeKindValueMap.S3 && <S3Form action={action} />}
</Form>
);
}
);
export default GPUServiceStorageTypeForm;
@@ -0,0 +1,80 @@
import { PageAction } from '@/config';
import { Input as CInput, ListInput, useAppUtils } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import { FormData } from '../config/types';
const NFSForm = ({ action }: { action: string }) => {
const intl = useIntl();
const { getRuleMessage } = useAppUtils();
return (
<>
<Form.Item<FormData>
name={['spec', 'nfs', 'server']}
rules={[
{
required: true,
message: getRuleMessage(
'input',
'gpuservice.storageType.nfs.server'
)
}
]}
>
<CInput.Input
required
label={intl.formatMessage({
id: 'gpuservice.storageType.nfs.server'
})}
/>
</Form.Item>
<Form.Item<FormData>
name={['spec', 'nfs', 'share']}
rules={[
{
required: true,
message: getRuleMessage('input', 'gpuservice.storageType.nfs.share')
}
]}
>
<CInput.Input
required
label={intl.formatMessage({ id: 'gpuservice.storageType.nfs.share' })}
/>
</Form.Item>
<Form.Item<FormData> name={['spec', 'nfs', 'subDirectory']}>
<CInput.Input
description={intl.formatMessage({
id: 'gpuservice.storageType.nfs.subDirectory.tips'
})}
label={intl.formatMessage({
id: 'gpuservice.storageType.nfs.subDirectory'
})}
/>
</Form.Item>
<Form.Item<FormData> name={['spec', 'nfs', 'mountPermissions']}>
<CInput.Input
placeholder="0755,0777,..."
description={intl.formatMessage({
id: 'gpuservice.storageType.nfs.mountPermissions.tips'
})}
label={intl.formatMessage({
id: 'gpuservice.storageType.nfs.mountPermissions'
})}
/>
</Form.Item>
<Form.Item<FormData> name={['spec', 'nfs', 'mountOptions']}>
<ListInput
disabled={action === PageAction.EDIT}
label={intl.formatMessage({
id: 'gpuservice.storageType.mountOptions'
})}
btnText={intl.formatMessage({ id: 'common.button.addParams' })}
/>
</Form.Item>
</>
);
};
export default NFSForm;
@@ -0,0 +1,109 @@
import { PageAction } from '@/config';
import {
CheckboxField,
Input as CInput,
ListInput,
useAppUtils
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Flex, Form } from 'antd';
import { FormData } from '../config/types';
const S3Form = ({ action }: { action: string }) => {
console.log('action', action);
const intl = useIntl();
const { getRuleMessage } = useAppUtils();
return (
<>
<Form.Item<FormData>
name={['spec', 's3', 'endpoint']}
rules={[
{
required: true,
message: getRuleMessage(
'input',
'gpuservice.storageType.s3.endpoint'
)
},
{
pattern: /^https?:\/\//,
message: intl.formatMessage({
id: 'gpuservice.storageType.s3.endpoint.rule'
})
}
]}
>
<CInput.Input
required
label={intl.formatMessage({
id: 'gpuservice.storageType.s3.endpoint'
})}
placeholder="http | https://..."
/>
</Form.Item>
<Flex gap={16}>
<div style={{ flex: 1 }}>
<Form.Item<FormData> name={['spec', 's3', 'region']}>
<CInput.Input
label={intl.formatMessage({
id: 'gpuservice.storageType.s3.region'
})}
/>
</Form.Item>
</div>
<div style={{ flex: 1 }}>
<Form.Item<FormData> name={['spec', 's3', 'bucket']}>
<CInput.Input
description={intl.formatMessage({
id: 'gpuservice.storageType.s3.bucket.tips'
})}
label={intl.formatMessage({
id: 'gpuservice.storageType.s3.bucket'
})}
/>
</Form.Item>
</div>
</Flex>
<Form.Item<FormData> name={['spec', 's3', 'accessKey']}>
<CInput.Input
label={intl.formatMessage({
id: 'gpuservice.storageType.s3.accessKey'
})}
/>
</Form.Item>
<Form.Item<FormData> name={['spec', 's3', 'secretKey']}>
<CInput.Password
label={intl.formatMessage({
id: 'gpuservice.storageType.s3.secretKey'
})}
/>
</Form.Item>
<Form.Item<FormData>
name={['spec', 's3', 'insecure']}
valuePropName="checked"
style={{ marginBottom: 12 }}
>
<CheckboxField
description={intl.formatMessage({
id: 'gpuservice.storageType.s3.insecure.tips'
})}
label={intl.formatMessage({
id: 'gpuservice.storageType.s3.insecure'
})}
/>
</Form.Item>
<Form.Item<FormData> name={['spec', 's3', 'mountOptions']}>
<ListInput
disabled={action === PageAction.EDIT}
label={intl.formatMessage({
id: 'gpuservice.storageType.mountOptions'
})}
btnText={intl.formatMessage({ id: 'common.button.addParams' })}
/>
</Form.Item>
</>
);
};
export default S3Form;
@@ -0,0 +1,52 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import useBodyScroll from '@/hooks/use-body-scroll';
import { useState } from 'react';
import { ListItem } from '../config/types';
const useCreateStorageTypeModal = () => {
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const [openModalStatus, setOpenModalStatus] = useState<{
open: boolean;
action: PageActionType;
currentData?: ListItem | null;
title: string;
}>({
open: false,
action: PageAction.CREATE,
currentData: null,
title: ''
});
const openModal = (
action: PageActionType,
title: string,
row?: ListItem | null
) => {
setOpenModalStatus({
open: true,
title,
action,
currentData: row ?? null
});
saveScrollHeight();
};
const closeModal = () => {
setOpenModalStatus({
open: false,
action: PageAction.CREATE,
currentData: null,
title: ''
});
restoreScrollHeight();
};
return {
openStorageTypeModalStatus: openModalStatus,
openStorageTypeModal: openModal,
closeStorageTypeModal: closeModal
};
};
export default useCreateStorageTypeModal;
@@ -0,0 +1,108 @@
import { FolderOutlined } from '@ant-design/icons';
import {
AutoTooltip,
DropdownButtons,
IconFont,
ThemeTag
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import type { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs';
import { useMemo } from 'react';
import { rowActionList, StorageTypeKindLabelMap } from '../config';
import { ListItem } from '../config/types';
interface ColumnsHookProps {
handleSelect: (val: string, record: ListItem) => void;
sortOrder: string[];
}
const getKindLabel = (record: ListItem) => {
if (record.spec?.nfs)
return (
<ThemeTag
style={{ width: 'fit-content' }}
color="cyan"
icon={<FolderOutlined />}
>
{StorageTypeKindLabelMap.nfs}
</ThemeTag>
);
if (record.spec?.s3)
return (
<ThemeTag
style={{ width: 'fit-content' }}
color="green"
icon={<IconFont type="icon-database" />}
>
{StorageTypeKindLabelMap.s3}
</ThemeTag>
);
return '-';
};
const useStorageTypeColumns = ({
handleSelect,
sortOrder
}: ColumnsHookProps): ColumnsType<ListItem> => {
const intl = useIntl();
return useMemo(() => {
return [
{
title: intl.formatMessage({ id: 'common.table.name' }),
dataIndex: 'name',
key: 'name',
sorter: false,
ellipsis: { showTitle: false },
render: (text: string, record: ListItem) => (
<AutoTooltip ghost minWidth={20}>
<span className="text-primary">{record.displayName || text}</span>
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'common.table.type' }),
key: 'kind',
sorter: false,
render: (_text, record) => getKindLabel(record)
},
{
title: intl.formatMessage({ id: 'common.table.description' }),
dataIndex: 'description',
key: 'description',
sorter: false,
ellipsis: { showTitle: false },
render: (text: string) => (
<AutoTooltip ghost minWidth={20}>
{text || '-'}
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'common.table.createTime' }),
dataIndex: 'created_at',
key: 'created_at',
sorter: false,
ellipsis: { showTitle: false },
render: (text: string) => (
<AutoTooltip ghost>
{text ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '-'}
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'common.table.operation' }),
key: 'operation',
dataIndex: 'operation',
render: (_text, record) => (
<DropdownButtons
items={rowActionList}
onSelect={(val) => handleSelect(val, record)}
/>
)
}
];
}, [handleSelect, sortOrder, intl]);
};
export default useStorageTypeColumns;
@@ -0,0 +1,181 @@
import { PageAction } from '@/config';
import { TABLE_SORT_DIRECTIONS } from '@/config/settings';
import useTableFetch from '@/hooks/use-table-fetch';
import { DeleteModal, FilterBar, IconFont, NoResult } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { ConfigProvider, message, Table } from 'antd';
import _ from 'lodash';
import PageBox from '../../_components/page-box';
import {
deleteGPUServiceStorageType,
GPU_SERVICE_STORAGE_TYPE_API,
queryGPUServiceStorageTypes
} from './apis';
import AddStorageTypeModal from './components/add-storage-type-modal';
import { FormData, ListItem } from './config/types';
import useCreateStorageTypeModal from './hooks/use-create-storage-type-modal';
import useStorageTypeColumns from './hooks/use-storage-type-columns';
import useCreateStorageType from './services/use-create-storage-type';
import useUpdateStorageType from './services/use-update-storage-type';
const GPUServiceStorageTypes: React.FC = () => {
const intl = useIntl();
const {
dataSource,
rowSelection,
queryParams,
sortOrder,
modalRef,
handleDelete,
handleDeleteBatch,
fetchData,
handlePageChange,
handleTableChange,
handleSearch,
handleNameChange
} = useTableFetch<ListItem>({
key: 'StorageTypes',
fetchAPI: queryGPUServiceStorageTypes,
deleteAPI: deleteGPUServiceStorageType,
watch: false,
polling: false,
API: GPU_SERVICE_STORAGE_TYPE_API,
contentForDelete: intl.formatMessage({ id: 'gpuservice.storageType' })
});
const { fetchData: createStorageType } = useCreateStorageType();
const { fetchData: updateStorageType } = useUpdateStorageType();
const {
openStorageTypeModalStatus,
openStorageTypeModal,
closeStorageTypeModal
} = useCreateStorageTypeModal();
const handleAdd = () => {
openStorageTypeModal(
PageAction.CREATE,
intl.formatMessage({ id: 'gpuservice.storageType.add' })
);
};
const handleEdit = (row: ListItem) => {
openStorageTypeModal(
PageAction.EDIT,
intl.formatMessage({ id: 'gpuservice.storageType.edit' }),
row
);
};
const handleModalOk = async (data: FormData) => {
try {
if (openStorageTypeModalStatus.action === PageAction.EDIT) {
await updateStorageType({
id: openStorageTypeModalStatus.currentData!.id,
data: data
});
} else {
await createStorageType({ data: data });
}
fetchData();
closeStorageTypeModal();
message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) {
// it's handled in interceptor
}
};
const handleSelect = useMemoizedFn((val: string, row: ListItem) => {
if (val === 'edit') {
handleEdit(row);
} else if (val === 'delete') {
handleDelete({ ...row, name: row.name });
}
});
const renderEmpty = (type?: string) => {
if (type !== 'Table') return;
return (
<NoResult
loading={dataSource.loading}
loadend={dataSource.loadend}
dataSource={dataSource.dataList}
image={<IconFont type="icon-storage-outlined" />}
filters={_.pick(queryParams, ['search'])}
noFoundText={intl.formatMessage({
id: 'noresult.gpuservice.storageType.nofound'
})}
title={intl.formatMessage({
id: 'noresult.gpuservice.storageType.title'
})}
subTitle={intl.formatMessage({
id: 'noresult.gpuservice.storageType.subTitle'
})}
onClick={handleAdd}
buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
/>
);
};
const columns = useStorageTypeColumns({
handleSelect,
sortOrder
});
return (
<>
<PageBox>
<FilterBar
marginBottom={22}
marginTop={30}
showSelect={false}
inputHolder={intl.formatMessage({
id: 'gpuservice.storageType.filter.name'
})}
buttonText={intl.formatMessage({ id: 'gpuservice.storageType.add' })}
handleSearch={handleSearch}
handleDeleteByBatch={handleDeleteBatch}
handleClickPrimary={handleAdd}
handleInputChange={handleNameChange}
rowSelection={rowSelection}
widths={{ input: 300 }}
/>
<ConfigProvider renderEmpty={renderEmpty}>
<Table
columns={columns}
dataSource={dataSource.dataList}
rowSelection={rowSelection}
loading={{
spinning: dataSource.loading,
size: 'middle'
}}
sortDirections={TABLE_SORT_DIRECTIONS}
showSorterTooltip={false}
rowKey={(record) => record.id}
onChange={handleTableChange}
pagination={{
showSizeChanger: true,
pageSize: queryParams.perPage,
current: queryParams.page,
total: dataSource.total,
hideOnSinglePage: queryParams.perPage === 10,
onChange: handlePageChange
}}
/>
</ConfigProvider>
</PageBox>
<AddStorageTypeModal
open={openStorageTypeModalStatus.open}
action={openStorageTypeModalStatus.action}
title={openStorageTypeModalStatus.title}
data={openStorageTypeModalStatus.currentData}
onCancel={closeStorageTypeModal}
onOk={handleModalOk}
/>
<DeleteModal ref={modalRef} />
</>
);
};
export default GPUServiceStorageTypes;
@@ -0,0 +1,31 @@
import { useQueryData } from '@gpustack/core-ui';
import { useCallback } from 'react';
import { createGPUServiceStorageType } from '../apis';
import { FormData, ListItem } from '../config/types';
interface CreateStorageTypeParams {
data: Omit<FormData, 'type'>;
}
export default function useCreateStorageType() {
const fetchDetail = useCallback(
(params: CreateStorageTypeParams) =>
createGPUServiceStorageType({ data: params.data }),
[]
);
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
ListItem,
CreateStorageTypeParams
>({
fetchDetail,
key: 'createStorageType'
});
return {
detailData,
loading,
cancelRequest,
fetchData
};
}
@@ -0,0 +1,27 @@
import { useQueryData } from '@gpustack/core-ui';
import { useCallback } from 'react';
import { queryGPUServiceStorageTypes } from '../apis';
import { ListItem } from '../config/types';
export default function useQueryStorageTypes() {
const fetchDetail = useCallback(
(params: Global.SearchParams = { page: 1, perPage: 100 }, options?: any) =>
queryGPUServiceStorageTypes(params, options),
[]
);
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
Global.PageResponse<ListItem>,
Global.SearchParams
>({
fetchDetail,
key: 'storageTypes'
});
return {
detailData,
loading,
cancelRequest,
fetchData
};
}
@@ -0,0 +1,32 @@
import { useQueryData } from '@gpustack/core-ui';
import { useCallback } from 'react';
import { updateGPUServiceStorageType } from '../apis';
import { FormData, ListItem } from '../config/types';
interface UpdateStorageTypeParams {
id: number;
data: Omit<FormData, 'type' | 'name'>;
}
export default function useUpdateStorageType() {
const fetchDetail = useCallback(
(params: UpdateStorageTypeParams) =>
updateGPUServiceStorageType({ id: params.id, data: params.data }),
[]
);
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
ListItem,
UpdateStorageTypeParams
>({
fetchDetail,
key: 'updateStorageType'
});
return {
detailData,
loading,
cancelRequest,
fetchData
};
}
+31 -100
View File
@@ -1,125 +1,56 @@
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import { omitPathParams } from '../../utils'; import {
import { ListItem, StorageClassItem } from '../config/types'; FormData,
ListItem,
StorageClassItem,
UpdateData
} from '../config/types';
export const GPU_SERVICE_STORAGE_API = (params: { export const GPU_SERVICE_STORAGE_API = '/gpu-instance-persistent-volumes';
namespace: string;
clusterID?: number;
}) =>
`/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instancepersistentvolumes`;
export const STORAGE_CLASS_API = (params: { clusterID?: number }) => export const STORAGE_CLASS_API = '/gpu-instance-persistent-volume-types';
`/clusters/${params.clusterID}/proxy/apis/storage.k8s.io/v1/storageclasses`;
export async function queryGPUServiceStorage( export async function queryGPUServiceStorage(
params: Global.K8sSearchParams & { params: Global.SearchParams,
namespace: string;
clusterID?: number;
},
options?: any options?: any
) { ) {
if (!params.clusterID) { return request<Global.PageResponse<ListItem>>(GPU_SERVICE_STORAGE_API, {
return;
}
return request<Global.K8sPageResponse<ListItem>>(
GPU_SERVICE_STORAGE_API({
namespace: params.namespace,
clusterID: params.clusterID
}),
{
method: 'GET', method: 'GET',
params: omitPathParams(params), params,
cancelToken: options?.token cancelToken: options?.token
} });
);
} }
export async function createGPUServiceStorage( export async function createGPUServiceStorage(params: { data: FormData }) {
params: { return request<ListItem>(GPU_SERVICE_STORAGE_API, {
namespace: string;
clusterID?: number;
data: Global.K8sCommonData;
},
option?: any
) {
if (!params.clusterID) {
return;
}
return request<ListItem>(
GPU_SERVICE_STORAGE_API({
namespace: params.namespace,
clusterID: params.clusterID
}),
{
method: 'POST', method: 'POST',
data: params.data, data: params.data
cancelToken: option?.token });
}
);
} }
export async function updateGPUServiceStorage( export async function updateGPUServiceStorage(params: {
params: {
namespace: string;
clusterID?: number;
id: number;
data: Global.K8sCommonData;
},
option?: any
) {
if (!params.clusterID) {
return;
}
return request<ListItem>(
`${GPU_SERVICE_STORAGE_API({
namespace: params.namespace,
clusterID: params.clusterID
})}/${params.id}`,
{
method: 'PUT',
data: params.data,
cancelToken: option?.token
}
);
}
export async function deleteGPUServiceStorage(params: {
namespace: string;
clusterID?: number;
id: number; id: number;
data: UpdateData;
}) { }) {
if (!params.clusterID) { return request<ListItem>(`${GPU_SERVICE_STORAGE_API}/${params.id}`, {
return; method: 'PUT',
data: params.data
});
} }
return request(
`${GPU_SERVICE_STORAGE_API({ export async function deleteGPUServiceStorage(id: number) {
namespace: params.namespace, return request(`${GPU_SERVICE_STORAGE_API}/${id}`, {
clusterID: params.clusterID
})}/${params.id}`,
{
method: 'DELETE' method: 'DELETE'
} });
);
} }
export async function queryStorageClass( export async function queryStorageClass(
params: Global.K8sSearchParams & { params: Global.SearchParams,
clusterID?: number;
},
options?: any options?: any
) { ) {
if (!params.clusterID) { return request<Global.PageResponse<StorageClassItem>>(STORAGE_CLASS_API, {
return;
}
return request<Global.K8sPageResponse<StorageClassItem>>(
STORAGE_CLASS_API({ clusterID: params.clusterID }),
{
method: 'GET', method: 'GET',
params: omitPathParams(params), params,
cancelToken: options?.token, cancelToken: options?.token
headers: { });
skipErrorHandler: 'true'
}
}
);
} }
+6 -83
View File
@@ -20,90 +20,13 @@ export const status: Record<string, StatusType> = {
[StoragePhaseValueMap.Pending]: StatusMaps.transitioning [StoragePhaseValueMap.Pending]: StatusMaps.transitioning
}; };
export const StorageTypeValueMap = {
Local: 'local',
Shared: 'shared',
Object: 'object'
};
export const StorageTypeLabelMap: Record<string, string> = {
[StorageTypeValueMap.Local]: 'gpuservice.storage.type.local',
[StorageTypeValueMap.Shared]: 'gpuservice.storage.type.shared',
[StorageTypeValueMap.Object]: 'gpuservice.storage.type.object'
};
export const StorageTypeOptions = [
{
label: StorageTypeLabelMap[StorageTypeValueMap.Local],
value: StorageTypeValueMap.Local,
locale: true
},
{
label: StorageTypeLabelMap[StorageTypeValueMap.Shared],
value: StorageTypeValueMap.Shared,
locale: true
},
{
label: StorageTypeLabelMap[StorageTypeValueMap.Object],
value: StorageTypeValueMap.Object,
locale: true
}
];
export const AccessModeValueMap = {
ReadWriteOnce: 'ReadWriteOnce',
ReadOnlyMany: 'ReadOnlyMany',
ReadWriteMany: 'ReadWriteMany',
ReadWriteOncePod: 'ReadWriteOncePod'
};
export const AccessModeOptions = [
{
label: 'ReadWriteOnce',
value: AccessModeValueMap.ReadWriteOnce
},
{
label: 'ReadWriteMany',
value: AccessModeValueMap.ReadWriteMany
},
{
label: 'ReadOnlyMany',
value: AccessModeValueMap.ReadOnlyMany
},
{
label: 'ReadWriteOncePod',
value: AccessModeValueMap.ReadWriteOncePod
}
];
export const ReClaimPolicyValueMap = {
Retain: 'Retain',
Delete: 'Delete',
Recycle: 'Recycle'
};
export const ReClaimPolicyOptions = [
{
label: 'Retain',
value: ReClaimPolicyValueMap.Retain
},
{
label: 'Delete',
value: ReClaimPolicyValueMap.Delete
},
{
label: 'Recycle',
value: ReClaimPolicyValueMap.Recycle
}
];
export const rowActionList = [ export const rowActionList = [
// { {
// label: 'common.button.edit', label: 'common.button.edit',
// key: 'edit', key: 'edit',
// locale: true, locale: true,
// icon: icons.EditOutlined icon: icons.EditOutlined
// }, },
{ {
label: 'common.button.delete', label: 'common.button.delete',
key: 'delete', key: 'delete',
+50 -42
View File
@@ -1,60 +1,68 @@
export type AccessModeType =
| 'ReadOnlyMany'
| 'ReadWriteMany'
| 'ReadWriteOnce'
| 'ReadWriteOncePod';
export type ReClaimPolicyType = 'Retain' | 'Delete' | 'Recycle';
export interface FormData { export interface FormData {
metadata: {
name: string; name: string;
namespace: string; owner_principal_id?: number | null;
}; displayName?: string | null;
description?: string | null;
spec: { spec: {
capacity: string;
type: string; type: string;
capacity: string;
}; };
} }
export interface ManagedField { // Per `GPUInstancePersistentVolumeUpdate`, only displayName / description /
manager: string; // owner_principal_id are mutable post-create.
operation: string; export interface UpdateData {
apiVersion: string; owner_principal_id?: number | null;
time: string; displayName?: string | null;
fieldsType: string; description?: string | null;
fieldsV1: Record<string, any>;
}
export interface Metadata {
name: string;
namespace?: string;
uid: string;
resourceVersion: string;
creationTimestamp: string;
annotations?: Record<string, string>;
finalizers?: string[];
managedFields?: ManagedField[];
} }
export interface ListItem { export interface ListItem {
metadata: Metadata; id: number;
created_at: string;
updated_at: string;
deleted_at?: string | null;
owner_principal_id?: number | null;
displayName?: string | null;
description?: string | null;
name: string;
spec: { spec: {
type: string; type: string;
capacity: string; capacity: string;
accessMode: AccessModeType;
};
status: {
phaseMessage?: string;
phase: 'Available' | 'Unavailable' | 'Pending';
volume: {
name: string;
};
}; };
status?: {
phase?: string | null;
} | null;
}
export interface StorageClassNFS {
server: string;
share: string;
subDirectory?: string | null;
mountPermissions?: string | null;
mountOptions?: string[] | null;
}
export interface StorageClassS3 {
endpoint: string;
region?: string | null;
insecure?: boolean | null;
accessKey?: string | null;
bucket?: string | null;
mountOptions?: string[] | null;
} }
export interface StorageClassItem { export interface StorageClassItem {
metadata: Metadata; id: number;
provisioner: string; created_at: string;
reclaimPolicy: 'Delete' | 'Retain'; updated_at: string;
volumeBindingMode: 'Immediate' | 'WaitForFirstConsumer'; deleted_at?: string | null;
owner_principal_id?: number | null;
displayName?: string | null;
description?: string | null;
name: string;
spec: {
nfs?: StorageClassNFS | null;
s3?: StorageClassS3 | null;
};
} }
+21 -3
View File
@@ -3,11 +3,13 @@ import {
Input as CInput, Input as CInput,
InputNumber, InputNumber,
Select as SealSelect, Select as SealSelect,
Textarea,
useAppUtils useAppUtils
} from '@gpustack/core-ui'; } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Flex, Form } from 'antd'; import { Flex, Form } from 'antd';
import { useEffect } from 'react'; import { useEffect } from 'react';
import OwnerPrincipalIdField from '../../../_components/owner-principal-id-field';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
import useQueryStorageClass from '../services/use-query-storage-class'; import useQueryStorageClass from '../services/use-query-storage-class';
@@ -18,14 +20,15 @@ const Basic = ({ open, action }: { open: boolean; action: string }) => {
useEffect(() => { useEffect(() => {
if (open) { if (open) {
fetchData({}); fetchData({ page: 1, perPage: 100 });
} }
}, [open]); }, [open]);
return ( return (
<> <>
<OwnerPrincipalIdField name="owner_principal_id" />
<Form.Item<FormData> <Form.Item<FormData>
name={['metadata', 'name']} name="name"
rules={[ rules={[
{ {
required: true, required: true,
@@ -43,6 +46,19 @@ const Basic = ({ open, action }: { open: boolean; action: string }) => {
required required
/> />
</Form.Item> </Form.Item>
<Form.Item<FormData> name="displayName">
<CInput.Input
trim={false}
label={intl.formatMessage({ id: 'common.table.displayName' })}
/>
</Form.Item>
<Form.Item<FormData> name="description">
<Textarea
trim={false}
scaleSize={true}
label={intl.formatMessage({ id: 'common.table.description' })}
/>
</Form.Item>
<Flex gap={16}> <Flex gap={16}>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<Form.Item<FormData> <Form.Item<FormData>
@@ -79,7 +95,9 @@ const Basic = ({ open, action }: { open: boolean; action: string }) => {
> >
<InputNumber <InputNumber
disabled={action === PageAction.EDIT} disabled={action === PageAction.EDIT}
label={intl.formatMessage({ id: 'gpuservice.storage.capacity' })} label={intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.capacity'
})}
required required
/> />
</Form.Item> </Form.Item>
+4 -12
View File
@@ -10,19 +10,12 @@ interface StorageFormProps {
open: boolean; open: boolean;
action: PageActionType; action: PageActionType;
currentData?: ListItem | null; currentData?: ListItem | null;
namespace?: string;
onFinish: (values: FormData) => Promise<void>; onFinish: (values: FormData) => Promise<void>;
} }
const GPUServiceStorageForm: React.FC<StorageFormProps> = forwardRef( const GPUServiceStorageForm: React.FC<StorageFormProps> = forwardRef(
(props, ref) => { (props, ref) => {
const { const { action, currentData, open, onFinish } = props;
action,
currentData,
open,
namespace = 'default',
onFinish
} = props;
const [form] = Form.useForm<FormData>(); const [form] = Form.useForm<FormData>();
useEffect(() => { useEffect(() => {
@@ -33,10 +26,9 @@ const GPUServiceStorageForm: React.FC<StorageFormProps> = forwardRef(
if (action === PageAction.EDIT && currentData) { if (action === PageAction.EDIT && currentData) {
form.setFieldsValue({ form.setFieldsValue({
metadata: { name: currentData.name,
name: currentData.metadata?.name, displayName: currentData.displayName,
namespace: currentData.metadata?.namespace description: currentData.description,
},
spec: { spec: {
capacity: currentData.spec?.capacity, capacity: currentData.spec?.capacity,
type: currentData.spec?.type type: currentData.spec?.type
@@ -0,0 +1,52 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import useBodyScroll from '@/hooks/use-body-scroll';
import { useState } from 'react';
import { ListItem } from '../config/types';
const useCreateStorageModal = () => {
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const [openModalStatus, setOpenModalStatus] = useState<{
open: boolean;
action: PageActionType;
currentData?: ListItem | null;
title: string;
}>({
open: false,
action: PageAction.CREATE,
currentData: null,
title: ''
});
const openModal = (
action: PageActionType,
title: string,
row?: ListItem | null
) => {
setOpenModalStatus({
open: true,
title,
action,
currentData: row ?? null
});
saveScrollHeight();
};
const closeModal = () => {
setOpenModalStatus({
open: false,
action: PageAction.CREATE,
currentData: null,
title: ''
});
restoreScrollHeight();
};
return {
openStorageModalStatus: openModalStatus,
openStorageModal: openModal,
closeStorageModal: closeModal
};
};
export default useCreateStorageModal;
@@ -3,12 +3,7 @@ import { useIntl } from '@umijs/max';
import type { ColumnsType } from 'antd/lib/table'; import type { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { import { rowActionList, status, StoragePhaseLabelMap } from '../config';
rowActionList,
status,
StoragePhaseLabelMap,
StorageTypeLabelMap
} from '../config';
import { ListItem } from '../config/types'; import { ListItem } from '../config/types';
interface ColumnsHookProps { interface ColumnsHookProps {
@@ -25,15 +20,15 @@ const useStorageColumns = ({
return [ return [
{ {
title: intl.formatMessage({ id: 'common.table.name' }), title: intl.formatMessage({ id: 'common.table.name' }),
dataIndex: ['metadata', 'name'], dataIndex: 'name',
key: 'name', key: 'name',
sorter: false, sorter: false,
ellipsis: { ellipsis: {
showTitle: false showTitle: false
}, },
render: (text: string) => ( render: (text: string, record: ListItem) => (
<AutoTooltip ghost style={{ maxWidth: 360 }}> <AutoTooltip ghost style={{ maxWidth: 360 }}>
<span className="text-primary">{text}</span> <span className="text-primary">{record.displayName || text}</span>
</AutoTooltip> </AutoTooltip>
) )
}, },
@@ -42,10 +37,7 @@ const useStorageColumns = ({
dataIndex: ['spec', 'type'], dataIndex: ['spec', 'type'],
key: 'type', key: 'type',
sorter: false, sorter: false,
render: (value: string) => render: (value: string) => value || '-'
StorageTypeLabelMap[value]
? intl.formatMessage({ id: StorageTypeLabelMap[value] })
: value || '-'
}, },
{ {
title: intl.formatMessage({ id: 'gpuservice.storage.capacity' }), title: intl.formatMessage({ id: 'gpuservice.storage.capacity' }),
@@ -59,20 +51,22 @@ const useStorageColumns = ({
dataIndex: ['status', 'phase'], dataIndex: ['status', 'phase'],
key: 'status', key: 'status',
sorter: false, sorter: false,
render: (value: string, record: ListItem) => ( render: (value: string) =>
value ? (
<StatusTag <StatusTag
statusValue={{ statusValue={{
status: status[value], status: status[value],
text: StoragePhaseLabelMap[value] || value, text: StoragePhaseLabelMap[value] || value
message: record.status?.phaseMessage || ''
}} }}
></StatusTag> ></StatusTag>
) : (
'-'
) )
}, },
{ {
title: intl.formatMessage({ id: 'common.table.createTime' }), title: intl.formatMessage({ id: 'common.table.createTime' }),
dataIndex: ['metadata', 'creationTimestamp'], dataIndex: 'created_at',
key: 'creationTimestamp', key: 'created_at',
sorter: false, sorter: false,
ellipsis: { ellipsis: {
showTitle: false showTitle: false
+43 -193
View File
@@ -1,85 +1,26 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { getCurrentOrgNamespace } from '@/atoms/user';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings'; import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
import type { PageActionType } from '@/config/types';
import useTableFetch from '@/hooks/use-table-fetch'; import useTableFetch from '@/hooks/use-table-fetch';
import { ProviderValueMap } from '@/pages/cluster-management/config'; import { DeleteModal, FilterBar, IconFont, NoResult } from '@gpustack/core-ui';
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
import {
BaseSelect,
DeleteModal,
FilterBar,
IconFont,
NoResult
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { ConfigProvider, Divider, Flex, message, Table } from 'antd'; import { ConfigProvider, message, Table } from 'antd';
import { useAtom } from 'jotai';
import _ from 'lodash'; import _ from 'lodash';
import { useCallback, useEffect, useMemo, useState } from 'react'; import PageBox from '../../_components/page-box';
import { PageContainerInner } from '../../_components/page-box';
import { import {
deleteGPUServiceStorage, deleteGPUServiceStorage,
GPU_SERVICE_STORAGE_API, GPU_SERVICE_STORAGE_API,
queryGPUServiceStorage queryGPUServiceStorage
} from './apis'; } from './apis';
import AddModal from './components/add-modal'; import AddModal from './components/add-modal';
import { FormData, ListItem } from './config/types'; import { FormData, ListItem } from './config/types';
import useCreateStorageModal from './hooks/use-create-storage-modal';
import useStorageColumns from './hooks/use-storage-columns'; import useStorageColumns from './hooks/use-storage-columns';
import useCreateStorage from './services/use-create-storage'; import useCreateStorage from './services/use-create-storage';
import useUpdateStorage from './services/use-update-storage'; import useUpdateStorage from './services/use-update-storage';
const GPUServiceStorage: React.FC = () => { const GPUServiceStorage: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom);
const clusterID = currentCluster?.id;
// Admin "All" view falls back to the cluster's owner Org name —
// see :func:`getCurrentOrgNamespace`.
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
const deleteStorage = useCallback(
(id: number) => deleteGPUServiceStorage({ namespace, clusterID, id }),
[namespace, clusterID]
);
const fetchStorage = useCallback(
async (
params: any,
options?: any
): Promise<Global.PageResponse<ListItem>> => {
const effectiveClusterID = params.cluster_id ?? clusterID;
if (!effectiveClusterID) {
return {
items: [],
pagination: {
total: 0,
totalPage: 0,
page: 1,
perPage: params.perPage || 10
}
} as Global.PageResponse<ListItem>;
}
const res = await queryGPUServiceStorage(
{ ...params, namespace, clusterID: effectiveClusterID },
options
);
const total = res?.items?.length ?? 0;
const perPage = params.perPage || 10;
return {
items: res?.items ?? [],
pagination: {
total,
totalPage: Math.ceil(total / perPage),
page: params.page || 1,
perPage
}
};
},
[namespace, clusterID]
);
const { const {
dataSource, dataSource,
@@ -92,116 +33,55 @@ const GPUServiceStorage: React.FC = () => {
fetchData, fetchData,
handlePageChange, handlePageChange,
handleTableChange, handleTableChange,
handleQueryChange,
handleSearch, handleSearch,
handleNameChange handleNameChange
} = useTableFetch<ListItem>({ } = useTableFetch<ListItem>({
key: PaginationKey.Storage, key: PaginationKey.Storage,
fetchAPI: fetchStorage, fetchAPI: queryGPUServiceStorage,
deleteAPI: deleteStorage, deleteAPI: deleteGPUServiceStorage,
watch: false, watch: false,
polling: true, polling: true,
API: GPU_SERVICE_STORAGE_API({ namespace, clusterID }), API: GPU_SERVICE_STORAGE_API,
contentForDelete: intl.formatMessage({ id: 'gpuservice.storage' }) contentForDelete: intl.formatMessage({ id: 'gpuservice.storage' })
}); });
const { fetchData: createStorage } = useCreateStorage(); const { fetchData: createStorage } = useCreateStorage();
const { fetchData: updateStorage } = useUpdateStorage(); const { fetchData: updateStorage } = useUpdateStorage();
const { const { openStorageModalStatus, openStorageModal, closeStorageModal } =
fetchClusterList, useCreateStorageModal();
cancelRequest: cancelClusterRequest,
clusterList
} = useQueryClusterList();
const k8sClusterList = useMemo(
() =>
clusterList.filter(
(item) => item.provider === ProviderValueMap.Kubernetes
),
[clusterList]
);
const [openAddModalStatus, setOpenAddModalStatus] = useState<{
action: PageActionType;
open: boolean;
title: string;
currentData?: ListItem | null;
}>({
action: PageAction.CREATE,
title: '',
open: false,
currentData: null
});
useEffect(() => {
fetchClusterList({ page: -1 }).then((clusters) => {
const k8sClusters = clusters.filter(
(item: any) => item.provider === ProviderValueMap.Kubernetes
);
if (k8sClusters.length === 0) {
return;
}
const storedCluster = k8sClusters.find(
(item: any) => item.id === currentCluster?.id
);
const targetCluster = storedCluster ?? k8sClusters[0];
if (!storedCluster) {
setCurrentCluster({
...targetCluster,
label: targetCluster.name,
value: targetCluster.id
});
}
handleQueryChange({
cluster_id: targetCluster.id,
page: 1
});
});
return () => {
cancelClusterRequest();
};
}, []);
const handleAddStorage = () => { const handleAddStorage = () => {
setOpenAddModalStatus({ openStorageModal(
action: PageAction.CREATE, PageAction.CREATE,
title: intl.formatMessage({ id: 'gpuservice.storage.add' }), intl.formatMessage({ id: 'gpuservice.storage.add' })
open: true, );
currentData: null
});
}; };
const handleEditStorage = (row: ListItem) => { const handleEditStorage = (row: ListItem) => {
setOpenAddModalStatus({ openStorageModal(
action: PageAction.EDIT, PageAction.EDIT,
title: intl.formatMessage({ id: 'gpuservice.storage.edit' }), intl.formatMessage({ id: 'gpuservice.storage.edit' }),
open: true, row
currentData: row );
});
};
const closeModal = () => {
setOpenAddModalStatus({
action: PageAction.CREATE,
title: '',
open: false,
currentData: null
});
}; };
const handleModalOk = async (data: FormData) => { const handleModalOk = async (data: FormData) => {
try { try {
if (openAddModalStatus.action === PageAction.EDIT) { if (openStorageModalStatus.action === PageAction.EDIT) {
await updateStorage({ await updateStorage({
id: openAddModalStatus.currentData!.metadata?.name as any, id: openStorageModalStatus.currentData!.id,
data data: {
owner_principal_id: data.owner_principal_id,
displayName: data.displayName,
description: data.description
}
}); });
} else { } else {
await createStorage({ data }); await createStorage({ data });
} }
fetchData(); fetchData();
closeModal(); closeStorageModal();
message.success(intl.formatMessage({ id: 'common.message.success' })); message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) { } catch (error) {
message.error(intl.formatMessage({ id: 'common.message.fail' })); message.error(intl.formatMessage({ id: 'common.message.fail' }));
@@ -214,23 +94,11 @@ const GPUServiceStorage: React.FC = () => {
} else if (val === 'delete') { } else if (val === 'delete') {
handleDelete({ handleDelete({
...row, ...row,
name: row.metadata?.name, id: row.id
id: row.metadata?.name as any
}); });
} }
}); });
const handleClusterChange = (value: number) => {
const cluster = k8sClusterList.find((item) => item.value === value);
if (cluster) {
setCurrentCluster(cluster);
}
handleQueryChange({
cluster_id: value,
page: 1
});
};
const renderEmpty = (type?: string) => { const renderEmpty = (type?: string) => {
if (type !== 'Table') return; if (type !== 'Table') return;
return ( return (
@@ -260,38 +128,13 @@ const GPUServiceStorage: React.FC = () => {
return ( return (
<> <>
<PageContainerInner <PageBox>
leftContent={
<Flex align="center">
<span>{intl.formatMessage({ id: 'menu.gpuService.storage' })}</span>
<Divider
orientation="vertical"
style={{
marginLeft: 16
}}
/>
<BaseSelect
variant="borderless"
value={queryParams.cluster_id}
options={k8sClusterList}
style={{ minWidth: 120, fontWeight: 500 }}
onChange={handleClusterChange}
></BaseSelect>
</Flex>
}
>
<FilterBar <FilterBar
marginBottom={22} marginBottom={22}
marginTop={30} marginTop={30}
showSelect={false} showSelect={false}
selectOptions={k8sClusterList}
select={{ showSearch: true }}
selectHolder={intl.formatMessage({
id: 'gpuservice.storage.filter.cluster'
})}
buttonText={intl.formatMessage({ id: 'gpuservice.storage.add' })} buttonText={intl.formatMessage({ id: 'gpuservice.storage.add' })}
handleSearch={handleSearch} handleSearch={handleSearch}
handleSelectChange={handleClusterChange}
handleDeleteByBatch={handleDeleteBatch} handleDeleteByBatch={handleDeleteBatch}
handleClickPrimary={handleAddStorage} handleClickPrimary={handleAddStorage}
handleInputChange={handleNameChange} handleInputChange={handleNameChange}
@@ -309,18 +152,25 @@ const GPUServiceStorage: React.FC = () => {
}} }}
sortDirections={TABLE_SORT_DIRECTIONS} sortDirections={TABLE_SORT_DIRECTIONS}
showSorterTooltip={false} showSorterTooltip={false}
rowKey={(record) => record.metadata?.name as any} rowKey={(record) => record.id}
onChange={handleTableChange} onChange={handleTableChange}
pagination={false} pagination={{
showSizeChanger: true,
pageSize: queryParams.perPage,
current: queryParams.page,
total: dataSource.total,
hideOnSinglePage: queryParams.perPage === 10,
onChange: handlePageChange
}}
/> />
</ConfigProvider> </ConfigProvider>
</PageContainerInner> </PageBox>
<AddModal <AddModal
open={openAddModalStatus.open} open={openStorageModalStatus.open}
action={openAddModalStatus.action} action={openStorageModalStatus.action}
title={openAddModalStatus.title} title={openStorageModalStatus.title}
data={openAddModalStatus.currentData} data={openStorageModalStatus.currentData}
onCancel={closeModal} onCancel={closeStorageModal}
onOk={handleModalOk} onOk={handleModalOk}
/> />
<DeleteModal ref={modalRef} /> <DeleteModal ref={modalRef} />
@@ -1,9 +1,5 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { getCurrentOrgNamespace } from '@/atoms/user';
import { useQueryData } from '@gpustack/core-ui'; import { useQueryData } from '@gpustack/core-ui';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { apiVersion, KindMapping } from '../../constants';
import { createGPUServiceStorage } from '../apis'; import { createGPUServiceStorage } from '../apis';
import { FormData, ListItem } from '../config/types'; import { FormData, ListItem } from '../config/types';
@@ -12,31 +8,10 @@ interface CreateStorageParams {
} }
export default function useCreateStorage() { export default function useCreateStorage() {
const currentCluster = useAtomValue(currentClusterAtom);
const clusterID = currentCluster?.id;
// Admin "All" view has no Org context — fall back to the
// selected cluster's owner Org name for the K8s namespace.
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
const fetchDetail = useCallback( const fetchDetail = useCallback(
(params: CreateStorageParams, option?: any) => (params: CreateStorageParams) =>
createGPUServiceStorage( createGPUServiceStorage({ data: params.data }),
{ []
namespace,
clusterID,
data: {
apiVersion,
kind: KindMapping.instancePersistentVolume,
metadata: {
name: params.data.metadata.name,
namespace
},
spec: params.data.spec
}
},
option
),
[namespace, clusterID]
); );
const { detailData, loading, cancelRequest, fetchData } = useQueryData< const { detailData, loading, cancelRequest, fetchData } = useQueryData<
@@ -1,28 +1,23 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { useQueryDataList } from '@gpustack/core-ui'; import { useQueryDataList } from '@gpustack/core-ui';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { queryStorageClass } from '../apis'; import { queryStorageClass } from '../apis';
import { StorageClassItem } from '../config/types'; import { StorageClassItem } from '../config/types';
export default function useQueryStorageClass() { export default function useQueryStorageClass() {
const currentCluster = useAtomValue(currentClusterAtom);
const clusterID = currentCluster?.id;
const fetchList = useCallback( const fetchList = useCallback(
(params: Global.K8sSearchParams = {}, options?: any) => (params: Global.SearchParams = { page: 1, perPage: 100 }, options?: any) =>
queryStorageClass({ ...params, clusterID }, options), queryStorageClass(params, options),
[clusterID] []
); );
const { dataList, loading, cancelRequest, fetchData } = useQueryDataList< const { dataList, loading, cancelRequest, fetchData } = useQueryDataList<
StorageClassItem, StorageClassItem,
Global.K8sSearchParams Global.SearchParams
>({ >({
key: 'storageClass', key: 'storageClass',
fetchList, fetchList,
getLabel: (item) => item?.metadata.name, getLabel: (item) => item?.displayName || item?.name,
getValue: (item) => item?.metadata.name getValue: (item) => item?.name
}); });
return { return {
@@ -1,27 +1,18 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { getCurrentOrgNamespace } from '@/atoms/user';
import { useQueryData } from '@gpustack/core-ui'; import { useQueryData } from '@gpustack/core-ui';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { queryGPUServiceStorage } from '../apis'; import { queryGPUServiceStorage } from '../apis';
import { ListItem } from '../config/types'; import { ListItem } from '../config/types';
export default function useQueryStorage() { export default function useQueryStorage() {
const currentCluster = useAtomValue(currentClusterAtom);
const clusterID = currentCluster?.id;
// Admin "All" view has no Org context — fall back to the
// selected cluster's owner Org name for the K8s namespace.
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
const fetchDetail = useCallback( const fetchDetail = useCallback(
(params: Global.K8sSearchParams = {}, options?: any) => (params: Global.SearchParams = { page: 1, perPage: 100 }, options?: any) =>
queryGPUServiceStorage({ ...params, namespace, clusterID }, options), queryGPUServiceStorage(params, options),
[namespace, clusterID] []
); );
const { detailData, loading, cancelRequest, fetchData } = useQueryData< const { detailData, loading, cancelRequest, fetchData } = useQueryData<
Global.K8sPageResponse<ListItem>, Global.PageResponse<ListItem>,
Global.K8sSearchParams Global.SearchParams
>({ >({
fetchDetail, fetchDetail,
key: 'storage' key: 'storage'
@@ -1,44 +1,18 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { getCurrentOrgNamespace } from '@/atoms/user';
import { useQueryData } from '@gpustack/core-ui'; import { useQueryData } from '@gpustack/core-ui';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { apiVersion, KindMapping } from '../../constants';
import { updateGPUServiceStorage } from '../apis'; import { updateGPUServiceStorage } from '../apis';
import { FormData, ListItem } from '../config/types'; import { ListItem, UpdateData } from '../config/types';
interface UpdateStorageParams { interface UpdateStorageParams {
id: number; id: number;
data: FormData; data: UpdateData;
} }
export default function useUpdateStorage() { export default function useUpdateStorage() {
const currentCluster = useAtomValue(currentClusterAtom);
const clusterID = currentCluster?.id;
// Admin "All" view has no Org context — fall back to the
// selected cluster's owner Org name for the K8s namespace.
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
const fetchDetail = useCallback( const fetchDetail = useCallback(
(params: UpdateStorageParams, option?: any) => (params: UpdateStorageParams) =>
updateGPUServiceStorage( updateGPUServiceStorage({ id: params.id, data: params.data }),
{ []
namespace,
clusterID,
id: params.id,
data: {
apiVersion,
kind: KindMapping.instancePersistentVolume,
metadata: {
name: params.data.metadata.name,
namespace
},
spec: params.data.spec
}
},
option
),
[namespace, clusterID]
); );
const { detailData, loading, cancelRequest, fetchData } = useQueryData< const { detailData, loading, cancelRequest, fetchData } = useQueryData<
@@ -58,7 +58,7 @@ const Basic: React.FC<BasicProps> = ({
<Flex gap={4} align="center"> <Flex gap={4} align="center">
{label} {label}
<span> <span>
({intl.formatMessage({ id: 'common.remaining' }, { count: max })}) ({intl.formatMessage({ id: 'common.max' }, { count: max })})
</span> </span>
</Flex> </Flex>
); );
@@ -126,6 +126,7 @@ const Basic: React.FC<BasicProps> = ({
id: 'gpuservice.template.displayName' id: 'gpuservice.template.displayName'
})} })}
showCount showCount
trim={false}
maxLength={63} maxLength={63}
/> />
</Form.Item> </Form.Item>