diff --git a/config/routes.ts b/config/routes.ts index 2e6adb6f..8b49e1ea 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -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', diff --git a/package.json b/package.json index d439de32..419081a4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d42e028..e32d59d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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' diff --git a/src/layouts/index.tsx b/src/layouts/index.tsx index b9772540..3c075fe2 100644 --- a/src/layouts/index.tsx +++ b/src/layouts/index.tsx @@ -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 }} diff --git a/src/locales/en-US/common.ts b/src/locales/en-US/common.ts index 20e3e7df..68b6b31d 100644 --- a/src/locales/en-US/common.ts +++ b/src/locales/en-US/common.ts @@ -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' }; diff --git a/src/locales/en-US/gpuservice.ts b/src/locales/en-US/gpuservice.ts index c2ee160b..11d0f861 100644 --- a/src/locales/en-US/gpuservice.ts +++ b/src/locales/en-US/gpuservice.ts @@ -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', diff --git a/src/locales/en-US/menu.ts b/src/locales/en-US/menu.ts index bd57b9a4..622f1373 100644 --- a/src/locales/en-US/menu.ts +++ b/src/locales/en-US/menu.ts @@ -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' }; diff --git a/src/locales/en-US/no-result.ts b/src/locales/en-US/no-result.ts index a7d91b8f..0f542225 100644 --- a/src/locales/en-US/no-result.ts +++ b/src/locales/en-US/no-result.ts @@ -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.', diff --git a/src/locales/ja-JP/common.ts b/src/locales/ja-JP/common.ts index a0448019..97d6a965 100644 --- a/src/locales/ja-JP/common.ts +++ b/src/locales/ja-JP/common.ts @@ -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' }; diff --git a/src/locales/ja-JP/gpuservice.ts b/src/locales/ja-JP/gpuservice.ts index 60a9e9b5..447d2fb5 100644 --- a/src/locales/ja-JP/gpuservice.ts +++ b/src/locales/ja-JP/gpuservice.ts @@ -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': '一時ストレージ容量を入力してください', diff --git a/src/locales/ja-JP/menu.ts b/src/locales/ja-JP/menu.ts index 2c7ce74c..1298dc0c 100644 --- a/src/locales/ja-JP/menu.ts +++ b/src/locales/ja-JP/menu.ts @@ -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' }; diff --git a/src/locales/ja-JP/no-result.ts b/src/locales/ja-JP/no-result.ts index 9e0e86ec..0638d312 100644 --- a/src/locales/ja-JP/no-result.ts +++ b/src/locales/ja-JP/no-result.ts @@ -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 公開鍵が見つかりません。' diff --git a/src/locales/ru-RU/common.ts b/src/locales/ru-RU/common.ts index 937a6caf..a703143e 100644 --- a/src/locales/ru-RU/common.ts +++ b/src/locales/ru-RU/common.ts @@ -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' }; diff --git a/src/locales/ru-RU/gpuservice.ts b/src/locales/ru-RU/gpuservice.ts index 3b755956..9ca24dc5 100644 --- a/src/locales/ru-RU/gpuservice.ts +++ b/src/locales/ru-RU/gpuservice.ts @@ -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': 'Введите объём временного хранилища', diff --git a/src/locales/ru-RU/menu.ts b/src/locales/ru-RU/menu.ts index 53b4629a..b6fbb72e 100644 --- a/src/locales/ru-RU/menu.ts +++ b/src/locales/ru-RU/menu.ts @@ -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' }; diff --git a/src/locales/ru-RU/no-result.ts b/src/locales/ru-RU/no-result.ts index 53b6b0db..d438e3a4 100644 --- a/src/locales/ru-RU/no-result.ts +++ b/src/locales/ru-RU/no-result.ts @@ -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': diff --git a/src/locales/tr-TR/common.ts b/src/locales/tr-TR/common.ts index e98a22d2..92b0ce8a 100644 --- a/src/locales/tr-TR/common.ts +++ b/src/locales/tr-TR/common.ts @@ -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' }; diff --git a/src/locales/tr-TR/gpuservice.ts b/src/locales/tr-TR/gpuservice.ts index c93cc2c3..436651c3 100644 --- a/src/locales/tr-TR/gpuservice.ts +++ b/src/locales/tr-TR/gpuservice.ts @@ -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', diff --git a/src/locales/tr-TR/menu.ts b/src/locales/tr-TR/menu.ts index 90136081..fb182e1c 100644 --- a/src/locales/tr-TR/menu.ts +++ b/src/locales/tr-TR/menu.ts @@ -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' }; diff --git a/src/locales/tr-TR/no-result.ts b/src/locales/tr-TR/no-result.ts index dd7c03d6..a5e78dfe 100644 --- a/src/locales/tr-TR/no-result.ts +++ b/src/locales/tr-TR/no-result.ts @@ -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ı.' diff --git a/src/locales/zh-CN/common.ts b/src/locales/zh-CN/common.ts index 9027672f..8482397f 100644 --- a/src/locales/zh-CN/common.ts +++ b/src/locales/zh-CN/common.ts @@ -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}配置' }; diff --git a/src/locales/zh-CN/gpuservice.ts b/src/locales/zh-CN/gpuservice.ts index cd98679f..cf2eeb0e 100644 --- a/src/locales/zh-CN/gpuservice.ts +++ b/src/locales/zh-CN/gpuservice.ts @@ -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': diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts index 5b01150a..800992b2 100644 --- a/src/locales/zh-CN/menu.ts +++ b/src/locales/zh-CN/menu.ts @@ -45,5 +45,6 @@ export default { 'menu.gpuService.instances': 'GPU 实例', 'menu.gpuService.templates': '实例模板', 'menu.gpuService.storage': '存储', + 'menu.gpuService.storageTypes': '存储类型', 'menu.gpuService.publicKeys': 'SSH 公钥' }; diff --git a/src/locales/zh-CN/no-result.ts b/src/locales/zh-CN/no-result.ts index d369efd2..803977e4 100644 --- a/src/locales/zh-CN/no-result.ts +++ b/src/locales/zh-CN/no-result.ts @@ -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 公钥' diff --git a/src/pages/_components/number-selection/index.tsx b/src/pages/_components/number-selection/index.tsx index 16a3baec..053cb31a 100644 --- a/src/pages/_components/number-selection/index.tsx +++ b/src/pages/_components/number-selection/index.tsx @@ -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 = ({ id, step = 1, @@ -36,21 +35,26 @@ const NumberSelection: React.FC = ({ 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(() => - 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 = ({ 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 = ({ } onChange?.(inputValue); }; - console.log('value+++', value, 'inputValue', items, inputValue); return (
= ({ })} > = ({ value={inputValue || undefined} style={{ fontSize: 14, - width: 140, + width: 60, height: 32, backgroundColor: 'var(--ant-color-bg-container)', ...style diff --git a/src/pages/_components/owner-principal-id-field.tsx b/src/pages/_components/owner-principal-id-field.tsx new file mode 100644 index 00000000..06919db6 --- /dev/null +++ b/src/pages/_components/owner-principal-id-field.tsx @@ -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 = ({ + 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 ( + + ); +}; + +export default OwnerPrincipalIdField; diff --git a/src/pages/gpu-service/instances/apis/index.ts b/src/pages/gpu-service/instances/apis/index.ts index 9dff0b68..5d47c98b 100644 --- a/src/pages/gpu-service/instances/apis/index.ts +++ b/src/pages/gpu-service/instances/apis/index.ts @@ -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>( - GPU_SERVICE_INSTANCES_API({ - namespace: params.namespace, - clusterID: params.clusterID - }), - { - method: 'GET', - params: omitPathParams(params), - cancelToken: options?.token - } - ); + return request>(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( - 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(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( - `${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; +}) { + return request(`${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>( - GPU_SERVICE_INSTANCES_TYPE_API({ clusterID: params.clusterID }), + return request>( + 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( - `${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( - `${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( - `${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( - `${GPU_SERVICE_INSTANCES_TYPE_API({ clusterID: params.clusterID })}/${params.name}`, - { - method: 'GET', - params: omitPathParams(params), - cancelToken: options?.token - } - ); -} diff --git a/src/pages/gpu-service/instances/components/add-modal.tsx b/src/pages/gpu-service/instances/components/add-modal.tsx index 29804ab4..f1539a78 100644 --- a/src/pages/gpu-service/instances/components/add-modal.tsx +++ b/src/pages/gpu-service/instances/components/add-modal.tsx @@ -101,7 +101,8 @@ const AddModal: React.FC = ({ 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 = ({ 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 = ({ } 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 = ({ 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 = ({ } > <> - {action !== PageAction.VIEW && ( + {action !== PageAction.EDIT && ( {intl.formatMessage({ id: 'common.title.config' })} diff --git a/src/pages/gpu-service/instances/components/instance-type-item.tsx b/src/pages/gpu-service/instances/components/instance-type-item.tsx index 45395f0c..cb39dd38 100644 --- a/src/pages/gpu-service/instances/components/instance-type-item.tsx +++ b/src/pages/gpu-service/instances/components/instance-type-item.tsx @@ -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 = ({ - 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 && } + + + + {label} + {value || '-'} + + + + ); +}; - const manufacturerKey = item.spec?.manufacturer; +const InstanceTypeItem: React.FC = ({ 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 = ({ <> <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> - )} {acceleratable && ( - - - - - {intl.formatMessage({ id: 'gpuservice.instance.memory' })} - - - {toDisplayUnit(convertKiToGi(item.spec?.memory)) ?? '-'} - - - - {item.spec?.sliced && ( - <> - - - - - - {intl.formatMessage({ - id: 'gpuservice.instance.sliced' - })} - - {item.spec?.sliced ?? '-'} - - - - )} - {item.status?.accelerator?.remaining && ( - <> - - - - - - {intl.formatMessage({ - id: 'gpuservice.instance.stock' - })} - - {item.status?.accelerator?.remaining ?? '-'} - - - - )} + + + )} - - - - - {intl.formatMessage({ id: 'gpuservice.instance.ram' })} - - {toDisplayUnit(item.status?.ram?.capacity) ?? '-'} - - - {acceleratable && ( - <> - - - - - vCPU - {item.status?.cpu?.capacity ?? '-'} - - - - )} + + diff --git a/src/pages/gpu-service/instances/components/instance-type-list.tsx b/src/pages/gpu-service/instances/components/instance-type-list.tsx index 1cef55c6..212056e2 100644 --- a/src/pages/gpu-service/instances/components/instance-type-list.tsx +++ b/src/pages/gpu-service/instances/components/instance-type-list.tsx @@ -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 = ({ value, @@ -53,7 +57,7 @@ const InstanceTypeList: React.FC = ({ {dataList.map((item) => { const disabled = !isAvailable(item); - const name = item.metadata?.name; + const name = item.name; return ( = { const ViewEventsModal: React.FC = (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 = (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(() => { diff --git a/src/pages/gpu-service/instances/config/form-context.ts b/src/pages/gpu-service/instances/config/form-context.ts new file mode 100644 index 00000000..9798e4e6 --- /dev/null +++ b/src/pages/gpu-service/instances/config/form-context.ts @@ -0,0 +1,14 @@ +import { PageActionType } from '@/config/types'; +import { createContext } from 'react'; + +export interface FormContextValue { + action: PageActionType; + currentData: any; +} + +export const FormContext = createContext({ + action: 'create', + currentData: null +}); + +export const FormContextProvider = FormContext.Provider; diff --git a/src/pages/gpu-service/instances/config/index.ts b/src/pages/gpu-service/instances/config/index.ts index c6975d13..1b10c5b4 100644 --- a/src/pages/gpu-service/instances/config/index.ts +++ b/src/pages/gpu-service/instances/config/index.ts @@ -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 = { [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 = { }; 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; }; diff --git a/src/pages/gpu-service/instances/config/types.ts b/src/pages/gpu-service/instances/config/types.ts index 6b6f5ccf..dc880244 100644 --- a/src/pages/gpu-service/instances/config/types.ts +++ b/src/pages/gpu-service/instances/config/types.ts @@ -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 { +export interface ListItem extends FormData { id: number; - status?: InstanceStatus; - metadata: { - name: string; - namespace?: string; - uid: string; - resourceVersion: string; - creationTimestamp: string; - annotations?: Record; - managedFields?: { - manager: string; - operation: string; - apiVersion: string; - time: string; - fieldsType: string; - fieldsV1: Record; - }[]; - }; + 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; diff --git a/src/pages/gpu-service/instances/forms/basic.tsx b/src/pages/gpu-service/instances/forms/basic.tsx index 9da367fb..2061adcc 100644 --- a/src/pages/gpu-service/instances/forms/basic.tsx +++ b/src/pages/gpu-service/instances/forms/basic.tsx @@ -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 ( <> + data-field="name" - name={['metadata', 'name']} + name="name" rules={[ { required: true, @@ -38,10 +40,17 @@ const Basic = ({ required /> + name="displayName"> + +
- name={['spec', 'description']}> + name="description" hidden> diff --git a/src/pages/gpu-service/instances/forms/index.tsx b/src/pages/gpu-service/instances/forms/index.tsx index db45a653..2cb83f66 100644 --- a/src/pages/gpu-service/instances/forms/index.tsx +++ b/src/pages/gpu-service/instances/forms/index.tsx @@ -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; } -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 = { + 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 = forwardRef( currentData, disabled, open, - namespace = 'default', instanceTypeList = [], onFinish } = props; @@ -108,7 +131,7 @@ const GPUServiceInstanceForm: React.FC = 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 = 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 = forwardRef( [intl] ); - const ports = Form.useWatch(['spec', 'ports'], form) || []; + const [selectedInstanceType, setSelectedInstanceType] = useState< + InstanceTypeItem | undefined + >(undefined); + const [onceMaxRequest, setOnceMaxRequest] = useState({ + cpu: null, + memory: null, + localStorage: null + }); - const selectedTypeName = Form.useWatch(['spec', 'type'], form) as - | string - | undefined; - - const onceMaxRequest = useMemo(() => { - 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 = 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 = forwardRef( setFieldsValue: (values: Partial) => { 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 = forwardRef( env: [], volumeMount: '', resources: { - accelerator: '1' + accelerator: 1 }, volume: { ephemeral: { @@ -334,6 +462,11 @@ const GPUServiceInstanceForm: React.FC = forwardRef( }} > + {/* Hidden form field: the candidate.cluster resolved from the + selected aggregate + accelerator count. */} + = forwardRef( ) }, @@ -392,13 +528,12 @@ const GPUServiceInstanceForm: React.FC = forwardRef( label={intl.formatMessage({ id: 'gpuservice.instance.ssh.enable' })} - onChange={handleSSHEnableChange} > {sshEnabled && (
- name={['spec', 'sshPublicKey', 'name']} + name={['spec', 'sshPublicKeys']} style={{ marginBottom: 12 }} @@ -425,7 +560,7 @@ const GPUServiceInstanceForm: React.FC = forwardRef( width: '100%' } }} - options={[]} + options={sshkeyOptions} notFoundContent={ = ({ - value, - dataList + selectedInstanceType }) => { - const selected = useMemo(() => { - return ( - dataList.find((item) => (item.metadata?.name || '') === value) || - ({} as InstanceTypeItemModel) - ); - }, [value, dataList]); - + const intl = useIntl(); return ( - + {selectedInstanceType ? ( + + ) : ( + + {intl.formatMessage({ id: 'gpuservice.instance.type.required' })} + + )} ); }; @@ -50,56 +49,62 @@ const InstanceTypePicker: React.FC = ({ interface InstanceTypeFormItemProps { action: PageActionType; disabled?: boolean; + currentData?: ListItem; + selectedInstanceType?: InstanceTypeItemModel; + onGPUCountChange?: (value: number) => void; } const InstanceTypeFormItem: React.FC = ({ action, - disabled + disabled, + currentData, + selectedInstanceType, + onGPUCountChange }) => { const intl = useIntl(); - const form = Form.useFormInstance(); - 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 ( + + + + {description.manufacturer + ? `${description.product} x ${currentData?.spec?.resources?.accelerator}` + : ''} + + + + ); + }; return (
@@ -115,17 +120,22 @@ const InstanceTypeFormItem: React.FC = ({ } ]} > - + {action === PageAction.CREATE && ( + + )} + {action === PageAction.EDIT && renderInstanceType()} - {selectedInstanceType?.spec?.acceleratable && ( - 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 = ({ ) ); } - 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 = ({ > void; onSubmit: (values: StorageFormData) => Promise | void; } const StorageOverlay: React.FC = ({ open, - namespace, onCancel, onSubmit }) => { @@ -66,7 +64,6 @@ const StorageOverlay: React.FC = ({ ref={formRef} action={PageAction.CREATE} open={open} - namespace={namespace} onFinish={handleFinish} /> diff --git a/src/pages/gpu-service/instances/forms/storage-volume.tsx b/src/pages/gpu-service/instances/forms/storage-volume.tsx index 1b91db71..0ed55854 100644 --- a/src/pages/gpu-service/instances/forms/storage-volume.tsx +++ b/src/pages/gpu-service/instances/forms/storage-volume.tsx @@ -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(); + const currentVolume = Form.useWatch(['spec', 'volume'], form); const [storageMode, setStorageMode] = useState( StorageModeValueMap.Temporary ); - - const form = Form.useFormInstance(); - - const currentCluster = useAtomValue(currentClusterAtom); - const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id); + const [releaseWithInstance, setReleaseWithInstance] = useState(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 ( <>
- - handleModeChange(e.target.value)} - options={[ - { - label: ( - - {intl.formatMessage({ - id: 'gpuservice.storage.temporary' - })} - - } - /> - ), - value: StorageModeValueMap.Temporary - }, - { - label: ( - - {intl.formatMessage({ - id: 'gpuservice.storage.persistentVolume' - })} - - } - /> - ), - value: StorageModeValueMap.Existing - } - ]} - /> - {action === PageAction.CREATE && - storageMode === StorageModeValueMap.Existing && ( - - )} - - - {storageMode === StorageModeValueMap.Existing && ( - - + + ({ + 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' + }) + } + ]} + > + + + + ) : ( + +