refactor: gpu service API alignment
This commit is contained in:
+10
-1
@@ -213,10 +213,19 @@ const baseRoutes = [
|
||||
name: 'storage',
|
||||
path: '/gpu-service/storage',
|
||||
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',
|
||||
selectedIcon: 'icon-storage-filled',
|
||||
defaultIcon: 'icon-storage-outlined',
|
||||
component: './gpu-service/storage'
|
||||
component: './gpu-service/storage-types'
|
||||
},
|
||||
{
|
||||
name: 'publicKeys',
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
"@ant-design/pro-components": "3.1.0-0",
|
||||
"@antv/g6": "^5.0.51",
|
||||
"@braintree/sanitize-url": "^7.1.1",
|
||||
"@gpustack/core-ui": "^1.0.14",
|
||||
"@gpustack/core-ui": "^1.0.16",
|
||||
"@huggingface/gguf": "^0.1.7",
|
||||
"@huggingface/hub": "^0.15.1",
|
||||
"@huggingface/tasks": "^0.11.6",
|
||||
|
||||
Generated
+79
-38
@@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^7.1.1
|
||||
version: 7.1.2
|
||||
'@gpustack/core-ui':
|
||||
specifier: ^1.0.14
|
||||
version: 1.0.14(czdvzceysqw7iv6pct2ucnb23e)
|
||||
specifier: ^1.0.16
|
||||
version: 1.0.16(czdvzceysqw7iv6pct2ucnb23e)
|
||||
'@huggingface/gguf':
|
||||
specifier: ^0.1.7
|
||||
version: 0.1.18
|
||||
@@ -49,7 +49,7 @@ importers:
|
||||
version: 4.17.24
|
||||
'@umijs/max':
|
||||
specifier: ^4.6.15
|
||||
version: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.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':
|
||||
specifier: ^0.10.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)
|
||||
umi-presets-pro:
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.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:
|
||||
specifier: ^7.8.8
|
||||
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}
|
||||
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':
|
||||
resolution: {integrity: sha512-nmkwYt4YA815uzaXc/9KZ4dGe0bW8FkD9yFXL8xoFXPH5gjGg8DGGXDvWJ4UqG3IIPjY6whwYm61sMxgfK192Q==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.14.tgz}
|
||||
'@gpustack/core-ui@1.0.16':
|
||||
resolution: {integrity: sha512-wFKDv7X0FXRmAmZPt4WKYV0+aGHcx82/3EZSJkrut/49XQd5XdrqKaimGtlFWFCNEWqsouwzs4uqaC7JFqfOkQ==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.16.tgz}
|
||||
peerDependencies:
|
||||
'@ant-design/icons': '>=6.0.0'
|
||||
'@ant-design/pro-components': 3.1.0-0
|
||||
@@ -2463,6 +2463,9 @@ packages:
|
||||
'@types/node@25.6.2':
|
||||
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':
|
||||
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':
|
||||
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':
|
||||
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}
|
||||
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:
|
||||
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'}
|
||||
@@ -8605,6 +8615,11 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
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:
|
||||
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'}
|
||||
@@ -8818,6 +8833,9 @@ packages:
|
||||
undici-types@7.19.2:
|
||||
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:
|
||||
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'}
|
||||
hasBin: true
|
||||
|
||||
webpack-sources@3.4.1:
|
||||
resolution: {integrity: sha512-eACpxRN02yaawnt+uUNIF7Qje6A9zArxBbcAJjK1PK3S9Ycg5jIuJ8pW4q8EMnwNZCEGltcjkRx1QzOxOkKD8A==, tarball: https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.1.tgz}
|
||||
webpack-sources@3.5.0:
|
||||
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'}
|
||||
|
||||
webpack@5.106.2:
|
||||
@@ -10790,7 +10808,7 @@ snapshots:
|
||||
|
||||
'@formatjs/intl-utils@2.3.0': {}
|
||||
|
||||
'@gpustack/core-ui@1.0.14(czdvzceysqw7iv6pct2ucnb23e)':
|
||||
'@gpustack/core-ui@1.0.16(czdvzceysqw7iv6pct2ucnb23e)':
|
||||
dependencies:
|
||||
'@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@ant-design/pro-components': 3.1.0-0(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -11811,7 +11829,7 @@ snapshots:
|
||||
|
||||
'@types/history@5.0.0':
|
||||
dependencies:
|
||||
history: 5.3.0
|
||||
history: 4.10.1
|
||||
|
||||
'@types/hoist-non-react-statics@3.3.7(@types/react@18.3.28)':
|
||||
dependencies:
|
||||
@@ -11865,6 +11883,10 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 7.19.2
|
||||
|
||||
'@types/node@25.9.1':
|
||||
dependencies:
|
||||
undici-types: 7.24.6
|
||||
|
||||
'@types/normalize-package-data@2.4.4': {}
|
||||
|
||||
'@types/parse-json@4.0.2': {}
|
||||
@@ -11885,26 +11907,31 @@ snapshots:
|
||||
'@types/react-router-dom@4.3.5':
|
||||
dependencies:
|
||||
'@types/history': 5.0.0
|
||||
'@types/react': 18.3.28
|
||||
'@types/react': 18.3.29
|
||||
'@types/react-router': 5.1.20
|
||||
|
||||
'@types/react-router-redux@5.0.27':
|
||||
dependencies:
|
||||
'@types/history': 4.7.11
|
||||
'@types/react': 18.3.28
|
||||
'@types/react': 18.3.29
|
||||
'@types/react-router': 5.1.20
|
||||
redux: 4.2.1
|
||||
redux: 3.7.2
|
||||
|
||||
'@types/react-router@5.1.20':
|
||||
dependencies:
|
||||
'@types/history': 4.7.11
|
||||
'@types/react': 18.3.28
|
||||
'@types/react': 18.3.29
|
||||
|
||||
'@types/react@18.3.28':
|
||||
dependencies:
|
||||
'@types/prop-types': 15.7.15
|
||||
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/semver@7.7.1': {}
|
||||
@@ -12287,18 +12314,18 @@ snapshots:
|
||||
- webpack-hot-middleware
|
||||
- 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:
|
||||
'@svgr/core': 6.5.1
|
||||
'@umijs/bundler-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
|
||||
less: 4.1.3
|
||||
postcss-preset-env: 7.5.0(postcss@8.5.14)
|
||||
rollup-plugin-visualizer: 5.9.0(rollup@3.30.0)
|
||||
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:
|
||||
- '@types/node'
|
||||
- lightningcss
|
||||
@@ -12526,14 +12553,14 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@umijs/max@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.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:
|
||||
'@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)
|
||||
antd: 4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
eslint: 8.35.0
|
||||
stylelint: 14.8.2
|
||||
umi: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.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:
|
||||
- '@babel/core'
|
||||
- '@rspack/core'
|
||||
@@ -12750,7 +12777,7 @@ snapshots:
|
||||
- react-native
|
||||
- supports-color
|
||||
|
||||
'@umijs/preset-umi@4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.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:
|
||||
'@iconify/utils': 2.1.1
|
||||
'@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-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-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/core': 4.6.51
|
||||
'@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-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:
|
||||
chokidar: 3.6.0
|
||||
express: 4.22.1
|
||||
lodash: 4.18.1
|
||||
prettier: 2.8.8
|
||||
umi: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.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:
|
||||
- supports-color
|
||||
|
||||
@@ -13027,13 +13054,13 @@ snapshots:
|
||||
- supports-color
|
||||
- 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:
|
||||
'@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)
|
||||
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:
|
||||
- supports-color
|
||||
|
||||
@@ -14645,6 +14672,11 @@ snapshots:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.3.3
|
||||
|
||||
enhanced-resolve@5.22.0:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.3.3
|
||||
|
||||
enhanced-resolve@5.9.3:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
@@ -16358,7 +16390,7 @@ snapshots:
|
||||
|
||||
jest-worker@27.5.1:
|
||||
dependencies:
|
||||
'@types/node': 25.6.2
|
||||
'@types/node': 25.9.1
|
||||
merge-stream: 2.0.0
|
||||
supports-color: 8.1.1
|
||||
|
||||
@@ -19928,7 +19960,7 @@ snapshots:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jest-worker: 27.5.1
|
||||
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)
|
||||
optionalDependencies:
|
||||
lightningcss: 1.22.1
|
||||
@@ -19941,6 +19973,13 @@ snapshots:
|
||||
commander: 2.20.3
|
||||
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:
|
||||
dependencies:
|
||||
'@istanbuljs/schema': 0.1.6
|
||||
@@ -20125,12 +20164,12 @@ snapshots:
|
||||
|
||||
ua-parser-js@0.7.41: {}
|
||||
|
||||
umi-presets-pro@2.0.3(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.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:
|
||||
'@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/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
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
@@ -20154,14 +20193,14 @@ snapshots:
|
||||
isomorphic-fetch: 2.2.1
|
||||
qs: 6.15.1
|
||||
|
||||
umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.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:
|
||||
'@babel/runtime': 7.23.6
|
||||
'@umijs/bundler-utils': 4.6.51
|
||||
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||
'@umijs/core': 4.6.51
|
||||
'@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3)
|
||||
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.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/server': 4.6.51
|
||||
'@umijs/test': 4.6.51(@babel/core@7.29.0)
|
||||
@@ -20208,14 +20247,14 @@ snapshots:
|
||||
- webpack-hot-middleware
|
||||
- webpack-plugin-serve
|
||||
|
||||
umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.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:
|
||||
'@babel/runtime': 7.23.6
|
||||
'@umijs/bundler-utils': 4.6.51
|
||||
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||
'@umijs/core': 4.6.51
|
||||
'@umijs/lint': 4.6.51(eslint@9.39.4(jiti@2.7.0))(stylelint@14.8.2)(typescript@5.9.3)
|
||||
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.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/server': 4.6.51
|
||||
'@umijs/test': 4.6.51(@babel/core@7.29.0)
|
||||
@@ -20273,6 +20312,8 @@ snapshots:
|
||||
|
||||
undici-types@7.19.2: {}
|
||||
|
||||
undici-types@7.24.6: {}
|
||||
|
||||
unfetch@5.0.0: {}
|
||||
|
||||
unified@11.0.5:
|
||||
@@ -20457,7 +20498,7 @@ snapshots:
|
||||
'@types/unist': 3.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:
|
||||
esbuild: 0.18.20
|
||||
postcss: 8.5.14
|
||||
@@ -20468,7 +20509,7 @@ snapshots:
|
||||
less: 4.1.3
|
||||
lightningcss: 1.22.1
|
||||
sass: 1.54.0
|
||||
terser: 5.47.1
|
||||
terser: 5.48.0
|
||||
|
||||
vm-browserify@1.1.2: {}
|
||||
|
||||
@@ -20530,7 +20571,7 @@ snapshots:
|
||||
- bufferutil
|
||||
- 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):
|
||||
dependencies:
|
||||
@@ -20544,7 +20585,7 @@ snapshots:
|
||||
acorn-import-phases: 1.0.4(acorn@8.16.0)
|
||||
browserslist: 4.28.2
|
||||
chrome-trace-event: 1.0.4
|
||||
enhanced-resolve: 5.21.2
|
||||
enhanced-resolve: 5.22.0
|
||||
es-module-lexer: 2.1.0
|
||||
eslint-scope: 5.1.1
|
||||
events: 3.3.0
|
||||
@@ -20557,7 +20598,7 @@ snapshots:
|
||||
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))
|
||||
watchpack: 2.5.1
|
||||
webpack-sources: 3.4.1
|
||||
webpack-sources: 3.5.0
|
||||
transitivePeerDependencies:
|
||||
- '@minify-html/node'
|
||||
- '@swc/core'
|
||||
|
||||
@@ -66,9 +66,7 @@ const NO_CONTAINER_PAGES = [
|
||||
'clusterCreate',
|
||||
'benchmarkDetail',
|
||||
'deployment',
|
||||
'video',
|
||||
'instances',
|
||||
'storage'
|
||||
'video'
|
||||
];
|
||||
|
||||
const CHECK_RESOURCE_PATH = [
|
||||
@@ -358,7 +356,7 @@ export default (props: any) => {
|
||||
config={{
|
||||
apiBaseUrl: GPUSTACK_API_BASE_URL,
|
||||
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,
|
||||
defaultColorPrimary: COLOR_PRIMARY
|
||||
}}
|
||||
|
||||
@@ -58,6 +58,7 @@ export default {
|
||||
'common.table.createTime': 'Created',
|
||||
'common.table.updateTime': 'Updated',
|
||||
'common.table.description': 'Description',
|
||||
'common.table.displayName': 'Display Name',
|
||||
'common.table.name': 'Name',
|
||||
'common.table.status': 'Status',
|
||||
'common.table.name.list': '{type} Name',
|
||||
@@ -283,5 +284,6 @@ export default {
|
||||
'common.image.limit.width': 'Image width must be {width}.',
|
||||
'common.image.limit.height': 'Image height must be {height}.',
|
||||
'common.remaining': 'Remaining {count}',
|
||||
'common.max': 'Maximum {count}',
|
||||
'common.validate.group': 'Please complete the {group} configuration'
|
||||
};
|
||||
|
||||
@@ -39,6 +39,35 @@ export default {
|
||||
'gpuservice.template.card.mount': 'Mount',
|
||||
'gpuservice.template.card.resources': 'Resources',
|
||||
'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.instance.ssh.enable': 'Enable SSH Access',
|
||||
'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.',
|
||||
'gpuservice.storage.persistentVolume.required':
|
||||
'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.required':
|
||||
'Please enter the temporary storage capacity',
|
||||
|
||||
@@ -45,5 +45,6 @@ export default {
|
||||
'menu.gpuService.instances': 'GPU Instances',
|
||||
'menu.gpuService.templates': 'Instance Templates',
|
||||
'menu.gpuService.storage': 'Storage',
|
||||
'menu.gpuService.storageTypes': 'Storage Types',
|
||||
'menu.gpuService.publicKeys': 'SSH Public Keys'
|
||||
};
|
||||
|
||||
@@ -65,6 +65,10 @@ export default {
|
||||
'noresult.gpuservice.storage.title': 'No Storage',
|
||||
'noresult.gpuservice.storage.subTitle': 'No storage has been added yet.',
|
||||
'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.subTitle':
|
||||
'No SSH public keys have been added yet.',
|
||||
|
||||
@@ -58,6 +58,7 @@ export default {
|
||||
'common.table.createTime': '作成日時',
|
||||
'common.table.updateTime': '更新日時',
|
||||
'common.table.description': '説明',
|
||||
'common.table.displayName': '表示名',
|
||||
'common.table.name': '名前',
|
||||
'common.table.status': 'ステータス',
|
||||
'common.table.name.list': '{type} 名称',
|
||||
@@ -283,6 +284,7 @@ export default {
|
||||
'common.image.limit.width': 'Image width must be {width}.',
|
||||
'common.image.limit.height': 'Image height must be {height}.',
|
||||
'common.remaining': '残り {count}',
|
||||
'common.max': 'Maximum {count}',
|
||||
'common.validate.group': 'Please complete the {group} configuration'
|
||||
};
|
||||
|
||||
|
||||
@@ -37,6 +37,36 @@ export default {
|
||||
'gpuservice.template.card.mount': 'マウント',
|
||||
'gpuservice.template.card.resources': 'リソース',
|
||||
'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.instance.ssh.enable': 'SSH アクセスを有効化',
|
||||
'gpuservice.instance.ssh.assignKey': 'SSH 公開鍵を割り当て',
|
||||
@@ -102,6 +132,11 @@ export default {
|
||||
'gpuservice.storage.persistentVolume': '永続ボリューム',
|
||||
'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.required':
|
||||
'一時ストレージ容量を入力してください',
|
||||
|
||||
@@ -45,6 +45,7 @@ export default {
|
||||
'menu.gpuService.instances': 'GPU Instances',
|
||||
'menu.gpuService.templates': 'Instance Templates',
|
||||
'menu.gpuService.storage': 'Storage',
|
||||
'menu.gpuService.storageTypes': 'ストレージタイプ',
|
||||
'menu.gpuService.publicKeys': 'SSH Public Keys'
|
||||
};
|
||||
|
||||
|
||||
@@ -67,6 +67,11 @@ export default {
|
||||
'noresult.gpuservice.storage.subTitle':
|
||||
'ストレージはまだ追加されていません。',
|
||||
'noresult.gpuservice.storage.nofound': '一致するストレージが見つかりません。',
|
||||
'noresult.gpuservice.storageType.title': 'ストレージタイプなし',
|
||||
'noresult.gpuservice.storageType.subTitle':
|
||||
'ストレージタイプはまだ追加されていません。',
|
||||
'noresult.gpuservice.storageType.nofound':
|
||||
'一致するストレージタイプが見つかりません。',
|
||||
'noresult.gpuservice.sshkey.title': 'SSH 公開鍵なし',
|
||||
'noresult.gpuservice.sshkey.subTitle': 'SSH 公開鍵はまだ追加されていません。',
|
||||
'noresult.gpuservice.sshkey.nofound': '一致する SSH 公開鍵が見つかりません。'
|
||||
|
||||
@@ -58,6 +58,7 @@ export default {
|
||||
'common.table.createTime': 'Создано',
|
||||
'common.table.updateTime': 'Обновлено',
|
||||
'common.table.description': 'Описание',
|
||||
'common.table.displayName': 'Отображаемое имя',
|
||||
'common.table.name': 'Название',
|
||||
'common.table.status': 'Статус',
|
||||
'common.table.name.list': 'Название {type}',
|
||||
@@ -282,6 +283,7 @@ export default {
|
||||
'common.image.limit.width': 'Image width must be {width}.',
|
||||
'common.image.limit.height': 'Image height must be {height}.',
|
||||
'common.remaining': 'Остаток {count}',
|
||||
'common.max': 'Maximum {count}',
|
||||
'common.validate.group': 'Please complete the {group} configuration'
|
||||
};
|
||||
|
||||
|
||||
@@ -40,6 +40,36 @@ export default {
|
||||
'gpuservice.template.card.mount': 'Монтирование',
|
||||
'gpuservice.template.card.resources': 'Ресурсы',
|
||||
'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.instance.ssh.enable': 'Включить SSH-доступ',
|
||||
'gpuservice.instance.ssh.assignKey': 'Назначить открытый ключ SSH',
|
||||
@@ -102,6 +132,10 @@ export default {
|
||||
'gpuservice.storage.temporary': 'Временное',
|
||||
'gpuservice.storage.persistentVolume': 'Постоянный том',
|
||||
'gpuservice.storage.persistentVolume.required': 'Выберите постоянный том',
|
||||
'gpuservice.storage.persistentVolume.capacity': 'Ёмкость (ГБ)',
|
||||
'gpuservice.storage.persistentVolume.capacity.required': 'Введите ёмкость',
|
||||
'gpuservice.storage.persistentVolume.releaseWithInstance':
|
||||
'Освобождать вместе с экземпляром',
|
||||
'gpuservice.storage.tempCapacity': 'Объём (ГБ)',
|
||||
'gpuservice.storage.tempCapacity.required':
|
||||
'Введите объём временного хранилища',
|
||||
|
||||
@@ -45,6 +45,7 @@ export default {
|
||||
'menu.gpuService.instances': 'GPU Instances',
|
||||
'menu.gpuService.templates': 'Instance Templates',
|
||||
'menu.gpuService.storage': 'Storage',
|
||||
'menu.gpuService.storageTypes': 'Типы хранилищ',
|
||||
'menu.gpuService.publicKeys': 'SSH Public Keys'
|
||||
};
|
||||
|
||||
|
||||
@@ -66,6 +66,11 @@ export default {
|
||||
'noresult.gpuservice.storage.title': 'Нет хранилищ',
|
||||
'noresult.gpuservice.storage.subTitle': 'Хранилища ещё не добавлены.',
|
||||
'noresult.gpuservice.storage.nofound': 'Подходящие хранилища не найдены.',
|
||||
'noresult.gpuservice.storageType.title': 'Нет типов хранилищ',
|
||||
'noresult.gpuservice.storageType.subTitle':
|
||||
'Типы хранилищ ещё не добавлены.',
|
||||
'noresult.gpuservice.storageType.nofound':
|
||||
'Подходящие типы хранилищ не найдены.',
|
||||
'noresult.gpuservice.sshkey.title': 'Нет открытых ключей SSH',
|
||||
'noresult.gpuservice.sshkey.subTitle': 'Открытые ключи SSH ещё не добавлены.',
|
||||
'noresult.gpuservice.sshkey.nofound':
|
||||
|
||||
@@ -58,6 +58,7 @@ export default {
|
||||
'common.table.createTime': 'Oluşturulma',
|
||||
'common.table.updateTime': 'Güncellenme',
|
||||
'common.table.description': 'Açıklama',
|
||||
'common.table.displayName': 'Görünen Ad',
|
||||
'common.table.name': 'Ad',
|
||||
'common.table.status': 'Durum',
|
||||
'common.table.name.list': '{type} Adı',
|
||||
@@ -286,5 +287,6 @@ export default {
|
||||
'common.image.limit.width': 'Image width must be {width}.',
|
||||
'common.image.limit.height': 'Image height must be {height}.',
|
||||
'common.remaining': 'Kalan {count}',
|
||||
'common.max': 'Maksimum {count}',
|
||||
'common.validate.group': 'Please complete the {group} configuration'
|
||||
};
|
||||
|
||||
@@ -39,6 +39,36 @@ export default {
|
||||
'gpuservice.template.card.mount': 'Bağlama',
|
||||
'gpuservice.template.card.resources': 'Kaynaklar',
|
||||
'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.instance.ssh.enable': 'SSH Erişimini Etkinleştir',
|
||||
'gpuservice.instance.ssh.assignKey': 'SSH Açık Anahtarı Ata',
|
||||
@@ -103,6 +133,11 @@ export default {
|
||||
'gpuservice.storage.persistentVolume': 'Kalıcı Hacim',
|
||||
'gpuservice.storage.persistentVolume.required':
|
||||
'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.required':
|
||||
'Lütfen geçici depolama kapasitesini girin',
|
||||
|
||||
@@ -45,6 +45,7 @@ export default {
|
||||
'menu.gpuService.instances': 'GPU Instances',
|
||||
'menu.gpuService.templates': 'Instance Templates',
|
||||
'menu.gpuService.storage': 'Storage',
|
||||
'menu.gpuService.storageTypes': 'Depolama Türleri',
|
||||
'menu.gpuService.publicKeys': 'SSH Public Keys'
|
||||
};
|
||||
|
||||
|
||||
@@ -64,6 +64,10 @@ export default {
|
||||
'noresult.gpuservice.storage.title': 'Depolama Yok',
|
||||
'noresult.gpuservice.storage.subTitle': 'Henüz depolama eklenmedi.',
|
||||
'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.subTitle': 'Henüz SSH açık anahtarı eklenmedi.',
|
||||
'noresult.gpuservice.sshkey.nofound': 'Eşleşen SSH açık anahtarı bulunamadı.'
|
||||
|
||||
@@ -56,6 +56,7 @@ export default {
|
||||
'common.table.createTime': '创建时间',
|
||||
'common.table.updateTime': '更新时间',
|
||||
'common.table.description': '描述',
|
||||
'common.table.displayName': '显示名称',
|
||||
'common.table.name': '名称',
|
||||
'common.table.status': '状态',
|
||||
'common.table.name.list': '{type}名称',
|
||||
@@ -276,5 +277,6 @@ export default {
|
||||
'common.image.limit.width': '图片宽度须为{width}。',
|
||||
'common.image.limit.height': '图片高度须为{height}。',
|
||||
'common.remaining': '剩余 {count}',
|
||||
'common.max': '最大 {count}',
|
||||
'common.validate.group': '请填写完整的{group}配置'
|
||||
};
|
||||
|
||||
@@ -15,8 +15,8 @@ export default {
|
||||
'gpuservice.template.mountPath': '挂载路径',
|
||||
'gpuservice.template.containerDisk': '容器磁盘(GB)',
|
||||
'gpuservice.template.memory': '内存(GB)',
|
||||
'gpuservice.instance.containerDisk.remaining': '容器磁盘(剩余{count}GB)',
|
||||
'gpuservice.instance.memory.remaining': '内存(剩余{count}GB)',
|
||||
'gpuservice.instance.containerDisk.remaining': '容器磁盘(最大{count}GB)',
|
||||
'gpuservice.instance.memory.remaining': '内存(最大{count}GB)',
|
||||
'gpuservice.template.displayName': '显示名称',
|
||||
'gpuservice.template.displayName.max': '显示名称不能超过 63 个字符',
|
||||
'gpuservice.template.ports': '端口',
|
||||
@@ -34,6 +34,35 @@ export default {
|
||||
'gpuservice.template.card.mount': '挂载',
|
||||
'gpuservice.template.card.resources': '资源',
|
||||
'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.instance.ssh.enable': '启用 SSH 访问',
|
||||
'gpuservice.instance.ssh.assignKey': '分配 SSH 公钥',
|
||||
@@ -98,6 +127,9 @@ export default {
|
||||
'gpuservice.storage.persistentVolume.tips':
|
||||
'数据在实例重启后仍会保留,仅在实例被终止时删除。无法被其它实例共享。',
|
||||
'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.required': '请输入临时存储容量',
|
||||
'gpuservice.form.rule.name':
|
||||
|
||||
@@ -45,5 +45,6 @@ export default {
|
||||
'menu.gpuService.instances': 'GPU 实例',
|
||||
'menu.gpuService.templates': '实例模板',
|
||||
'menu.gpuService.storage': '存储',
|
||||
'menu.gpuService.storageTypes': '存储类型',
|
||||
'menu.gpuService.publicKeys': 'SSH 公钥'
|
||||
};
|
||||
|
||||
@@ -60,6 +60,9 @@ export default {
|
||||
'noresult.gpuservice.storage.title': '暂无存储',
|
||||
'noresult.gpuservice.storage.subTitle': '尚未添加任何存储。',
|
||||
'noresult.gpuservice.storage.nofound': '未找到匹配的存储',
|
||||
'noresult.gpuservice.storageType.title': '暂无存储类型',
|
||||
'noresult.gpuservice.storageType.subTitle': '尚未添加任何存储类型。',
|
||||
'noresult.gpuservice.storageType.nofound': '未找到匹配的存储类型',
|
||||
'noresult.gpuservice.sshkey.title': '暂无 SSH 公钥',
|
||||
'noresult.gpuservice.sshkey.subTitle': '尚未添加任何 SSH 公钥。',
|
||||
'noresult.gpuservice.sshkey.nofound': '未找到匹配的 SSH 公钥'
|
||||
|
||||
@@ -20,11 +20,10 @@ interface NumberSelectionProps {
|
||||
input?: React.CSSProperties;
|
||||
};
|
||||
labelExtra?: React.ReactNode;
|
||||
maxCount?: number;
|
||||
onChange?: (value: number) => void;
|
||||
}
|
||||
|
||||
const PRESET_ITEMS = [1, 2, 4, 8];
|
||||
|
||||
const NumberSelection: React.FC<NumberSelectionProps> = ({
|
||||
id,
|
||||
step = 1,
|
||||
@@ -36,21 +35,26 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
|
||||
required,
|
||||
labelExtra,
|
||||
className,
|
||||
maxCount = 8,
|
||||
style,
|
||||
onChange
|
||||
}) => {
|
||||
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
|
||||
? PRESET_ITEMS.filter((n) => n >= min && n <= max)
|
||||
? presetItems.filter((n) => n >= min && n <= max)
|
||||
: Array.from(
|
||||
{ length: Math.max(0, Math.floor((max - min) / step) + 1) },
|
||||
(_, i) => min + i * step
|
||||
);
|
||||
const items = computedItems.length === 0 ? [0] : computedItems;
|
||||
|
||||
const [inputValue, setInputValue] = useState<number | null>(() =>
|
||||
value !== undefined && value !== null && !PRESET_ITEMS.includes(value)
|
||||
value !== undefined && value !== null && !presetItems.includes(value)
|
||||
? value
|
||||
: null
|
||||
);
|
||||
@@ -58,7 +62,7 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
|
||||
useEffect(() => {
|
||||
if (value === undefined || value === null) {
|
||||
setInputValue(null);
|
||||
} else if (PRESET_ITEMS.includes(value)) {
|
||||
} else if (presetItems.includes(value)) {
|
||||
setInputValue(null);
|
||||
} else {
|
||||
setInputValue(value);
|
||||
@@ -83,7 +87,6 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
|
||||
}
|
||||
onChange?.(inputValue);
|
||||
};
|
||||
console.log('value+++', value, 'inputValue', items, inputValue);
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
@@ -137,7 +140,7 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
|
||||
})}
|
||||
>
|
||||
<InputNumber
|
||||
controls
|
||||
controls={false}
|
||||
variant="borderless"
|
||||
min={min}
|
||||
max={max}
|
||||
@@ -146,7 +149,7 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
|
||||
value={inputValue || undefined}
|
||||
style={{
|
||||
fontSize: 14,
|
||||
width: 140,
|
||||
width: 60,
|
||||
height: 32,
|
||||
backgroundColor: 'var(--ant-color-bg-container)',
|
||||
...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;
|
||||
@@ -2,6 +2,7 @@ import { request } from '@umijs/max';
|
||||
import _ from 'lodash';
|
||||
import { omitPathParams } from '../../utils';
|
||||
import {
|
||||
FormData,
|
||||
InstanceEvents,
|
||||
InstanceLog,
|
||||
InstanceLogQueryParams,
|
||||
@@ -9,176 +10,89 @@ import {
|
||||
ListItem
|
||||
} from '../config/types';
|
||||
|
||||
export const GPU_SERVICE_INSTANCES_API = (params: {
|
||||
namespace: string;
|
||||
clusterID?: number;
|
||||
}) =>
|
||||
`/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instances`;
|
||||
export const GPU_SERVICE_INSTANCES_API = '/gpu-instances';
|
||||
|
||||
export const GPU_SERVICE_INSTANCES_TYPE_API = (params: {
|
||||
clusterID?: number;
|
||||
}) => {
|
||||
return `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/instancetypes`;
|
||||
};
|
||||
export const GPU_SERVICE_INSTANCES_TYPE_API = '/gpu-instance-types';
|
||||
|
||||
// View logs / events still go through the K8s proxy until the /v2
|
||||
// /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: {
|
||||
namespace: string;
|
||||
name: string;
|
||||
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: {
|
||||
namespace: string;
|
||||
name: string;
|
||||
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: {
|
||||
namespace: string;
|
||||
name: string;
|
||||
clusterID?: number;
|
||||
}) => {
|
||||
return `/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`;
|
||||
};
|
||||
}) =>
|
||||
`/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instancepersistentvolumes/${params.name}/events`;
|
||||
|
||||
// =========== Instances ===========
|
||||
|
||||
export async function queryGPUServiceInstances(
|
||||
params: Global.K8sSearchParams & {
|
||||
namespace: string;
|
||||
clusterID?: number;
|
||||
},
|
||||
params: Global.SearchParams,
|
||||
options?: any
|
||||
) {
|
||||
if (!params.clusterID) {
|
||||
return;
|
||||
}
|
||||
return request<Global.K8sPageResponse<ListItem>>(
|
||||
GPU_SERVICE_INSTANCES_API({
|
||||
namespace: params.namespace,
|
||||
clusterID: params.clusterID
|
||||
}),
|
||||
{
|
||||
method: 'GET',
|
||||
params: omitPathParams(params),
|
||||
cancelToken: options?.token
|
||||
}
|
||||
);
|
||||
return request<Global.PageResponse<ListItem>>(GPU_SERVICE_INSTANCES_API, {
|
||||
method: 'GET',
|
||||
params,
|
||||
cancelToken: options?.token
|
||||
});
|
||||
}
|
||||
|
||||
export async function createGPUServiceInstance(
|
||||
params: {
|
||||
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',
|
||||
data: params.data,
|
||||
cancelToken: option?.token
|
||||
}
|
||||
);
|
||||
export async function createGPUServiceInstance(params: { data: FormData }) {
|
||||
return request<ListItem>(GPU_SERVICE_INSTANCES_API, {
|
||||
method: 'POST',
|
||||
data: params.data
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateGPUServiceInstance(
|
||||
params: {
|
||||
namespace: string;
|
||||
clusterID?: number;
|
||||
id: number;
|
||||
data: Global.K8sCommonData;
|
||||
},
|
||||
option?: any
|
||||
) {
|
||||
if (!params.clusterID) {
|
||||
return;
|
||||
}
|
||||
return request<ListItem>(
|
||||
`${GPU_SERVICE_INSTANCES_API({
|
||||
namespace: params.namespace,
|
||||
clusterID: params.clusterID
|
||||
})}/${params.data?.metadata?.name}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
data: params.data,
|
||||
cancelToken: option?.token
|
||||
}
|
||||
);
|
||||
export async function updateGPUServiceInstance(params: {
|
||||
id: number;
|
||||
data: Partial<FormData>;
|
||||
}) {
|
||||
return request<ListItem>(`${GPU_SERVICE_INSTANCES_API}/${params.id}`, {
|
||||
method: 'PUT',
|
||||
data: params.data
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteGPUServiceInstance(
|
||||
params: {
|
||||
namespace: string;
|
||||
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
|
||||
}
|
||||
);
|
||||
export async function deleteGPUServiceInstance(id: number) {
|
||||
return request(`${GPU_SERVICE_INSTANCES_API}/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
// =========== Instance Types ===========
|
||||
|
||||
export async function queryGPUServiceInstanceTypes(
|
||||
params: Global.K8sSearchParams & { clusterID?: number },
|
||||
params: Global.SearchParams = { page: 1, perPage: 100 },
|
||||
options?: any
|
||||
) {
|
||||
if (!params.clusterID) {
|
||||
return;
|
||||
}
|
||||
return request<Global.K8sPageResponse<InstanceTypeItem>>(
|
||||
GPU_SERVICE_INSTANCES_TYPE_API({ clusterID: params.clusterID }),
|
||||
return request<Global.PageResponse<InstanceTypeItem>>(
|
||||
GPU_SERVICE_INSTANCES_TYPE_API,
|
||||
{
|
||||
method: 'GET',
|
||||
params: omitPathParams(params),
|
||||
params,
|
||||
cancelToken: options?.token
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// =========== Logs / Events (K8s proxy) ===========
|
||||
|
||||
export async function queryGPUServiceInstanceEvents(
|
||||
params: {
|
||||
namespace: string;
|
||||
@@ -192,11 +106,11 @@ export async function queryGPUServiceInstanceEvents(
|
||||
return;
|
||||
}
|
||||
return request<InstanceEvents>(
|
||||
`${GPU_SERVICE_INSTANCES_EVENTS_API({
|
||||
GPU_SERVICE_INSTANCES_EVENTS_API({
|
||||
namespace: params.namespace,
|
||||
clusterID: params.clusterID,
|
||||
name: params.name
|
||||
})}`,
|
||||
}),
|
||||
{
|
||||
method: 'GET',
|
||||
params: omitPathParams(_.omit(params, ['name'])),
|
||||
@@ -217,11 +131,11 @@ export async function queryGPUServiceInstancePVEvents(
|
||||
return;
|
||||
}
|
||||
return request<InstanceEvents>(
|
||||
`${GPU_SERVICE_INSTANCE_PV_EVENTS_API({
|
||||
GPU_SERVICE_INSTANCE_PV_EVENTS_API({
|
||||
namespace: params.namespace,
|
||||
clusterID: params.clusterID,
|
||||
name: params.name
|
||||
})}`,
|
||||
}),
|
||||
{
|
||||
method: 'GET',
|
||||
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(
|
||||
params: InstanceLogQueryParams & {
|
||||
namespace: string;
|
||||
@@ -292,11 +156,11 @@ export async function queryGPUServiceInstanceLog(
|
||||
return;
|
||||
}
|
||||
return request<InstanceLog>(
|
||||
`${GPU_SERVICE_INSTANCES_LOG_API({
|
||||
GPU_SERVICE_INSTANCES_LOG_API({
|
||||
namespace: params.namespace,
|
||||
clusterID: params.clusterID,
|
||||
name: params.name
|
||||
})}`,
|
||||
}),
|
||||
{
|
||||
method: 'GET',
|
||||
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 templateList = templatesData?.items || [];
|
||||
const readonly = action === PageAction.VIEW;
|
||||
// const readonly = action === PageAction.VIEW;
|
||||
const readonly = false;
|
||||
const isRecreate = realAction === PageAction.CREATE;
|
||||
const showResourceSelectors = action === PageAction.CREATE || isRecreate;
|
||||
const shouldAutoSelectResource = action === PageAction.CREATE && !isRecreate;
|
||||
@@ -119,27 +120,33 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
instanceType: InstanceTypeItem,
|
||||
template: TemplateItem | undefined
|
||||
) => {
|
||||
const name = instanceType.metadata?.name;
|
||||
const manufacturer = instanceType.spec?.manufacturer;
|
||||
|
||||
setInstanceTypeSelection({ instanceType: name, manufacturer });
|
||||
setInstanceTypeSelection({
|
||||
instanceType: instanceType.name,
|
||||
manufacturer
|
||||
});
|
||||
setTemplateId(template?.id);
|
||||
|
||||
const currentSpec = form.current?.getFieldsValue()?.spec || {};
|
||||
const updatedSpec = {
|
||||
...currentSpec,
|
||||
type: name,
|
||||
resources: { ...(currentSpec.resources || {}), accelerator: '1' }
|
||||
};
|
||||
|
||||
if (template) {
|
||||
const currentSpec = form.current?.getFieldsValue()?.spec || {};
|
||||
form.current?.setFieldsValue({
|
||||
manufacturer: template.manufacturer,
|
||||
description: JSON.stringify(
|
||||
{
|
||||
name: instanceType.name,
|
||||
spec: {
|
||||
...instanceType.spec
|
||||
}
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
spec: {
|
||||
...updatedSpec,
|
||||
...currentSpec,
|
||||
...template.spec,
|
||||
resources: {
|
||||
...(updatedSpec.resources || {}),
|
||||
...(currentSpec.resources || {}),
|
||||
...(template.spec?.resources || {})
|
||||
}
|
||||
}
|
||||
@@ -147,16 +154,46 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
} else {
|
||||
form.current?.setFieldsValue({
|
||||
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 = (
|
||||
instanceTypes: InstanceTypeItem[],
|
||||
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;
|
||||
|
||||
const first = instanceTypes[0];
|
||||
@@ -178,18 +215,21 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const session = ++sessionRef.current;
|
||||
Promise.all([fetchData({}), fetchTemplates({ page: -1 })]).then(
|
||||
([instanceRes, templatesRes]) => {
|
||||
if (action === PageAction.CREATE) {
|
||||
const session = ++sessionRef.current;
|
||||
Promise.all([
|
||||
fetchData({ page: 1, perPage: 100 }),
|
||||
fetchTemplates({ page: -1 })
|
||||
]).then(([instanceRes, templatesRes]) => {
|
||||
if (sessionRef.current !== session) return;
|
||||
applyAutoSelection(instanceRes?.items || [], templatesRes?.items || []);
|
||||
}
|
||||
);
|
||||
}, [open, shouldAutoSelectResource]);
|
||||
});
|
||||
}
|
||||
}, [open, shouldAutoSelectResource, action]);
|
||||
|
||||
// filter instance types
|
||||
const filteredInstanceTypes = instanceTypeList.filter((item) =>
|
||||
matchKeyword([item.metadata?.name], instanceKeyword)
|
||||
matchKeyword([item.name], instanceKeyword)
|
||||
);
|
||||
|
||||
// filter templates based on selection and keyword
|
||||
@@ -370,7 +410,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
}
|
||||
>
|
||||
<>
|
||||
{action !== PageAction.VIEW && (
|
||||
{action !== PageAction.EDIT && (
|
||||
<ColTitle>
|
||||
{intl.formatMessage({ id: 'common.title.config' })}
|
||||
</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 { Flex } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import { manufactureColorMap } from '../../templates/config';
|
||||
import {
|
||||
convertKiToGi,
|
||||
InstanceTypePhaseLabelMap,
|
||||
InstanceTypePhaseStatus
|
||||
} from '../config';
|
||||
import { convertKiToGi } from '../config';
|
||||
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;
|
||||
|
||||
const Title = styled.div`
|
||||
@@ -60,24 +56,48 @@ interface InstanceTypeItemProps {
|
||||
showStatus?: boolean;
|
||||
}
|
||||
|
||||
const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({
|
||||
item,
|
||||
showStatus = true
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const name = item.metadata?.name;
|
||||
const acceleratable = item.spec?.acceleratable;
|
||||
const MetaItem: React.FC<{
|
||||
icon: string;
|
||||
label: string;
|
||||
value?: string | null;
|
||||
showDot?: boolean;
|
||||
show?: boolean;
|
||||
}> = ({ 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 manufacturerColor =
|
||||
(manufacturerKey && manufactureColorMap[manufacturerKey]) ?? 'purple';
|
||||
|
||||
// resource remaining status
|
||||
const remainingData = item.status?.remaining || {};
|
||||
|
||||
const renderName = () => {
|
||||
const product = item.spec?.product;
|
||||
const product = specData.product;
|
||||
const cpuCapacity = remainingData.cpu;
|
||||
const displayName =
|
||||
product ||
|
||||
(!acceleratable ? `${item.status?.cpu?.capacity} vCPUs` : name);
|
||||
(!acceleratable && cpuCapacity ? `${cpuCapacity} vCPUs` : name);
|
||||
return displayName;
|
||||
};
|
||||
|
||||
@@ -85,8 +105,8 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({
|
||||
<>
|
||||
<Title>
|
||||
<Flex gap={8} align="center">
|
||||
<AutoTooltip ghost minWidth={20} maxWidth={180}>
|
||||
{item.spec ? renderName() : '-'}
|
||||
<AutoTooltip ghost minWidth={20} maxWidth={200}>
|
||||
{renderName() || '-'}
|
||||
</AutoTooltip>
|
||||
{acceleratable && manufacturer && (
|
||||
<span
|
||||
@@ -95,99 +115,59 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
<ThemeTag color={manufacturerColor}>{manufacturer}</ThemeTag>
|
||||
<ThemeTag color={manufacturerColor} disabled={false}>
|
||||
{manufacturer}
|
||||
</ThemeTag>
|
||||
</span>
|
||||
)}
|
||||
</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>
|
||||
<Meta>
|
||||
<span style={{ display: 'flex', height: 15 }}>
|
||||
{acceleratable && (
|
||||
<span className="meta-row">
|
||||
<span className="meta-item">
|
||||
<IconFont type="icon-gpu1" className="meta-icon" />
|
||||
<Flex align="center" gap={4}>
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'gpuservice.instance.memory' })}
|
||||
</span>
|
||||
<span>
|
||||
{toDisplayUnit(convertKiToGi(item.spec?.memory)) ?? '-'}
|
||||
</span>
|
||||
</Flex>
|
||||
</span>
|
||||
{item.spec?.sliced && (
|
||||
<>
|
||||
<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'
|
||||
})}
|
||||
</span>
|
||||
<span>{item.spec?.sliced ?? '-'}</span>
|
||||
</Flex>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{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'
|
||||
})}
|
||||
</span>
|
||||
<span>{item.status?.accelerator?.remaining ?? '-'}</span>
|
||||
</Flex>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<MetaItem
|
||||
showDot={false}
|
||||
icon="icon-gpu1"
|
||||
label={intl.formatMessage({ id: 'gpuservice.instance.memory' })}
|
||||
value={
|
||||
toDisplayUnit(convertKiToGi(specData?.memory ?? undefined)) ??
|
||||
'-'
|
||||
}
|
||||
></MetaItem>
|
||||
<MetaItem
|
||||
show={!!specData?.sliced}
|
||||
icon="icon-sliced"
|
||||
label={intl.formatMessage({
|
||||
id: 'gpuservice.instance.sliced'
|
||||
})}
|
||||
value={specData?.sliced}
|
||||
></MetaItem>
|
||||
<MetaItem
|
||||
show={!!remainingData.accelerator}
|
||||
icon="icon-database"
|
||||
label={intl.formatMessage({
|
||||
id: 'gpuservice.instance.stock'
|
||||
})}
|
||||
value={remainingData.accelerator}
|
||||
></MetaItem>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="meta-row">
|
||||
<span className="meta-item">
|
||||
<IconFont type="icon-ram-02" className="meta-icon" />
|
||||
<Flex align="center" gap={4}>
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'gpuservice.instance.ram' })}
|
||||
</span>
|
||||
<span>{toDisplayUnit(item.status?.ram?.capacity) ?? '-'}</span>
|
||||
</Flex>
|
||||
</span>
|
||||
{acceleratable && (
|
||||
<>
|
||||
<span className="dot"></span>
|
||||
<span className="meta-item">
|
||||
<IconFont type="icon-cpu" className="meta-icon" />
|
||||
<Flex align="center" gap={4}>
|
||||
<span>vCPU</span>
|
||||
<span>{item.status?.cpu?.capacity ?? '-'}</span>
|
||||
</Flex>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<MetaItem
|
||||
showDot={false}
|
||||
icon="icon-ram-02"
|
||||
label={intl.formatMessage({ id: 'gpuservice.instance.ram' })}
|
||||
value={toDisplayUnit(convertKiToGi(remainingData.ram)) ?? '-'}
|
||||
></MetaItem>
|
||||
<MetaItem
|
||||
show={acceleratable}
|
||||
showDot={true}
|
||||
icon="icon-cpu"
|
||||
label="vCPU"
|
||||
value={remainingData.cpu ?? '-'}
|
||||
></MetaItem>
|
||||
</span>
|
||||
</Meta>
|
||||
</>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { TemplateCard } from '@gpustack/core-ui';
|
||||
import { Empty, Spin } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
import { InstanceTypePhaseValueMap } from '../config';
|
||||
import { getAcceleratorMax } from '../config';
|
||||
import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types';
|
||||
import InstanceTypeItem from './instance-type-item';
|
||||
|
||||
@@ -20,8 +20,12 @@ interface InstanceTypeListProps {
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const isAvailable = (item: InstanceTypeItemModel) =>
|
||||
item.status?.phase === InstanceTypePhaseValueMap.Active;
|
||||
// CPU types always have stock (no accelerator). Accelerated types are
|
||||
// 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> = ({
|
||||
value,
|
||||
@@ -53,7 +57,7 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
|
||||
<TypeGrid>
|
||||
{dataList.map((item) => {
|
||||
const disabled = !isAvailable(item);
|
||||
const name = item.metadata?.name;
|
||||
const name = item.name;
|
||||
return (
|
||||
<TemplateCard
|
||||
key={name}
|
||||
|
||||
@@ -31,7 +31,7 @@ const eventTypeStatus: Record<string, 'success' | 'warning' | 'error'> = {
|
||||
|
||||
const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
|
||||
const intl = useIntl();
|
||||
const { open, onCancel, name } = props || {};
|
||||
const { open, onCancel, name, namespace, clusterID } = props || {};
|
||||
const [activeKey, setActiveKey] = useState('instance');
|
||||
|
||||
const {
|
||||
@@ -52,9 +52,9 @@ const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
|
||||
const volumeEvents = volumeEventsData?.items ?? [];
|
||||
|
||||
const refreshAll = () => {
|
||||
if (!name) return;
|
||||
fetchInstanceEvents({ name });
|
||||
fetchVolumeEvents({ name });
|
||||
if (!name || !namespace || !clusterID) return;
|
||||
fetchInstanceEvents({ name, namespace, clusterID });
|
||||
fetchVolumeEvents({ name, namespace, clusterID });
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -3,11 +3,6 @@ import { StatusType } from '@/config/types';
|
||||
import { IconFont, icons } from '@gpustack/core-ui';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import {
|
||||
InstanceTypeItem,
|
||||
InstanceTypeResource,
|
||||
InstanceTypeStatus
|
||||
} from './types';
|
||||
|
||||
export const InstanceStatusValueMap = {
|
||||
Scheduling: 'Scheduling',
|
||||
@@ -45,13 +40,22 @@ export const status: Record<string, StatusType> = {
|
||||
[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 = [
|
||||
{
|
||||
label: 'common.button.view',
|
||||
key: 'view',
|
||||
label: 'common.button.edit',
|
||||
key: 'edit',
|
||||
locale: true,
|
||||
icon: icons.DetailInfo
|
||||
icon: icons.EditOutlined
|
||||
},
|
||||
// {
|
||||
// label: 'common.button.view',
|
||||
// key: 'view',
|
||||
// locale: true,
|
||||
// icon: icons.DetailInfo
|
||||
// },
|
||||
{
|
||||
label: 'common.button.viewlog',
|
||||
key: 'viewlog',
|
||||
@@ -64,30 +68,30 @@ export const rowActionList = [
|
||||
locale: true,
|
||||
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',
|
||||
// key: 'edit',
|
||||
// label: 'common.button.start',
|
||||
// key: 'start',
|
||||
// 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',
|
||||
key: 'delete',
|
||||
@@ -103,12 +107,18 @@ export const batchActionList = [
|
||||
{
|
||||
label: 'common.button.start',
|
||||
key: 'start',
|
||||
icon: icons.Play
|
||||
icon: icons.Play,
|
||||
props: {
|
||||
disabled: true
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'common.button.stop',
|
||||
key: 'stop',
|
||||
icon: icons.Stop
|
||||
icon: icons.Stop,
|
||||
props: {
|
||||
disabled: true
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'common.button.delete',
|
||||
@@ -121,8 +131,6 @@ export const batchActionList = [
|
||||
];
|
||||
|
||||
export const InstanceTypePhaseValueMap = {
|
||||
// Available: 'Available',
|
||||
// Unavailable: 'Unavailable'
|
||||
PreParing: 'Preparing',
|
||||
Inactive: 'Inactive',
|
||||
Active: 'Active'
|
||||
@@ -141,10 +149,12 @@ export const InstanceTypePhaseStatus: Record<string, StatusType> = {
|
||||
};
|
||||
|
||||
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
|
||||
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`;
|
||||
};
|
||||
|
||||
export const transformInstanceTypeResource = (
|
||||
resource?: InstanceTypeResource
|
||||
): InstanceTypeResource | undefined => {
|
||||
if (!resource) return resource;
|
||||
return {
|
||||
...resource,
|
||||
capacity: convertKiToGi(
|
||||
resource.capacity
|
||||
) as InstanceTypeResource['capacity'],
|
||||
onceMaxRequest: convertKiToGi(
|
||||
resource.onceMaxRequest
|
||||
) as InstanceTypeResource['onceMaxRequest'],
|
||||
remaining: convertKiToGi(
|
||||
resource.remaining
|
||||
) as InstanceTypeResource['remaining']
|
||||
};
|
||||
const parseQuantity = (value?: string | null): number => {
|
||||
if (!value) return 0;
|
||||
const match = /^(-?\d+(?:\.\d+)?)/.exec(String(value));
|
||||
return match ? Number(match[1]) || 0 : 0;
|
||||
};
|
||||
|
||||
export const transformInstanceType = (
|
||||
item: InstanceTypeItem
|
||||
): InstanceTypeItem => {
|
||||
if (!item?.status) return item;
|
||||
const status = item.status;
|
||||
return {
|
||||
...item,
|
||||
spec: {
|
||||
...item.spec,
|
||||
manufacturer: !item.spec?.acceleratable ? 'cpu' : item.spec.manufacturer
|
||||
},
|
||||
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']
|
||||
}
|
||||
};
|
||||
// Returns the slider max for the accelerator count: the largest
|
||||
// tier.onceMaxRequest across all acceleratorTiers (not from candidates).
|
||||
export const getAcceleratorMax = (
|
||||
tiers?: { onceMaxRequest: string }[] | null
|
||||
) => {
|
||||
if (!tiers?.length) return 0;
|
||||
return tiers.reduce((acc, tier) => {
|
||||
const n = parseQuantity(tier.onceMaxRequest);
|
||||
return n > acc ? n : acc;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
@@ -2,16 +2,16 @@ export type ImagePullPolicy = 'Always' | 'IfNotPresent' | 'Never';
|
||||
|
||||
// instance form data
|
||||
export interface FormData {
|
||||
metadata: {
|
||||
name: string;
|
||||
namespace: string;
|
||||
};
|
||||
name: string;
|
||||
clusterId?: number | null;
|
||||
owner_principal_id?: number | null;
|
||||
displayName?: string | null;
|
||||
description?: string | null;
|
||||
enable_ssh?: boolean;
|
||||
spec: {
|
||||
type: string;
|
||||
image: string;
|
||||
imagePullPolicy: ImagePullPolicy;
|
||||
displayName: string;
|
||||
command: string[];
|
||||
ports: {
|
||||
port: number;
|
||||
@@ -27,9 +27,11 @@ export interface FormData {
|
||||
cpu?: string;
|
||||
ram?: 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: {
|
||||
ephemeral?: {
|
||||
capacity?: string;
|
||||
@@ -37,162 +39,136 @@ export interface FormData {
|
||||
persistent?: {
|
||||
name: string;
|
||||
};
|
||||
persistentTemplate?: {
|
||||
name?: string;
|
||||
spec: {
|
||||
type: string;
|
||||
capacity: string;
|
||||
};
|
||||
releaseWithInstance?: boolean;
|
||||
};
|
||||
};
|
||||
sshPublicKey: {
|
||||
name: string;
|
||||
};
|
||||
sshPublicKeys?: { name: string }[];
|
||||
};
|
||||
}
|
||||
|
||||
export type InstanceServicePortProtocol = 'TCP' | 'UDP';
|
||||
|
||||
export interface InstanceIP {
|
||||
ip: string;
|
||||
}
|
||||
|
||||
export interface InstanceServicePort {
|
||||
port: number;
|
||||
name: string;
|
||||
nodePort?: number;
|
||||
protocol?: InstanceServicePortProtocol;
|
||||
}
|
||||
|
||||
export interface InstanceStatus {
|
||||
hostIPs?: {
|
||||
ip: string;
|
||||
}[];
|
||||
namespace?: string;
|
||||
phase?: string;
|
||||
phaseMessage?: string;
|
||||
podIPs?: {
|
||||
ip: string;
|
||||
}[];
|
||||
ports?: {
|
||||
port: number;
|
||||
name: string;
|
||||
nodePort?: number;
|
||||
protocol?: InstanceServicePortProtocol;
|
||||
}[];
|
||||
hostIPs?: InstanceIP[];
|
||||
podIPs?: InstanceIP[];
|
||||
ports?: InstanceServicePort[];
|
||||
}
|
||||
|
||||
// instance list item
|
||||
export interface ListItem extends Omit<FormData, 'metadata'> {
|
||||
export interface ListItem extends FormData {
|
||||
id: number;
|
||||
status?: InstanceStatus;
|
||||
metadata: {
|
||||
name: string;
|
||||
namespace?: string;
|
||||
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>;
|
||||
}[];
|
||||
};
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
deleted_at?: string | null;
|
||||
status?: InstanceStatus | null;
|
||||
}
|
||||
|
||||
type Quality =
|
||||
| `${number}`
|
||||
| `${number}Ki`
|
||||
| `${number}Mi`
|
||||
| `${number}Gi`
|
||||
| `${number}Ti`;
|
||||
// =========== Instance Types ===========
|
||||
|
||||
export interface InstanceTypeResource {
|
||||
capacity?: Quality;
|
||||
onceMaxRequest?: Quality;
|
||||
remaining?: Quality;
|
||||
onceMaxRequest: string;
|
||||
remaining: string;
|
||||
capacity: string;
|
||||
}
|
||||
|
||||
export interface InstanceTypeStatus {
|
||||
export interface InstanceTypeCandidate {
|
||||
cluster: string;
|
||||
name: string;
|
||||
accelerator: InstanceTypeResource;
|
||||
cpu: InstanceTypeResource;
|
||||
localStorage: InstanceTypeResource;
|
||||
phase: string;
|
||||
phaseMessage?: string;
|
||||
ram: InstanceTypeResource;
|
||||
localStorage: InstanceTypeResource;
|
||||
}
|
||||
|
||||
export interface InstanceTypeMetadata {
|
||||
name: string;
|
||||
namespace?: string;
|
||||
uid?: string;
|
||||
resourceVersion?: string;
|
||||
creationTimestamp?: string;
|
||||
generateName?: string;
|
||||
generation?: number;
|
||||
finalizers?: string[];
|
||||
export interface InstanceTypeTier {
|
||||
onceMaxRequest: string;
|
||||
candidates?: InstanceTypeCandidate[] | null;
|
||||
}
|
||||
|
||||
export interface InstanceTypeRemainingResource {
|
||||
accelerator?: string | null;
|
||||
cpu: string;
|
||||
ram: string;
|
||||
localStorage: string;
|
||||
}
|
||||
|
||||
export interface InstanceTypeSpec {
|
||||
acceleratable: boolean;
|
||||
group: string;
|
||||
computeCapability?: string;
|
||||
family?: string;
|
||||
manufacturer?: string;
|
||||
memory?: string;
|
||||
product?: string;
|
||||
sliced?: number;
|
||||
acceleratable: boolean;
|
||||
manufacturer: string;
|
||||
product?: string | null;
|
||||
memory?: string | null;
|
||||
family?: string | null;
|
||||
computeCapability?: string | null;
|
||||
sliced?: string | null;
|
||||
}
|
||||
|
||||
export interface InstanceTypeStatus {
|
||||
remaining: InstanceTypeRemainingResource;
|
||||
acceleratorTiers?: InstanceTypeTier[] | null;
|
||||
}
|
||||
|
||||
export interface InstanceTypeItem {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
id: number;
|
||||
metadata: InstanceTypeMetadata;
|
||||
name: string;
|
||||
spec: InstanceTypeSpec;
|
||||
status: InstanceTypeStatus;
|
||||
}
|
||||
|
||||
export interface InstanceEventObjectReference {
|
||||
apiVersion?: string;
|
||||
fieldPath?: string;
|
||||
kind?: string;
|
||||
name?: string;
|
||||
namespace?: string;
|
||||
resourceVersion?: string;
|
||||
uid?: string;
|
||||
}
|
||||
|
||||
export interface InstanceEventSource {
|
||||
component?: string;
|
||||
host?: string;
|
||||
}
|
||||
|
||||
export interface InstanceEventSeries {
|
||||
count?: number;
|
||||
lastObservedTime?: string;
|
||||
}
|
||||
// =========== Events / logs (placeholder) ===========
|
||||
// The /v2/gpu-instances API does not yet expose log/event endpoints. These
|
||||
// shapes are retained as minimal placeholders so disabled UI continues to
|
||||
// compile until backend support lands.
|
||||
|
||||
export interface InstanceEventItem {
|
||||
action?: string;
|
||||
apiVersion?: string;
|
||||
type?: string;
|
||||
reason?: string;
|
||||
message?: string;
|
||||
count?: number;
|
||||
eventTime?: string;
|
||||
firstTimestamp?: string;
|
||||
involvedObject: InstanceEventObjectReference;
|
||||
kind?: string;
|
||||
lastTimestamp?: string;
|
||||
message?: string;
|
||||
metadata: {
|
||||
name?: string;
|
||||
namespace?: string;
|
||||
uid?: string;
|
||||
resourceVersion?: string;
|
||||
creationTimestamp?: string;
|
||||
};
|
||||
reason?: string;
|
||||
related?: InstanceEventObjectReference;
|
||||
reportingComponent?: string;
|
||||
reportingInstance?: string;
|
||||
series?: InstanceEventSeries;
|
||||
source?: InstanceEventSource;
|
||||
type?: string;
|
||||
source?: {
|
||||
component?: string;
|
||||
host?: string;
|
||||
};
|
||||
series?: {
|
||||
count?: number;
|
||||
lastObservedTime?: string;
|
||||
};
|
||||
metadata?: {
|
||||
name?: string;
|
||||
namespace?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InstanceEvents {
|
||||
apiVersion?: string;
|
||||
kind?: string;
|
||||
metadata?: {
|
||||
resourceVersion?: string;
|
||||
};
|
||||
items: InstanceEventItem[];
|
||||
}
|
||||
|
||||
export type InstanceLog = string;
|
||||
|
||||
export interface InstanceLogQueryParams {
|
||||
follow?: boolean;
|
||||
limitBytes?: number;
|
||||
@@ -201,5 +177,3 @@ export interface InstanceLogQueryParams {
|
||||
timestamps?: boolean;
|
||||
pretty?: string;
|
||||
}
|
||||
|
||||
export type InstanceLog = string;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { PageActionType } from '@/config/types';
|
||||
import { Input as CInput } 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';
|
||||
import formStyles from '../styles/instances.module.less';
|
||||
|
||||
@@ -16,9 +17,10 @@ const Basic = ({
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<>
|
||||
<OwnerPrincipalIdField name="owner_principal_id" />
|
||||
<Form.Item<FormData>
|
||||
data-field="name"
|
||||
name={['metadata', 'name']}
|
||||
name="name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
@@ -38,10 +40,17 @@ const Basic = ({
|
||||
required
|
||||
/>
|
||||
</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}>
|
||||
<Form.Item<FormData> name={['spec', 'description']}>
|
||||
<Form.Item<FormData> name="description" hidden>
|
||||
<CInput.TextArea
|
||||
disabled={disabled}
|
||||
disabled={false}
|
||||
label={intl.formatMessage({ id: 'common.table.description' })}
|
||||
scaleSize
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PageActionType } from '@/config/types';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
CheckboxField,
|
||||
Input as CInput,
|
||||
CollapsePanel,
|
||||
IconFont,
|
||||
MultipleSelect,
|
||||
@@ -21,13 +22,15 @@ import {
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef
|
||||
useRef,
|
||||
useState
|
||||
} 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 TemplateBasicForm, {
|
||||
BasicResourceMax
|
||||
} from '../../templates/forms/basic';
|
||||
import { pickCandidateForAccelerator } from '../config';
|
||||
import { FormData, InstanceTypeItem, ListItem } from '../config/types';
|
||||
import instanceStyles from '../styles/instances.module.less';
|
||||
import Basic from './basic';
|
||||
@@ -53,7 +56,7 @@ interface InstanceFormProps {
|
||||
onFinish: (values: FormData) => Promise<void>;
|
||||
}
|
||||
|
||||
const parseQuantityToNumber = (value?: string): number | null => {
|
||||
const parseQuantityToNumber = (value?: string | null): number | null => {
|
||||
if (!value) return null;
|
||||
const match = /^(-?\d+(?:\.\d+)?)/.exec(String(value));
|
||||
if (!match) return null;
|
||||
@@ -61,6 +64,27 @@ const parseQuantityToNumber = (value?: string): number | 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 = {
|
||||
BASIC: 'basic',
|
||||
INSTANCE_TYPE: 'instanceType',
|
||||
@@ -71,7 +95,7 @@ const TABKeysMap = {
|
||||
const requiredFields = {
|
||||
[TABKeysMap.BASIC]: {
|
||||
sort: 1,
|
||||
fields: ['metadata.name']
|
||||
fields: ['name']
|
||||
},
|
||||
[TABKeysMap.INSTANCE_TYPE]: {
|
||||
sort: 2,
|
||||
@@ -95,7 +119,6 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
currentData,
|
||||
disabled,
|
||||
open,
|
||||
namespace = 'default',
|
||||
instanceTypeList = [],
|
||||
onFinish
|
||||
} = props;
|
||||
@@ -108,7 +131,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
const formAction =
|
||||
realAction === PageAction.CREATE ? PageAction.CREATE : action;
|
||||
const sshEnabled = Form.useWatch('enable_ssh', form);
|
||||
const { detailData: sshKeyData, fetchData: fetchSSHData } = useGetSshkey();
|
||||
const { sshkeyOptions, fetchData: fetchSSHData } = useQuerySshkeys();
|
||||
const { getScrollElementScrollableHeight } = useWrapperContext();
|
||||
const {
|
||||
activeKey,
|
||||
@@ -127,25 +150,25 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
fetchSSHData({});
|
||||
fetchSSHData({ page: 1, perPage: 100 });
|
||||
}
|
||||
}, [open, action]);
|
||||
|
||||
const handleSSHEnableChange = (e: any) => {
|
||||
if (!e?.target?.checked) {
|
||||
return;
|
||||
const ports = Form.useWatch(['spec', 'ports'], form) || [];
|
||||
|
||||
const hasSSHPort = useMemo(
|
||||
() =>
|
||||
ports.some(
|
||||
(item: any) => item?.protocol === 'TCP' && item?.port === SSH_PORT
|
||||
),
|
||||
[ports]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasSSHPort && !form.getFieldValue('enable_ssh')) {
|
||||
form.setFieldValue('enable_ssh', true);
|
||||
}
|
||||
const currentPorts = form.getFieldValue(['spec', 'ports']) || [];
|
||||
const hasSSHPort = currentPorts.some(
|
||||
(item: any) => item?.protocol === 'TCP' && item?.port === SSH_PORT
|
||||
);
|
||||
if (!hasSSHPort) {
|
||||
form.setFieldValue(
|
||||
['spec', 'ports'],
|
||||
[...currentPorts, { protocol: 'TCP', port: SSH_PORT, name: 'SSH' }]
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [hasSSHPort, form]);
|
||||
|
||||
const segmentOptions = useMemo(
|
||||
() => [
|
||||
@@ -183,25 +206,50 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
[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
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
const onceMaxRequest = useMemo<BasicResourceMax>(() => {
|
||||
const selected = instanceTypeList.find(
|
||||
(item) => (item.metadata?.name || '') === selectedTypeName
|
||||
const resolveAndApply = (
|
||||
instanceType: InstanceTypeItem | undefined,
|
||||
count: number,
|
||||
options: { writeAccelerator?: boolean } = {}
|
||||
) => {
|
||||
if (!instanceType) {
|
||||
setSelectedInstanceType(undefined);
|
||||
setOnceMaxRequest({ cpu: null, memory: null, localStorage: null });
|
||||
return;
|
||||
}
|
||||
const candidate = pickCandidateForAccelerator(
|
||||
instanceType.status?.acceleratorTiers,
|
||||
count
|
||||
);
|
||||
const status = selected?.status;
|
||||
return {
|
||||
cpu: parseQuantityToNumber(status?.cpu?.onceMaxRequest),
|
||||
memory: parseQuantityToNumber(status?.ram?.onceMaxRequest),
|
||||
localStorage: parseQuantityToNumber(
|
||||
status?.localStorage?.onceMaxRequest
|
||||
)
|
||||
};
|
||||
}, [instanceTypeList, selectedTypeName]);
|
||||
setSelectedInstanceType(instanceType);
|
||||
setOnceMaxRequest({
|
||||
cpu: parseQuantityToNumber(candidate?.cpu?.onceMaxRequest),
|
||||
memory: parseQuantityToGi(candidate?.ram?.onceMaxRequest),
|
||||
localStorage: parseQuantityToGi(candidate?.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);
|
||||
};
|
||||
|
||||
const onTargetChange = (key: string) => {
|
||||
scrollTabsRef.current?.handleTargetChange(key);
|
||||
@@ -235,34 +283,110 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
action === PageAction.VIEW ||
|
||||
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({
|
||||
metadata: {
|
||||
name: currentData.metadata?.name,
|
||||
namespace: currentData.metadata?.namespace
|
||||
},
|
||||
name: currentData.name,
|
||||
displayName: currentData.displayName,
|
||||
description: currentData.description,
|
||||
clusterId: currentData.clusterId,
|
||||
spec: {
|
||||
...currentData.spec,
|
||||
imagePullPolicy:
|
||||
currentData.spec?.imagePullPolicy || DefaultImagePullPolicy
|
||||
},
|
||||
enable_ssh: !!currentData.spec?.sshPublicKey?.name
|
||||
currentData.spec?.imagePullPolicy || DefaultImagePullPolicy,
|
||||
resources: {
|
||||
...currentData.spec?.resources,
|
||||
accelerator: acceleratorAsNumber
|
||||
},
|
||||
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;
|
||||
}
|
||||
}, [action, currentData, form, open, namespace, realAction]);
|
||||
}, [action, currentData, form, open, realAction, instanceTypeList]);
|
||||
|
||||
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({
|
||||
...values,
|
||||
spec: {
|
||||
...values.spec,
|
||||
sshPublicKey: { name: sshKeyData?.name }
|
||||
ports: submittedPorts,
|
||||
resources: normalizedResources,
|
||||
volume: normalizedVolume,
|
||||
sshPublicKeys: values.enable_ssh
|
||||
? (selectedKeys ?? []).map((name) => ({ name }))
|
||||
: []
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const segmentedTop =
|
||||
action === PageAction.VIEW
|
||||
action === PageAction.EDIT
|
||||
? {
|
||||
top: 0,
|
||||
offsetTop: 100
|
||||
@@ -282,7 +406,11 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
setFieldsValue: (values: Partial<InstanceFormValues>) => {
|
||||
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 = () => {
|
||||
@@ -319,7 +447,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
env: [],
|
||||
volumeMount: '',
|
||||
resources: {
|
||||
accelerator: '1'
|
||||
accelerator: 1
|
||||
},
|
||||
volume: {
|
||||
ephemeral: {
|
||||
@@ -334,6 +462,11 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
}}
|
||||
>
|
||||
<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
|
||||
activeKey={collapseKeys}
|
||||
accordion={false}
|
||||
@@ -349,6 +482,9 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
<InstanceTypeFormItem
|
||||
action={formAction}
|
||||
disabled={disabled}
|
||||
selectedInstanceType={selectedInstanceType}
|
||||
currentData={currentData as any}
|
||||
onGPUCountChange={handleAcceleratorChange}
|
||||
/>
|
||||
)
|
||||
},
|
||||
@@ -392,13 +528,12 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
label={intl.formatMessage({
|
||||
id: 'gpuservice.instance.ssh.enable'
|
||||
})}
|
||||
onChange={handleSSHEnableChange}
|
||||
></CheckboxField>
|
||||
</Form.Item>
|
||||
{sshEnabled && (
|
||||
<div className={instanceStyles.sshkeySelection}>
|
||||
<Form.Item<FormData>
|
||||
name={['spec', 'sshPublicKey', 'name']}
|
||||
name={['spec', 'sshPublicKeys']}
|
||||
style={{
|
||||
marginBottom: 12
|
||||
}}
|
||||
@@ -425,7 +560,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
width: '100%'
|
||||
}
|
||||
}}
|
||||
options={[]}
|
||||
options={sshkeyOptions}
|
||||
notFoundContent={
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
|
||||
@@ -2,16 +2,17 @@ import { PageAction } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import NumberSelection from '@/pages/_components/number-selection';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Alert, Form } from 'antd';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { Alert, Flex, Form } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import InstanceTypeItem from '../components/instance-type-item';
|
||||
import { InstanceTypePhaseValueMap } from '../config';
|
||||
import { getAcceleratorMax } from '../config';
|
||||
import {
|
||||
FormData,
|
||||
InstanceTypeItem as InstanceTypeItemModel
|
||||
InstanceTypeItem as InstanceTypeItemModel,
|
||||
ListItem
|
||||
} from '../config/types';
|
||||
import useQueryInstanceTypes from '../services/use-query-instance-types';
|
||||
|
||||
const FieldBlock = styled.div`
|
||||
margin-bottom: 24px;
|
||||
@@ -25,24 +26,22 @@ const SelectedCard = styled.div`
|
||||
`;
|
||||
|
||||
interface InstanceTypePickerProps {
|
||||
value?: string;
|
||||
dataList: InstanceTypeItemModel[];
|
||||
selectedInstanceType?: InstanceTypeItemModel;
|
||||
}
|
||||
|
||||
const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
|
||||
value,
|
||||
dataList
|
||||
selectedInstanceType
|
||||
}) => {
|
||||
const selected = useMemo(() => {
|
||||
return (
|
||||
dataList.find((item) => (item.metadata?.name || '') === value) ||
|
||||
({} as InstanceTypeItemModel)
|
||||
);
|
||||
}, [value, dataList]);
|
||||
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -50,56 +49,62 @@ const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
|
||||
interface InstanceTypeFormItemProps {
|
||||
action: PageActionType;
|
||||
disabled?: boolean;
|
||||
currentData?: ListItem;
|
||||
selectedInstanceType?: InstanceTypeItemModel;
|
||||
onGPUCountChange?: (value: number) => void;
|
||||
}
|
||||
|
||||
const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
||||
action,
|
||||
disabled
|
||||
disabled,
|
||||
currentData,
|
||||
selectedInstanceType,
|
||||
onGPUCountChange
|
||||
}) => {
|
||||
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(() => {
|
||||
fetchData({});
|
||||
}, [fetchData]);
|
||||
const handleOnGPUCountChange = (value: number) => {
|
||||
onGPUCountChange?.(value);
|
||||
};
|
||||
|
||||
const dataList = detailData?.items || [];
|
||||
|
||||
const selectedInstanceType = useMemo(() => {
|
||||
return dataList.find((item) => (item.metadata?.name || '') === typeName);
|
||||
}, [dataList, typeName]);
|
||||
|
||||
const maxGpuCount = useMemo(() => {
|
||||
const onceMaxRequest =
|
||||
selectedInstanceType?.status?.accelerator?.onceMaxRequest;
|
||||
const num = onceMaxRequest ? Number(onceMaxRequest) : 0;
|
||||
return num;
|
||||
}, [selectedInstanceType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!typeName && dataList.length > 0) {
|
||||
const defaultType = dataList.find(
|
||||
(item) => item.status?.phase === InstanceTypePhaseValueMap.Active
|
||||
);
|
||||
if (defaultType) {
|
||||
form.setFieldValue(['spec', 'type'], defaultType.metadata?.name || '');
|
||||
}
|
||||
return;
|
||||
const showGPUCount = useMemo(() => {
|
||||
if (action === PageAction.EDIT) {
|
||||
return _.toNumber(currentData?.spec?.resources?.accelerator) > 0;
|
||||
}
|
||||
return selectedInstanceType?.spec?.acceleratable;
|
||||
}, [selectedInstanceType, action]);
|
||||
|
||||
const currentAccelerator =
|
||||
Number(form.getFieldValue(['spec', 'resources', 'accelerator'])) || 1;
|
||||
if (currentAccelerator > maxGpuCount) {
|
||||
form.setFieldValue(['spec', 'resources', 'accelerator'], maxGpuCount);
|
||||
const selectedInstanceData = useMemo(() => {
|
||||
if (action === PageAction.EDIT) {
|
||||
return JSON.parse(currentData?.description || '{}') || {};
|
||||
}
|
||||
return selectedInstanceType;
|
||||
}, [selectedInstanceType, action, currentData]);
|
||||
|
||||
if (currentAccelerator < 1) {
|
||||
form.setFieldValue(['spec', 'resources', 'accelerator'], 1);
|
||||
}
|
||||
}, [form, typeName, maxGpuCount, dataList]);
|
||||
const renderInstanceType = () => {
|
||||
const description = JSON.parse(currentData?.description || '{}').spec || {};
|
||||
return (
|
||||
<SelectedCard>
|
||||
<Flex align="flex-start" orientation="vertical">
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--ant-color-text)',
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
{description.manufacturer
|
||||
? `${description.product} x ${currentData?.spec?.resources?.accelerator}`
|
||||
: ''}
|
||||
</span>
|
||||
</Flex>
|
||||
</SelectedCard>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
</FieldBlock>
|
||||
{selectedInstanceType?.spec?.acceleratable && (
|
||||
<Form.Item
|
||||
{showGPUCount && (
|
||||
<Form.Item<FormData>
|
||||
name={['spec', 'resources', 'accelerator']}
|
||||
hidden={action === PageAction.EDIT}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
validator: (_, value) => {
|
||||
if (value > maxGpuCount) {
|
||||
const num = Number(value);
|
||||
if (num > maxGpuCount) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
intl.formatMessage(
|
||||
@@ -135,16 +145,12 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
||||
)
|
||||
);
|
||||
}
|
||||
if (value < 1) {
|
||||
if (num < 1) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
intl.formatMessage(
|
||||
{
|
||||
id: 'gpuservice.instance.gpuCount.min'
|
||||
},
|
||||
{
|
||||
count: 1
|
||||
}
|
||||
{ id: 'gpuservice.instance.gpuCount.min' },
|
||||
{ count: 1 }
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -156,12 +162,14 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
||||
>
|
||||
<NumberSelection
|
||||
min={1}
|
||||
onChange={handleOnGPUCountChange}
|
||||
max={maxGpuCount}
|
||||
step={1}
|
||||
required
|
||||
disabled={disabled || action === PageAction.EDIT}
|
||||
labelExtra={
|
||||
!maxGpuCount && (
|
||||
!maxGpuCount &&
|
||||
action !== PageAction.EDIT && (
|
||||
<Alert
|
||||
showIcon
|
||||
type="warning"
|
||||
|
||||
@@ -8,14 +8,12 @@ import GPUServiceStorageForm from '../../storage/forms';
|
||||
|
||||
interface StorageOverlayProps {
|
||||
open: boolean;
|
||||
namespace?: string;
|
||||
onCancel: () => void;
|
||||
onSubmit: (values: StorageFormData) => Promise<void> | void;
|
||||
}
|
||||
|
||||
const StorageOverlay: React.FC<StorageOverlayProps> = ({
|
||||
open,
|
||||
namespace,
|
||||
onCancel,
|
||||
onSubmit
|
||||
}) => {
|
||||
@@ -66,7 +64,6 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
|
||||
ref={formRef}
|
||||
action={PageAction.CREATE}
|
||||
open={open}
|
||||
namespace={namespace}
|
||||
onFinish={handleFinish}
|
||||
/>
|
||||
</FormOverlayView>
|
||||
|
||||
@@ -1,31 +1,36 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { getCurrentOrgNamespace } from '@/atoms/user';
|
||||
import { PageAction } from '@/config';
|
||||
import {
|
||||
Input as CInput,
|
||||
InputNumber as CInputNumber,
|
||||
CheckboxField,
|
||||
LabelInfo,
|
||||
Select,
|
||||
useAppUtils
|
||||
} from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Flex, Form, Radio } from 'antd';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { FormData as StorageFormData } from '../../storage/config/types';
|
||||
import useCreateStorage from '../../storage/services/use-create-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 StorageOverlay from './storage-overlay';
|
||||
|
||||
const FieldBlock = styled.div`
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
|
||||
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 = ({
|
||||
disabled,
|
||||
action
|
||||
@@ -35,50 +40,86 @@ const StorageVolume = ({
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { getRuleMessage } = useAppUtils();
|
||||
const { fetchData: createStorage } = useCreateStorage();
|
||||
const { detailData: storageData, fetchData: fetchStorage } =
|
||||
useQueryStorage();
|
||||
const [overlayOpen, setOverlayOpen] = useState(false);
|
||||
const form = Form.useFormInstance<FormData>();
|
||||
const currentVolume = Form.useWatch(['spec', 'volume'], form);
|
||||
const [storageMode, setStorageMode] = useState<string>(
|
||||
StorageModeValueMap.Temporary
|
||||
);
|
||||
|
||||
const form = Form.useFormInstance<FormData>();
|
||||
|
||||
const currentCluster = useAtomValue(currentClusterAtom);
|
||||
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
|
||||
const [releaseWithInstance, setReleaseWithInstance] = useState<boolean>(false);
|
||||
|
||||
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(
|
||||
() =>
|
||||
(storageData?.items || []).map((item) => ({
|
||||
label: `${item.metadata?.name} / ${item.spec?.capacity ?? '-'}`,
|
||||
value: item.metadata?.name
|
||||
label: `${item.name} / ${item.spec?.capacity ?? '-'}`,
|
||||
value: item.name
|
||||
})),
|
||||
[storageData]
|
||||
);
|
||||
|
||||
const handleModeChange = (mode: string) => {
|
||||
console.log('selected storage mode', mode);
|
||||
if (mode === StorageModeValueMap.Existing) {
|
||||
form.setFieldValue(['spec', 'volume'], { persistent: { name: '' } });
|
||||
} else {
|
||||
const storageTypeOptions = useMemo(
|
||||
() =>
|
||||
(storageTypesData?.items || []).map((item) => ({
|
||||
label: item.displayName || item.name,
|
||||
value: item.name
|
||||
})),
|
||||
[storageTypesData]
|
||||
);
|
||||
|
||||
const applyMode = (mode: string, release: boolean) => {
|
||||
if (mode === StorageModeValueMap.Temporary) {
|
||||
form.setFieldValue(['spec', 'volume'], {
|
||||
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);
|
||||
applyMode(mode, releaseWithInstance);
|
||||
};
|
||||
|
||||
const handleReleaseChange = (e: any) => {
|
||||
const next = !!e?.target?.checked;
|
||||
setReleaseWithInstance(next);
|
||||
applyMode(storageMode, next);
|
||||
};
|
||||
|
||||
const handleCreateStorage = async (values: StorageFormData) => {
|
||||
try {
|
||||
await createStorage({ data: values });
|
||||
await fetchStorage({});
|
||||
await fetchStorage({ page: 1, perPage: 100 });
|
||||
form.setFieldValue(['spec', 'volume'], {
|
||||
persistent: { name: values.metadata.name }
|
||||
persistent: { name: values.name }
|
||||
});
|
||||
setOverlayOpen(false);
|
||||
} catch (error) {
|
||||
@@ -89,90 +130,48 @@ const StorageVolume = ({
|
||||
return (
|
||||
<>
|
||||
<div data-field="storage"></div>
|
||||
<Flex
|
||||
style={{
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 6
|
||||
}}
|
||||
>
|
||||
<Radio.Group
|
||||
disabled={disabled}
|
||||
style={{ marginBottom: 12 }}
|
||||
value={storageMode}
|
||||
onChange={(e) => handleModeChange(e.target.value)}
|
||||
options={[
|
||||
{
|
||||
label: (
|
||||
<LabelInfo
|
||||
description={intl.formatMessage({
|
||||
id: 'gpuservice.storage.temporary.tips'
|
||||
})}
|
||||
label={
|
||||
<span className="text-primary">
|
||||
{intl.formatMessage({
|
||||
id: 'gpuservice.storage.temporary'
|
||||
})}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
),
|
||||
value: StorageModeValueMap.Temporary
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<LabelInfo
|
||||
description={intl.formatMessage({
|
||||
id: 'gpuservice.storage.persistentVolume.tips'
|
||||
})}
|
||||
label={
|
||||
<span className="text-primary">
|
||||
{intl.formatMessage({
|
||||
id: 'gpuservice.storage.persistentVolume'
|
||||
})}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
),
|
||||
value: StorageModeValueMap.Existing
|
||||
}
|
||||
]}
|
||||
/>
|
||||
{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>
|
||||
)}
|
||||
<Radio.Group
|
||||
disabled={disabled}
|
||||
style={{ marginBottom: 12 }}
|
||||
value={storageMode}
|
||||
onChange={(e) => handleModeChange(e.target.value)}
|
||||
options={[
|
||||
{
|
||||
label: (
|
||||
<LabelInfo
|
||||
description={intl.formatMessage({
|
||||
id: 'gpuservice.storage.temporary.tips'
|
||||
})}
|
||||
label={
|
||||
<span className="text-primary">
|
||||
{intl.formatMessage({
|
||||
id: 'gpuservice.storage.temporary'
|
||||
})}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
),
|
||||
value: StorageModeValueMap.Temporary
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<LabelInfo
|
||||
description={intl.formatMessage({
|
||||
id: 'gpuservice.storage.persistentVolume.tips'
|
||||
})}
|
||||
label={
|
||||
<span className="text-primary">
|
||||
{intl.formatMessage({
|
||||
id: 'gpuservice.storage.persistentVolume'
|
||||
})}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
),
|
||||
value: StorageModeValueMap.Persistent
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
{storageMode === StorageModeValueMap.Temporary && (
|
||||
<Form.Item
|
||||
@@ -203,6 +202,112 @@ const StorageVolume = ({
|
||||
/>
|
||||
</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>
|
||||
name={['spec', 'volumeMount']}
|
||||
rules={[
|
||||
@@ -226,7 +331,6 @@ const StorageVolume = ({
|
||||
|
||||
<StorageOverlay
|
||||
open={overlayOpen}
|
||||
namespace={namespace}
|
||||
onCancel={() => setOverlayOpen(false)}
|
||||
onSubmit={handleCreateStorage}
|
||||
/>
|
||||
|
||||
@@ -34,7 +34,7 @@ type ConnectEntry =
|
||||
const getConnectEntries = (record: ListItem): ConnectEntry[] => {
|
||||
const ip = record.status?.hostIPs?.[0]?.ip;
|
||||
const ports = record.status?.ports || [];
|
||||
const hasSshKey = !!record.spec?.sshPublicKey?.name;
|
||||
const hasSshKey = !!record.spec?.sshPublicKeys?.length;
|
||||
const configPorts = record.spec?.ports || [];
|
||||
|
||||
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 {
|
||||
handleSelect: (val: string, record: ListItem) => void;
|
||||
sortOrder: string[];
|
||||
@@ -80,15 +101,15 @@ const useInstancesColumns = ({
|
||||
return [
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.name' }),
|
||||
dataIndex: ['metadata', 'name'],
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
sorter: false,
|
||||
ellipsis: {
|
||||
showTitle: false
|
||||
},
|
||||
render: (text: string) => (
|
||||
render: (text: string, record: ListItem) => (
|
||||
<AutoTooltip ghost style={{ maxWidth: 360 }}>
|
||||
<span className="text-primary">{text}</span>
|
||||
<span className="text-primary">{text || record.displayName}</span>
|
||||
</AutoTooltip>
|
||||
)
|
||||
},
|
||||
@@ -198,40 +219,19 @@ const useInstancesColumns = ({
|
||||
ellipsis: {
|
||||
showTitle: false
|
||||
},
|
||||
width: 230,
|
||||
render: (text: string) => (
|
||||
render: (_text: string, record: ListItem) => (
|
||||
<AutoTooltip
|
||||
ghost
|
||||
style={{ maxWidth: 360, color: 'var(--ant-color-text) !important' }}
|
||||
>
|
||||
<Flex align="center">
|
||||
<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>
|
||||
{renderInstanceType(record)}
|
||||
</AutoTooltip>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
||||
dataIndex: ['metadata', 'creationTimestamp'],
|
||||
key: 'creationTimestamp',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
sorter: false,
|
||||
ellipsis: {
|
||||
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 { 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 currentCluster = useAtomValue(currentClusterAtom);
|
||||
const clusterID = currentCluster?.id;
|
||||
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
|
||||
|
||||
const [openModalStatus, setOpenModalStatus] = useState<{
|
||||
open: boolean;
|
||||
name: string;
|
||||
@@ -22,13 +17,11 @@ const useViewEvents = () => {
|
||||
});
|
||||
|
||||
const openModal = (row?: ListItem) => {
|
||||
const name = row?.metadata?.name || '';
|
||||
const rowNamespace = row?.metadata?.namespace || namespace;
|
||||
setOpenModalStatus({
|
||||
open: true,
|
||||
name,
|
||||
namespace: rowNamespace,
|
||||
clusterID
|
||||
name: row?.name || '',
|
||||
namespace: row?.status?.namespace || '',
|
||||
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 { GPU_SERVICE_INSTANCES_LOG_API } from '../apis';
|
||||
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 currentCluster = useAtomValue(currentClusterAtom);
|
||||
const clusterID = currentCluster?.id;
|
||||
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
|
||||
|
||||
const [openModalStatus, setOpenModalStatus] = useState<{
|
||||
open: boolean;
|
||||
url: string;
|
||||
@@ -23,17 +19,14 @@ const useViewLogs = () => {
|
||||
});
|
||||
|
||||
const openModal = (row?: ListItem) => {
|
||||
const name = row?.metadata?.name;
|
||||
const rowNamespace = row?.metadata?.namespace || namespace;
|
||||
const name = row?.name;
|
||||
const namespace = row?.status?.namespace;
|
||||
const clusterID = row?.clusterId ?? undefined;
|
||||
setOpenModalStatus({
|
||||
open: true,
|
||||
url:
|
||||
name && clusterID
|
||||
? `${GPU_SERVICE_INSTANCES_LOG_API({
|
||||
namespace: rowNamespace,
|
||||
clusterID,
|
||||
name
|
||||
})}`
|
||||
name && namespace && clusterID
|
||||
? GPU_SERVICE_INSTANCES_LOG_API({ namespace, clusterID, name })
|
||||
: '',
|
||||
tail: 1000,
|
||||
status: row?.status?.phase || undefined
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { getCurrentOrgNamespace } from '@/atoms/user';
|
||||
import { PageAction } from '@/config';
|
||||
import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
|
||||
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 {
|
||||
BaseSelect,
|
||||
DeleteModal,
|
||||
DropdownButtons,
|
||||
FilterBar,
|
||||
@@ -17,26 +11,13 @@ import {
|
||||
} from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import {
|
||||
Button,
|
||||
ConfigProvider,
|
||||
Divider,
|
||||
Flex,
|
||||
message,
|
||||
Modal,
|
||||
Space,
|
||||
Table
|
||||
} from 'antd';
|
||||
import { useAtom } from 'jotai';
|
||||
import { Button, ConfigProvider, message, Modal, Space, Table } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { PageContainerInner } from '../../_components/page-box';
|
||||
import PageBox from '../../_components/page-box';
|
||||
import {
|
||||
deleteGPUServiceInstance,
|
||||
GPU_SERVICE_INSTANCES_API,
|
||||
queryGPUServiceInstances,
|
||||
startGPUServiceInstance,
|
||||
stopGPUServiceInstance
|
||||
queryGPUServiceInstances
|
||||
} from './apis';
|
||||
import AddModal from './components/add-modal';
|
||||
import ViewEventsModal from './components/view-events-modal';
|
||||
@@ -52,72 +33,7 @@ import useUpdateInstance from './services/use-update-instance';
|
||||
|
||||
const GPUService: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const [modal, 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 [, modalContextHolder] = Modal.useModal();
|
||||
|
||||
const {
|
||||
dataSource,
|
||||
@@ -130,16 +46,14 @@ const GPUService: React.FC = () => {
|
||||
fetchData,
|
||||
handlePageChange,
|
||||
handleTableChange,
|
||||
handleQueryChange,
|
||||
handleSearch,
|
||||
handleNameChange
|
||||
} = useTableFetch<ListItem>({
|
||||
key: PaginationKey.Instances,
|
||||
fetchAPI: fetchInstances,
|
||||
deleteAPI: deleteInstance,
|
||||
watch: false,
|
||||
polling: true,
|
||||
API: GPU_SERVICE_INSTANCES_API({ namespace, clusterID }),
|
||||
fetchAPI: queryGPUServiceInstances,
|
||||
deleteAPI: deleteGPUServiceInstance,
|
||||
watch: true,
|
||||
API: GPU_SERVICE_INSTANCES_API,
|
||||
contentForDelete: intl.formatMessage({ id: 'gpuservice.instance' })
|
||||
});
|
||||
|
||||
@@ -160,55 +74,11 @@ const GPUService: React.FC = () => {
|
||||
closeViewEventsModal,
|
||||
openViewEventsModalStatus
|
||||
} = 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) => {
|
||||
try {
|
||||
if (openInstanceModalStatus.realAction === PageAction.CREATE) {
|
||||
await deleteInstance(
|
||||
openInstanceModalStatus.currentData!.metadata?.name as any
|
||||
);
|
||||
await deleteGPUServiceInstance(openInstanceModalStatus.currentData!.id);
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
@@ -226,161 +96,34 @@ const GPUService: React.FC = () => {
|
||||
closeInstanceModal();
|
||||
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||
} 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) => {
|
||||
if (val === 'view') {
|
||||
openViewInstanceModal(row);
|
||||
} else if (val === 'edit') {
|
||||
openEditInstanceModal(row);
|
||||
} else if (val === 'delete') {
|
||||
handleDelete({
|
||||
...row,
|
||||
name: row.metadata?.name,
|
||||
id: row.metadata?.name as any
|
||||
});
|
||||
handleDelete({ ...row, name: row.name });
|
||||
} 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);
|
||||
} else if (val === 'viewlog') {
|
||||
openViewLogsModal(row);
|
||||
} else if (val === 'viewevent') {
|
||||
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) => {
|
||||
if (val === 'start') {
|
||||
handleStartBatch();
|
||||
} else if (val === 'stop') {
|
||||
handleStopBatch();
|
||||
} else if (val === 'delete') {
|
||||
if (val === 'delete') {
|
||||
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) => {
|
||||
if (type !== 'Table') return;
|
||||
return (
|
||||
@@ -389,7 +132,7 @@ const GPUService: React.FC = () => {
|
||||
loadend={dataSource.loadend}
|
||||
dataSource={dataSource.dataList}
|
||||
image={<IconFont type="icon-cloud-outlined" />}
|
||||
filters={_.pick(queryParams, ['search', 'manufacturer'])}
|
||||
filters={_.pick(queryParams, ['search'])}
|
||||
noFoundText={intl.formatMessage({
|
||||
id: 'noresult.gpuservice.instance.nofound'
|
||||
})}
|
||||
@@ -411,80 +154,64 @@ const GPUService: React.FC = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<PageContainerInner
|
||||
leftContent={
|
||||
<Flex align="center">
|
||||
<span>{intl.formatMessage({ id: 'menu.gpuService.instances' })}</span>
|
||||
<Divider
|
||||
orientation="vertical"
|
||||
style={{
|
||||
marginLeft: 16
|
||||
<>
|
||||
<PageBox>
|
||||
<FilterBar
|
||||
marginBottom={22}
|
||||
marginTop={30}
|
||||
showSelect={false}
|
||||
handleSearch={handleSearch}
|
||||
handleInputChange={handleNameChange}
|
||||
rowSelection={rowSelection}
|
||||
widths={{ input: 300 }}
|
||||
right={
|
||||
<Space size={16}>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
onClick={openCreateInstanceModal}
|
||||
>
|
||||
{intl.formatMessage({ id: 'gpuservice.instance.add' })}
|
||||
</Button>
|
||||
<DropdownButtons
|
||||
items={batchActionList}
|
||||
extra={
|
||||
rowSelection.selectedRowKeys.length > 0 && (
|
||||
<span>({rowSelection.selectedRowKeys.length})</span>
|
||||
)
|
||||
}
|
||||
size="large"
|
||||
showText={true}
|
||||
disabled={!rowSelection.selectedRowKeys.length}
|
||||
onSelect={handleBatchActionSelect}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
<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
|
||||
}}
|
||||
/>
|
||||
<BaseSelect
|
||||
variant="borderless"
|
||||
value={queryParams.cluster_id}
|
||||
options={k8sClusterList}
|
||||
onChange={handleClusterChange}
|
||||
style={{ minWidth: 120, fontWeight: 500 }}
|
||||
></BaseSelect>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<FilterBar
|
||||
marginBottom={22}
|
||||
marginTop={30}
|
||||
showSelect={false}
|
||||
selectOptions={k8sClusterList}
|
||||
select={{ showSearch: true }}
|
||||
selectHolder={intl.formatMessage({
|
||||
id: 'gpuservice.instance.filter.cluster'
|
||||
})}
|
||||
handleSearch={handleSearch}
|
||||
handleSelectChange={handleClusterChange}
|
||||
handleInputChange={handleNameChange}
|
||||
rowSelection={rowSelection}
|
||||
widths={{ input: 300 }}
|
||||
right={
|
||||
<Space size={16}>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
onClick={openCreateInstanceModal}
|
||||
>
|
||||
{intl.formatMessage({ id: 'gpuservice.instance.add' })}
|
||||
</Button>
|
||||
<DropdownButtons
|
||||
items={batchActionList}
|
||||
extra={
|
||||
rowSelection.selectedRowKeys.length > 0 && (
|
||||
<span>({rowSelection.selectedRowKeys.length})</span>
|
||||
)
|
||||
}
|
||||
size="large"
|
||||
showText={true}
|
||||
disabled={!rowSelection.selectedRowKeys.length}
|
||||
onSelect={handleBatchActionSelect}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
<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.metadata.name}
|
||||
onChange={handleTableChange}
|
||||
pagination={false}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
</ConfigProvider>
|
||||
</PageBox>
|
||||
<AddModal
|
||||
open={openInstanceModalStatus.open}
|
||||
action={openInstanceModalStatus.action}
|
||||
@@ -510,7 +237,7 @@ const GPUService: React.FC = () => {
|
||||
/>
|
||||
<DeleteModal ref={modalRef} />
|
||||
{modalContextHolder}
|
||||
</PageContainerInner>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { getCurrentOrgNamespace } from '@/atoms/user';
|
||||
import { useQueryData } from '@gpustack/core-ui';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { apiVersion, KindMapping } from '../../constants';
|
||||
import { createGPUServiceInstance } from '../apis';
|
||||
import { FormData, ListItem } from '../config/types';
|
||||
|
||||
@@ -12,31 +8,10 @@ interface CreateInstanceParams {
|
||||
}
|
||||
|
||||
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(
|
||||
(params: CreateInstanceParams, option?: any) =>
|
||||
createGPUServiceInstance(
|
||||
{
|
||||
namespace,
|
||||
clusterID,
|
||||
data: {
|
||||
apiVersion,
|
||||
kind: KindMapping.instance,
|
||||
metadata: {
|
||||
name: params.data.metadata.name,
|
||||
namespace
|
||||
},
|
||||
spec: params.data.spec
|
||||
}
|
||||
},
|
||||
option
|
||||
),
|
||||
[namespace, clusterID]
|
||||
(params: CreateInstanceParams) =>
|
||||
createGPUServiceInstance({ data: params.data }),
|
||||
[]
|
||||
);
|
||||
|
||||
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 { useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { queryGPUServiceInstanceEvents } from '../apis';
|
||||
import { InstanceEvents } from '../config/types';
|
||||
|
||||
interface QueryInstanceEventsParams {
|
||||
name: string;
|
||||
namespace: string;
|
||||
clusterID?: number;
|
||||
pretty?: string;
|
||||
}
|
||||
|
||||
export default function useQueryInstanceEvents() {
|
||||
const currentCluster = useAtomValue(currentClusterAtom);
|
||||
const clusterID = currentCluster?.id;
|
||||
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
|
||||
|
||||
const fetchDetail = useCallback(
|
||||
(params: QueryInstanceEventsParams, options?: any) =>
|
||||
queryGPUServiceInstanceEvents(
|
||||
{
|
||||
namespace,
|
||||
clusterID,
|
||||
name: params.name,
|
||||
pretty: params.pretty
|
||||
},
|
||||
options
|
||||
),
|
||||
[namespace, clusterID]
|
||||
queryGPUServiceInstanceEvents(params, options),
|
||||
[]
|
||||
);
|
||||
|
||||
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 { useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { queryGPUServiceInstanceLog } from '../apis';
|
||||
import { InstanceLog, InstanceLogQueryParams } from '../config/types';
|
||||
|
||||
interface QueryInstanceLogParams extends InstanceLogQueryParams {
|
||||
name: string;
|
||||
namespace: string;
|
||||
clusterID?: number;
|
||||
}
|
||||
|
||||
export default function useQueryInstanceLog() {
|
||||
const currentCluster = useAtomValue(currentClusterAtom);
|
||||
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
|
||||
const clusterID = currentCluster?.id;
|
||||
|
||||
const fetchDetail = useCallback(
|
||||
(params: QueryInstanceLogParams, options?: any) =>
|
||||
queryGPUServiceInstanceLog(
|
||||
{
|
||||
...params,
|
||||
namespace,
|
||||
clusterID
|
||||
},
|
||||
options
|
||||
),
|
||||
[namespace, clusterID]
|
||||
queryGPUServiceInstanceLog(params, options),
|
||||
[]
|
||||
);
|
||||
|
||||
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
|
||||
|
||||
@@ -1,32 +1,18 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { useQueryData } from '@gpustack/core-ui';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { queryGPUServiceInstanceTypes } from '../apis';
|
||||
import { transformInstanceType } from '../config';
|
||||
import { InstanceTypeItem } from '../config/types';
|
||||
|
||||
export default function useQueryInstanceTypes() {
|
||||
const currentCluster = useAtomValue(currentClusterAtom);
|
||||
const clusterID = currentCluster?.id;
|
||||
|
||||
const fetchDetail = useCallback(
|
||||
async (params: Global.K8sSearchParams = {}, options?: any) => {
|
||||
const res = await queryGPUServiceInstanceTypes(
|
||||
{ ...params, clusterID },
|
||||
options
|
||||
);
|
||||
return {
|
||||
...res,
|
||||
items: (res?.items || []).map(transformInstanceType)
|
||||
};
|
||||
},
|
||||
[clusterID]
|
||||
(params: Global.SearchParams = { page: 1, perPage: 100 }, options?: any) =>
|
||||
queryGPUServiceInstanceTypes(params, options),
|
||||
[]
|
||||
);
|
||||
|
||||
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
|
||||
Global.K8sPageResponse<InstanceTypeItem>,
|
||||
Global.K8sSearchParams
|
||||
Global.PageResponse<InstanceTypeItem>,
|
||||
Global.SearchParams
|
||||
>({
|
||||
fetchDetail,
|
||||
key: 'instanceTypes'
|
||||
|
||||
@@ -1,32 +1,19 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { getCurrentOrgNamespace } from '@/atoms/user';
|
||||
import { useQueryData } from '@gpustack/core-ui';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { queryGPUServiceInstancePVEvents } from '../apis';
|
||||
import { InstanceEvents } from '../config/types';
|
||||
|
||||
interface QueryVolumeEventsParams {
|
||||
name: string;
|
||||
pretty?: string;
|
||||
namespace: string;
|
||||
clusterID?: number;
|
||||
}
|
||||
|
||||
export default function useQueryVolumeEvents() {
|
||||
const currentCluster = useAtomValue(currentClusterAtom);
|
||||
const clusterID = currentCluster?.id;
|
||||
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
|
||||
|
||||
const fetchDetail = useCallback(
|
||||
(params: QueryVolumeEventsParams, options?: any) =>
|
||||
queryGPUServiceInstancePVEvents(
|
||||
{
|
||||
namespace,
|
||||
clusterID,
|
||||
name: params.name
|
||||
},
|
||||
options
|
||||
),
|
||||
[namespace, clusterID]
|
||||
queryGPUServiceInstancePVEvents(params, options),
|
||||
[]
|
||||
);
|
||||
|
||||
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 { useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { apiVersion, KindMapping } from '../../constants';
|
||||
import { updateGPUServiceInstance } from '../apis';
|
||||
import { FormData, ListItem } from '../config/types';
|
||||
|
||||
interface UpdateInstanceParams {
|
||||
id: number;
|
||||
data: FormData;
|
||||
data: Partial<FormData>;
|
||||
}
|
||||
|
||||
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(
|
||||
(params: UpdateInstanceParams, option?: any) =>
|
||||
updateGPUServiceInstance(
|
||||
{
|
||||
namespace,
|
||||
clusterID,
|
||||
id: params.id,
|
||||
data: {
|
||||
apiVersion,
|
||||
kind: KindMapping.instance,
|
||||
metadata: {
|
||||
name: params.data.metadata.name,
|
||||
namespace
|
||||
},
|
||||
spec: params.data.spec
|
||||
}
|
||||
},
|
||||
option
|
||||
),
|
||||
[namespace, clusterID]
|
||||
(params: UpdateInstanceParams) =>
|
||||
updateGPUServiceInstance({ id: params.id, data: params.data }),
|
||||
[]
|
||||
);
|
||||
|
||||
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
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) {
|
||||
return request<ListItem>(GPU_SERVICE_PUBLIC_KEY_API, {
|
||||
export async function queryGPUServicePublicKeys(
|
||||
params: Global.SearchParams,
|
||||
options?: any
|
||||
) {
|
||||
return request<Global.PageResponse<ListItem>>(GPU_SERVICE_PUBLIC_KEY_API, {
|
||||
method: 'GET',
|
||||
params,
|
||||
cancelToken: options?.token
|
||||
@@ -18,8 +21,11 @@ export async function createGPUServicePublicKey(params: { data: FormData }) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateGPUServicePublicKey(params: { data: FormData }) {
|
||||
return request<ListItem>(`${GPU_SERVICE_PUBLIC_KEY_API}`, {
|
||||
export async function updateGPUServicePublicKey(params: {
|
||||
id: number;
|
||||
data: Omit<FormData, 'name'>;
|
||||
}) {
|
||||
return request<ListItem>(`${GPU_SERVICE_PUBLIC_KEY_API}/${params.id}`, {
|
||||
method: 'PUT',
|
||||
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;
|
||||
@@ -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 { Button, Form, message } from 'antd';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { ConfigProvider, message, Table } from 'antd';
|
||||
import _ from 'lodash';
|
||||
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 { FormData } from './types';
|
||||
|
||||
const GPUServicePublicKeys: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const [form] = Form.useForm<FormData>();
|
||||
const { getRuleMessage } = useAppUtils();
|
||||
const { fetchData: fetchSshkey } = useGetSshkey();
|
||||
const { fetchData: updateSshkey, loading: updating } = useUpdateSshkey();
|
||||
|
||||
useEffect(() => {
|
||||
fetchSshkey({}).then((res) => {
|
||||
const data = res?.spec?.data || '';
|
||||
form.setFieldsValue({
|
||||
name: res?.name,
|
||||
spec: {
|
||||
data
|
||||
}
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
const {
|
||||
dataSource,
|
||||
rowSelection,
|
||||
queryParams,
|
||||
sortOrder,
|
||||
modalRef,
|
||||
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 () => {
|
||||
form.submit();
|
||||
const { fetchData: createSshkey } = useCreateSshkey();
|
||||
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 {
|
||||
await updateSshkey({
|
||||
data: values
|
||||
});
|
||||
if (openPublicKeyModalStatus.action === PageAction.EDIT) {
|
||||
await updateSshkey({
|
||||
id: openPublicKeyModalStatus.currentData!.id,
|
||||
data: data
|
||||
});
|
||||
} else {
|
||||
await createSshkey({ data });
|
||||
}
|
||||
fetchData();
|
||||
closePublicKeyModal();
|
||||
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||
} 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 (
|
||||
<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.sshkey.nofound'
|
||||
})}
|
||||
title={intl.formatMessage({ id: 'noresult.gpuservice.sshkey.title' })}
|
||||
subTitle={intl.formatMessage({
|
||||
id: 'noresult.gpuservice.sshkey.subTitle'
|
||||
})}
|
||||
onClick={handleAdd}
|
||||
buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const columns = usePublicKeyColumns({
|
||||
handleSelect,
|
||||
sortOrder
|
||||
});
|
||||
|
||||
return (
|
||||
<PageBox>
|
||||
<Form
|
||||
style={{ width: 700 }}
|
||||
name="gpuServicePublicKeyForm"
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
initialValues={{
|
||||
name: 'SSHPublicKey',
|
||||
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}
|
||||
alwaysFocus
|
||||
autoSize={{ minRows: 8, maxRows: 16 }}
|
||||
style={{ width: 700, minHeight: 186 }}
|
||||
<>
|
||||
<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
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={updating}
|
||||
style={{ width: 120, marginTop: 24 }}
|
||||
onClick={handleSave}
|
||||
>
|
||||
{intl.formatMessage({ id: 'common.button.save' })}
|
||||
</Button>
|
||||
</Form>
|
||||
</PageBox>
|
||||
</ConfigProvider>
|
||||
</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 { useCallback } from 'react';
|
||||
import { createGPUServicePublicKey } from '../apis';
|
||||
import { FormData, ListItem } from '../types';
|
||||
import { FormData, ListItem } from '../config/types';
|
||||
|
||||
interface CreateSshkeyParams {
|
||||
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 { useCallback } from 'react';
|
||||
import { updateGPUServicePublicKey } from '../apis';
|
||||
import { FormData, ListItem } from '../types';
|
||||
import { FormData, ListItem } from '../config/types';
|
||||
|
||||
interface UpdateSshkeyParams {
|
||||
data: FormData;
|
||||
id: number;
|
||||
data: Omit<FormData, 'name'>;
|
||||
}
|
||||
|
||||
export default function useUpdateSshkey() {
|
||||
const fetchDetail = useCallback(
|
||||
(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
|
||||
};
|
||||
}
|
||||
@@ -1,125 +1,56 @@
|
||||
import { request } from '@umijs/max';
|
||||
import { omitPathParams } from '../../utils';
|
||||
import { ListItem, StorageClassItem } from '../config/types';
|
||||
import {
|
||||
FormData,
|
||||
ListItem,
|
||||
StorageClassItem,
|
||||
UpdateData
|
||||
} from '../config/types';
|
||||
|
||||
export const GPU_SERVICE_STORAGE_API = (params: {
|
||||
namespace: string;
|
||||
clusterID?: number;
|
||||
}) =>
|
||||
`/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instancepersistentvolumes`;
|
||||
export const GPU_SERVICE_STORAGE_API = '/gpu-instance-persistent-volumes';
|
||||
|
||||
export const STORAGE_CLASS_API = (params: { clusterID?: number }) =>
|
||||
`/clusters/${params.clusterID}/proxy/apis/storage.k8s.io/v1/storageclasses`;
|
||||
export const STORAGE_CLASS_API = '/gpu-instance-persistent-volume-types';
|
||||
|
||||
export async function queryGPUServiceStorage(
|
||||
params: Global.K8sSearchParams & {
|
||||
namespace: string;
|
||||
clusterID?: number;
|
||||
},
|
||||
params: Global.SearchParams,
|
||||
options?: any
|
||||
) {
|
||||
if (!params.clusterID) {
|
||||
return;
|
||||
}
|
||||
return request<Global.K8sPageResponse<ListItem>>(
|
||||
GPU_SERVICE_STORAGE_API({
|
||||
namespace: params.namespace,
|
||||
clusterID: params.clusterID
|
||||
}),
|
||||
{
|
||||
method: 'GET',
|
||||
params: omitPathParams(params),
|
||||
cancelToken: options?.token
|
||||
}
|
||||
);
|
||||
return request<Global.PageResponse<ListItem>>(GPU_SERVICE_STORAGE_API, {
|
||||
method: 'GET',
|
||||
params,
|
||||
cancelToken: options?.token
|
||||
});
|
||||
}
|
||||
|
||||
export async function createGPUServiceStorage(
|
||||
params: {
|
||||
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',
|
||||
data: params.data,
|
||||
cancelToken: option?.token
|
||||
}
|
||||
);
|
||||
export async function createGPUServiceStorage(params: { data: FormData }) {
|
||||
return request<ListItem>(GPU_SERVICE_STORAGE_API, {
|
||||
method: 'POST',
|
||||
data: params.data
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateGPUServiceStorage(
|
||||
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;
|
||||
export async function updateGPUServiceStorage(params: {
|
||||
id: number;
|
||||
data: UpdateData;
|
||||
}) {
|
||||
if (!params.clusterID) {
|
||||
return;
|
||||
}
|
||||
return request(
|
||||
`${GPU_SERVICE_STORAGE_API({
|
||||
namespace: params.namespace,
|
||||
clusterID: params.clusterID
|
||||
})}/${params.id}`,
|
||||
{
|
||||
method: 'DELETE'
|
||||
}
|
||||
);
|
||||
return request<ListItem>(`${GPU_SERVICE_STORAGE_API}/${params.id}`, {
|
||||
method: 'PUT',
|
||||
data: params.data
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteGPUServiceStorage(id: number) {
|
||||
return request(`${GPU_SERVICE_STORAGE_API}/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
export async function queryStorageClass(
|
||||
params: Global.K8sSearchParams & {
|
||||
clusterID?: number;
|
||||
},
|
||||
params: Global.SearchParams,
|
||||
options?: any
|
||||
) {
|
||||
if (!params.clusterID) {
|
||||
return;
|
||||
}
|
||||
return request<Global.K8sPageResponse<StorageClassItem>>(
|
||||
STORAGE_CLASS_API({ clusterID: params.clusterID }),
|
||||
{
|
||||
method: 'GET',
|
||||
params: omitPathParams(params),
|
||||
cancelToken: options?.token,
|
||||
headers: {
|
||||
skipErrorHandler: 'true'
|
||||
}
|
||||
}
|
||||
);
|
||||
return request<Global.PageResponse<StorageClassItem>>(STORAGE_CLASS_API, {
|
||||
method: 'GET',
|
||||
params,
|
||||
cancelToken: options?.token
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,90 +20,13 @@ export const status: Record<string, StatusType> = {
|
||||
[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 = [
|
||||
// {
|
||||
// label: 'common.button.edit',
|
||||
// key: 'edit',
|
||||
// locale: true,
|
||||
// icon: icons.EditOutlined
|
||||
// },
|
||||
{
|
||||
label: 'common.button.edit',
|
||||
key: 'edit',
|
||||
locale: true,
|
||||
icon: icons.EditOutlined
|
||||
},
|
||||
{
|
||||
label: 'common.button.delete',
|
||||
key: 'delete',
|
||||
|
||||
@@ -1,60 +1,68 @@
|
||||
export type AccessModeType =
|
||||
| 'ReadOnlyMany'
|
||||
| 'ReadWriteMany'
|
||||
| 'ReadWriteOnce'
|
||||
| 'ReadWriteOncePod';
|
||||
|
||||
export type ReClaimPolicyType = 'Retain' | 'Delete' | 'Recycle';
|
||||
export interface FormData {
|
||||
metadata: {
|
||||
name: string;
|
||||
namespace: string;
|
||||
};
|
||||
spec: {
|
||||
capacity: string;
|
||||
type: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ManagedField {
|
||||
manager: string;
|
||||
operation: string;
|
||||
apiVersion: string;
|
||||
time: string;
|
||||
fieldsType: string;
|
||||
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[];
|
||||
owner_principal_id?: number | null;
|
||||
displayName?: string | null;
|
||||
description?: string | null;
|
||||
spec: {
|
||||
type: string;
|
||||
capacity: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Per `GPUInstancePersistentVolumeUpdate`, only displayName / description /
|
||||
// owner_principal_id are mutable post-create.
|
||||
export interface UpdateData {
|
||||
owner_principal_id?: number | null;
|
||||
displayName?: string | null;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
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: {
|
||||
type: 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 {
|
||||
metadata: Metadata;
|
||||
provisioner: string;
|
||||
reclaimPolicy: 'Delete' | 'Retain';
|
||||
volumeBindingMode: 'Immediate' | 'WaitForFirstConsumer';
|
||||
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?: StorageClassNFS | null;
|
||||
s3?: StorageClassS3 | null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ import {
|
||||
Input as CInput,
|
||||
InputNumber,
|
||||
Select as SealSelect,
|
||||
Textarea,
|
||||
useAppUtils
|
||||
} from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Flex, Form } from 'antd';
|
||||
import { useEffect } from 'react';
|
||||
import OwnerPrincipalIdField from '../../../_components/owner-principal-id-field';
|
||||
import { FormData } from '../config/types';
|
||||
import useQueryStorageClass from '../services/use-query-storage-class';
|
||||
|
||||
@@ -18,14 +20,15 @@ const Basic = ({ open, action }: { open: boolean; action: string }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
fetchData({});
|
||||
fetchData({ page: 1, perPage: 100 });
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<OwnerPrincipalIdField name="owner_principal_id" />
|
||||
<Form.Item<FormData>
|
||||
name={['metadata', 'name']}
|
||||
name="name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
@@ -43,6 +46,19 @@ const Basic = ({ open, action }: { open: boolean; action: string }) => {
|
||||
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
|
||||
trim={false}
|
||||
scaleSize={true}
|
||||
label={intl.formatMessage({ id: 'common.table.description' })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Flex gap={16}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Form.Item<FormData>
|
||||
@@ -79,7 +95,9 @@ const Basic = ({ open, action }: { open: boolean; action: string }) => {
|
||||
>
|
||||
<InputNumber
|
||||
disabled={action === PageAction.EDIT}
|
||||
label={intl.formatMessage({ id: 'gpuservice.storage.capacity' })}
|
||||
label={intl.formatMessage({
|
||||
id: 'gpuservice.storage.persistentVolume.capacity'
|
||||
})}
|
||||
required
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -10,19 +10,12 @@ interface StorageFormProps {
|
||||
open: boolean;
|
||||
action: PageActionType;
|
||||
currentData?: ListItem | null;
|
||||
namespace?: string;
|
||||
onFinish: (values: FormData) => Promise<void>;
|
||||
}
|
||||
|
||||
const GPUServiceStorageForm: React.FC<StorageFormProps> = forwardRef(
|
||||
(props, ref) => {
|
||||
const {
|
||||
action,
|
||||
currentData,
|
||||
open,
|
||||
namespace = 'default',
|
||||
onFinish
|
||||
} = props;
|
||||
const { action, currentData, open, onFinish } = props;
|
||||
const [form] = Form.useForm<FormData>();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -33,10 +26,9 @@ const GPUServiceStorageForm: React.FC<StorageFormProps> = forwardRef(
|
||||
|
||||
if (action === PageAction.EDIT && currentData) {
|
||||
form.setFieldsValue({
|
||||
metadata: {
|
||||
name: currentData.metadata?.name,
|
||||
namespace: currentData.metadata?.namespace
|
||||
},
|
||||
name: currentData.name,
|
||||
displayName: currentData.displayName,
|
||||
description: currentData.description,
|
||||
spec: {
|
||||
capacity: currentData.spec?.capacity,
|
||||
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 dayjs from 'dayjs';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
rowActionList,
|
||||
status,
|
||||
StoragePhaseLabelMap,
|
||||
StorageTypeLabelMap
|
||||
} from '../config';
|
||||
import { rowActionList, status, StoragePhaseLabelMap } from '../config';
|
||||
import { ListItem } from '../config/types';
|
||||
|
||||
interface ColumnsHookProps {
|
||||
@@ -25,15 +20,15 @@ const useStorageColumns = ({
|
||||
return [
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.name' }),
|
||||
dataIndex: ['metadata', 'name'],
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
sorter: false,
|
||||
ellipsis: {
|
||||
showTitle: false
|
||||
},
|
||||
render: (text: string) => (
|
||||
render: (text: string, record: ListItem) => (
|
||||
<AutoTooltip ghost style={{ maxWidth: 360 }}>
|
||||
<span className="text-primary">{text}</span>
|
||||
<span className="text-primary">{record.displayName || text}</span>
|
||||
</AutoTooltip>
|
||||
)
|
||||
},
|
||||
@@ -42,10 +37,7 @@ const useStorageColumns = ({
|
||||
dataIndex: ['spec', 'type'],
|
||||
key: 'type',
|
||||
sorter: false,
|
||||
render: (value: string) =>
|
||||
StorageTypeLabelMap[value]
|
||||
? intl.formatMessage({ id: StorageTypeLabelMap[value] })
|
||||
: value || '-'
|
||||
render: (value: string) => value || '-'
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'gpuservice.storage.capacity' }),
|
||||
@@ -59,20 +51,22 @@ const useStorageColumns = ({
|
||||
dataIndex: ['status', 'phase'],
|
||||
key: 'status',
|
||||
sorter: false,
|
||||
render: (value: string, record: ListItem) => (
|
||||
<StatusTag
|
||||
statusValue={{
|
||||
status: status[value],
|
||||
text: StoragePhaseLabelMap[value] || value,
|
||||
message: record.status?.phaseMessage || ''
|
||||
}}
|
||||
></StatusTag>
|
||||
)
|
||||
render: (value: string) =>
|
||||
value ? (
|
||||
<StatusTag
|
||||
statusValue={{
|
||||
status: status[value],
|
||||
text: StoragePhaseLabelMap[value] || value
|
||||
}}
|
||||
></StatusTag>
|
||||
) : (
|
||||
'-'
|
||||
)
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
||||
dataIndex: ['metadata', 'creationTimestamp'],
|
||||
key: 'creationTimestamp',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
sorter: false,
|
||||
ellipsis: {
|
||||
showTitle: false
|
||||
|
||||
@@ -1,85 +1,26 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { getCurrentOrgNamespace } from '@/atoms/user';
|
||||
import { PageAction } from '@/config';
|
||||
import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
|
||||
import type { PageActionType } from '@/config/types';
|
||||
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 {
|
||||
BaseSelect,
|
||||
DeleteModal,
|
||||
FilterBar,
|
||||
IconFont,
|
||||
NoResult
|
||||
} from '@gpustack/core-ui';
|
||||
import { DeleteModal, FilterBar, IconFont, NoResult } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { ConfigProvider, Divider, Flex, message, Table } from 'antd';
|
||||
import { useAtom } from 'jotai';
|
||||
import { ConfigProvider, message, Table } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { PageContainerInner } from '../../_components/page-box';
|
||||
import PageBox from '../../_components/page-box';
|
||||
import {
|
||||
deleteGPUServiceStorage,
|
||||
GPU_SERVICE_STORAGE_API,
|
||||
queryGPUServiceStorage
|
||||
} from './apis';
|
||||
|
||||
import AddModal from './components/add-modal';
|
||||
import { FormData, ListItem } from './config/types';
|
||||
import useCreateStorageModal from './hooks/use-create-storage-modal';
|
||||
import useStorageColumns from './hooks/use-storage-columns';
|
||||
import useCreateStorage from './services/use-create-storage';
|
||||
import useUpdateStorage from './services/use-update-storage';
|
||||
|
||||
const GPUServiceStorage: React.FC = () => {
|
||||
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 {
|
||||
dataSource,
|
||||
@@ -92,116 +33,55 @@ const GPUServiceStorage: React.FC = () => {
|
||||
fetchData,
|
||||
handlePageChange,
|
||||
handleTableChange,
|
||||
handleQueryChange,
|
||||
handleSearch,
|
||||
handleNameChange
|
||||
} = useTableFetch<ListItem>({
|
||||
key: PaginationKey.Storage,
|
||||
fetchAPI: fetchStorage,
|
||||
deleteAPI: deleteStorage,
|
||||
fetchAPI: queryGPUServiceStorage,
|
||||
deleteAPI: deleteGPUServiceStorage,
|
||||
watch: false,
|
||||
polling: true,
|
||||
API: GPU_SERVICE_STORAGE_API({ namespace, clusterID }),
|
||||
API: GPU_SERVICE_STORAGE_API,
|
||||
contentForDelete: intl.formatMessage({ id: 'gpuservice.storage' })
|
||||
});
|
||||
|
||||
const { fetchData: createStorage } = useCreateStorage();
|
||||
const { fetchData: updateStorage } = useUpdateStorage();
|
||||
const {
|
||||
fetchClusterList,
|
||||
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 { openStorageModalStatus, openStorageModal, closeStorageModal } =
|
||||
useCreateStorageModal();
|
||||
|
||||
const handleAddStorage = () => {
|
||||
setOpenAddModalStatus({
|
||||
action: PageAction.CREATE,
|
||||
title: intl.formatMessage({ id: 'gpuservice.storage.add' }),
|
||||
open: true,
|
||||
currentData: null
|
||||
});
|
||||
openStorageModal(
|
||||
PageAction.CREATE,
|
||||
intl.formatMessage({ id: 'gpuservice.storage.add' })
|
||||
);
|
||||
};
|
||||
|
||||
const handleEditStorage = (row: ListItem) => {
|
||||
setOpenAddModalStatus({
|
||||
action: PageAction.EDIT,
|
||||
title: intl.formatMessage({ id: 'gpuservice.storage.edit' }),
|
||||
open: true,
|
||||
currentData: row
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setOpenAddModalStatus({
|
||||
action: PageAction.CREATE,
|
||||
title: '',
|
||||
open: false,
|
||||
currentData: null
|
||||
});
|
||||
openStorageModal(
|
||||
PageAction.EDIT,
|
||||
intl.formatMessage({ id: 'gpuservice.storage.edit' }),
|
||||
row
|
||||
);
|
||||
};
|
||||
|
||||
const handleModalOk = async (data: FormData) => {
|
||||
try {
|
||||
if (openAddModalStatus.action === PageAction.EDIT) {
|
||||
if (openStorageModalStatus.action === PageAction.EDIT) {
|
||||
await updateStorage({
|
||||
id: openAddModalStatus.currentData!.metadata?.name as any,
|
||||
data
|
||||
id: openStorageModalStatus.currentData!.id,
|
||||
data: {
|
||||
owner_principal_id: data.owner_principal_id,
|
||||
displayName: data.displayName,
|
||||
description: data.description
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await createStorage({ data });
|
||||
}
|
||||
|
||||
fetchData();
|
||||
closeModal();
|
||||
closeStorageModal();
|
||||
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||
} catch (error) {
|
||||
message.error(intl.formatMessage({ id: 'common.message.fail' }));
|
||||
@@ -214,23 +94,11 @@ const GPUServiceStorage: React.FC = () => {
|
||||
} else if (val === 'delete') {
|
||||
handleDelete({
|
||||
...row,
|
||||
name: row.metadata?.name,
|
||||
id: row.metadata?.name as any
|
||||
id: row.id
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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) => {
|
||||
if (type !== 'Table') return;
|
||||
return (
|
||||
@@ -260,38 +128,13 @@ const GPUServiceStorage: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageContainerInner
|
||||
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>
|
||||
}
|
||||
>
|
||||
<PageBox>
|
||||
<FilterBar
|
||||
marginBottom={22}
|
||||
marginTop={30}
|
||||
showSelect={false}
|
||||
selectOptions={k8sClusterList}
|
||||
select={{ showSearch: true }}
|
||||
selectHolder={intl.formatMessage({
|
||||
id: 'gpuservice.storage.filter.cluster'
|
||||
})}
|
||||
buttonText={intl.formatMessage({ id: 'gpuservice.storage.add' })}
|
||||
handleSearch={handleSearch}
|
||||
handleSelectChange={handleClusterChange}
|
||||
handleDeleteByBatch={handleDeleteBatch}
|
||||
handleClickPrimary={handleAddStorage}
|
||||
handleInputChange={handleNameChange}
|
||||
@@ -309,18 +152,25 @@ const GPUServiceStorage: React.FC = () => {
|
||||
}}
|
||||
sortDirections={TABLE_SORT_DIRECTIONS}
|
||||
showSorterTooltip={false}
|
||||
rowKey={(record) => record.metadata?.name as any}
|
||||
rowKey={(record) => record.id}
|
||||
onChange={handleTableChange}
|
||||
pagination={false}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
pageSize: queryParams.perPage,
|
||||
current: queryParams.page,
|
||||
total: dataSource.total,
|
||||
hideOnSinglePage: queryParams.perPage === 10,
|
||||
onChange: handlePageChange
|
||||
}}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
</PageContainerInner>
|
||||
</PageBox>
|
||||
<AddModal
|
||||
open={openAddModalStatus.open}
|
||||
action={openAddModalStatus.action}
|
||||
title={openAddModalStatus.title}
|
||||
data={openAddModalStatus.currentData}
|
||||
onCancel={closeModal}
|
||||
open={openStorageModalStatus.open}
|
||||
action={openStorageModalStatus.action}
|
||||
title={openStorageModalStatus.title}
|
||||
data={openStorageModalStatus.currentData}
|
||||
onCancel={closeStorageModal}
|
||||
onOk={handleModalOk}
|
||||
/>
|
||||
<DeleteModal ref={modalRef} />
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { getCurrentOrgNamespace } from '@/atoms/user';
|
||||
import { useQueryData } from '@gpustack/core-ui';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { apiVersion, KindMapping } from '../../constants';
|
||||
import { createGPUServiceStorage } from '../apis';
|
||||
import { FormData, ListItem } from '../config/types';
|
||||
|
||||
@@ -12,31 +8,10 @@ interface CreateStorageParams {
|
||||
}
|
||||
|
||||
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(
|
||||
(params: CreateStorageParams, option?: any) =>
|
||||
createGPUServiceStorage(
|
||||
{
|
||||
namespace,
|
||||
clusterID,
|
||||
data: {
|
||||
apiVersion,
|
||||
kind: KindMapping.instancePersistentVolume,
|
||||
metadata: {
|
||||
name: params.data.metadata.name,
|
||||
namespace
|
||||
},
|
||||
spec: params.data.spec
|
||||
}
|
||||
},
|
||||
option
|
||||
),
|
||||
[namespace, clusterID]
|
||||
(params: CreateStorageParams) =>
|
||||
createGPUServiceStorage({ data: params.data }),
|
||||
[]
|
||||
);
|
||||
|
||||
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
|
||||
|
||||
@@ -1,28 +1,23 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { useQueryDataList } from '@gpustack/core-ui';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { queryStorageClass } from '../apis';
|
||||
import { StorageClassItem } from '../config/types';
|
||||
|
||||
export default function useQueryStorageClass() {
|
||||
const currentCluster = useAtomValue(currentClusterAtom);
|
||||
const clusterID = currentCluster?.id;
|
||||
|
||||
const fetchList = useCallback(
|
||||
(params: Global.K8sSearchParams = {}, options?: any) =>
|
||||
queryStorageClass({ ...params, clusterID }, options),
|
||||
[clusterID]
|
||||
(params: Global.SearchParams = { page: 1, perPage: 100 }, options?: any) =>
|
||||
queryStorageClass(params, options),
|
||||
[]
|
||||
);
|
||||
|
||||
const { dataList, loading, cancelRequest, fetchData } = useQueryDataList<
|
||||
StorageClassItem,
|
||||
Global.K8sSearchParams
|
||||
Global.SearchParams
|
||||
>({
|
||||
key: 'storageClass',
|
||||
fetchList,
|
||||
getLabel: (item) => item?.metadata.name,
|
||||
getValue: (item) => item?.metadata.name
|
||||
getLabel: (item) => item?.displayName || item?.name,
|
||||
getValue: (item) => item?.name
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { getCurrentOrgNamespace } from '@/atoms/user';
|
||||
import { useQueryData } from '@gpustack/core-ui';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { queryGPUServiceStorage } from '../apis';
|
||||
import { ListItem } from '../config/types';
|
||||
|
||||
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(
|
||||
(params: Global.K8sSearchParams = {}, options?: any) =>
|
||||
queryGPUServiceStorage({ ...params, namespace, clusterID }, options),
|
||||
[namespace, clusterID]
|
||||
(params: Global.SearchParams = { page: 1, perPage: 100 }, options?: any) =>
|
||||
queryGPUServiceStorage(params, options),
|
||||
[]
|
||||
);
|
||||
|
||||
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
|
||||
Global.K8sPageResponse<ListItem>,
|
||||
Global.K8sSearchParams
|
||||
Global.PageResponse<ListItem>,
|
||||
Global.SearchParams
|
||||
>({
|
||||
fetchDetail,
|
||||
key: 'storage'
|
||||
|
||||
@@ -1,44 +1,18 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { getCurrentOrgNamespace } from '@/atoms/user';
|
||||
import { useQueryData } from '@gpustack/core-ui';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { apiVersion, KindMapping } from '../../constants';
|
||||
import { updateGPUServiceStorage } from '../apis';
|
||||
import { FormData, ListItem } from '../config/types';
|
||||
import { ListItem, UpdateData } from '../config/types';
|
||||
|
||||
interface UpdateStorageParams {
|
||||
id: number;
|
||||
data: FormData;
|
||||
data: UpdateData;
|
||||
}
|
||||
|
||||
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(
|
||||
(params: UpdateStorageParams, option?: any) =>
|
||||
updateGPUServiceStorage(
|
||||
{
|
||||
namespace,
|
||||
clusterID,
|
||||
id: params.id,
|
||||
data: {
|
||||
apiVersion,
|
||||
kind: KindMapping.instancePersistentVolume,
|
||||
metadata: {
|
||||
name: params.data.metadata.name,
|
||||
namespace
|
||||
},
|
||||
spec: params.data.spec
|
||||
}
|
||||
},
|
||||
option
|
||||
),
|
||||
[namespace, clusterID]
|
||||
(params: UpdateStorageParams) =>
|
||||
updateGPUServiceStorage({ id: params.id, data: params.data }),
|
||||
[]
|
||||
);
|
||||
|
||||
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
|
||||
|
||||
@@ -58,7 +58,7 @@ const Basic: React.FC<BasicProps> = ({
|
||||
<Flex gap={4} align="center">
|
||||
{label}
|
||||
<span>
|
||||
({intl.formatMessage({ id: 'common.remaining' }, { count: max })})
|
||||
({intl.formatMessage({ id: 'common.max' }, { count: max })})
|
||||
</span>
|
||||
</Flex>
|
||||
);
|
||||
@@ -126,6 +126,7 @@ const Basic: React.FC<BasicProps> = ({
|
||||
id: 'gpuservice.template.displayName'
|
||||
})}
|
||||
showCount
|
||||
trim={false}
|
||||
maxLength={63}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
Reference in New Issue
Block a user