Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
207cef350f | ||
|
|
fa3fed8671 |
@@ -124,24 +124,13 @@ Always check `@gpustack/core-ui` first. Frequently reused:
|
|||||||
- **Form fields**: `BaseSelect`, `Input` (labeled).
|
- **Form fields**: `BaseSelect`, `Input` (labeled).
|
||||||
- **Text overflow**: `AutoTooltip`.
|
- **Text overflow**: `AutoTooltip`.
|
||||||
- **Icons**: `IconFont`.
|
- **Icons**: `IconFont`.
|
||||||
- **Tags & status** (4 variants): see the section below.
|
- **Status display** (success/failed/processing/warning): `StatusTag`.
|
||||||
- **Permission-gated visibility**: `Access` / `useAccess`.
|
- **Permission-gated visibility**: `Access` / `useAccess`.
|
||||||
- **Request hooks**: `useRequest` / `useQueryData` / `useQueryDataList`.
|
- **Request hooks**: `useRequest` / `useQueryData` / `useQueryDataList`.
|
||||||
- **Table data fetching**: `useTableFetch`.
|
- **Table data fetching**: `useTableFetch`.
|
||||||
- **Submit guard** (prevent double-submit): `useSubmitLock`.
|
- **Submit guard** (prevent double-submit): `useSubmitLock`.
|
||||||
- **Tabbed forms**: `ScrollSpyTabs`.
|
- **Tabbed forms**: `ScrollSpyTabs`.
|
||||||
|
|
||||||
# Tags & status indicators
|
|
||||||
|
|
||||||
Four core-ui components cover tag/status display in tables and lists. Pick by **what the value means**, not by how it looks — don't reach for a generic antd `Tag`:
|
|
||||||
|
|
||||||
- **`StatusTag`** — semantic status with a **dynamic message/detail** (tooltip, download, extra content). Use when a row's status carries variable text, e.g. a failed job with an error message. Colors come from `StatusColorMap` (error/warning/transitioning/success/inactive).
|
|
||||||
- **`StatusDot`** — colored dot + short label, **no message**. Use for a plain status/type cell where the value is a fixed enum (e.g. an event-type or log column). Same `StatusColorMap` palette; `inactive` dot is quaternary. If the status needs dynamic text, use `StatusTag` instead.
|
|
||||||
- **`ThemeTag`** — a **standalone category label** (independent content, e.g. a permission scope or a model name). Default neutral; wraps antd `Tag`.
|
|
||||||
- **`TextAttribute`** — a small neutral pill that is a **subordinate annotation following a primary text** (e.g. `key-name [custom]`), not a standalone tag. Manages its own leading margin. Two variants: `filled` (default) and `outlined`. Ref the name column in `src/pages/api-keys/hooks/use-keys-columns.tsx`.
|
|
||||||
|
|
||||||
Rule of thumb: semantic + dynamic text → `StatusTag`; semantic + fixed enum → `StatusDot`; independent category → `ThemeTag`; annotation of nearby text → `TextAttribute`.
|
|
||||||
|
|
||||||
# Dynamic add-item form fields
|
# Dynamic add-item form fields
|
||||||
|
|
||||||
When building a form, select the add-item component from the **shape of the field's data** (its schema). Match the schema, don't hand-roll a list UI:
|
When building a form, select the add-item component from the **shape of the field's data** (its schema). Match the schema, don't hand-roll a list UI:
|
||||||
|
|||||||
+21
-10
@@ -100,15 +100,6 @@ const baseRoutes = [
|
|||||||
path: '/models',
|
path: '/models',
|
||||||
redirect: '/models/deployments'
|
redirect: '/models/deployments'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'userModels',
|
|
||||||
path: '/models/user-models',
|
|
||||||
key: 'userModels',
|
|
||||||
icon: 'icon-models',
|
|
||||||
selectedIcon: 'icon-models-filled',
|
|
||||||
defaultIcon: 'icon-models',
|
|
||||||
component: './llmodels/user-models'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'modelCatalog',
|
name: 'modelCatalog',
|
||||||
path: '/models/catalog',
|
path: '/models/catalog',
|
||||||
@@ -119,6 +110,16 @@ const baseRoutes = [
|
|||||||
access: 'canSeeOrgAdmin',
|
access: 'canSeeOrgAdmin',
|
||||||
component: './llmodels/catalog'
|
component: './llmodels/catalog'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'userModels',
|
||||||
|
path: '/models/user-models',
|
||||||
|
key: 'userModels',
|
||||||
|
icon: 'icon-models',
|
||||||
|
selectedIcon: 'icon-models-filled',
|
||||||
|
defaultIcon: 'icon-models',
|
||||||
|
access: 'canSeeUser',
|
||||||
|
component: './llmodels/user-models'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'deployment',
|
name: 'deployment',
|
||||||
path: '/models/deployments',
|
path: '/models/deployments',
|
||||||
@@ -273,7 +274,7 @@ const baseRoutes = [
|
|||||||
selectedIcon: 'icon-cluster2-filled',
|
selectedIcon: 'icon-cluster2-filled',
|
||||||
defaultIcon: 'icon-cluster2-outline',
|
defaultIcon: 'icon-cluster2-outline',
|
||||||
component: './cluster-management/clusters',
|
component: './cluster-management/clusters',
|
||||||
subMenu: ['/resources/clusters/create']
|
subMenu: ['/resources/clusters/detail', '/resources/clusters/create']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'workers',
|
name: 'workers',
|
||||||
@@ -301,6 +302,16 @@ const baseRoutes = [
|
|||||||
selectedIcon: 'icon-credential-filled',
|
selectedIcon: 'icon-credential-filled',
|
||||||
defaultIcon: 'icon-credential-outline',
|
defaultIcon: 'icon-credential-outline',
|
||||||
component: './cluster-management/credentials'
|
component: './cluster-management/credentials'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'clusterDetail',
|
||||||
|
path: '/resources/clusters/detail',
|
||||||
|
key: 'clusterDetail',
|
||||||
|
icon: 'icon-cluster2-outline',
|
||||||
|
selectedIcon: 'icon-cluster2-filled',
|
||||||
|
defaultIcon: 'icon-cluster2-outline',
|
||||||
|
hideInMenu: true,
|
||||||
|
component: './cluster-management/cluster-detail'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
"@ant-design/pro-components": "3.1.0-0",
|
"@ant-design/pro-components": "3.1.0-0",
|
||||||
"@antv/g6": "^5.0.51",
|
"@antv/g6": "^5.0.51",
|
||||||
"@braintree/sanitize-url": "^7.1.1",
|
"@braintree/sanitize-url": "^7.1.1",
|
||||||
"@gpustack/core-ui": "^1.0.39",
|
"@gpustack/core-ui": "^1.0.35",
|
||||||
"@huggingface/gguf": "^0.1.7",
|
"@huggingface/gguf": "^0.1.7",
|
||||||
"@huggingface/hub": "^0.15.1",
|
"@huggingface/hub": "^0.15.1",
|
||||||
"@huggingface/tasks": "^0.11.6",
|
"@huggingface/tasks": "^0.11.6",
|
||||||
|
|||||||
Generated
+5
-5
@@ -24,8 +24,8 @@ importers:
|
|||||||
specifier: ^7.1.1
|
specifier: ^7.1.1
|
||||||
version: 7.1.2
|
version: 7.1.2
|
||||||
'@gpustack/core-ui':
|
'@gpustack/core-ui':
|
||||||
specifier: ^1.0.39
|
specifier: ^1.0.35
|
||||||
version: 1.0.39(czdvzceysqw7iv6pct2ucnb23e)
|
version: 1.0.35(czdvzceysqw7iv6pct2ucnb23e)
|
||||||
'@huggingface/gguf':
|
'@huggingface/gguf':
|
||||||
specifier: ^0.1.7
|
specifier: ^0.1.7
|
||||||
version: 0.1.18
|
version: 0.1.18
|
||||||
@@ -1481,8 +1481,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz}
|
resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz}
|
||||||
deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package
|
deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package
|
||||||
|
|
||||||
'@gpustack/core-ui@1.0.39':
|
'@gpustack/core-ui@1.0.35':
|
||||||
resolution: {integrity: sha512-6NS0TDkBFHK6RipfvLwvDnLZlAoa/yFbHV+xeERAnUb+4t0mDyIQFqvYAeDgrUW/JIZ6WScrm31FDZe2sKiuaw==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.39.tgz}
|
resolution: {integrity: sha512-MaEmCM3FikeKZdUgDSyUtIfXtNMbNOLeFiTynl6DcKEraUOpk6tgQXPDginBY3uXuisG4hzoIQbpHHkzaYiQew==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.35.tgz}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@ant-design/icons': ^6.1.0
|
'@ant-design/icons': ^6.1.0
|
||||||
'@ant-design/pro-components': 3.1.0-0
|
'@ant-design/pro-components': 3.1.0-0
|
||||||
@@ -10802,7 +10802,7 @@ snapshots:
|
|||||||
|
|
||||||
'@formatjs/intl-utils@2.3.0': {}
|
'@formatjs/intl-utils@2.3.0': {}
|
||||||
|
|
||||||
'@gpustack/core-ui@1.0.39(czdvzceysqw7iv6pct2ucnb23e)':
|
'@gpustack/core-ui@1.0.35(czdvzceysqw7iv6pct2ucnb23e)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@ant-design/pro-components': 3.1.0-0(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@ant-design/pro-components': 3.1.0-0(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
|
||||||
<defs>
|
|
||||||
<!-- 顶面填充(亮面,高透明度) -->
|
|
||||||
<linearGradient id="cube-top-grad" x1="12" y1="2" x2="12" y2="12" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0%" stop-color="#f759ab" stop-opacity="0.18"/>
|
|
||||||
<stop offset="100%" stop-color="#c41d7f" stop-opacity="0.1"/>
|
|
||||||
</linearGradient>
|
|
||||||
<!-- 左侧面填充(暗面,低透明度) -->
|
|
||||||
<linearGradient id="cube-left-grad" x1="2" y1="7" x2="12" y2="17" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0%" stop-color="#c41d7f" stop-opacity="0.12"/>
|
|
||||||
<stop offset="100%" stop-color="#c41d7f" stop-opacity="0.04"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="cube-stroke-grad" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0%" stop-color="#f759ab"/>
|
|
||||||
<stop offset="100%" stop-color="#c41d7f"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<!-- 顶面填充 -->
|
|
||||||
<polygon points="12,2 21.5,6.7 12,11.5 2.5,6.7" fill="url(#cube-top-grad)" />
|
|
||||||
<!-- 左侧面填充 -->
|
|
||||||
<polygon points="2.5,6.7 12,11.5 12,21.3 2.5,16.5" fill="url(#cube-left-grad)" />
|
|
||||||
<!-- 立方体全纯线外骨架(细化为圆角衔接) -->
|
|
||||||
<path d="M12 2L2.5 6.7M12 2l9.5 4.7M21.5 6.7L12 11.5M2.5 6.7L12 11.5M2.5 6.7v9.8l9.5 4.8M21.5 6.7v9.8l-9.5 4.8M12 11.5v9.8" stroke="url(#cube-stroke-grad)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,22 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
|
||||||
<defs>
|
|
||||||
<!-- 1. 定义专属微通透渐变填充 -->
|
|
||||||
<linearGradient id="img-fill-grad" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0%" stop-color="#d46b08" stop-opacity="0.12"/>
|
|
||||||
<stop offset="100%" stop-color="#d46b08" stop-opacity="0.04"/>
|
|
||||||
</linearGradient>
|
|
||||||
<!-- 2. 定义边框高精度渐变(亮橙到深橙,拉开层次) -->
|
|
||||||
<linearGradient id="img-stroke-grad" x1="4" y1="4" x2="20" y2="20" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0%" stop-color="#fa8c16"/>
|
|
||||||
<stop offset="100%" stop-color="#d46b08"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<!-- 3. 精装底色充填层 -->
|
|
||||||
<rect x="3" y="3" width="18" height="18" rx="4" fill="url(#img-fill-grad)" />
|
|
||||||
<!-- 4. 高级柔和微圆角边框层 -->
|
|
||||||
<rect x="3" y="3" width="18" height="18" rx="4" stroke="url(#img-stroke-grad)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<!-- 内部几何现代山脉线条 -->
|
|
||||||
<path d="M3 16l4-4a2 2 0 0 1 2.8 0l5.2 5.2M13 15l2.5-2.5a2 2 0 0 1 2.8 0l2.7 2.7" stroke="url(#img-stroke-grad)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<!-- 标志性通透小钻石 -->
|
|
||||||
<rect x="14" y="6" width="4" height="4" rx="1.5" transform="rotate(45 16 8)" fill="#fa8c16" fill-opacity="0.3" stroke="url(#img-stroke-grad)" stroke-width="1"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,17 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="chat-fill-grad" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0%" stop-color="#389e0d" stop-opacity="0.1"/>
|
|
||||||
<stop offset="100%" stop-color="#389e0d" stop-opacity="0.02"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="chat-stroke-grad" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0%" stop-color="#73d13d"/>
|
|
||||||
<stop offset="100%" stop-color="#389e0d"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<!-- 主现代对话框体(全部改为圆润R角,精装填充) -->
|
|
||||||
<path d="M18 4H6a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h7.5l3.5 3.5a1 1 0 0 0 1.5-.5V17a3 3 0 0 0 3-3V7a3 3 0 0 0-3-3z" fill="url(#chat-fill-grad)" stroke="url(#chat-stroke-grad)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<!-- 内部通透对话线条(细化为圆角代码采样块感) -->
|
|
||||||
<rect x="7" y="8" width="8" height="1.5" rx="0.75" fill="#73d13d" fill-opacity="0.3"/>
|
|
||||||
<rect x="7" y="11.5" width="10" height="1.5" rx="0.75" fill="#73d13d" fill-opacity="0.2"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,19 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="rank-fill-grad" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0%" stop-color="#08979c" stop-opacity="0.1"/>
|
|
||||||
<stop offset="100%" stop-color="#08979c" stop-opacity="0.01"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="rank-stroke-grad" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0%" stop-color="#36cfc9"/>
|
|
||||||
<stop offset="100%" stop-color="#08979c"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<!-- 数据权重条(全部改为高级圆角) -->
|
|
||||||
<rect x="11" y="4" width="10" height="2.5" rx="1.25" fill="#36cfc9" fill-opacity="0.1" stroke="url(#rank-stroke-grad)" stroke-width="1.5"/>
|
|
||||||
<rect x="11" y="9" width="7.5" height="2.5" rx="1.25" fill="#36cfc9" fill-opacity="0.05" stroke="url(#rank-stroke-grad)" stroke-width="1.5"/>
|
|
||||||
<rect x="11" y="14" width="5" height="2.5" rx="1.25" stroke="url(#rank-stroke-grad)" stroke-width="1.5"/>
|
|
||||||
<!-- 基准线与立体双向指引箭头(优化为圆角) -->
|
|
||||||
<path d="M3 17l3 3 3-3M6 4v16" stroke="url(#rank-stroke-grad)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<path d="M4.5 5.5L6 4l1.5 1.5" stroke="url(#rank-stroke-grad)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,18 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="stt-fill-grad" x1="12" y1="3" x2="12" y2="14" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0%" stop-color="#40a9ff" stop-opacity="0.2"/>
|
|
||||||
<stop offset="100%" stop-color="#1677ff" stop-opacity="0.05"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="stt-stroke-grad" x1="12" y1="3" x2="12" y2="21" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0%" stop-color="#40a9ff"/>
|
|
||||||
<stop offset="100%" stop-color="#1677ff"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<!-- 麦克风核心体:精装通透填充 -->
|
|
||||||
<rect x="8.5" y="3" width="7" height="11" rx="3.5" fill="url(#stt-fill-grad)" stroke="url(#stt-stroke-grad)" stroke-width="1.8" />
|
|
||||||
<!-- 内部音膜立体结构线(细化为点状) -->
|
|
||||||
<line x1="10" y1="8" x2="14" y2="8" stroke="url(#stt-stroke-grad)" stroke-width="1" stroke-dasharray="1 2"/>
|
|
||||||
<!-- 悬挂外托架与底座(全部改为高级圆角) -->
|
|
||||||
<path d="M5 10a7 7 0 0 0 14 0M12 17v4M8 21h8" stroke="url(#stt-stroke-grad)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,23 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
|
||||||
<defs>
|
|
||||||
<!-- 1. 核心高通透深蓝渐变充填 -->
|
|
||||||
<linearGradient id="tts-v2-fill" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0%" stop-color="#2f54eb" stop-opacity="0.15"/>
|
|
||||||
<stop offset="100%" stop-color="#1d39c4" stop-opacity="0.03"/>
|
|
||||||
</linearGradient>
|
|
||||||
<!-- 2. 精准调校的专属蓝色渐变边框 -->
|
|
||||||
<linearGradient id="tts-v2-stroke" x1="4" y1="4" x2="20" y2="20" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0%" stop-color="#2f54eb"/>
|
|
||||||
<stop offset="100%" stop-color="#1d39c4"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- 左侧:低频辅助声波(大间距,带现代圆角) -->
|
|
||||||
<rect x="4" y="8" width="2.2" height="8" rx="1.1" stroke="url(#tts-v2-stroke)" stroke-width="1.8" stroke-linecap="round"/>
|
|
||||||
|
|
||||||
<!-- 中央:核心高频声波主体(拉大宽度,注入通透水晶质感) -->
|
|
||||||
<rect x="10.4" y="2" width="3.2" height="20" rx="1.6" fill="url(#tts-v2-fill)" stroke="url(#tts-v2-stroke)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
|
|
||||||
<!-- 右侧:高频衰减声波(保持几何对称与呼吸感) -->
|
|
||||||
<rect x="17.8" y="5" width="2.2" height="14" rx="1.1" stroke="url(#tts-v2-stroke)" stroke-width="1.8" stroke-linecap="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -9,15 +9,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes tableEmptyFadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-table {
|
.scroll-table {
|
||||||
.ant-table {
|
.ant-table {
|
||||||
.ant-table-container {
|
.ant-table-container {
|
||||||
@@ -27,27 +18,5 @@
|
|||||||
scrollbar-color: var(--color-scrollbar-thumb) transparent;
|
scrollbar-color: var(--color-scrollbar-thumb) transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reserve a stable block for the empty/loading state so the first-load
|
|
||||||
// spinner and the empty result occupy the same height as eventual data —
|
|
||||||
// this removes the layout jump when entering the page. Scoped to
|
|
||||||
// `.ant-table-content` so it only targets x-scroll tables (whose empty
|
|
||||||
// row lives here) and leaves fixed-height `scroll.y` tables untouched.
|
|
||||||
// Height must match the `minHeight` passed to <NoResult> in
|
|
||||||
// use-no-resource-result.
|
|
||||||
.ant-table-content {
|
|
||||||
.ant-table-placeholder {
|
|
||||||
> .ant-table-cell {
|
|
||||||
height: calc(100vh - 300px);
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoResult renders nothing while loading and mounts an <Empty> only
|
|
||||||
// once the request settles, so this fires exactly when the empty
|
|
||||||
// state appears — a seamless fade-in instead of a hard pop.
|
|
||||||
.ant-empty {
|
|
||||||
animation: tableEmptyFadeIn 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { GPUStackVersionAtom } from '@/atoms/user';
|
import { GPUStackVersionAtom } from '@/atoms/user';
|
||||||
|
import { getAtomStorage } from '@/atoms/utils';
|
||||||
import VersionInfo, { modalConfig } from '@/components/version-info';
|
import VersionInfo, { modalConfig } from '@/components/version-info';
|
||||||
import externalLinks from '@/constants/external-links';
|
import externalLinks from '@/constants/external-links';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Divider, Modal, Typography } from 'antd';
|
import { Button, Divider, Modal, Typography } from 'antd';
|
||||||
import { createStyles } from 'antd-style';
|
import { createStyles } from 'antd-style';
|
||||||
import { useAtomValue } from 'jotai';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const CompanyWrapper = styled.div`
|
const CompanyWrapper = styled.div`
|
||||||
@@ -35,7 +35,6 @@ const Footer: React.FC = () => {
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [modal, contextHolder] = Modal.useModal();
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
const { styles } = useStyles();
|
const { styles } = useStyles();
|
||||||
const version = useAtomValue(GPUStackVersionAtom);
|
|
||||||
|
|
||||||
const showVersion = () => {
|
const showVersion = () => {
|
||||||
modal.info({
|
modal.info({
|
||||||
@@ -74,7 +73,7 @@ const Footer: React.FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Divider orientation="vertical" />
|
<Divider orientation="vertical" />
|
||||||
<Button type="link" size="small" onClick={showVersion}>
|
<Button type="link" size="small" onClick={showVersion}>
|
||||||
{version?.version}
|
{getAtomStorage(GPUStackVersionAtom)?.version}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import Logo from '@/assets/images/gpustack-logo.png';
|
import Logo from '@/assets/images/gpustack-logo.png';
|
||||||
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
|
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
|
||||||
import externalLinks from '@/constants/external-links';
|
import externalLinks from '@/constants/external-links';
|
||||||
import { useLogo } from '@/hooks/use-logo';
|
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -19,7 +18,7 @@ const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
|
|||||||
isProd,
|
isProd,
|
||||||
isDev
|
isDev
|
||||||
} = gpuStackVersionAtom;
|
} = gpuStackVersionAtom;
|
||||||
const { sidebarLogo } = useLogo();
|
|
||||||
// user info
|
// user info
|
||||||
const { is_admin } = userDataAtom || {};
|
const { is_admin } = userDataAtom || {};
|
||||||
|
|
||||||
@@ -31,7 +30,7 @@ const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="version-box">
|
<div className="version-box">
|
||||||
<div className="img">
|
<div className="img">
|
||||||
<img src={sidebarLogo || Logo} alt="logo" />
|
<img src={Logo} alt="logo" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="ver">
|
<div className="ver">
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ html {
|
|||||||
// ======== container ============
|
// ======== container ============
|
||||||
--color-border-container: #ededed;
|
--color-border-container: #ededed;
|
||||||
--color-text-table-header: #71717a;
|
--color-text-table-header: #71717a;
|
||||||
--seal-table-row-min-height: 68px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme='realDark'] {
|
html[data-theme='realDark'] {
|
||||||
|
|||||||
@@ -40,11 +40,6 @@ export default function useWatchList<T = Record<string, any>>(API: string) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const cancelWatch = useMemoizedFn(() => {
|
|
||||||
chunkRequestRef.current?.current?.cancel?.();
|
|
||||||
listRequestTokenRef.current?.cancel?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
const queryAllDataList = async (
|
const queryAllDataList = async (
|
||||||
params: Global.SearchParams,
|
params: Global.SearchParams,
|
||||||
options?: any
|
options?: any
|
||||||
@@ -83,15 +78,14 @@ export default function useWatchList<T = Record<string, any>>(API: string) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
createWatchChunkRequest();
|
createWatchChunkRequest();
|
||||||
return () => {
|
return () => {
|
||||||
cancelWatch();
|
chunkRequestRef.current?.cancel?.();
|
||||||
|
listRequestTokenRef.current?.cancel?.();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
watchDataList,
|
watchDataList,
|
||||||
setWatchDataList,
|
setWatchDataList,
|
||||||
startWatch: createWatchChunkRequest,
|
|
||||||
cancelWatch,
|
|
||||||
deleteItemFromCache: handleDeleteItemFromCache
|
deleteItemFromCache: handleDeleteItemFromCache
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import VersionInfo, { modalConfig } from '@/components/version-info';
|
|||||||
import externalLinks from '@/constants/external-links';
|
import externalLinks from '@/constants/external-links';
|
||||||
import useBodyScroll from '@/hooks/use-body-scroll';
|
import useBodyScroll from '@/hooks/use-body-scroll';
|
||||||
import { logout } from '@/pages/login/apis';
|
import { logout } from '@/pages/login/apis';
|
||||||
import { getGPUStackPlugin } from '@/plugins';
|
|
||||||
import { useModel } from '@@/plugin-model';
|
import { useModel } from '@@/plugin-model';
|
||||||
import {
|
import {
|
||||||
DiscordOutlined,
|
DiscordOutlined,
|
||||||
@@ -99,7 +98,6 @@ const CustomItem = styled.div`
|
|||||||
|
|
||||||
export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
||||||
const { isDarkTheme } = props;
|
const { isDarkTheme } = props;
|
||||||
const plugin = getGPUStackPlugin();
|
|
||||||
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
|
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
|
||||||
const [modal, contextHolder] = Modal.useModal();
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
const [version] = useAtom(GPUStackVersionAtom);
|
const [version] = useAtom(GPUStackVersionAtom);
|
||||||
@@ -288,7 +286,6 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
|||||||
</NewLabel>
|
</NewLabel>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!plugin && (
|
|
||||||
<DropdownActions menu={{ ...helpMenu }} popupRender={helpPopupRender}>
|
<DropdownActions menu={{ ...helpMenu }} popupRender={helpPopupRender}>
|
||||||
<IconWrapper>
|
<IconWrapper>
|
||||||
<IconFont
|
<IconFont
|
||||||
@@ -298,7 +295,6 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
|||||||
/>
|
/>
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
</DropdownActions>
|
</DropdownActions>
|
||||||
)}
|
|
||||||
<PluginExtraField name="GlobalSettings" />
|
<PluginExtraField name="GlobalSettings" />
|
||||||
<DropdownActions menu={{ ...userMenu }} popupRender={userPopupRender}>
|
<DropdownActions menu={{ ...userMenu }} popupRender={userPopupRender}>
|
||||||
<IconWrapper>
|
<IconWrapper>
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ export default {
|
|||||||
'On the Worker that needs to be added, run the following command to join it to the cluster.',
|
'On the Worker that needs to be added, run the following command to join it to the cluster.',
|
||||||
'clusters.create.addCommand.k8s.tips':
|
'clusters.create.addCommand.k8s.tips':
|
||||||
'On the Kubernetes cluster that needs to be registered, run the following command to create the Kubernetes resources and register the cluster.',
|
'On the Kubernetes cluster that needs to be registered, run the following command to create the Kubernetes resources and register the cluster.',
|
||||||
'clusters.create.addCommand.k8s.version.warning':
|
|
||||||
'The minimum supported Kubernetes version is 1.23. To use the GPU Service feature, the minimum supported Kubernetes version is 1.27.',
|
|
||||||
'clusters.create.register.tips':
|
'clusters.create.register.tips':
|
||||||
'On the Kubernetes cluster that needs to be added, run the following command to join its nodes to the cluster.',
|
'On the Kubernetes cluster that needs to be added, run the following command to join its nodes to the cluster.',
|
||||||
'cluster.create.checkEnv.tips':
|
'cluster.create.checkEnv.tips':
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ export default {
|
|||||||
'gpuservice.template': 'GPU Instance Template',
|
'gpuservice.template': 'GPU Instance Template',
|
||||||
'gpuservice.template.add': 'Add Instance Template',
|
'gpuservice.template.add': 'Add Instance Template',
|
||||||
'gpuservice.template.edit': 'Edit Instance Template',
|
'gpuservice.template.edit': 'Edit Instance Template',
|
||||||
'gpuservice.template.clone': 'Clone Instance Template',
|
|
||||||
'gpuservice.template.filter.name': 'Filter by name',
|
'gpuservice.template.filter.name': 'Filter by name',
|
||||||
'gpuservice.template.filter.vendor': 'Filter by vendor',
|
'gpuservice.template.filter.vendor': 'Filter by vendor',
|
||||||
'gpuservice.template.image': 'Image',
|
'gpuservice.template.image': 'Image',
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default {
|
|||||||
'menu.models.modelCatalog': 'Catalog',
|
'menu.models.modelCatalog': 'Catalog',
|
||||||
'menu.models.catalog': 'Model Catalog',
|
'menu.models.catalog': 'Model Catalog',
|
||||||
'menu.models.deployment': 'Deployments',
|
'menu.models.deployment': 'Deployments',
|
||||||
'menu.models.userModels': 'Models',
|
'menu.models.userModels': 'My Models',
|
||||||
'menu.models.benchmark': 'Benchmarks',
|
'menu.models.benchmark': 'Benchmarks',
|
||||||
'menu.models.benchmarkDetail': 'Benchmark Details',
|
'menu.models.benchmarkDetail': 'Benchmark Details',
|
||||||
'menu.models.providers': 'Providers',
|
'menu.models.providers': 'Providers',
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ export default {
|
|||||||
'On the Worker that needs to be added, run the following command to join it to the cluster.',
|
'On the Worker that needs to be added, run the following command to join it to the cluster.',
|
||||||
'clusters.create.addCommand.k8s.tips':
|
'clusters.create.addCommand.k8s.tips':
|
||||||
'登録する Kubernetes クラスターで以下のコマンドを実行し、Kubernetes リソースを作成してクラスターを登録します。',
|
'登録する Kubernetes クラスターで以下のコマンドを実行し、Kubernetes リソースを作成してクラスターを登録します。',
|
||||||
'clusters.create.addCommand.k8s.version.warning':
|
|
||||||
'サポートされる Kubernetes の最小バージョンは 1.23 です。GPU Service 機能を使用する場合、サポートされる Kubernetes の最小バージョンは 1.27 です。',
|
|
||||||
'cluster.create.checkEnv.tips':
|
'cluster.create.checkEnv.tips':
|
||||||
'Use the following command to check if the environment is ready.',
|
'Use the following command to check if the environment is ready.',
|
||||||
'clusters.create.register.tips':
|
'clusters.create.register.tips':
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ export default {
|
|||||||
'gpuservice.template': 'GPU インスタンステンプレート',
|
'gpuservice.template': 'GPU インスタンステンプレート',
|
||||||
'gpuservice.template.add': 'インスタンステンプレートを追加',
|
'gpuservice.template.add': 'インスタンステンプレートを追加',
|
||||||
'gpuservice.template.edit': 'インスタンステンプレートを編集',
|
'gpuservice.template.edit': 'インスタンステンプレートを編集',
|
||||||
'gpuservice.template.clone': 'インスタンステンプレートを複製',
|
|
||||||
'gpuservice.template.filter.name': '名前でフィルター',
|
'gpuservice.template.filter.name': '名前でフィルター',
|
||||||
'gpuservice.template.filter.vendor': 'ベンダーでフィルター',
|
'gpuservice.template.filter.vendor': 'ベンダーでフィルター',
|
||||||
'gpuservice.template.image': 'コンテナイメージ',
|
'gpuservice.template.image': 'コンテナイメージ',
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default {
|
|||||||
'menu.accessControl.organizations': 'Organizations',
|
'menu.accessControl.organizations': 'Organizations',
|
||||||
'menu.resources.clusters': 'Clusters',
|
'menu.resources.clusters': 'Clusters',
|
||||||
'menu.resources.credentials': 'Cloud Credentials',
|
'menu.resources.credentials': 'Cloud Credentials',
|
||||||
'menu.models.userModels': 'Models',
|
'menu.models.userModels': 'My Models',
|
||||||
'menu.resources.clusterDetail': 'Cluster Detail',
|
'menu.resources.clusterDetail': 'Cluster Detail',
|
||||||
'menu.resources.clusterCreate': 'Create Cluster',
|
'menu.resources.clusterCreate': 'Create Cluster',
|
||||||
'menu.models.backendsList': 'Inference Backends',
|
'menu.models.backendsList': 'Inference Backends',
|
||||||
@@ -62,11 +62,11 @@ export default {
|
|||||||
// 6. 'menu.accessControl.apikeys': 'API Keys',
|
// 6. 'menu.accessControl.apikeys': 'API Keys',
|
||||||
// 7. 'menu.accessControl.users': 'Users',
|
// 7. 'menu.accessControl.users': 'Users',
|
||||||
// 8. 'menu.clusterManagement': 'Cluster Management',
|
// 8. 'menu.clusterManagement': 'Cluster Management',
|
||||||
// 9. 'menu.resources.clusters': 'Clusters',
|
// 9. 'menu.clusterManagement.clusters': 'Clusters',
|
||||||
// 10. 'menu.resources.credentials': 'Cloud Credentials',
|
// 10. 'menu.clusterManagement.credentials': 'Cloud Credentials',
|
||||||
// 11. 'menu.models.userModels': 'Models'
|
// 11. 'menu.models.userModels': 'My Models'
|
||||||
// 12. 'menu.resources.clusterDetail': 'Cluster Detail',
|
// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail',
|
||||||
// 13. 'menu.resources.clusterCreate': 'Create Cluster',
|
// 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster',
|
||||||
// 14. 'menu.models.backendsList': 'Inference Backends',
|
// 14. 'menu.models.backendsList': 'Inference Backends',
|
||||||
// 15. 'menu.models.benchmark': 'Benchmarks',
|
// 15. 'menu.models.benchmark': 'Benchmarks',
|
||||||
// 15. 'menu.models.provider': 'Provider',
|
// 15. 'menu.models.provider': 'Provider',
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ export default {
|
|||||||
'На воркере, который необходимо добавить, выполните следующую команду, чтобы присоединить его к кластеру.',
|
'На воркере, который необходимо добавить, выполните следующую команду, чтобы присоединить его к кластеру.',
|
||||||
'clusters.create.addCommand.k8s.tips':
|
'clusters.create.addCommand.k8s.tips':
|
||||||
'На Kubernetes-кластере, который необходимо зарегистрировать, выполните следующую команду, чтобы создать ресурсы Kubernetes и зарегистрировать этот кластер.',
|
'На Kubernetes-кластере, который необходимо зарегистрировать, выполните следующую команду, чтобы создать ресурсы Kubernetes и зарегистрировать этот кластер.',
|
||||||
'clusters.create.addCommand.k8s.version.warning':
|
|
||||||
'Минимальная поддерживаемая версия Kubernetes — 1.23. Для использования функции GPU Service минимальная поддерживаемая версия Kubernetes — 1.27.',
|
|
||||||
'cluster.create.checkEnv.tips':
|
'cluster.create.checkEnv.tips':
|
||||||
'Используйте следующую команду для проверки готовности окружения',
|
'Используйте следующую команду для проверки готовности окружения',
|
||||||
'clusters.create.register.tips':
|
'clusters.create.register.tips':
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ export default {
|
|||||||
'gpuservice.template': 'Шаблон экземпляра GPU',
|
'gpuservice.template': 'Шаблон экземпляра GPU',
|
||||||
'gpuservice.template.add': 'Добавить шаблон экземпляра',
|
'gpuservice.template.add': 'Добавить шаблон экземпляра',
|
||||||
'gpuservice.template.edit': 'Редактировать шаблон экземпляра',
|
'gpuservice.template.edit': 'Редактировать шаблон экземпляра',
|
||||||
'gpuservice.template.clone': 'Клонировать шаблон экземпляра',
|
|
||||||
'gpuservice.template.filter.name': 'Фильтр по имени',
|
'gpuservice.template.filter.name': 'Фильтр по имени',
|
||||||
'gpuservice.template.filter.vendor': 'Фильтр по производителю',
|
'gpuservice.template.filter.vendor': 'Фильтр по производителю',
|
||||||
'gpuservice.template.image': 'Образ',
|
'gpuservice.template.image': 'Образ',
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default {
|
|||||||
'menu.accessControl.organizations': 'Организации',
|
'menu.accessControl.organizations': 'Организации',
|
||||||
'menu.resources.clusters': 'Кластеры',
|
'menu.resources.clusters': 'Кластеры',
|
||||||
'menu.resources.credentials': 'Облачные аккаунты',
|
'menu.resources.credentials': 'Облачные аккаунты',
|
||||||
'menu.models.userModels': 'Модели',
|
'menu.models.userModels': 'Мои модели',
|
||||||
'menu.resources.clusterDetail': 'Детали кластера',
|
'menu.resources.clusterDetail': 'Детали кластера',
|
||||||
'menu.resources.clusterCreate': 'Создать кластер',
|
'menu.resources.clusterCreate': 'Создать кластер',
|
||||||
'menu.models.backendsList': 'Бэкенды запуска',
|
'menu.models.backendsList': 'Бэкенды запуска',
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ export default {
|
|||||||
'Eklenmesi gereken İşçi Düğümde, kümeye katılması için aşağıdaki komutu çalıştırın.',
|
'Eklenmesi gereken İşçi Düğümde, kümeye katılması için aşağıdaki komutu çalıştırın.',
|
||||||
'clusters.create.addCommand.k8s.tips':
|
'clusters.create.addCommand.k8s.tips':
|
||||||
'Kaydedilmesi gereken Kubernetes kümesinde, Kubernetes kaynaklarını oluşturmak ve kümeyi kaydetmek için aşağıdaki komutu çalıştırın.',
|
'Kaydedilmesi gereken Kubernetes kümesinde, Kubernetes kaynaklarını oluşturmak ve kümeyi kaydetmek için aşağıdaki komutu çalıştırın.',
|
||||||
'clusters.create.addCommand.k8s.version.warning':
|
|
||||||
'Desteklenen minimum Kubernetes sürümü 1.23’tür. GPU Service özelliğini kullanmak için desteklenen minimum Kubernetes sürümü 1.27’dir.',
|
|
||||||
'clusters.create.register.tips':
|
'clusters.create.register.tips':
|
||||||
'Eklenmesi gereken Kubernetes kümesinde, düğümlerini kümeye katılması için aşağıdaki komutu çalıştırın.',
|
'Eklenmesi gereken Kubernetes kümesinde, düğümlerini kümeye katılması için aşağıdaki komutu çalıştırın.',
|
||||||
'cluster.create.checkEnv.tips':
|
'cluster.create.checkEnv.tips':
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ export default {
|
|||||||
'gpuservice.template': 'GPU Örnek Şablonu',
|
'gpuservice.template': 'GPU Örnek Şablonu',
|
||||||
'gpuservice.template.add': 'Örnek Şablonu Ekle',
|
'gpuservice.template.add': 'Örnek Şablonu Ekle',
|
||||||
'gpuservice.template.edit': 'Örnek Şablonunu Düzenle',
|
'gpuservice.template.edit': 'Örnek Şablonunu Düzenle',
|
||||||
'gpuservice.template.clone': 'Örnek Şablonunu Klonla',
|
|
||||||
'gpuservice.template.filter.name': 'Ada göre filtrele',
|
'gpuservice.template.filter.name': 'Ada göre filtrele',
|
||||||
'gpuservice.template.filter.vendor': 'Tedarikçiye göre filtrele',
|
'gpuservice.template.filter.vendor': 'Tedarikçiye göre filtrele',
|
||||||
'gpuservice.template.image': 'İmaj',
|
'gpuservice.template.image': 'İmaj',
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ export default {
|
|||||||
'menu.models.modelCatalog': 'Katalog',
|
'menu.models.modelCatalog': 'Katalog',
|
||||||
'menu.models.catalog': 'Model Kataloğu',
|
'menu.models.catalog': 'Model Kataloğu',
|
||||||
'menu.models.deployment': 'Dağıtımlar',
|
'menu.models.deployment': 'Dağıtımlar',
|
||||||
'menu.models.userModels': 'Modeller',
|
'menu.models.userModels': 'Modellerim',
|
||||||
'menu.models.benchmark': 'Kıyaslamalar',
|
'menu.models.benchmark': 'Kıyaslamalar',
|
||||||
'menu.models.benchmarkDetail': 'Kıyaslama Detayları',
|
'menu.models.benchmarkDetail': 'Kıyaslama Detayları',
|
||||||
'menu.models.providers': 'Sağlayıcılar',
|
'menu.models.providers': 'Sağlayıcılar',
|
||||||
'menu.models.routes': 'Yönlendirmeler',
|
'menu.models.routes': 'Yönlendirmeler',
|
||||||
'menu.models.usage': 'Kullanım',
|
'menu.models.usage': 'Usage',
|
||||||
'menu.modelCatalog': 'Katalog',
|
'menu.modelCatalog': 'Katalog',
|
||||||
'menu.resources': 'Kaynaklar',
|
'menu.resources': 'Kaynaklar',
|
||||||
'menu.apikeys': 'API Anahtarları',
|
'menu.apikeys': 'API Anahtarları',
|
||||||
|
|||||||
@@ -43,8 +43,6 @@ export default {
|
|||||||
'在需要添加的节点上运行以下命令,将其加入到集群中。',
|
'在需要添加的节点上运行以下命令,将其加入到集群中。',
|
||||||
'clusters.create.addCommand.k8s.tips':
|
'clusters.create.addCommand.k8s.tips':
|
||||||
'在需要注册的 Kubernetes 集群中运行以下命令,创建 Kubernetes 资源,注册该集群。',
|
'在需要注册的 Kubernetes 集群中运行以下命令,创建 Kubernetes 资源,注册该集群。',
|
||||||
'clusters.create.addCommand.k8s.version.warning':
|
|
||||||
'Kubernetes 版本最低支持 1.23,如果要使用 GPU Service 功能,Kubernetes 版本最低支持 1.27。',
|
|
||||||
'clusters.create.register.tips':
|
'clusters.create.register.tips':
|
||||||
'在需要添加的 Kubernetes 集群上运行以下命令,将其中的节点加入到集群中。',
|
'在需要添加的 Kubernetes 集群上运行以下命令,将其中的节点加入到集群中。',
|
||||||
'cluster.create.checkEnv.tips': '使用以下命令检查环境是否准备妥当。',
|
'cluster.create.checkEnv.tips': '使用以下命令检查环境是否准备妥当。',
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ export default {
|
|||||||
'gpuservice.template': 'GPU 实例模板',
|
'gpuservice.template': 'GPU 实例模板',
|
||||||
'gpuservice.template.add': '添加实例模板',
|
'gpuservice.template.add': '添加实例模板',
|
||||||
'gpuservice.template.edit': '编辑实例模板',
|
'gpuservice.template.edit': '编辑实例模板',
|
||||||
'gpuservice.template.clone': '克隆实例模板',
|
|
||||||
'gpuservice.template.filter.name': '按名称过滤',
|
'gpuservice.template.filter.name': '按名称过滤',
|
||||||
'gpuservice.template.filter.vendor': '按厂商过滤',
|
'gpuservice.template.filter.vendor': '按厂商过滤',
|
||||||
'gpuservice.template.image': '镜像',
|
'gpuservice.template.image': '镜像',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default {
|
|||||||
'menu.models.modelList': '部署与管理',
|
'menu.models.modelList': '部署与管理',
|
||||||
'menu.models.modelCatalog': '模型库',
|
'menu.models.modelCatalog': '模型库',
|
||||||
'menu.models.deployment': '部署',
|
'menu.models.deployment': '部署',
|
||||||
'menu.models.userModels': '模型广场',
|
'menu.models.userModels': '我的模型',
|
||||||
'menu.models.benchmark': '基准测试',
|
'menu.models.benchmark': '基准测试',
|
||||||
'menu.models.benchmarkDetail': '基准测试详情',
|
'menu.models.benchmarkDetail': '基准测试详情',
|
||||||
'menu.models.providers': '提供商',
|
'menu.models.providers': '提供商',
|
||||||
|
|||||||
@@ -60,8 +60,7 @@ const ModelTag: React.FC<ModelTagProps> = ({ categoryKey, size }) => {
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
paddingInline: 8,
|
paddingInline: 8,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
transform: 'scale(0.9)',
|
transform: 'scale(0.9)'
|
||||||
backgroundColor: 'transparent'
|
|
||||||
}}
|
}}
|
||||||
color={config.color}
|
color={config.color}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,12 +6,10 @@ import {
|
|||||||
AutoTooltip,
|
AutoTooltip,
|
||||||
DropdownButtons,
|
DropdownButtons,
|
||||||
IconFont,
|
IconFont,
|
||||||
icons,
|
icons
|
||||||
TextAttribute,
|
|
||||||
ThemeTag
|
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { MenuProps, Tooltip } from 'antd';
|
import { MenuProps, Tag, Tooltip } from 'antd';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
@@ -141,14 +139,23 @@ const useModelsColumns = ({
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
sorter: tableSorter(1),
|
sorter: tableSorter(1),
|
||||||
render: (text: string, record: ListItem) => (
|
render: (text: string, record: ListItem) => (
|
||||||
<span className="flex items-center gap-8">
|
<span className="flex items-center">
|
||||||
<AutoTooltip ghost style={{ maxWidth: 400 }} title={text}>
|
<AutoTooltip ghost style={{ maxWidth: 400 }} title={text}>
|
||||||
<span className="text-primary">{text}</span>
|
<span className="text-primary">{text}</span>
|
||||||
</AutoTooltip>
|
</AutoTooltip>
|
||||||
{record.is_custom && (
|
{record.is_custom && (
|
||||||
<TextAttribute>
|
<Tag
|
||||||
|
style={{
|
||||||
|
marginLeft: 8,
|
||||||
|
borderRadius: 12,
|
||||||
|
color: 'var(--ant-color-text-tertiary)',
|
||||||
|
borderColor: 'var(--ant-color-split)',
|
||||||
|
backgroundColor: 'transparent'
|
||||||
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
{intl.formatMessage({ id: 'playground.params.custom' })}
|
{intl.formatMessage({ id: 'playground.params.custom' })}
|
||||||
</TextAttribute>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -196,13 +203,26 @@ const useModelsColumns = ({
|
|||||||
)}
|
)}
|
||||||
{(record.scope?.includes('inference') ||
|
{(record.scope?.includes('inference') ||
|
||||||
record.scope?.includes('*')) && (
|
record.scope?.includes('*')) && (
|
||||||
<ThemeTag>
|
<div
|
||||||
|
style={{
|
||||||
|
border: '1px solid var(--ant-color-split)',
|
||||||
|
color: 'var(--ant-color-text-tertiary)',
|
||||||
|
backgroundColor: 'var(--ant-color-fill-quaternary)',
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 13,
|
||||||
|
paddingInline: 8,
|
||||||
|
flexGrow: 0,
|
||||||
|
maxWidth: '100%',
|
||||||
|
width: 'max-content',
|
||||||
|
display: 'flex'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<AutoTooltip ghost>
|
<AutoTooltip ghost>
|
||||||
{record.allowed_model_names?.length
|
{record.allowed_model_names?.length
|
||||||
? record.allowed_model_names.join(', ')
|
? record.allowed_model_names.join(', ')
|
||||||
: intl.formatMessage({ id: 'apikeys.models.all' })}
|
: intl.formatMessage({ id: 'apikeys.models.all' })}
|
||||||
</AutoTooltip>
|
</AutoTooltip>
|
||||||
</ThemeTag>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -183,15 +183,11 @@ const APIKeys: React.FC = () => {
|
|||||||
if (type !== 'Table') return;
|
if (type !== 'Table') return;
|
||||||
return (
|
return (
|
||||||
<NoResult
|
<NoResult
|
||||||
minHeight="calc(100vh - 300px)"
|
|
||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
image={<IconFont type="icon-key" />}
|
image={<IconFont type="icon-key" />}
|
||||||
filters={{
|
filters={_.omit(queryParams, ['sort_by'])}
|
||||||
..._.omit(queryParams, ['sort_by']),
|
|
||||||
user_id: queryParams.user_id === '*' ? undefined : queryParams.user_id
|
|
||||||
}}
|
|
||||||
noFoundText={intl.formatMessage({
|
noFoundText={intl.formatMessage({
|
||||||
id: 'noresult.keys.nofound'
|
id: 'noresult.keys.nofound'
|
||||||
})}
|
})}
|
||||||
@@ -232,7 +228,6 @@ const APIKeys: React.FC = () => {
|
|||||||
></FilterBar>
|
></FilterBar>
|
||||||
<ConfigProvider renderEmpty={renderEmpty}>
|
<ConfigProvider renderEmpty={renderEmpty}>
|
||||||
<Table
|
<Table
|
||||||
className={'scroll-table'}
|
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
|
|||||||
@@ -164,7 +164,6 @@ const CommunityBackends: React.FC<{
|
|||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
></BackendCardList>
|
></BackendCardList>
|
||||||
<NoResult
|
<NoResult
|
||||||
minHeight="calc(100vh - 300px)"
|
|
||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
|
|||||||
@@ -282,7 +282,6 @@ const BackendList = () => {
|
|||||||
onSelect={handleOnSelect}
|
onSelect={handleOnSelect}
|
||||||
></BackendCardList>
|
></BackendCardList>
|
||||||
<NoResult
|
<NoResult
|
||||||
minHeight="calc(100vh - 300px)"
|
|
||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
|
|||||||
@@ -177,7 +177,6 @@ const Benchmark: React.FC = () => {
|
|||||||
if (type !== 'Table') return;
|
if (type !== 'Table') return;
|
||||||
return (
|
return (
|
||||||
<NoResult
|
<NoResult
|
||||||
minHeight="calc(100vh - 300px)"
|
|
||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={[]}
|
dataSource={[]}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import type { PageActionType } from '@/config/types';
|
|||||||
import useExpandedRowKeys from '@/hooks/use-expanded-row-keys';
|
import useExpandedRowKeys from '@/hooks/use-expanded-row-keys';
|
||||||
import useTableFetch from '@/hooks/use-table-fetch';
|
import useTableFetch from '@/hooks/use-table-fetch';
|
||||||
import useWatchList from '@/hooks/use-watch-list';
|
import useWatchList from '@/hooks/use-watch-list';
|
||||||
import { getGPUStackPlugin } from '@/plugins';
|
|
||||||
import {
|
import {
|
||||||
DeleteModal,
|
DeleteModal,
|
||||||
FilterBar,
|
FilterBar,
|
||||||
@@ -15,7 +14,7 @@ import {
|
|||||||
TableOrder,
|
TableOrder,
|
||||||
TableProvider
|
TableProvider
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl, useNavigate } from '@umijs/max';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
@@ -74,7 +73,7 @@ const Clusters: React.FC = () => {
|
|||||||
deleteAPI: deleteCluster,
|
deleteAPI: deleteCluster,
|
||||||
watch: true,
|
watch: true,
|
||||||
API: CLUSTERS_API,
|
API: CLUSTERS_API,
|
||||||
contentForDelete: 'menu.resources.clusters',
|
contentForDelete: 'menu.clusterManagement.clusters',
|
||||||
defaultQueryParams: {
|
defaultQueryParams: {
|
||||||
// Management view: drop cross-Org cluster_access grants. Org
|
// Management view: drop cross-Org cluster_access grants. Org
|
||||||
// Owner only sees the clusters they own here. Pickers that
|
// Owner only sees the clusters they own here. Pickers that
|
||||||
@@ -83,13 +82,10 @@ const Clusters: React.FC = () => {
|
|||||||
mine: true
|
mine: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const navigate = useNavigate();
|
||||||
const { goToGrafana, ActionButton } = useGranfanaLink({
|
const { goToGrafana, ActionButton } = useGranfanaLink({
|
||||||
type: 'cluster'
|
type: 'cluster'
|
||||||
});
|
});
|
||||||
// Cluster Access lives in the enterprise plugin: it contributes the
|
|
||||||
// row action and this self-controlled drawer, owning its own
|
|
||||||
// open/close state. OSS just mounts it (nothing without a plugin).
|
|
||||||
const AccessDrawer = getGPUStackPlugin()?.clusterDetail?.AccessDrawer;
|
|
||||||
const { watchDataList: allWorkerPoolList } = useWatchList(WORKER_POOLS_API);
|
const { watchDataList: allWorkerPoolList } = useWatchList(WORKER_POOLS_API);
|
||||||
const [expandAtom] = useAtom(expandKeysAtom);
|
const [expandAtom] = useAtom(expandKeysAtom);
|
||||||
const [clusterSession, setClusterSession] = useAtom(clusterSessionAtom);
|
const [clusterSession, setClusterSession] = useAtom(clusterSessionAtom);
|
||||||
@@ -276,6 +272,14 @@ const Clusters: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnCell = useMemoizedFn((record: ClusterListItem, dataIndex) => {
|
||||||
|
if (dataIndex === 'name') {
|
||||||
|
navigate(
|
||||||
|
`/resources/clusters/detail?id=${record.id}&name=${record.name}&page=clusters`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchCredentialList = async () => {
|
const fetchCredentialList = async () => {
|
||||||
const data = await queryCredentialList({ page: -1 });
|
const data = await queryCredentialList({ page: -1 });
|
||||||
@@ -358,14 +362,11 @@ const Clusters: React.FC = () => {
|
|||||||
dataList={list}
|
dataList={list}
|
||||||
provider={options.parent?.provider}
|
provider={options.parent?.provider}
|
||||||
clusterId={options.parent?.id}
|
clusterId={options.parent?.id}
|
||||||
gridTemplate={options.gridTemplate}
|
|
||||||
prefixWidth={options.prefixWidth}
|
|
||||||
columns={options.columns}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = useClusterColumns(handleSelect);
|
const columns = useClusterColumns(handleSelect, handleOnCell);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -398,7 +399,6 @@ const Clusters: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<SealTable
|
<SealTable
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
emptyMinHeight="calc(100vh - 300px)"
|
|
||||||
loadChildren={getWorkerPoolList}
|
loadChildren={getWorkerPoolList}
|
||||||
sortDirections={TABLE_SORT_DIRECTIONS}
|
sortDirections={TABLE_SORT_DIRECTIONS}
|
||||||
expandedRowKeys={expandedRowKeys}
|
expandedRowKeys={expandedRowKeys}
|
||||||
@@ -420,7 +420,7 @@ const Clusters: React.FC = () => {
|
|||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
image={<IconFont type="icon-cluster-outline" />}
|
image={<IconFont type="icon-cluster-outline" />}
|
||||||
filters={_.omit(queryParams, ['sort_by', 'mine'])}
|
filters={_.omit(queryParams, ['sort_by'])}
|
||||||
noFoundText={intl.formatMessage({
|
noFoundText={intl.formatMessage({
|
||||||
id: 'noresult.cluster.nofound'
|
id: 'noresult.cluster.nofound'
|
||||||
})}
|
})}
|
||||||
@@ -482,7 +482,6 @@ const Clusters: React.FC = () => {
|
|||||||
onClose={handleClusterModalClose}
|
onClose={handleClusterModalClose}
|
||||||
></ClusterModal>
|
></ClusterModal>
|
||||||
{AddWorkerModal}
|
{AddWorkerModal}
|
||||||
{AccessDrawer && <AccessDrawer />}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
|
||||||
import { AlertBlockInfo } from '@gpustack/core-ui';
|
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import RegisterClusterInner from '../register-cluster-inner';
|
import RegisterClusterInner from '../register-cluster-inner';
|
||||||
@@ -36,14 +34,6 @@ const K8sRunCommand: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
|||||||
id: 'clusters.create.addCommand.k8s.tips'
|
id: 'clusters.create.addCommand.k8s.tips'
|
||||||
})}
|
})}
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
<AlertBlockInfo
|
|
||||||
type="warning"
|
|
||||||
style={{ marginBottom: 8 }}
|
|
||||||
icon={<ExclamationCircleFilled />}
|
|
||||||
message={intl.formatMessage({
|
|
||||||
id: 'clusters.create.addCommand.k8s.version.warning'
|
|
||||||
})}
|
|
||||||
></AlertBlockInfo>
|
|
||||||
<RegisterClusterInner
|
<RegisterClusterInner
|
||||||
registrationInfo={registrationInfo}
|
registrationInfo={registrationInfo}
|
||||||
currentGPU={currentGPU}
|
currentGPU={currentGPU}
|
||||||
|
|||||||
@@ -59,12 +59,6 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [activeKey, setActiveKey] = React.useState<string[]>([]);
|
const [activeKey, setActiveKey] = React.useState<string[]>([]);
|
||||||
const [submitAttempted, setSubmitAttempted] = useState(false);
|
const [submitAttempted, setSubmitAttempted] = useState(false);
|
||||||
// Single source of truth for the K8s cluster type, seeded from the cluster
|
|
||||||
// being edited. Shared via FormContext so the type selector and the
|
|
||||||
// GPU-only fields stay in sync deterministically (no cross-component watch).
|
|
||||||
const [clusterType, setClusterType] = useState<'model' | 'gpu'>(() =>
|
|
||||||
currentData?.k8s_options?.gpuInstanceOptions ? 'gpu' : 'model'
|
|
||||||
);
|
|
||||||
const advanceConfigRef = React.useRef<any>(null);
|
const advanceConfigRef = React.useRef<any>(null);
|
||||||
const systemConfig = useAtomValue(systemConfigAtom);
|
const systemConfig = useAtomValue(systemConfigAtom);
|
||||||
|
|
||||||
@@ -93,20 +87,6 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
|
|
||||||
const next: any = { ...opts };
|
const next: any = { ...opts };
|
||||||
|
|
||||||
// "model" clusters must not carry GPU-instance config. The field's UI is
|
|
||||||
// unmounted when model is selected, but strip it here too so the payload
|
|
||||||
// never keeps a stale gpuInstanceOptions shape from a prior "gpu" choice.
|
|
||||||
if (clusterType === 'model') {
|
|
||||||
next.gpuInstanceOptions = null;
|
|
||||||
} else if (clusterType === 'gpu' && !next.gpuInstanceOptions) {
|
|
||||||
// gpuInstanceOptions has no always-mounted Form.Item (its only child,
|
|
||||||
// the optional static address, may be unmounted or empty), so with
|
|
||||||
// preserve={false} onFinish's `values` can omit it. Read it straight
|
|
||||||
// from the store so a "gpu" cluster always carries the field.
|
|
||||||
next.gpuInstanceOptions =
|
|
||||||
form.getFieldValue(['k8s_options', 'gpuInstanceOptions']) ?? {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const creds = opts.imageCredentials;
|
const creds = opts.imageCredentials;
|
||||||
if (Array.isArray(creds)) {
|
if (Array.isArray(creds)) {
|
||||||
next.imageCredentials = creds.map((c: any) => ({
|
next.imageCredentials = creds.map((c: any) => ({
|
||||||
@@ -240,9 +220,7 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormContext.Provider
|
<FormContext.Provider value={{ submitAttempted }}>
|
||||||
value={{ submitAttempted, clusterType, setClusterType }}
|
|
||||||
>
|
|
||||||
<Form
|
<Form
|
||||||
name="clusterForm"
|
name="clusterForm"
|
||||||
form={form}
|
form={form}
|
||||||
|
|||||||
@@ -2,12 +2,10 @@ import { PageAction } from '@/config';
|
|||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import { Input as CInput, LabelSelector } from '@gpustack/core-ui';
|
import { Input as CInput, LabelSelector } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
|
||||||
import { Form } from 'antd';
|
import { Form } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { useEffect, useId, useMemo } from 'react';
|
import React, { useEffect, useId, useMemo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { useFormContext } from '../config/form-context';
|
|
||||||
import { useStepsContext } from '../config/steps-context';
|
import { useStepsContext } from '../config/steps-context';
|
||||||
import { ClusterListItem as ListItem } from '../config/types';
|
import { ClusterListItem as ListItem } from '../config/types';
|
||||||
import ImageCredential from './image-credential';
|
import ImageCredential from './image-credential';
|
||||||
@@ -70,6 +68,13 @@ export const OperatorImageForm: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The presence of `gpuInstanceOptions` on `k8s_options` is the source of truth
|
||||||
|
// for whether GPU instances are enabled. Both the cluster-type selector
|
||||||
|
// (rendered up top) and the static-address field (rendered in the advanced
|
||||||
|
// section) watch this same path so they stay in sync without sharing local
|
||||||
|
// state.
|
||||||
|
const GPU_INSTANCE_OPTIONS_PATH = ['k8s_options', 'gpuInstanceOptions'];
|
||||||
|
|
||||||
// Visual parity with @gpustack/core-ui's SwitchCard so the selector blends
|
// Visual parity with @gpustack/core-ui's SwitchCard so the selector blends
|
||||||
// in with surrounding form fields: same border, radius, padding, and
|
// in with surrounding form fields: same border, radius, padding, and
|
||||||
// typography. The only differences are the two-column grid layout and an
|
// typography. The only differences are the two-column grid layout and an
|
||||||
@@ -96,7 +101,6 @@ const ClusterTypeGrid = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const ClusterTypeCard = styled.div<{ $active: boolean }>`
|
const ClusterTypeCard = styled.div<{ $active: boolean }>`
|
||||||
position: relative;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -136,17 +140,6 @@ const ClusterTypeCard = styled.div<{ $active: boolean }>`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ExperimentalTag = styled.span`
|
|
||||||
position: absolute;
|
|
||||||
right: 2px;
|
|
||||||
top: 2px;
|
|
||||||
padding: 2px;
|
|
||||||
border-radius: 2px;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 400;
|
|
||||||
background-color: var(--ant-blue-1);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const RadioDot = styled.span<{ $active: boolean }>`
|
const RadioDot = styled.span<{ $active: boolean }>`
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -176,30 +169,37 @@ const RadioDot = styled.span<{ $active: boolean }>`
|
|||||||
|
|
||||||
// Card-based selector for cluster type. The two options are mutually exclusive
|
// Card-based selector for cluster type. The two options are mutually exclusive
|
||||||
// and the choice maps directly to the presence/absence of `gpuInstanceOptions`
|
// and the choice maps directly to the presence/absence of `gpuInstanceOptions`
|
||||||
// on the submitted payload. No standalone form field is registered; the click
|
// on the form — "model" clears it, "gpu" seeds it to {} (preserving any
|
||||||
// only updates the shared `clusterType` state (see FormContext) — the payload's
|
// already-entered static address). No standalone form field is registered;
|
||||||
// `gpuInstanceOptions` shape is derived from it at submit (see cluster-form's
|
// state is read via useWatch with `preserve: true` so it tracks updates made
|
||||||
// normalizeOutgoing), and the static-address field mounts/unmounts off it.
|
// through setFieldValue.
|
||||||
export const ClusterTypeSelector: React.FC = () => {
|
export const ClusterTypeSelector: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const form = Form.useFormInstance();
|
||||||
const { presetClusterType } = useStepsContext();
|
const { presetClusterType } = useStepsContext();
|
||||||
const labelId = useId();
|
const labelId = useId();
|
||||||
// Cluster type is shared, explicit state (see FormContext): the click is the
|
const gpuInstanceOptions = Form.useWatch(GPU_INSTANCE_OPTIONS_PATH, {
|
||||||
// source of truth. This replaced a Form.useWatch on an unregistered path that
|
form,
|
||||||
// did not re-render reliably when cleared to undefined.
|
preserve: true
|
||||||
const { clusterType, setClusterType } = useFormContext();
|
|
||||||
const value: 'model' | 'gpu' = clusterType ?? 'model';
|
|
||||||
|
|
||||||
const handleSelect = useMemoizedFn((next: 'model' | 'gpu') => {
|
|
||||||
if (next === value) return;
|
|
||||||
setClusterType?.(next);
|
|
||||||
});
|
});
|
||||||
|
const value: 'model' | 'gpu' = gpuInstanceOptions ? 'gpu' : 'model';
|
||||||
|
|
||||||
|
const handleSelect = (next: 'model' | 'gpu') => {
|
||||||
|
if (!form || next === value) return;
|
||||||
|
if (next === 'gpu') {
|
||||||
|
form.setFieldValue(
|
||||||
|
GPU_INSTANCE_OPTIONS_PATH,
|
||||||
|
form.getFieldValue(GPU_INSTANCE_OPTIONS_PATH) ?? {}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
form.setFieldValue(GPU_INSTANCE_OPTIONS_PATH, undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const options: {
|
const options: {
|
||||||
key: 'model' | 'gpu';
|
key: 'model' | 'gpu';
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
experimental?: boolean;
|
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
key: 'model',
|
key: 'model',
|
||||||
@@ -209,8 +209,7 @@ export const ClusterTypeSelector: React.FC = () => {
|
|||||||
{
|
{
|
||||||
key: 'gpu',
|
key: 'gpu',
|
||||||
title: intl.formatMessage({ id: 'clusters.gpuInstances.title' }),
|
title: intl.formatMessage({ id: 'clusters.gpuInstances.title' }),
|
||||||
description: intl.formatMessage({ id: 'clusters.gpuInstances.tip' }),
|
description: intl.formatMessage({ id: 'clusters.gpuInstances.tip' })
|
||||||
experimental: true
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -245,11 +244,6 @@ export const ClusterTypeSelector: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RadioDot $active={active} />
|
<RadioDot $active={active} />
|
||||||
{opt.experimental && (
|
|
||||||
<ExperimentalTag>
|
|
||||||
{intl.formatMessage({ id: 'common.tag.experimental' })}
|
|
||||||
</ExperimentalTag>
|
|
||||||
)}
|
|
||||||
<div className="body">
|
<div className="body">
|
||||||
<div className="title">{opt.title}</div>
|
<div className="title">{opt.title}</div>
|
||||||
<div className="description">{opt.description}</div>
|
<div className="description">{opt.description}</div>
|
||||||
@@ -267,13 +261,14 @@ export const ClusterTypeSelector: React.FC = () => {
|
|||||||
// default container registry and the worker config (节点配置).
|
// default container registry and the worker config (节点配置).
|
||||||
export const GpuInstancesStaticAddressForm: React.FC = () => {
|
export const GpuInstancesStaticAddressForm: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
// Visibility tracks the shared cluster-type state (see FormContext), so this
|
// See note in ClusterTypeSelector: watch the full store so this field's
|
||||||
// field mounts/unmounts deterministically with the selector. Its Form.Item is
|
// visibility tracks the selector even before it has mounted its own
|
||||||
// the only thing keeping gpuInstanceOptions alive, so unmounting it here (with
|
// Form.Item.
|
||||||
// the form's preserve={false}) also clears that path from the store.
|
const enabled = !!Form.useWatch(GPU_INSTANCE_OPTIONS_PATH, {
|
||||||
const { clusterType } = useFormContext();
|
preserve: true
|
||||||
|
});
|
||||||
|
|
||||||
if (clusterType !== 'gpu') {
|
if (!enabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,13 @@ import { PageAction } from '@/config';
|
|||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import {
|
import {
|
||||||
CellContent,
|
CellContent,
|
||||||
type ChildGridOptions,
|
|
||||||
DeleteModal,
|
DeleteModal,
|
||||||
ExpandedRowGrid,
|
RowChildren,
|
||||||
TableRowProvider
|
TableRowProvider
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
import { message } from 'antd';
|
import { Col, message, Row } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { deleteWorkerPool, updateWorkerPool } from '../apis';
|
import { deleteWorkerPool, updateWorkerPool } from '../apis';
|
||||||
@@ -17,10 +16,7 @@ import { ProviderType } from '../config';
|
|||||||
import { NodePoolFormData, NodePoolListItem } from '../config/types';
|
import { NodePoolFormData, NodePoolListItem } from '../config/types';
|
||||||
import usePoolsColumns from '../hooks/use-pools-columns';
|
import usePoolsColumns from '../hooks/use-pools-columns';
|
||||||
import AddPool from './add-pool';
|
import AddPool from './add-pool';
|
||||||
interface PoolRowsProps extends Pick<
|
interface PoolRowsProps {
|
||||||
ChildGridOptions,
|
|
||||||
'gridTemplate' | 'prefixWidth' | 'columns'
|
|
||||||
> {
|
|
||||||
dataList: NodePoolListItem[];
|
dataList: NodePoolListItem[];
|
||||||
provider: ProviderType;
|
provider: ProviderType;
|
||||||
clusterId: number | string;
|
clusterId: number | string;
|
||||||
@@ -29,35 +25,9 @@ interface PoolRowsProps extends Pick<
|
|||||||
const PoolRows: React.FC<PoolRowsProps> = ({
|
const PoolRows: React.FC<PoolRowsProps> = ({
|
||||||
dataList,
|
dataList,
|
||||||
provider,
|
provider,
|
||||||
clusterId,
|
clusterId
|
||||||
gridTemplate,
|
|
||||||
prefixWidth = 0,
|
|
||||||
columns: parentColumns
|
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
// The child row shares the parent's column grid; cells flow left-to-right and
|
|
||||||
// only declare a span, keyed on the pool column's OWN dataIndex — never on a
|
|
||||||
// parent cluster column key. Parent layout: name (1) | provider…state middle
|
|
||||||
// region | created_at (1) | operations (1). The three middle pool columns
|
|
||||||
// cover that region: `replicas`→state (last, 1 track), `image_name`→
|
|
||||||
// models+workers (2 tracks), `instance_type` absorbs the rest (plugins +
|
|
||||||
// provider + gpus).
|
|
||||||
const columnCount = parentColumns?.length ?? 0;
|
|
||||||
const middleSpan = Math.max(columnCount - 3, 1);
|
|
||||||
const spanFor = (dataIndex: string): number => {
|
|
||||||
switch (dataIndex) {
|
|
||||||
case 'instance_type':
|
|
||||||
return Math.max(middleSpan - 3, 1);
|
|
||||||
case 'image_name':
|
|
||||||
return 2;
|
|
||||||
case 'replicas':
|
|
||||||
return 1;
|
|
||||||
default:
|
|
||||||
// name / created_at / operations align 1:1 with their parent column.
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const modalRef = useRef<any>(null);
|
const modalRef = useRef<any>(null);
|
||||||
const [addPoolStatus, setAddPoolStatus] = useState<{
|
const [addPoolStatus, setAddPoolStatus] = useState<{
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -92,10 +62,8 @@ const PoolRows: React.FC<PoolRowsProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnCell = async (
|
const handleOnCell = async (row: NodePoolListItem, dataIndex: string) => {
|
||||||
row: NodePoolListItem,
|
console.log('handleOncell===', row, dataIndex);
|
||||||
_data: { dataIndex: string; newValue: any; oldValue: any }
|
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
await updateWorkerPool({
|
await updateWorkerPool({
|
||||||
data: row,
|
data: row,
|
||||||
@@ -150,29 +118,31 @@ const PoolRows: React.FC<PoolRowsProps> = ({
|
|||||||
<>
|
<>
|
||||||
{dataList?.map((data: NodePoolListItem) => {
|
{dataList?.map((data: NodePoolListItem) => {
|
||||||
return (
|
return (
|
||||||
<TableRowProvider
|
<div
|
||||||
key={data.id}
|
key={data.id}
|
||||||
value={{ row: data, onCell: handleOnCell }}
|
style={{ borderRadius: 'var(--ant-table-header-border-radius)' }}
|
||||||
>
|
>
|
||||||
<ExpandedRowGrid
|
<TableRowProvider value={{ row: data, onCell: handleOnCell }}>
|
||||||
gridTemplate={gridTemplate}
|
<RowChildren>
|
||||||
prefixWidth={prefixWidth}
|
<Row style={{ width: '100%' }} align="middle">
|
||||||
>
|
{columns.map((col: Record<string, any>) => {
|
||||||
{columns.map((col: Record<string, any>) => (
|
return (
|
||||||
<ExpandedRowGrid.Cell
|
<Col
|
||||||
key={col.dataIndex || col.key}
|
key={col.dataIndex || col.key}
|
||||||
span={spanFor(col.dataIndex)}
|
span={col.span}
|
||||||
style={{
|
style={{
|
||||||
color: 'var(--ant-color-text-secondary)'
|
paddingInline: 0,
|
||||||
|
...(col.style || {})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CellContent
|
<CellContent {..._.omit(col, ['key'])}></CellContent>
|
||||||
{..._.omit(col, ['key', 'style', 'span'])}
|
</Col>
|
||||||
></CellContent>
|
);
|
||||||
</ExpandedRowGrid.Cell>
|
})}
|
||||||
))}
|
</Row>
|
||||||
</ExpandedRowGrid>
|
</RowChildren>
|
||||||
</TableRowProvider>
|
</TableRowProvider>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<AddPool
|
<AddPool
|
||||||
|
|||||||
@@ -5,12 +5,6 @@ import { ClusterListItem } from './types';
|
|||||||
interface FormContextProps {
|
interface FormContextProps {
|
||||||
currentData?: ClusterListItem;
|
currentData?: ClusterListItem;
|
||||||
submitAttempted?: boolean;
|
submitAttempted?: boolean;
|
||||||
// K8s cluster type. `gpuInstanceOptions` on the form is derived from this —
|
|
||||||
// the selector, the static-address field, and submit all read this single
|
|
||||||
// source of truth instead of independently watching the (unregistered) form
|
|
||||||
// path, which did not re-render reliably.
|
|
||||||
clusterType?: 'model' | 'gpu';
|
|
||||||
setClusterType?: (type: 'model' | 'gpu') => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FormContext = createContext<FormContextProps>({});
|
export const FormContext = createContext<FormContextProps>({});
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ const Credentials: React.FC = () => {
|
|||||||
key: PaginationKey.Credentials,
|
key: PaginationKey.Credentials,
|
||||||
fetchAPI: queryCredentialList,
|
fetchAPI: queryCredentialList,
|
||||||
deleteAPI: deleteCredential,
|
deleteAPI: deleteCredential,
|
||||||
contentForDelete: 'menu.resources.credentials'
|
contentForDelete: 'menu.clusterManagement.credentials'
|
||||||
});
|
});
|
||||||
const [, setClusterSession] = useAtom(clusterSessionAtom);
|
const [, setClusterSession] = useAtom(clusterSessionAtom);
|
||||||
const [isFromCluster, setIsFromCluster] = useAtom(fromClusterCreationAtom);
|
const [isFromCluster, setIsFromCluster] = useAtom(fromClusterCreationAtom);
|
||||||
@@ -157,7 +157,6 @@ const Credentials: React.FC = () => {
|
|||||||
if (type !== 'Table') return;
|
if (type !== 'Table') return;
|
||||||
return (
|
return (
|
||||||
<NoResult
|
<NoResult
|
||||||
minHeight="calc(100vh - 300px)"
|
|
||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={[]}
|
dataSource={[]}
|
||||||
@@ -200,7 +199,6 @@ const Credentials: React.FC = () => {
|
|||||||
|
|
||||||
<ConfigProvider renderEmpty={renderEmpty}>
|
<ConfigProvider renderEmpty={renderEmpty}>
|
||||||
<Table
|
<Table
|
||||||
className={'scroll-table'}
|
|
||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
type TableColumnProps as SealColumnProps
|
type TableColumnProps as SealColumnProps
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Tooltip } from 'antd';
|
import { Tooltip, Typography } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
@@ -85,15 +85,21 @@ const clusterActionList = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const useClusterColumns = (
|
const useClusterColumns = (
|
||||||
handleSelect: (val: string, record: ClusterListItem, item?: any) => void
|
handleSelect: (val: string, record: ClusterListItem, item?: any) => void,
|
||||||
|
onCellClick?: (record: ClusterListItem, dataIndex: string) => void
|
||||||
): SealColumnProps[] => {
|
): SealColumnProps[] => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const systemConfig = useAtomValue(systemConfigAtom);
|
const systemConfig = useAtomValue(systemConfigAtom);
|
||||||
const pluginCols = usePluginListColumns('clusters');
|
const pluginCols = usePluginListColumns('clusters');
|
||||||
// The cluster name is plain text: there is no cluster-detail page
|
// The cluster-detail page is shipped in OSS source, but OSS keeps
|
||||||
// to route into. A plugin may still contribute extra row actions
|
// it unreachable from the cluster list — the link is only
|
||||||
// (topology, Cluster Access) via `clusterDetail.useGenerateActions`.
|
// surfaced when a plugin opts in via
|
||||||
const { useGenerateActions } = getGPUStackPlugin()?.clusterDetail || {};
|
// `clusterDetail.linkableName`. Without a plugin we render the
|
||||||
|
// name as plain text (matches the pre-restore behaviour); with one
|
||||||
|
// we use Typography.Link wired to the parent's `onCellClick`.
|
||||||
|
|
||||||
|
const { linkableName: nameLinkable, useGenerateActions } =
|
||||||
|
getGPUStackPlugin()?.clusterDetail || {};
|
||||||
|
|
||||||
const actionList =
|
const actionList =
|
||||||
useGenerateActions?.({ actions: clusterActionList }) || clusterActionList;
|
useGenerateActions?.({ actions: clusterActionList }) || clusterActionList;
|
||||||
@@ -154,7 +160,13 @@ const useClusterColumns = (
|
|||||||
render: (text: string, record: ClusterListItem) => (
|
render: (text: string, record: ClusterListItem) => (
|
||||||
<>
|
<>
|
||||||
<AutoTooltip ghost title={text}>
|
<AutoTooltip ghost title={text}>
|
||||||
|
{nameLinkable ? (
|
||||||
|
<Typography.Link onClick={() => onCellClick?.(record, 'name')}>
|
||||||
|
{record.name}
|
||||||
|
</Typography.Link>
|
||||||
|
) : (
|
||||||
<span className="text-primary">{record.name}</span>
|
<span className="text-primary">{record.name}</span>
|
||||||
|
)}
|
||||||
</AutoTooltip>
|
</AutoTooltip>
|
||||||
{record.is_default && (
|
{record.is_default && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -225,7 +237,7 @@ const useClusterColumns = (
|
|||||||
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
||||||
dataIndex: 'created_at',
|
dataIndex: 'created_at',
|
||||||
sorter: tableSorter(6),
|
sorter: tableSorter(6),
|
||||||
width: 180,
|
span: spans.createTime,
|
||||||
render: (value: string) => (
|
render: (value: string) => (
|
||||||
<AutoTooltip ghost minWidth={20}>
|
<AutoTooltip ghost minWidth={20}>
|
||||||
{dayjs(value).format('YYYY-MM-DD HH:mm:ss')}
|
{dayjs(value).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
@@ -246,7 +258,7 @@ const useClusterColumns = (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}, [handleSelect, intl, pluginCols]);
|
}, [handleSelect, onCellClick, intl, pluginCols]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useClusterColumns;
|
export default useClusterColumns;
|
||||||
|
|||||||
@@ -43,9 +43,12 @@ const usePoolsColumns = (
|
|||||||
showTitle: false
|
showTitle: false
|
||||||
},
|
},
|
||||||
span: 3,
|
span: 3,
|
||||||
|
style: {
|
||||||
|
paddingInline: 'var(--ant-table-cell-padding-inline)'
|
||||||
|
},
|
||||||
render: (text: string) => (
|
render: (text: string) => (
|
||||||
<AutoTooltip title={text} ghost minWidth={20}>
|
<AutoTooltip title={text} ghost minWidth={20}>
|
||||||
<span className="text-primary">{text}</span>
|
{text}
|
||||||
</AutoTooltip>
|
</AutoTooltip>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -57,6 +60,9 @@ const usePoolsColumns = (
|
|||||||
showTitle: false
|
showTitle: false
|
||||||
},
|
},
|
||||||
span: 4,
|
span: 4,
|
||||||
|
style: {
|
||||||
|
paddingLeft: 62
|
||||||
|
},
|
||||||
render: (text: string, record: ListItem) => (
|
render: (text: string, record: ListItem) => (
|
||||||
<AutoTooltip
|
<AutoTooltip
|
||||||
title={
|
title={
|
||||||
@@ -92,6 +98,9 @@ const usePoolsColumns = (
|
|||||||
ellipsis: {
|
ellipsis: {
|
||||||
showTitle: false
|
showTitle: false
|
||||||
},
|
},
|
||||||
|
style: {
|
||||||
|
paddingLeft: 56
|
||||||
|
},
|
||||||
render: (text: string) => (
|
render: (text: string) => (
|
||||||
<AutoTooltip
|
<AutoTooltip
|
||||||
title={
|
title={
|
||||||
@@ -116,6 +125,9 @@ const usePoolsColumns = (
|
|||||||
dataIndex: 'replicas',
|
dataIndex: 'replicas',
|
||||||
span: 6,
|
span: 6,
|
||||||
key: 'replicas',
|
key: 'replicas',
|
||||||
|
style: {
|
||||||
|
paddingLeft: 50
|
||||||
|
},
|
||||||
editable: {
|
editable: {
|
||||||
valueType: 'number',
|
valueType: 'number',
|
||||||
title: intl.formatMessage({ id: 'models.table.replicas.edit' })
|
title: intl.formatMessage({ id: 'models.table.replicas.edit' })
|
||||||
@@ -153,6 +165,9 @@ const usePoolsColumns = (
|
|||||||
ellipsis: {
|
ellipsis: {
|
||||||
showTitle: false
|
showTitle: false
|
||||||
},
|
},
|
||||||
|
style: {
|
||||||
|
paddingLeft: 42
|
||||||
|
},
|
||||||
render: (text: string) => (
|
render: (text: string) => (
|
||||||
<AutoTooltip ghost minWidth={20}>
|
<AutoTooltip ghost minWidth={20}>
|
||||||
{dayjs(text).format('YYYY-MM-DD HH:mm:ss')}
|
{dayjs(text).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
@@ -164,6 +179,9 @@ const usePoolsColumns = (
|
|||||||
key: 'operations',
|
key: 'operations',
|
||||||
dataIndex: 'operations',
|
dataIndex: 'operations',
|
||||||
span: 3,
|
span: 3,
|
||||||
|
style: {
|
||||||
|
paddingLeft: 36
|
||||||
|
},
|
||||||
render: (text: string, record: ListItem) => (
|
render: (text: string, record: ListItem) => (
|
||||||
<DropdownButtons
|
<DropdownButtons
|
||||||
items={actionItems}
|
items={actionItems}
|
||||||
|
|||||||
@@ -22,12 +22,7 @@ export const useQueryClusterList = (options?: { useStateData?: boolean }) => {
|
|||||||
loading,
|
loading,
|
||||||
cancel
|
cancel
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
async (params: {
|
async (params: { page: number; perPage?: number }) => {
|
||||||
page: number;
|
|
||||||
perPage?: number;
|
|
||||||
mine?: boolean;
|
|
||||||
gpu_instance_enabled?: boolean;
|
|
||||||
}) => {
|
|
||||||
axiosTokenRef.current?.cancel();
|
axiosTokenRef.current?.cancel();
|
||||||
axiosTokenRef.current = createAxiosToken();
|
axiosTokenRef.current = createAxiosToken();
|
||||||
const res = await queryClusterList(params, {
|
const res = await queryClusterList(params, {
|
||||||
|
|||||||
@@ -18,17 +18,10 @@ const BasicForm = forwardRef((props: BasicFormProps, ref) => {
|
|||||||
console.log(values);
|
console.log(values);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Forward through to the live ClusterForm ref on each call instead of
|
|
||||||
// snapshotting its methods. ClusterForm rebuilds these closures whenever its
|
|
||||||
// internal state (e.g. clusterType) changes, but BasicForm does not re-render
|
|
||||||
// with it — a frozen snapshot would keep calling stale closures (reading the
|
|
||||||
// initial clusterType) and be null on the first render before the ref attaches.
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
validateFields: (...args: any[]) =>
|
validateFields: formRef.current?.validateFields,
|
||||||
formRef.current?.validateFields(...args),
|
getFieldsValue: formRef.current?.getFieldsValue,
|
||||||
getFieldsValue: (...args: any[]) =>
|
submit: formRef.current?.submit
|
||||||
formRef.current?.getFieldsValue(...args),
|
|
||||||
submit: (...args: any[]) => formRef.current?.submit(...args)
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,20 +2,6 @@ import {
|
|||||||
BreakdownItem,
|
BreakdownItem,
|
||||||
UsageBreakdownResponse
|
UsageBreakdownResponse
|
||||||
} from '@/pages/usage/config/types';
|
} from '@/pages/usage/config/types';
|
||||||
import { withDeletedMark } from '@/pages/usage/utils/deleted-label';
|
|
||||||
import { getIntl } from '@umijs/max';
|
|
||||||
|
|
||||||
// group dimension → the id field inside ``identity.current`` (the backend nulls
|
|
||||||
// it for deleted entities, so the marker degrades to just "[Deleted]").
|
|
||||||
const GROUP_ID_KEY: Record<
|
|
||||||
UsageGroupBy,
|
|
||||||
'model_id' | 'route_id' | 'user_id' | 'api_key_id'
|
|
||||||
> = {
|
|
||||||
model: 'model_id',
|
|
||||||
route: 'route_id',
|
|
||||||
user: 'user_id',
|
|
||||||
api_key: 'api_key_id'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const overviewConfigs = [
|
export const overviewConfigs = [
|
||||||
{
|
{
|
||||||
@@ -102,7 +88,6 @@ export const buildUsageLabel = (item: BreakdownItem, groupBy: UsageGroupBy) => {
|
|||||||
return groupItem;
|
return groupItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
let baseLabel: string;
|
|
||||||
if (groupBy === 'model') {
|
if (groupBy === 'model') {
|
||||||
const providerName =
|
const providerName =
|
||||||
identityValue?.provider_name || groupValue?.provider_name;
|
identityValue?.provider_name || groupValue?.provider_name;
|
||||||
@@ -112,18 +97,23 @@ export const buildUsageLabel = (item: BreakdownItem, groupBy: UsageGroupBy) => {
|
|||||||
rawItem.model_name ||
|
rawItem.model_name ||
|
||||||
rawItem.model;
|
rawItem.model;
|
||||||
|
|
||||||
baseLabel =
|
if (providerName && modelName) {
|
||||||
providerName && modelName
|
return `${providerName}/${modelName}`;
|
||||||
? `${providerName}/${modelName}`
|
}
|
||||||
: groupItem?.label || modelName || '-';
|
|
||||||
} else if (groupBy === 'route') {
|
return groupItem?.label || modelName || '-';
|
||||||
baseLabel =
|
}
|
||||||
|
|
||||||
|
if (groupBy === 'route') {
|
||||||
|
return (
|
||||||
groupItem?.label ||
|
groupItem?.label ||
|
||||||
identityValue?.route_name ||
|
identityValue?.route_name ||
|
||||||
groupValue?.route_name ||
|
groupValue?.route_name ||
|
||||||
'-';
|
'-'
|
||||||
} else {
|
);
|
||||||
baseLabel =
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
groupItem?.label ||
|
groupItem?.label ||
|
||||||
identityValue?.user_name ||
|
identityValue?.user_name ||
|
||||||
identityValue?.api_key_name ||
|
identityValue?.api_key_name ||
|
||||||
@@ -135,15 +125,8 @@ export const buildUsageLabel = (item: BreakdownItem, groupBy: UsageGroupBy) => {
|
|||||||
rawItem.api_key_name ||
|
rawItem.api_key_name ||
|
||||||
rawItem.access_key ||
|
rawItem.access_key ||
|
||||||
rawItem[groupBy] ||
|
rawItem[groupBy] ||
|
||||||
'-';
|
'-'
|
||||||
}
|
);
|
||||||
|
|
||||||
// Mark deleted entities in the chart legend / tooltip as text (a legend can't
|
|
||||||
// render a tag), matching the usage tabs. The id degrades to just "[Deleted]"
|
|
||||||
// when the backend nulls ``identity.current`` for a deleted entity.
|
|
||||||
const deletedWord = getIntl().formatMessage({ id: 'usage.table.deleted' });
|
|
||||||
const id = groupItem?.identity?.current?.[GROUP_ID_KEY[groupBy]];
|
|
||||||
return withDeletedMark(baseLabel, groupItem?.deleted, deletedWord, id);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUsageResponseItems = (
|
export const getUsageResponseItems = (
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import useUserDirectory from '../hooks/use-user-directory';
|
|||||||
/**
|
/**
|
||||||
* Owner tag for template cards, disambiguating same-name templates in
|
* Owner tag for template cards, disambiguating same-name templates in
|
||||||
* the admin's cross-tenant view. Renders nothing for non-admin callers
|
* the admin's cross-tenant view. Renders nothing for non-admin callers
|
||||||
* (gated on `canSeeAdmin`) and when a plugin provides its own
|
* (the management page is `mine`-scoped for them) and when a plugin
|
||||||
* `OwnerScopeTag` slot.
|
* provides its own `OwnerScopeTag` slot.
|
||||||
*/
|
*/
|
||||||
const OwnerTag: React.FC<{ ownerId?: number | null }> = ({ ownerId }) => {
|
const OwnerTag: React.FC<{ ownerId?: number | null }> = ({ ownerId }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|||||||
@@ -10,18 +10,12 @@ import useOverlayLayout from '../hooks/use-overlay-layout';
|
|||||||
|
|
||||||
interface StorageOverlayProps {
|
interface StorageOverlayProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
// Org the surrounding instance create form targets (platform admin "All"
|
|
||||||
// view). The storage inherits this scope, so the type list is pinned to
|
|
||||||
// it — the picker only offers types that org can reference. Undefined
|
|
||||||
// when there's no create-scope picker (the ambient org context applies).
|
|
||||||
scopeOrgId?: number | null;
|
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSubmit: (values: StorageFormData) => Promise<void> | void;
|
onSubmit: (values: StorageFormData) => Promise<void> | void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StorageOverlay: React.FC<StorageOverlayProps> = ({
|
const StorageOverlay: React.FC<StorageOverlayProps> = ({
|
||||||
open,
|
open,
|
||||||
scopeOrgId,
|
|
||||||
onCancel,
|
onCancel,
|
||||||
onSubmit
|
onSubmit
|
||||||
}) => {
|
}) => {
|
||||||
@@ -34,14 +28,9 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
fetchStorageClass(
|
fetchStorageClass({ page: -1 });
|
||||||
{ page: -1 },
|
|
||||||
scopeOrgId != null
|
|
||||||
? { headers: { 'X-Organization-Id': String(scopeOrgId) } }
|
|
||||||
: undefined
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}, [open, scopeOrgId]);
|
}, [open]);
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
formRef.current?.submit();
|
formRef.current?.submit();
|
||||||
@@ -86,7 +75,6 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
|
|||||||
ref={formRef}
|
ref={formRef}
|
||||||
action={PageAction.CREATE}
|
action={PageAction.CREATE}
|
||||||
open={open}
|
open={open}
|
||||||
showOrgScope={false}
|
|
||||||
onFinish={handleFinish}
|
onFinish={handleFinish}
|
||||||
/>
|
/>
|
||||||
</FormContext.Provider>
|
</FormContext.Provider>
|
||||||
|
|||||||
@@ -30,10 +30,6 @@ const StorageVolume = ({
|
|||||||
const { getRuleMessage } = useAppUtils();
|
const { getRuleMessage } = useAppUtils();
|
||||||
const form = Form.useFormInstance<FormData>();
|
const form = Form.useFormInstance<FormData>();
|
||||||
const storageMode = Form.useWatch('storageMode', form);
|
const storageMode = Form.useWatch('storageMode', form);
|
||||||
// Owned by the instance create-scope picker (platform admin "All" view).
|
|
||||||
// A storage added inline belongs to the same org as the instance, so pass
|
|
||||||
// it to the overlay to scope the storage-type list to that org.
|
|
||||||
const scopeOrgId = Form.useWatch('organization_id', form);
|
|
||||||
const { fetchData: createStorage } = useCreateStorage();
|
const { fetchData: createStorage } = useCreateStorage();
|
||||||
const { detailData: storageData, fetchData: fetchStorage } =
|
const { detailData: storageData, fetchData: fetchStorage } =
|
||||||
useQueryStorage();
|
useQueryStorage();
|
||||||
@@ -236,7 +232,6 @@ const StorageVolume = ({
|
|||||||
|
|
||||||
<StorageOverlay
|
<StorageOverlay
|
||||||
open={overlayOpen}
|
open={overlayOpen}
|
||||||
scopeOrgId={scopeOrgId}
|
|
||||||
onCancel={() => setOverlayOpen(false)}
|
onCancel={() => setOverlayOpen(false)}
|
||||||
onSubmit={handleCreateStorage}
|
onSubmit={handleCreateStorage}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -271,7 +271,6 @@ const GPUService: React.FC = () => {
|
|||||||
if (!clusterLoading && !hasK8sCluster) {
|
if (!clusterLoading && !hasK8sCluster) {
|
||||||
return (
|
return (
|
||||||
<NoResult
|
<NoResult
|
||||||
minHeight="calc(100vh - 300px)"
|
|
||||||
loading={dataSource.loading || clusterLoading}
|
loading={dataSource.loading || clusterLoading}
|
||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={[]}
|
dataSource={[]}
|
||||||
@@ -295,7 +294,6 @@ const GPUService: React.FC = () => {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<NoResult
|
<NoResult
|
||||||
minHeight="calc(100vh - 300px)"
|
|
||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
@@ -362,7 +360,6 @@ const GPUService: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<ConfigProvider renderEmpty={renderEmpty}>
|
<ConfigProvider renderEmpty={renderEmpty}>
|
||||||
<Table
|
<Table
|
||||||
className={'scroll-table'}
|
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
|
|||||||
@@ -110,7 +110,6 @@ const GPUServicePublicKeys: React.FC = () => {
|
|||||||
if (type !== 'Table') return;
|
if (type !== 'Table') return;
|
||||||
return (
|
return (
|
||||||
<NoResult
|
<NoResult
|
||||||
minHeight="calc(100vh - 300px)"
|
|
||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
@@ -154,7 +153,6 @@ const GPUServicePublicKeys: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<ConfigProvider renderEmpty={renderEmpty}>
|
<ConfigProvider renderEmpty={renderEmpty}>
|
||||||
<Table
|
<Table
|
||||||
className={'scroll-table'}
|
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
|
|||||||
@@ -1,23 +1,6 @@
|
|||||||
import { StatusMaps } from '@/config';
|
|
||||||
import { StatusType } from '@/config/types';
|
|
||||||
import { icons } from '@gpustack/core-ui';
|
import { icons } from '@gpustack/core-ui';
|
||||||
import { StorageTypeKind } from './types';
|
import { StorageTypeKind } from './types';
|
||||||
|
|
||||||
export const StorageTypePhaseValueMap = {
|
|
||||||
Ready: 'Ready',
|
|
||||||
Deleting: 'Deleting'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const StorageTypePhaseLabelMap: Record<string, string> = {
|
|
||||||
[StorageTypePhaseValueMap.Ready]: 'Ready',
|
|
||||||
[StorageTypePhaseValueMap.Deleting]: 'Deleting'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const status: Record<string, StatusType> = {
|
|
||||||
[StorageTypePhaseValueMap.Ready]: StatusMaps.success,
|
|
||||||
[StorageTypePhaseValueMap.Deleting]: StatusMaps.warning
|
|
||||||
};
|
|
||||||
|
|
||||||
export const StorageTypeKindValueMap: Record<string, StorageTypeKind> = {
|
export const StorageTypeKindValueMap: Record<string, StorageTypeKind> = {
|
||||||
NFS: 'nfs',
|
NFS: 'nfs',
|
||||||
S3: 's3'
|
S3: 's3'
|
||||||
|
|||||||
@@ -44,8 +44,4 @@ export interface ListItem {
|
|||||||
nfs?: StorageTypeNFS | null;
|
nfs?: StorageTypeNFS | null;
|
||||||
s3?: Omit<StorageTypeS3, 'secretKey'> | null;
|
s3?: Omit<StorageTypeS3, 'secretKey'> | null;
|
||||||
};
|
};
|
||||||
status?: {
|
|
||||||
phase?: string | null;
|
|
||||||
phaseMessage?: string | null;
|
|
||||||
} | null;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,19 +5,13 @@ import {
|
|||||||
AutoTooltip,
|
AutoTooltip,
|
||||||
DropdownButtons,
|
DropdownButtons,
|
||||||
IconFont,
|
IconFont,
|
||||||
StatusTag,
|
|
||||||
ThemeTag
|
ThemeTag
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import type { ColumnsType } from 'antd/lib/table';
|
import type { ColumnsType } from 'antd/lib/table';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import {
|
import { rowActionList, StorageTypeKindLabelMap } from '../config';
|
||||||
rowActionList,
|
|
||||||
status,
|
|
||||||
StorageTypeKindLabelMap,
|
|
||||||
StorageTypePhaseLabelMap
|
|
||||||
} from '../config';
|
|
||||||
import { ListItem } from '../config/types';
|
import { ListItem } from '../config/types';
|
||||||
|
|
||||||
interface ColumnsHookProps {
|
interface ColumnsHookProps {
|
||||||
@@ -28,13 +22,21 @@ interface ColumnsHookProps {
|
|||||||
const getKindLabel = (record: ListItem) => {
|
const getKindLabel = (record: ListItem) => {
|
||||||
if (record.spec?.nfs)
|
if (record.spec?.nfs)
|
||||||
return (
|
return (
|
||||||
<ThemeTag color="cyan" icon={<FolderOutlined />}>
|
<ThemeTag
|
||||||
|
style={{ width: 'fit-content' }}
|
||||||
|
color="cyan"
|
||||||
|
icon={<FolderOutlined />}
|
||||||
|
>
|
||||||
{StorageTypeKindLabelMap.nfs}
|
{StorageTypeKindLabelMap.nfs}
|
||||||
</ThemeTag>
|
</ThemeTag>
|
||||||
);
|
);
|
||||||
if (record.spec?.s3)
|
if (record.spec?.s3)
|
||||||
return (
|
return (
|
||||||
<ThemeTag color="green" icon={<IconFont type="icon-database" />}>
|
<ThemeTag
|
||||||
|
style={{ width: 'fit-content' }}
|
||||||
|
color="green"
|
||||||
|
icon={<IconFont type="icon-database" />}
|
||||||
|
>
|
||||||
{StorageTypeKindLabelMap.s3}
|
{StorageTypeKindLabelMap.s3}
|
||||||
</ThemeTag>
|
</ThemeTag>
|
||||||
);
|
);
|
||||||
@@ -79,24 +81,6 @@ const useStorageTypeColumns = ({
|
|||||||
sorter: false,
|
sorter: false,
|
||||||
render: (_text, record) => getKindLabel(record)
|
render: (_text, record) => getKindLabel(record)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: intl.formatMessage({ id: 'common.table.status' }),
|
|
||||||
dataIndex: ['status', 'phase'],
|
|
||||||
key: 'status',
|
|
||||||
sorter: false,
|
|
||||||
render: (value: string, record: ListItem) =>
|
|
||||||
value ? (
|
|
||||||
<StatusTag
|
|
||||||
statusValue={{
|
|
||||||
status: status[value],
|
|
||||||
text: StorageTypePhaseLabelMap[value] || value,
|
|
||||||
message: record?.status?.phaseMessage || ''
|
|
||||||
}}
|
|
||||||
></StatusTag>
|
|
||||||
) : (
|
|
||||||
'-'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
...creatorCols,
|
...creatorCols,
|
||||||
// {
|
// {
|
||||||
// title: intl.formatMessage({ id: 'common.table.description' }),
|
// title: intl.formatMessage({ id: 'common.table.description' }),
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ const GPUServiceStorageTypes: React.FC = () => {
|
|||||||
if (type !== 'Table') return;
|
if (type !== 'Table') return;
|
||||||
return (
|
return (
|
||||||
<NoResult
|
<NoResult
|
||||||
minHeight="calc(100vh - 300px)"
|
|
||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
@@ -150,7 +149,6 @@ const GPUServiceStorageTypes: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<ConfigProvider renderEmpty={renderEmpty}>
|
<ConfigProvider renderEmpty={renderEmpty}>
|
||||||
<Table
|
<Table
|
||||||
className={'scroll-table'}
|
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
|
|||||||
@@ -51,11 +51,6 @@ export async function queryStorageClass(
|
|||||||
return request<Global.PageResponse<StorageClassItem>>(STORAGE_CLASS_API, {
|
return request<Global.PageResponse<StorageClassItem>>(STORAGE_CLASS_API, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params,
|
params,
|
||||||
// `headers` lets the create form pin the request to a specific org so
|
|
||||||
// the picker only offers storage types that org can reference. GETs
|
|
||||||
// otherwise inherit the ambient org context, which is empty in the
|
|
||||||
// platform-admin "All" view and would list every org's types.
|
|
||||||
headers: options?.headers,
|
|
||||||
cancelToken: options?.token
|
cancelToken: options?.token
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import useSubmitLock from '@/hooks/use-submit-lock';
|
import useSubmitLock from '@/hooks/use-submit-lock';
|
||||||
import { FormDrawer, ModalFooter } from '@gpustack/core-ui';
|
import { FormDrawer, ModalFooter } from '@gpustack/core-ui';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { FormContext } from '../config/form-context';
|
import { FormContext } from '../config/form-context';
|
||||||
import { FormData, ListItem } from '../config/types';
|
import { FormData, ListItem } from '../config/types';
|
||||||
import GPUServiceStorageForm from '../forms';
|
import GPUServiceStorageForm from '../forms';
|
||||||
import useQueryStorageClass from '../services/use-query-storage-class';
|
|
||||||
|
|
||||||
type AddModalProps = {
|
type AddModalProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -14,6 +13,7 @@ type AddModalProps = {
|
|||||||
onOk: (values: FormData) => void;
|
onOk: (values: FormData) => void;
|
||||||
data?: ListItem | null;
|
data?: ListItem | null;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
storageClassList: Global.BaseOption<string>[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddModal: React.FC<AddModalProps> = ({
|
const AddModal: React.FC<AddModalProps> = ({
|
||||||
@@ -22,33 +22,11 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
open,
|
open,
|
||||||
onOk,
|
onOk,
|
||||||
data,
|
data,
|
||||||
onCancel
|
onCancel,
|
||||||
|
storageClassList
|
||||||
}) => {
|
}) => {
|
||||||
const form = useRef<any>(null);
|
const form = useRef<any>(null);
|
||||||
const { loading, guard, run, release } = useSubmitLock();
|
const { loading, guard, run, release } = useSubmitLock();
|
||||||
// The dropdown gets its own storage-type list, scoped to the org the
|
|
||||||
// create-scope picker targets — distinct from the page-level list, which
|
|
||||||
// stays unscoped so the table can label every org's rows.
|
|
||||||
const { storageClassList, fetchData: fetchStorageClass } =
|
|
||||||
useQueryStorageClass();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
fetchStorageClass({ page: -1 });
|
|
||||||
}
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
// Platform admin picked a target org in the create-scope slot: reload the
|
|
||||||
// storage-type list pinned to that org so the dropdown only offers types
|
|
||||||
// the org can reference (its own plus any reachable via cluster access).
|
|
||||||
const handleScopeChange = (orgId?: number | null) => {
|
|
||||||
fetchStorageClass(
|
|
||||||
{ page: -1 },
|
|
||||||
orgId != null
|
|
||||||
? { headers: { 'X-Organization-Id': String(orgId) } }
|
|
||||||
: undefined
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
guard(() => form.current?.submit());
|
guard(() => form.current?.submit());
|
||||||
@@ -92,7 +70,6 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
ref={form}
|
ref={form}
|
||||||
action={action}
|
action={action}
|
||||||
currentData={data}
|
currentData={data}
|
||||||
onScopeChange={handleScopeChange}
|
|
||||||
onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
onFinishFailed={release}
|
onFinishFailed={release}
|
||||||
open={open}
|
open={open}
|
||||||
|
|||||||
@@ -3,18 +3,21 @@ import { StatusType } from '@/config/types';
|
|||||||
import { icons } from '@gpustack/core-ui';
|
import { icons } from '@gpustack/core-ui';
|
||||||
|
|
||||||
export const StoragePhaseValueMap = {
|
export const StoragePhaseValueMap = {
|
||||||
Ready: 'Ready',
|
Available: 'Available',
|
||||||
Deleting: 'Deleting'
|
Unavailable: 'Unavailable',
|
||||||
|
Pending: 'Pending'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StoragePhaseLabelMap: Record<string, string> = {
|
export const StoragePhaseLabelMap: Record<string, string> = {
|
||||||
[StoragePhaseValueMap.Ready]: 'Ready',
|
[StoragePhaseValueMap.Available]: 'Available',
|
||||||
[StoragePhaseValueMap.Deleting]: 'Deleting'
|
[StoragePhaseValueMap.Unavailable]: 'Unavailable',
|
||||||
|
[StoragePhaseValueMap.Pending]: 'Pending'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const status: Record<string, StatusType> = {
|
export const status: Record<string, StatusType> = {
|
||||||
[StoragePhaseValueMap.Ready]: StatusMaps.success,
|
[StoragePhaseValueMap.Available]: StatusMaps.success,
|
||||||
[StoragePhaseValueMap.Deleting]: StatusMaps.warning
|
[StoragePhaseValueMap.Unavailable]: StatusMaps.error,
|
||||||
|
[StoragePhaseValueMap.Pending]: StatusMaps.transitioning
|
||||||
};
|
};
|
||||||
|
|
||||||
export const rowActionList = [
|
export const rowActionList = [
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export interface ListItem {
|
|||||||
};
|
};
|
||||||
status?: {
|
status?: {
|
||||||
phase?: string | null;
|
phase?: string | null;
|
||||||
phaseMessage?: string | null;
|
|
||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,23 +12,7 @@ import { useContext } from 'react';
|
|||||||
import { FormContext } from '../config/form-context';
|
import { FormContext } from '../config/form-context';
|
||||||
import { FormData } from '../config/types';
|
import { FormData } from '../config/types';
|
||||||
|
|
||||||
const Basic = ({
|
const Basic = ({ action, open }: { action: string; open: boolean }) => {
|
||||||
action,
|
|
||||||
open,
|
|
||||||
showOrgScope = true,
|
|
||||||
onOrgScopeChange
|
|
||||||
}: {
|
|
||||||
action: string;
|
|
||||||
open: boolean;
|
|
||||||
// Hosts that already fix the tenant scope elsewhere (e.g. the instance
|
|
||||||
// create form, which owns the org picker) hide this slot so the storage
|
|
||||||
// is created in the surrounding scope rather than a second, conflicting
|
|
||||||
// one.
|
|
||||||
showOrgScope?: boolean;
|
|
||||||
// Fired on a user selection in the create-scope picker so the host can
|
|
||||||
// reload the org-scoped storage-type list.
|
|
||||||
onOrgScopeChange?: (orgId: number | null | undefined) => void;
|
|
||||||
}) => {
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { getRuleMessage } = useAppUtils();
|
const { getRuleMessage } = useAppUtils();
|
||||||
const { storageClassList } = useContext(FormContext);
|
const { storageClassList } = useContext(FormContext);
|
||||||
@@ -60,12 +44,7 @@ const Basic = ({
|
|||||||
label={intl.formatMessage({ id: 'common.table.displayName' })}
|
label={intl.formatMessage({ id: 'common.table.displayName' })}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{showOrgScope && (
|
<PluginExtraFields name="CreateOrgScopeField" context={{ action }} />
|
||||||
<PluginExtraFields
|
|
||||||
name="CreateOrgScopeField"
|
|
||||||
context={{ action, onChange: onOrgScopeChange }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Flex gap={16}>
|
<Flex gap={16}>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
|
||||||
import { Form } from 'antd';
|
import { Form } from 'antd';
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
import { forwardRef, useEffect, useImperativeHandle } from 'react';
|
||||||
import { FormData, ListItem } from '../config/types';
|
import { FormData, ListItem } from '../config/types';
|
||||||
import Basic from './basic';
|
import Basic from './basic';
|
||||||
|
|
||||||
@@ -11,67 +10,14 @@ interface StorageFormProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
action: PageActionType;
|
action: PageActionType;
|
||||||
currentData?: ListItem | null;
|
currentData?: ListItem | null;
|
||||||
// Hide the create-scope org picker when the host already fixes the
|
|
||||||
// tenant scope (e.g. the instance create form). Defaults to shown.
|
|
||||||
showOrgScope?: boolean;
|
|
||||||
// Fired when the create-scope picker (platform admin "All" view)
|
|
||||||
// retargets the form to another org. Only emitted on genuine changes,
|
|
||||||
// never on the initial mount, and never in builds where the picker
|
|
||||||
// isn't mounted (the watched field stays undefined). Lets the parent
|
|
||||||
// reload the org-scoped storage-type options.
|
|
||||||
onScopeChange?: (orgId: number | null | undefined) => void;
|
|
||||||
onFinish: (values: FormData) => Promise<void>;
|
onFinish: (values: FormData) => Promise<void>;
|
||||||
onFinishFailed?: (errorInfo: any) => void;
|
onFinishFailed?: (errorInfo: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GPUServiceStorageForm: React.FC<StorageFormProps> = forwardRef(
|
const GPUServiceStorageForm: React.FC<StorageFormProps> = forwardRef(
|
||||||
(props, ref) => {
|
(props, ref) => {
|
||||||
const {
|
const { action, currentData, open, onFinish, onFinishFailed } = props;
|
||||||
action,
|
|
||||||
currentData,
|
|
||||||
open,
|
|
||||||
showOrgScope = true,
|
|
||||||
onScopeChange,
|
|
||||||
onFinish,
|
|
||||||
onFinishFailed
|
|
||||||
} = props;
|
|
||||||
const [form] = Form.useForm<FormData>();
|
const [form] = Form.useForm<FormData>();
|
||||||
// `organization_id` is owned by the create-scope picker slot; it only
|
|
||||||
// exists/changes when a platform admin retargets the form. Watch it so
|
|
||||||
// the initial/default scope can be propagated once the picker resolves it.
|
|
||||||
const scopeOrgId = Form.useWatch('organization_id', form);
|
|
||||||
const scopeInitRef = useRef(true);
|
|
||||||
|
|
||||||
// Stable wrapper so the effect / change handler always call the latest
|
|
||||||
// callback without re-subscribing on the parent's fn identity.
|
|
||||||
const orgScope = useMemoizedFn((orgId?: number | null) => {
|
|
||||||
onScopeChange?.(orgId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Genuine retarget by the create-scope picker: drop the stale type pick
|
|
||||||
// (it may name a type the newly chosen org can't reference) and reload the
|
|
||||||
// org-scoped list. Wired to the picker's own onChange, so it fires only on
|
|
||||||
// a user selection — never on the picker's programmatic default.
|
|
||||||
const handleOrgScopeChange = useMemoizedFn((orgId?: number | null) => {
|
|
||||||
scopeInitRef.current = false;
|
|
||||||
form.setFieldValue(['spec', 'type'], undefined);
|
|
||||||
orgScope(orgId ?? null);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) {
|
|
||||||
// Re-arm the initial-scope propagation for the next open.
|
|
||||||
scopeInitRef.current = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Propagate the initial/default scope once the picker resolves it so the
|
|
||||||
// parent loads the org-scoped type list, leaving any pre-filled type
|
|
||||||
// intact. User-driven retargets go through handleOrgScopeChange instead.
|
|
||||||
if (scopeInitRef.current && scopeOrgId != null) {
|
|
||||||
scopeInitRef.current = false;
|
|
||||||
orgScope(scopeOrgId);
|
|
||||||
}
|
|
||||||
}, [open, scopeOrgId, orgScope]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
@@ -110,12 +56,7 @@ const GPUServiceStorageForm: React.FC<StorageFormProps> = forwardRef(
|
|||||||
preserve={false}
|
preserve={false}
|
||||||
initialValues={{}}
|
initialValues={{}}
|
||||||
>
|
>
|
||||||
<Basic
|
<Basic action={action} open={open} />
|
||||||
action={action}
|
|
||||||
open={open}
|
|
||||||
showOrgScope={showOrgScope}
|
|
||||||
onOrgScopeChange={handleOrgScopeChange}
|
|
||||||
/>
|
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import useCreatorColumn from '@/pages/gpu-service/hooks/use-creator-column';
|
import useCreatorColumn from '@/pages/gpu-service/hooks/use-creator-column';
|
||||||
import { usePluginListColumns } from '@/plugins/list-extra-columns';
|
import { usePluginListColumns } from '@/plugins/list-extra-columns';
|
||||||
import { AutoTooltip, DropdownButtons, StatusTag } from '@gpustack/core-ui';
|
import { AutoTooltip, DropdownButtons } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import type { ColumnsType } from 'antd/lib/table';
|
import type { ColumnsType } from 'antd/lib/table';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { rowActionList, status, StoragePhaseLabelMap } from '../config';
|
import { rowActionList } from '../config';
|
||||||
import { ListItem } from '../config/types';
|
import { ListItem } from '../config/types';
|
||||||
|
|
||||||
interface ColumnsHookProps {
|
interface ColumnsHookProps {
|
||||||
@@ -70,25 +70,24 @@ const useStorageColumns = ({
|
|||||||
sorter: false,
|
sorter: false,
|
||||||
render: (value: string) => (value ? value.replace(/Gi$/, 'GB') : '-')
|
render: (value: string) => (value ? value.replace(/Gi$/, 'GB') : '-')
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: intl.formatMessage({ id: 'common.table.status' }),
|
|
||||||
dataIndex: ['status', 'phase'],
|
|
||||||
key: 'status',
|
|
||||||
sorter: false,
|
|
||||||
render: (value: string, record: ListItem) =>
|
|
||||||
value ? (
|
|
||||||
<StatusTag
|
|
||||||
statusValue={{
|
|
||||||
status: status[value],
|
|
||||||
text: StoragePhaseLabelMap[value] || value,
|
|
||||||
message: record?.status?.phaseMessage || ''
|
|
||||||
}}
|
|
||||||
></StatusTag>
|
|
||||||
) : (
|
|
||||||
'-'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
...creatorCols,
|
...creatorCols,
|
||||||
|
// {
|
||||||
|
// title: intl.formatMessage({ id: 'common.table.status' }),
|
||||||
|
// dataIndex: ['status', 'phase'],
|
||||||
|
// key: 'status',
|
||||||
|
// sorter: false,
|
||||||
|
// render: (value: string) =>
|
||||||
|
// value ? (
|
||||||
|
// <StatusTag
|
||||||
|
// statusValue={{
|
||||||
|
// status: status[value],
|
||||||
|
// text: StoragePhaseLabelMap[value] || value
|
||||||
|
// }}
|
||||||
|
// ></StatusTag>
|
||||||
|
// ) : (
|
||||||
|
// '-'
|
||||||
|
// )
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
||||||
dataIndex: 'created_at',
|
dataIndex: 'created_at',
|
||||||
|
|||||||
@@ -109,7 +109,6 @@ const GPUServiceStorage: React.FC = () => {
|
|||||||
if (type !== 'Table') return;
|
if (type !== 'Table') return;
|
||||||
return (
|
return (
|
||||||
<NoResult
|
<NoResult
|
||||||
minHeight="calc(100vh - 300px)"
|
|
||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
@@ -151,7 +150,6 @@ const GPUServiceStorage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<ConfigProvider renderEmpty={renderEmpty}>
|
<ConfigProvider renderEmpty={renderEmpty}>
|
||||||
<Table
|
<Table
|
||||||
className={'scroll-table'}
|
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
@@ -179,6 +177,7 @@ const GPUServiceStorage: React.FC = () => {
|
|||||||
action={openStorageModalStatus.action}
|
action={openStorageModalStatus.action}
|
||||||
title={openStorageModalStatus.title}
|
title={openStorageModalStatus.title}
|
||||||
data={openStorageModalStatus.currentData}
|
data={openStorageModalStatus.currentData}
|
||||||
|
storageClassList={storageClassList}
|
||||||
onCancel={closeStorageModal}
|
onCancel={closeStorageModal}
|
||||||
onOk={handleModalOk}
|
onOk={handleModalOk}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
TemplateCard,
|
TemplateCard,
|
||||||
ThemeTag
|
ThemeTag
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useAccess, useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Tag } from 'antd';
|
import { Button, Tag } from 'antd';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@@ -144,26 +144,8 @@ interface TemplateCardProps {
|
|||||||
|
|
||||||
const TemplateCardItem: React.FC<TemplateCardProps> = ({ data, onSelect }) => {
|
const TemplateCardItem: React.FC<TemplateCardProps> = ({ data, onSelect }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const access = useAccess();
|
|
||||||
const { isDarkTheme } = useUserSettings();
|
const { isDarkTheme } = useUserSettings();
|
||||||
|
|
||||||
// Only an explicit NULL owner (Global, admin-curated) is admin-only.
|
|
||||||
// A principal-owned row reaches a non-admin's list only when they own
|
|
||||||
// it, and an absent id (single-owner builds omit it on the wire) has
|
|
||||||
// no tenancy to restrict — both are manageable, so the check is strict
|
|
||||||
// ``!== null``. Clone stays available regardless — it reads the source
|
|
||||||
// and creates a fresh copy in the caller's own scope — so a non-admin
|
|
||||||
// can fork a Global preset into their org.
|
|
||||||
const canManage = !!access.canSeeAdmin || data.owner_principal_id !== null;
|
|
||||||
|
|
||||||
const actions = useMemo(
|
|
||||||
() =>
|
|
||||||
canManage
|
|
||||||
? templateActions
|
|
||||||
: templateActions.filter((action) => action.key === 'clone'),
|
|
||||||
[canManage]
|
|
||||||
);
|
|
||||||
|
|
||||||
const manufacturerLabelMap: Record<string, string> = useMemo(() => {
|
const manufacturerLabelMap: Record<string, string> = useMemo(() => {
|
||||||
return Object.values(GPUsConfigs).reduce(
|
return Object.values(GPUsConfigs).reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
@@ -256,7 +238,7 @@ const TemplateCardItem: React.FC<TemplateCardProps> = ({ data, onSelect }) => {
|
|||||||
<span onClick={handleonClickAction} className="operations">
|
<span onClick={handleonClickAction} className="operations">
|
||||||
<DropdownActions
|
<DropdownActions
|
||||||
menu={{
|
menu={{
|
||||||
items: actions,
|
items: templateActions,
|
||||||
onClick: handleOnSelect
|
onClick: handleOnSelect
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -114,12 +114,6 @@ export const templateActions: Array<{
|
|||||||
locale: true,
|
locale: true,
|
||||||
icon: icons.EditOutlined
|
icon: icons.EditOutlined
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'common.button.clone',
|
|
||||||
key: 'clone',
|
|
||||||
locale: true,
|
|
||||||
icon: icons.CopyOutlined
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'common.button.delete',
|
label: 'common.button.delete',
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { PageAction } from '@/config';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import { Form } from 'antd';
|
import { Form } from 'antd';
|
||||||
import { forwardRef, useEffect, useImperativeHandle } from 'react';
|
import { forwardRef, useEffect, useImperativeHandle } from 'react';
|
||||||
@@ -25,13 +26,11 @@ const GPUServiceTemplateForm: React.FC<TemplateFormProps> = forwardRef(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefill on Edit and on Clone (Create carrying a source row).
|
if (action === PageAction.EDIT && currentData) {
|
||||||
// A plain Create opens with no ``currentData`` and keeps the
|
|
||||||
// blank ``initialValues``.
|
|
||||||
if (currentData) {
|
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
...currentData
|
...currentData
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}, [action, currentData, form, open]);
|
}, [action, currentData, form, open]);
|
||||||
|
|
||||||
|
|||||||
@@ -45,11 +45,13 @@ const GPUServiceTemplates: React.FC = () => {
|
|||||||
isInfiniteScroll: true,
|
isInfiniteScroll: true,
|
||||||
contentForDelete: intl.formatMessage({ id: 'gpuservice.template' }),
|
contentForDelete: intl.formatMessage({ id: 'gpuservice.template' }),
|
||||||
defaultQueryParams: {
|
defaultQueryParams: {
|
||||||
// Default (non-``mine``) scope: Global rows plus rows owned by the
|
perPage: 24,
|
||||||
// caller's current principal, matching the Inference Backend page.
|
// Management view: drop Global rows for non-admin callers — the
|
||||||
// Global rows a non-admin can't edit render read-only — the card
|
// page is a CRUD surface, and admin-curated Global templates
|
||||||
// gates Edit/Delete on ownership.
|
// they can't edit only add visual noise. The instance-create
|
||||||
perPage: 24
|
// picker (uses ``useQueryTemplates`` separately) doesn't set
|
||||||
|
// this and so still sees Global presets.
|
||||||
|
mine: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const { openTemplateModalStatus, openTemplateModal, closeTemplateModal } =
|
const { openTemplateModalStatus, openTemplateModal, closeTemplateModal } =
|
||||||
@@ -90,42 +92,6 @@ const GPUServiceTemplates: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloneTemplate = (row: ListItem) => {
|
|
||||||
// Clone reuses the create flow: drop the source's identity and
|
|
||||||
// ownership so the backend assigns a fresh id and scopes the copy
|
|
||||||
// to the caller's own principal. The prefilled name is editable —
|
|
||||||
// the create endpoint enforces per-owner name uniqueness.
|
|
||||||
const source = _.omit(row, [
|
|
||||||
'id',
|
|
||||||
'owner_principal_id',
|
|
||||||
'creator_id',
|
|
||||||
'created_at',
|
|
||||||
'updated_at',
|
|
||||||
'status'
|
|
||||||
]) as ListItem;
|
|
||||||
// Default to a ``-clone`` suffix so cloning within the same scope
|
|
||||||
// (e.g. a Global preset kept Global) doesn't immediately collide on
|
|
||||||
// the unique name. Keep within the 63-char name limit by trimming
|
|
||||||
// the base first.
|
|
||||||
const suffix = '-clone';
|
|
||||||
const base = (row.name ?? '').slice(0, 63 - suffix.length);
|
|
||||||
source.name = `${base}${suffix}`;
|
|
||||||
// The card renders ``displayName || name``, so a copied displayName
|
|
||||||
// would make the clone indistinguishable from its source. Append a
|
|
||||||
// localized clone label, trimmed to the same 63-char field limit.
|
|
||||||
if (row.displayName) {
|
|
||||||
const cloneLabel = intl.formatMessage({ id: 'common.button.clone' });
|
|
||||||
const displaySuffix = ` (${cloneLabel})`;
|
|
||||||
const displayBase = row.displayName.slice(0, 63 - displaySuffix.length);
|
|
||||||
source.displayName = `${displayBase}${displaySuffix}`;
|
|
||||||
}
|
|
||||||
openTemplateModal(
|
|
||||||
PageAction.CREATE,
|
|
||||||
intl.formatMessage({ id: 'gpuservice.template.clone' }),
|
|
||||||
source
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleModalOk = async (data: FormData) => {
|
const handleModalOk = async (data: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (openTemplateModalStatus.action === PageAction.EDIT) {
|
if (openTemplateModalStatus.action === PageAction.EDIT) {
|
||||||
@@ -148,10 +114,6 @@ const GPUServiceTemplates: React.FC = () => {
|
|||||||
handleEditTemplate(item.data);
|
handleEditTemplate(item.data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (item.action === 'clone') {
|
|
||||||
handleCloneTemplate(item.data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (item.action === 'delete') {
|
if (item.action === 'delete') {
|
||||||
handleDelete({ ...item.data, name: item.data.name });
|
handleDelete({ ...item.data, name: item.data.name });
|
||||||
}
|
}
|
||||||
@@ -210,7 +172,6 @@ const GPUServiceTemplates: React.FC = () => {
|
|||||||
onSelect={handleOnSelect}
|
onSelect={handleOnSelect}
|
||||||
/>
|
/>
|
||||||
<NoResult
|
<NoResult
|
||||||
minHeight="calc(100vh - 300px)"
|
|
||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
|
|||||||
@@ -166,7 +166,6 @@ const Catalog: React.FC = () => {
|
|||||||
isFirst={!dataSource.loadend}
|
isFirst={!dataSource.loadend}
|
||||||
></CatalogList>
|
></CatalogList>
|
||||||
<NoResult
|
<NoResult
|
||||||
minHeight="calc(100vh - 300px)"
|
|
||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ const Metric: React.FC<{ label: React.ReactNode; value: React.ReactNode }> = ({
|
|||||||
value
|
value
|
||||||
}) => (
|
}) => (
|
||||||
<span style={{ lineHeight: 1.2 }}>
|
<span style={{ lineHeight: 1.2 }}>
|
||||||
{label}: <span>{value}</span>
|
{label}:{' '}
|
||||||
|
<span className="font-500" style={{ color: 'var(--ant-color-text)' }}>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -67,7 +70,7 @@ const GPUCard: React.FC<{
|
|||||||
}
|
}
|
||||||
description={
|
description={
|
||||||
info || (
|
info || (
|
||||||
<Flex wrap gap={8} style={{ fontSize: 11 }}>
|
<Flex wrap gap={8} style={{ fontSize: 13 }}>
|
||||||
<Metric
|
<Metric
|
||||||
label={intl.formatMessage({ id: 'resources.table.total' })}
|
label={intl.formatMessage({ id: 'resources.table.total' })}
|
||||||
value={convertFileSize(data?.memory?.total || 0)}
|
value={convertFileSize(data?.memory?.total || 0)}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ListItem as WorkerListItem } from '@/pages/resources/config/types';
|
import { ListItem as WorkerListItem } from '@/pages/resources/config/types';
|
||||||
import { AutoTooltip, ExpandedRowGrid } from '@gpustack/core-ui';
|
import { AutoTooltip, RowChildren } from '@gpustack/core-ui';
|
||||||
|
import { Col, Row } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ModelInstanceListItem } from '../../config/types';
|
import { ModelInstanceListItem } from '../../config/types';
|
||||||
@@ -10,17 +11,11 @@ import DistributeInfoCell from '../instance-cells/distribute-info-cell';
|
|||||||
import DownloadingStatusCell from '../instance-cells/downloading-status-cell';
|
import DownloadingStatusCell from '../instance-cells/downloading-status-cell';
|
||||||
import InstanceStatusCell from '../instance-cells/instance-status-cell';
|
import InstanceStatusCell from '../instance-cells/instance-status-cell';
|
||||||
import NameCell from '../instance-cells/name-cell';
|
import NameCell from '../instance-cells/name-cell';
|
||||||
|
|
||||||
interface InstanceItemProps {
|
interface InstanceItemProps {
|
||||||
instanceData: ModelInstanceListItem;
|
instanceData: ModelInstanceListItem;
|
||||||
workerList: WorkerListItem[];
|
workerList: WorkerListItem[];
|
||||||
modelData?: any;
|
modelData?: any;
|
||||||
defaultOpenId: string;
|
defaultOpenId: string;
|
||||||
// Column grid shared from the parent SealTable so this child row aligns
|
|
||||||
// its cells to the parent columns instead of guessing paddings.
|
|
||||||
gridTemplate?: string;
|
|
||||||
prefixWidth?: number;
|
|
||||||
columns?: any[];
|
|
||||||
handleChildSelect: (val: string, item: ModelInstanceListItem) => void;
|
handleChildSelect: (val: string, item: ModelInstanceListItem) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,43 +24,48 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
|
|||||||
workerList,
|
workerList,
|
||||||
modelData,
|
modelData,
|
||||||
defaultOpenId,
|
defaultOpenId,
|
||||||
gridTemplate,
|
|
||||||
prefixWidth = 0,
|
|
||||||
columns,
|
|
||||||
handleChildSelect
|
handleChildSelect
|
||||||
}) => {
|
}) => {
|
||||||
// The child row shares the parent's column grid; cells flow left-to-right and
|
|
||||||
// only declare a span, so there is no dependency on parent column keys.
|
|
||||||
// Parent layout: name (1) | middle plugin/source region | replicas,
|
|
||||||
// created_at, operation (last 3). The middle absorbs whatever columns sit
|
|
||||||
// between name and replicas (cluster_id, source, any plugin column).
|
|
||||||
const columnCount = columns?.length ?? 0;
|
|
||||||
const middleSpan = Math.max(columnCount - 4, 1);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExpandedRowGrid
|
<div style={{ borderRadius: 'var(--ant-table-header-border-radius)' }}>
|
||||||
gridTemplate={gridTemplate}
|
<RowChildren>
|
||||||
prefixWidth={prefixWidth}
|
<Row
|
||||||
style={{ color: 'var(--ant-color-text-secondary)' }}
|
style={{ width: '100%', color: 'var(--ant-color-text-secondary)' }}
|
||||||
|
align="middle"
|
||||||
|
>
|
||||||
|
<Col
|
||||||
|
span={6}
|
||||||
|
style={{
|
||||||
|
paddingInline: 'var(--ant-table-cell-padding-inline)'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ExpandedRowGrid.Cell span={1}>
|
|
||||||
<NameCell
|
<NameCell
|
||||||
record={instanceData}
|
record={instanceData}
|
||||||
modelData={modelData}
|
modelData={modelData}
|
||||||
defaultOpenId={defaultOpenId}
|
defaultOpenId={defaultOpenId}
|
||||||
></NameCell>
|
></NameCell>
|
||||||
</ExpandedRowGrid.Cell>
|
</Col>
|
||||||
<ExpandedRowGrid.Cell
|
<Col span={7}>
|
||||||
span={middleSpan}
|
<span
|
||||||
style={{ flexWrap: 'wrap', gap: 8 }}
|
style={{
|
||||||
|
paddingLeft: '58px',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: '8px'
|
||||||
|
}}
|
||||||
|
className="flex align-center"
|
||||||
>
|
>
|
||||||
<CPUOffloadingCell record={instanceData}></CPUOffloadingCell>
|
<CPUOffloadingCell record={instanceData}></CPUOffloadingCell>
|
||||||
<DistributeInfoCell
|
<DistributeInfoCell
|
||||||
record={instanceData}
|
record={instanceData}
|
||||||
workerList={workerList}
|
workerList={workerList}
|
||||||
></DistributeInfoCell>
|
></DistributeInfoCell>
|
||||||
</ExpandedRowGrid.Cell>
|
</span>
|
||||||
<ExpandedRowGrid.Cell span={1} style={{ gap: 4 }}>
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<span
|
||||||
|
style={{ paddingLeft: '40px', gap: 4 }}
|
||||||
|
className="flex-center"
|
||||||
|
>
|
||||||
<InstanceStatusCell
|
<InstanceStatusCell
|
||||||
record={instanceData}
|
record={instanceData}
|
||||||
onSelect={handleChildSelect}
|
onSelect={handleChildSelect}
|
||||||
@@ -76,20 +76,27 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
|
|||||||
workerList={workerList}
|
workerList={workerList}
|
||||||
record={instanceData}
|
record={instanceData}
|
||||||
></DownloadingStatusCell>
|
></DownloadingStatusCell>
|
||||||
</ExpandedRowGrid.Cell>
|
</span>
|
||||||
<ExpandedRowGrid.Cell span={1}>
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<span style={{ paddingLeft: 43 }} className="flex">
|
||||||
<AutoTooltip ghost>
|
<AutoTooltip ghost>
|
||||||
{dayjs(instanceData.created_at).format('YYYY-MM-DD HH:mm:ss')}
|
{dayjs(instanceData.created_at).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
</AutoTooltip>
|
</AutoTooltip>
|
||||||
</ExpandedRowGrid.Cell>
|
</span>
|
||||||
<ExpandedRowGrid.Cell span={1}>
|
</Col>
|
||||||
|
<Col span={3}>
|
||||||
|
<div style={{ paddingLeft: 36 }}>
|
||||||
<ActionsCell
|
<ActionsCell
|
||||||
record={instanceData}
|
record={instanceData}
|
||||||
modelData={modelData}
|
modelData={modelData}
|
||||||
onSelect={handleChildSelect}
|
onSelect={handleChildSelect}
|
||||||
></ActionsCell>
|
></ActionsCell>
|
||||||
</ExpandedRowGrid.Cell>
|
</div>
|
||||||
</ExpandedRowGrid>
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</RowChildren>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default InstanceItem;
|
export default InstanceItem;
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ interface InstanceItemProps {
|
|||||||
workerList: WorkerListItem[];
|
workerList: WorkerListItem[];
|
||||||
modelData?: any;
|
modelData?: any;
|
||||||
currentExpanded?: string;
|
currentExpanded?: string;
|
||||||
gridTemplate?: string;
|
|
||||||
prefixWidth?: number;
|
|
||||||
columns?: any[];
|
|
||||||
handleChildSelect: (val: string, item: ModelInstanceListItem) => void;
|
handleChildSelect: (val: string, item: ModelInstanceListItem) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,9 +25,6 @@ const Instances: React.FC<InstanceItemProps> = ({
|
|||||||
workerList,
|
workerList,
|
||||||
modelData,
|
modelData,
|
||||||
currentExpanded,
|
currentExpanded,
|
||||||
gridTemplate,
|
|
||||||
prefixWidth,
|
|
||||||
columns,
|
|
||||||
handleChildSelect
|
handleChildSelect
|
||||||
}) => {
|
}) => {
|
||||||
const [firstLoad, setFirstLoad] = React.useState(true);
|
const [firstLoad, setFirstLoad] = React.useState(true);
|
||||||
@@ -61,9 +55,6 @@ const Instances: React.FC<InstanceItemProps> = ({
|
|||||||
instanceData={item}
|
instanceData={item}
|
||||||
defaultOpenId={firstLoad ? defaultOpenId : ''}
|
defaultOpenId={firstLoad ? defaultOpenId : ''}
|
||||||
handleChildSelect={handleChildSelect}
|
handleChildSelect={handleChildSelect}
|
||||||
gridTemplate={gridTemplate}
|
|
||||||
prefixWidth={prefixWidth}
|
|
||||||
columns={columns}
|
|
||||||
></InstanceItem>
|
></InstanceItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
IconFont,
|
|
||||||
StatusTag,
|
StatusTag,
|
||||||
TagsWrapper,
|
TagsWrapper,
|
||||||
TemplateCard,
|
TemplateCard,
|
||||||
@@ -20,11 +19,7 @@ import {
|
|||||||
MyModelsStatusValueMap
|
MyModelsStatusValueMap
|
||||||
} from '../config';
|
} from '../config';
|
||||||
import { categoryToPathMap } from '../config/button-actions';
|
import { categoryToPathMap } from '../config/button-actions';
|
||||||
import {
|
import { getModelLogo } from '../utils/model-logo';
|
||||||
defaultModelLogo,
|
|
||||||
getCategoryLogo,
|
|
||||||
getModelLogo
|
|
||||||
} from '../utils/model-logo';
|
|
||||||
|
|
||||||
const CardWrapper = styled.div`
|
const CardWrapper = styled.div`
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -49,7 +44,7 @@ const ModelItemContent = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
cursor: pointer;
|
cursor: default;
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -90,8 +85,8 @@ const ModelItemContent = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const ModelLogo = styled.img`
|
const ModelLogo = styled.img`
|
||||||
width: 32px;
|
width: 24px;
|
||||||
height: 32px;
|
height: 24px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
flex: none;
|
flex: none;
|
||||||
@@ -132,23 +127,15 @@ const renderTag = (item: any, index = 0) => {
|
|||||||
|
|
||||||
const ModelItem: React.FC<{
|
const ModelItem: React.FC<{
|
||||||
model: Record<string, any>;
|
model: Record<string, any>;
|
||||||
onClick?: (model: Record<string, any>) => void;
|
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const { model, onClick } = props;
|
const { model } = props;
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleCardClick = () => {
|
|
||||||
onClick?.(model);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ``model.name`` from ``/v2/my-models`` is the OpenAI-style id
|
// ``model.name`` from ``/v2/my-models`` is the OpenAI-style id
|
||||||
// (org-prefixed for non-platform routes, bare for platform). Use it
|
// (org-prefixed for non-platform routes, bare for platform). Use it
|
||||||
// verbatim — the playground / dispatcher both key off that exact id.
|
// verbatim — the playground / dispatcher both key off that exact id.
|
||||||
const handleOpenPlayGroundClick = (e: React.MouseEvent) => {
|
const handleOpenPlayGroundClick = () => {
|
||||||
// Card is clickable (opens API access info); keep the playground
|
|
||||||
// action isolated so it doesn't also trigger the card click.
|
|
||||||
e.stopPropagation();
|
|
||||||
const modelName = encodeURIComponent(model.name);
|
const modelName = encodeURIComponent(model.name);
|
||||||
for (const [category, path] of Object.entries(categoryToPathMap)) {
|
for (const [category, path] of Object.entries(categoryToPathMap)) {
|
||||||
if (
|
if (
|
||||||
@@ -169,11 +156,6 @@ const ModelItem: React.FC<{
|
|||||||
navigate(`/playground/chat?model=${modelName}`);
|
navigate(`/playground/chat?model=${modelName}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Logo priority: brand logo matched from the name → tinted category
|
|
||||||
// icon (from model_icons) → generic default image.
|
|
||||||
const brandLogo = getModelLogo(model.name);
|
|
||||||
const categoryLogo = brandLogo ? null : getCategoryLogo(model.categories);
|
|
||||||
|
|
||||||
// context length
|
// context length
|
||||||
const maxToken = useMemo(() => {
|
const maxToken = useMemo(() => {
|
||||||
const meta = model.meta || {};
|
const meta = model.meta || {};
|
||||||
@@ -196,20 +178,13 @@ const ModelItem: React.FC<{
|
|||||||
<CardWrapper>
|
<CardWrapper>
|
||||||
<TemplateCard
|
<TemplateCard
|
||||||
height={140}
|
height={140}
|
||||||
clickable={true}
|
clickable={false}
|
||||||
hoverable={true}
|
hoverable={true}
|
||||||
ghost
|
ghost
|
||||||
onClick={handleCardClick}
|
|
||||||
header={
|
header={
|
||||||
<Header>
|
<Header>
|
||||||
<span className="text gap-16">
|
<span className="text gap-8">
|
||||||
{brandLogo ? (
|
<ModelLogo src={getModelLogo(model.name)} alt="" />
|
||||||
<ModelLogo src={brandLogo} alt="" />
|
|
||||||
) : categoryLogo ? (
|
|
||||||
<ModelLogo src={categoryLogo} alt="" />
|
|
||||||
) : (
|
|
||||||
<ModelLogo src={defaultModelLogo} alt="" />
|
|
||||||
)}
|
|
||||||
<span>{model.name}</span>
|
<span>{model.name}</span>
|
||||||
</span>
|
</span>
|
||||||
<StatusTag
|
<StatusTag
|
||||||
@@ -264,23 +239,14 @@ const ModelItem: React.FC<{
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{[MyModelsStatusValueMap.Ready].includes(model.status) && (
|
{[MyModelsStatusValueMap.Active].includes(model.status) && (
|
||||||
<Button
|
<Button
|
||||||
size="middle"
|
size="middle"
|
||||||
type="default"
|
|
||||||
className="btn"
|
className="btn"
|
||||||
style={{
|
type="primary"
|
||||||
borderRadius: 6,
|
|
||||||
paddingInline: 12
|
|
||||||
}}
|
|
||||||
onClick={handleOpenPlayGroundClick}
|
onClick={handleOpenPlayGroundClick}
|
||||||
>
|
>
|
||||||
{intl.formatMessage({ id: 'models.openinplayground' })}
|
{intl.formatMessage({ id: 'models.openinplayground' })}
|
||||||
<IconFont
|
|
||||||
type="icon-down2"
|
|
||||||
rotate={-90}
|
|
||||||
style={{ marginLeft: 4 }}
|
|
||||||
></IconFont>
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { PageActionType } from '@/config/types';
|
|||||||
import useBodyScroll from '@/hooks/use-body-scroll';
|
import useBodyScroll from '@/hooks/use-body-scroll';
|
||||||
import useExpandedRowKeys from '@/hooks/use-expanded-row-keys';
|
import useExpandedRowKeys from '@/hooks/use-expanded-row-keys';
|
||||||
import useTableRowSelection from '@/hooks/use-table-row-selection';
|
import useTableRowSelection from '@/hooks/use-table-row-selection';
|
||||||
|
import useWatchList from '@/hooks/use-watch-list';
|
||||||
import useNoResourceResult from '@/pages/llmodels/hooks/use-no-resource-result';
|
import useNoResourceResult from '@/pages/llmodels/hooks/use-no-resource-result';
|
||||||
|
import { MODEL_ROUTE_TARGETS } from '@/pages/model-routes/apis';
|
||||||
import { TargetStatusValueMap } from '@/pages/model-routes/config';
|
import { TargetStatusValueMap } from '@/pages/model-routes/config';
|
||||||
import useOpenPlayground from '@/pages/model-routes/hooks/use-open-playground';
|
import useOpenPlayground from '@/pages/model-routes/hooks/use-open-playground';
|
||||||
import useGranfanaLink from '@/pages/resources/hooks/use-grafana-link';
|
import useGranfanaLink from '@/pages/resources/hooks/use-grafana-link';
|
||||||
@@ -90,7 +92,6 @@ interface ModelsProps {
|
|||||||
loadend: boolean;
|
loadend: boolean;
|
||||||
total: number;
|
total: number;
|
||||||
filterValues?: Record<string, any>;
|
filterValues?: Record<string, any>;
|
||||||
targetList?: any[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFormattedData = (record: any, extraData = {}) => ({
|
const getFormattedData = (record: any, extraData = {}) => ({
|
||||||
@@ -128,8 +129,7 @@ const Models: React.FC<ModelsProps> = ({
|
|||||||
queryParams,
|
queryParams,
|
||||||
loading,
|
loading,
|
||||||
loadend,
|
loadend,
|
||||||
total,
|
total
|
||||||
targetList = []
|
|
||||||
}) => {
|
}) => {
|
||||||
const { generateFormValues, clusterList, workerList } =
|
const { generateFormValues, clusterList, workerList } =
|
||||||
useDeploymentsContext();
|
useDeploymentsContext();
|
||||||
@@ -155,6 +155,7 @@ const Models: React.FC<ModelsProps> = ({
|
|||||||
expandedRowKeys
|
expandedRowKeys
|
||||||
} = useExpandedRowKeys(expandAtom);
|
} = useExpandedRowKeys(expandAtom);
|
||||||
const { handleOpenPlayGround } = useOpenPlayground();
|
const { handleOpenPlayGround } = useOpenPlayground();
|
||||||
|
const { watchDataList: targetList } = useWatchList(MODEL_ROUTE_TARGETS);
|
||||||
const { openViewLogsModal, openViewLogsModalStatus, closeViewLogsModal } =
|
const { openViewLogsModal, openViewLogsModalStatus, closeViewLogsModal } =
|
||||||
useViewInstanceLogs();
|
useViewInstanceLogs();
|
||||||
|
|
||||||
@@ -441,9 +442,6 @@ const Models: React.FC<ModelsProps> = ({
|
|||||||
modelData={options.parent}
|
modelData={options.parent}
|
||||||
workerList={workerList}
|
workerList={workerList}
|
||||||
handleChildSelect={handleChildSelect}
|
handleChildSelect={handleChildSelect}
|
||||||
gridTemplate={options.gridTemplate}
|
|
||||||
prefixWidth={options.prefixWidth}
|
|
||||||
columns={options.columns}
|
|
||||||
></Instances>
|
></Instances>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -616,7 +614,6 @@ const Models: React.FC<ModelsProps> = ({
|
|||||||
></PageTools>
|
></PageTools>
|
||||||
<SealTable
|
<SealTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
emptyMinHeight="calc(100vh - 300px)"
|
|
||||||
sortDirections={TABLE_SORT_DIRECTIONS}
|
sortDirections={TABLE_SORT_DIRECTIONS}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
|
|||||||
@@ -196,21 +196,21 @@ export const status: any = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const MyModelsStatusValueMap = {
|
export const MyModelsStatusValueMap = {
|
||||||
Stopped: 'stopped',
|
Inactive: 'stopped',
|
||||||
NotReady: 'not_ready',
|
Degrade: 'not_ready',
|
||||||
Ready: 'ready'
|
Active: 'ready'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MyModelsStatusMap = {
|
export const MyModelsStatusMap = {
|
||||||
[MyModelsStatusValueMap.Stopped]: StatusMaps.inactive,
|
[MyModelsStatusValueMap.Inactive]: StatusMaps.inactive,
|
||||||
[MyModelsStatusValueMap.NotReady]: StatusMaps.error,
|
[MyModelsStatusValueMap.Degrade]: StatusMaps.error,
|
||||||
[MyModelsStatusValueMap.Ready]: StatusMaps.success
|
[MyModelsStatusValueMap.Active]: StatusMaps.success
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MyModelsStatusLabelMap = {
|
export const MyModelsStatusLabelMap = {
|
||||||
[MyModelsStatusValueMap.Stopped]: 'models.mymodels.status.inactive',
|
[MyModelsStatusValueMap.Inactive]: 'models.mymodels.status.inactive',
|
||||||
[MyModelsStatusValueMap.NotReady]: 'models.mymodels.status.degrade',
|
[MyModelsStatusValueMap.Degrade]: 'models.mymodels.status.degrade',
|
||||||
[MyModelsStatusValueMap.Ready]: 'models.mymodels.status.active'
|
[MyModelsStatusValueMap.Active]: 'models.mymodels.status.active'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ScheduleValueMap = {
|
export const ScheduleValueMap = {
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import useSetChunkRequest from '@/hooks/use-chunk-request';
|
|||||||
import { usePaginationStatus } from '@/hooks/use-pagination-status';
|
import { usePaginationStatus } from '@/hooks/use-pagination-status';
|
||||||
import { useTableMultiSort } from '@/hooks/use-table-sort';
|
import { useTableMultiSort } from '@/hooks/use-table-sort';
|
||||||
import useUpdateChunkedList from '@/hooks/use-update-chunk-list';
|
import useUpdateChunkedList from '@/hooks/use-update-chunk-list';
|
||||||
import useWatchList from '@/hooks/use-watch-list';
|
|
||||||
import { MODEL_ROUTE_TARGETS } from '@/pages/model-routes/apis';
|
|
||||||
import { TableOrder, TableProvider } from '@gpustack/core-ui';
|
import { TableOrder, TableProvider } from '@gpustack/core-ui';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
@@ -33,11 +31,6 @@ const Models = forwardRef((props, ref) => {
|
|||||||
const { setChunkRequest, createAxiosToken } = useSetChunkRequest();
|
const { setChunkRequest, createAxiosToken } = useSetChunkRequest();
|
||||||
const { setChunkRequest: setModelInstanceChunkRequest } =
|
const { setChunkRequest: setModelInstanceChunkRequest } =
|
||||||
useSetChunkRequest();
|
useSetChunkRequest();
|
||||||
const {
|
|
||||||
watchDataList: targetList,
|
|
||||||
startWatch: startTargetsWatch,
|
|
||||||
cancelWatch: cancelTargetsWatch
|
|
||||||
} = useWatchList(MODEL_ROUTE_TARGETS);
|
|
||||||
const [modelInstances, setModelInstances] = useState<any[]>([]);
|
const [modelInstances, setModelInstances] = useState<any[]>([]);
|
||||||
const [dataSource, setDataSource] = useState<{
|
const [dataSource, setDataSource] = useState<{
|
||||||
dataList: ListItem[];
|
dataList: ListItem[];
|
||||||
@@ -262,7 +255,6 @@ const Models = forwardRef((props, ref) => {
|
|||||||
cacheInsDataListRef.current = [];
|
cacheInsDataListRef.current = [];
|
||||||
chunkInstanceRequedtRef.current?.current?.cancel?.();
|
chunkInstanceRequedtRef.current?.current?.cancel?.();
|
||||||
instancesToken.current?.cancel?.();
|
instancesToken.current?.cancel?.();
|
||||||
cancelTargetsWatch();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const resumeRequestsOnPageActive = useMemoizedFn(async () => {
|
const resumeRequestsOnPageActive = useMemoizedFn(async () => {
|
||||||
@@ -273,7 +265,6 @@ const Models = forwardRef((props, ref) => {
|
|||||||
await getAllModelInstances();
|
await getAllModelInstances();
|
||||||
await createModelsInstanceChunkRequest();
|
await createModelsInstanceChunkRequest();
|
||||||
await createModelsChunkRequest();
|
await createModelsChunkRequest();
|
||||||
await startTargetsWatch();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleOnCancelViewLogs = useMemoizedFn(async () => {
|
const handleOnCancelViewLogs = useMemoizedFn(async () => {
|
||||||
@@ -492,7 +483,6 @@ const Models = forwardRef((props, ref) => {
|
|||||||
total={dataSource.total}
|
total={dataSource.total}
|
||||||
deleteIds={dataSource.deletedIds}
|
deleteIds={dataSource.deletedIds}
|
||||||
filterValues={filterValues}
|
filterValues={filterValues}
|
||||||
targetList={targetList}
|
|
||||||
></TableList>
|
></TableList>
|
||||||
</TableProvider>
|
</TableProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ interface BasicFormProps {
|
|||||||
}
|
}
|
||||||
>[];
|
>[];
|
||||||
handleClusterChange: (value: number) => void;
|
handleClusterChange: (value: number) => void;
|
||||||
onClusterSeed: (value: number) => void;
|
|
||||||
onSourceChange?: (value: string) => void;
|
onSourceChange?: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +94,6 @@ const BasicForm: React.FC<BasicFormProps> = (props) => {
|
|||||||
clusterList,
|
clusterList,
|
||||||
sourceDisable,
|
sourceDisable,
|
||||||
handleClusterChange,
|
handleClusterChange,
|
||||||
onClusterSeed,
|
|
||||||
onSourceChange
|
onSourceChange
|
||||||
} = props;
|
} = props;
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -150,8 +148,6 @@ const BasicForm: React.FC<BasicFormProps> = (props) => {
|
|||||||
// selection and fall back to the scope's default cluster (then a Ready one,
|
// selection and fall back to the scope's default cluster (then a Ready one,
|
||||||
// then the first) so GPU/backend options refetch for it. A selection that's
|
// then the first) so GPU/backend options refetch for it. A selection that's
|
||||||
// still valid is left untouched, so a user's (or edit's) choice is kept.
|
// still valid is left untouched, so a user's (or edit's) choice is kept.
|
||||||
// Use the seed callback (not handleClusterChange) so this auto-pick refreshes
|
|
||||||
// options without firing an evaluate request before a model is selected.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!clusterOptions?.length) {
|
if (!clusterOptions?.length) {
|
||||||
return;
|
return;
|
||||||
@@ -171,8 +167,8 @@ const BasicForm: React.FC<BasicFormProps> = (props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
form.setFieldValue('cluster_id', next);
|
form.setFieldValue('cluster_id', next);
|
||||||
onClusterSeed?.(next);
|
handleClusterChange?.(next);
|
||||||
}, [clusterOptions, form, onClusterSeed]);
|
}, [clusterOptions, form, handleClusterChange]);
|
||||||
|
|
||||||
const clusterOptionRender = (option: any) => {
|
const clusterOptionRender = (option: any) => {
|
||||||
const { data } = option;
|
const { data } = option;
|
||||||
|
|||||||
@@ -236,35 +236,20 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
|
|||||||
onOk(allValues);
|
onOk(allValues);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Shared work when the target cluster changes: refetch the GPU/backend
|
const handleClusterChange = async (value: number) => {
|
||||||
// options for the new cluster and reset schedule/gpu selection.
|
await onClusterChange?.(value);
|
||||||
const applyClusterScopedOptions = (value: number) => {
|
|
||||||
getGPUOptionList({ clusterId: value });
|
getGPUOptionList({ clusterId: value });
|
||||||
getBackendOptions({ cluster_id: value });
|
getBackendOptions({ cluster_id: value });
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
scheduleType: ScheduleValueMap.Auto,
|
scheduleType: ScheduleValueMap.Auto,
|
||||||
gpu_selector: null
|
gpu_selector: null
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
// User explicitly picked a cluster: refresh scoped options and re-evaluate.
|
|
||||||
const handleClusterChange = async (value: number) => {
|
|
||||||
await onClusterChange?.(value);
|
|
||||||
applyClusterScopedOptions(value);
|
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
setTimeout(resolve, 150);
|
setTimeout(resolve, 150);
|
||||||
});
|
});
|
||||||
onValuesChange?.({}, form.getFieldsValue());
|
onValuesChange?.({}, form.getFieldsValue());
|
||||||
};
|
};
|
||||||
|
|
||||||
// The basic form seeds a default cluster on open, before a model is picked.
|
|
||||||
// Refresh scoped options for it but don't fire the evaluate request — there
|
|
||||||
// is no model to evaluate yet.
|
|
||||||
const handleClusterSeed = async (value: number) => {
|
|
||||||
await onClusterChange?.(value);
|
|
||||||
applyClusterScopedOptions(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFieldPaths = (obj: Record<string, any>, prefix = ''): string => {
|
const getFieldPaths = (obj: Record<string, any>, prefix = ''): string => {
|
||||||
const result = Object.entries(obj).flatMap(([key, value]) => {
|
const result = Object.entries(obj).flatMap(([key, value]) => {
|
||||||
const path = prefix ? `${prefix}.${key}` : key;
|
const path = prefix ? `${prefix}.${key}` : key;
|
||||||
@@ -482,7 +467,6 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
|
|||||||
clusterList={clusterList}
|
clusterList={clusterList}
|
||||||
sourceDisable={sourceDisable}
|
sourceDisable={sourceDisable}
|
||||||
handleClusterChange={handleClusterChange}
|
handleClusterChange={handleClusterChange}
|
||||||
onClusterSeed={handleClusterSeed}
|
|
||||||
onSourceChange={onSourceChange}
|
onSourceChange={onSourceChange}
|
||||||
></BasicForm>
|
></BasicForm>
|
||||||
<CollapsePanel
|
<CollapsePanel
|
||||||
|
|||||||
@@ -401,24 +401,6 @@ export const useCheckCompatibility = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Evaluation needs a model reference. The basic form seeds a default cluster
|
|
||||||
// on open, which can fire onValuesChange before the user has picked a model —
|
|
||||||
// skip evaluation until the current source's model field is filled.
|
|
||||||
const noModelSelected = (allValues: any) => {
|
|
||||||
switch (allValues.source) {
|
|
||||||
case modelSourceMap.huggingface_value:
|
|
||||||
return !allValues.huggingface_repo_id;
|
|
||||||
case modelSourceMap.modelscope_value:
|
|
||||||
return !allValues.model_scope_model_id;
|
|
||||||
case modelSourceMap.ollama_library_value:
|
|
||||||
return !allValues.ollama_library_model_name;
|
|
||||||
case modelSourceMap.local_path_value:
|
|
||||||
return !allValues.local_path;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnValuesChange = async (params: {
|
const handleOnValuesChange = async (params: {
|
||||||
changedValues: any;
|
changedValues: any;
|
||||||
allValues: any;
|
allValues: any;
|
||||||
@@ -428,7 +410,6 @@ export const useCheckCompatibility = () => {
|
|||||||
if (
|
if (
|
||||||
_.isEqual(cacheFormValuesRef.current, allValues) ||
|
_.isEqual(cacheFormValuesRef.current, allValues) ||
|
||||||
noLocalPathValue(allValues) ||
|
noLocalPathValue(allValues) ||
|
||||||
noModelSelected(allValues) ||
|
|
||||||
!allValues.replicas
|
!allValues.replicas
|
||||||
) {
|
) {
|
||||||
console.log('No changes detected, skipping evaluation.');
|
console.log('No changes detected, skipping evaluation.');
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const useFilterStatus = (options?: {
|
|||||||
|
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
{
|
{
|
||||||
value: MyModelsStatusValueMap.Ready,
|
value: MyModelsStatusValueMap.Active,
|
||||||
color: 'var(--ant-color-success)',
|
color: 'var(--ant-color-success)',
|
||||||
icon: <Dot color="var(--ant-color-success)"></Dot>,
|
icon: <Dot color="var(--ant-color-success)"></Dot>,
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
@@ -32,7 +32,7 @@ const useFilterStatus = (options?: {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: MyModelsStatusValueMap.Stopped,
|
value: MyModelsStatusValueMap.Inactive,
|
||||||
color: 'var(--ant-color-fill-secondary)',
|
color: 'var(--ant-color-fill-secondary)',
|
||||||
icon: <Dot color="var(--ant-color-fill-secondary)"></Dot>,
|
icon: <Dot color="var(--ant-color-fill-secondary)"></Dot>,
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
@@ -40,7 +40,7 @@ const useFilterStatus = (options?: {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: MyModelsStatusValueMap.NotReady,
|
value: MyModelsStatusValueMap.Degrade,
|
||||||
color: 'var(--ant-color-warning)',
|
color: 'var(--ant-color-warning)',
|
||||||
icon: <Dot color="var(--ant-color-warning)"></Dot>,
|
icon: <Dot color="var(--ant-color-warning)"></Dot>,
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
|
|||||||
@@ -199,13 +199,7 @@ export const useGenerateWorkerOptions = () => {
|
|||||||
page: -1
|
page: -1
|
||||||
}),
|
}),
|
||||||
queryClusterList({
|
queryClusterList({
|
||||||
page: -1,
|
page: -1
|
||||||
// Own-org clusters only. Feeds the deploy-from-model-file cluster
|
|
||||||
// picker, which must not offer another org's cluster (e.g. the
|
|
||||||
// Default org's "shared with everyone" clusters). The worker
|
|
||||||
// cascader on this page is already own-org via the owner-scoped
|
|
||||||
// worker list.
|
|
||||||
mine: true
|
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
const workerList = workerRes.items || ([] as WorkerListItem[]);
|
const workerList = workerRes.items || ([] as WorkerListItem[]);
|
||||||
@@ -278,13 +272,7 @@ export default function useFormInitialValues() {
|
|||||||
// Exclude clusters that opt in to GPU-instance handling
|
// Exclude clusters that opt in to GPU-instance handling
|
||||||
// (k8s_options.gpu_instance_options set) — those are for the
|
// (k8s_options.gpu_instance_options set) — those are for the
|
||||||
// GPU-service flow, not model deployment.
|
// GPU-service flow, not model deployment.
|
||||||
gpu_instance_enabled: false,
|
gpu_instance_enabled: false
|
||||||
// Only clusters owned by the current org. Drops cross-org grants
|
|
||||||
// (e.g. the Default org's "shared with everyone" clusters) so a
|
|
||||||
// tenant can't deploy onto another org's infrastructure. Platform
|
|
||||||
// admin in the "All" view bypasses this and is scoped instead by
|
|
||||||
// the org picker (see basic.tsx).
|
|
||||||
mine: true
|
|
||||||
});
|
});
|
||||||
const list = response.items.map((item) => ({
|
const list = response.items.map((item) => ({
|
||||||
label: item.name,
|
label: item.name,
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ const useModelsColumns = ({
|
|||||||
dataIndex: 'created_at',
|
dataIndex: 'created_at',
|
||||||
key: 'created_at',
|
key: 'created_at',
|
||||||
sorter: tableSorter(5),
|
sorter: tableSorter(5),
|
||||||
width: 180,
|
span: spans.createTime,
|
||||||
render: (text: number) => (
|
render: (text: number) => (
|
||||||
<AutoTooltip ghost>
|
<AutoTooltip ghost>
|
||||||
{dayjs(text).format('YYYY-MM-DD HH:mm:ss')}
|
{dayjs(text).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ const useNoResourceResult = (props: {
|
|||||||
|
|
||||||
const noResourceResult = (
|
const noResourceResult = (
|
||||||
<NoResult
|
<NoResult
|
||||||
minHeight="calc(100vh - 300px)"
|
|
||||||
loading={loading}
|
loading={loading}
|
||||||
loadend={loadend}
|
loadend={loadend}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
const useViewApIInfo = () => {
|
|
||||||
const [apiAccessInfo, setAPIAccessInfo] = useState<any>({
|
|
||||||
show: false,
|
|
||||||
data: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
const openViewAPIInfo = (row: any) => {
|
|
||||||
setAPIAccessInfo({
|
|
||||||
show: true,
|
|
||||||
data: row
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeViewAPIInfo = () => {
|
|
||||||
setAPIAccessInfo({
|
|
||||||
show: false,
|
|
||||||
data: {}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
apiAccessInfo,
|
|
||||||
openViewAPIInfo,
|
|
||||||
closeViewAPIInfo
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useViewApIInfo;
|
|
||||||
@@ -31,16 +31,6 @@ const filterOptions = {
|
|||||||
label: 'Error',
|
label: 'Error',
|
||||||
value: 'error',
|
value: 'error',
|
||||||
color: 'var(--ant-color-error)'
|
color: 'var(--ant-color-error)'
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Pending',
|
|
||||||
value: 'pending',
|
|
||||||
color: 'var(--ant-color-info)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Starting',
|
|
||||||
value: 'starting',
|
|
||||||
color: 'var(--ant-color-info)'
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@@ -109,7 +99,6 @@ const InstanceView = forwardRef((props, ref) => {
|
|||||||
if (type !== 'Table') return;
|
if (type !== 'Table') return;
|
||||||
return (
|
return (
|
||||||
<NoResult
|
<NoResult
|
||||||
minHeight="calc(100vh - 300px)"
|
|
||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
|
|||||||
@@ -2,22 +2,20 @@ import useTableFetch from '@/hooks/use-table-fetch';
|
|||||||
import { SyncOutlined } from '@ant-design/icons';
|
import { SyncOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
BaseSelect,
|
BaseSelect,
|
||||||
|
IconFont,
|
||||||
InfiniteScrollerProvider,
|
InfiniteScrollerProvider,
|
||||||
|
NoResult,
|
||||||
PageTools,
|
PageTools,
|
||||||
TemplateCardList
|
TemplateCardList
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useAccess, useIntl, useNavigate } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
|
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
|
||||||
import { Button, Input, Space } from 'antd';
|
import { Button, Input, Space } from 'antd';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import PageBox from '../_components/page-box';
|
import PageBox from '../_components/page-box';
|
||||||
import { MY_MODELS_API, queryMyModels } from './apis';
|
import { MY_MODELS_API, queryMyModels } from './apis';
|
||||||
import APIAccessInfoModal from './components/api-access-info';
|
|
||||||
import ModelItem from './components/model-item';
|
import ModelItem from './components/model-item';
|
||||||
import { categoryOptions, MyModelsStatusValueMap } from './config';
|
import { categoryOptions, MyModelsStatusValueMap } from './config';
|
||||||
import useFormInitialValues from './hooks/use-form-initial-values';
|
|
||||||
import useNoResourceResult from './hooks/use-no-resource-result';
|
|
||||||
import useViewApIInfo from './hooks/use-view-api-info';
|
|
||||||
const Dot = ({ color }: { color: string }) => {
|
const Dot = ({ color }: { color: string }) => {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
@@ -55,56 +53,29 @@ const UserModels: React.FC = () => {
|
|||||||
watch: false,
|
watch: false,
|
||||||
isInfiniteScroll: true,
|
isInfiniteScroll: true,
|
||||||
defaultQueryParams: {
|
defaultQueryParams: {
|
||||||
perPage: 24,
|
perPage: 24
|
||||||
state: MyModelsStatusValueMap.Ready
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const access = useAccess();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { apiAccessInfo, openViewAPIInfo, closeViewAPIInfo } = useViewApIInfo();
|
|
||||||
|
|
||||||
// Only managers (platform admin or org owner) can see / manage
|
|
||||||
// clusters and workers, so only they hit those endpoints. A plain
|
|
||||||
// user falls straight through to the default "no models" empty state
|
|
||||||
// without the infra-guidance queries firing.
|
|
||||||
const canManageResources = access?.canSeeAdmin || access?.canSeeOrgAdmin;
|
|
||||||
const { getClusterList, getWorkerList, clusterList, workerList } =
|
|
||||||
useFormInitialValues();
|
|
||||||
|
|
||||||
// Managers start in a loading state so the empty state waits for the
|
|
||||||
// infra queries to resolve — otherwise the initially-empty
|
|
||||||
// cluster/worker lists briefly flash the "no clusters" / "no workers"
|
|
||||||
// guidance before the real data lands.
|
|
||||||
const [infraLoading, setInfraLoading] = useState(!!canManageResources);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (canManageResources) {
|
|
||||||
setInfraLoading(true);
|
|
||||||
Promise.all([getClusterList(), getWorkerList()]).finally(() => {
|
|
||||||
setInfraLoading(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [canManageResources]);
|
|
||||||
|
|
||||||
const statusOptions = useMemo(() => {
|
const statusOptions = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
value: MyModelsStatusValueMap.Ready,
|
value: MyModelsStatusValueMap.Active,
|
||||||
color: 'var(--ant-color-success)',
|
color: 'var(--ant-color-success)',
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
id: 'models.mymodels.status.active'
|
id: 'models.mymodels.status.active'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: MyModelsStatusValueMap.Stopped,
|
value: MyModelsStatusValueMap.Inactive,
|
||||||
color: 'var(--ant-color-fill)',
|
color: 'var(--ant-color-fill)',
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
id: 'models.mymodels.status.inactive'
|
id: 'models.mymodels.status.inactive'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: MyModelsStatusValueMap.NotReady,
|
value: MyModelsStatusValueMap.Degrade,
|
||||||
color: 'var(--ant-color-warning)',
|
color: 'var(--ant-color-warning)',
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
id: 'models.mymodels.status.degrade'
|
id: 'models.mymodels.status.degrade'
|
||||||
@@ -120,7 +91,7 @@ const UserModels: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderCard = (data: any) => {
|
const renderCard = (data: any) => {
|
||||||
return <ModelItem model={data} onClick={openViewAPIInfo} />;
|
return <ModelItem model={data} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadMore = useMemoizedFn((nextPage: number) => {
|
const loadMore = useMemoizedFn((nextPage: number) => {
|
||||||
@@ -151,17 +122,17 @@ const UserModels: React.FC = () => {
|
|||||||
|
|
||||||
const getStatus = useCallback((model: any) => {
|
const getStatus = useCallback((model: any) => {
|
||||||
if (!model.targets && !model.ready_targets) {
|
if (!model.targets && !model.ready_targets) {
|
||||||
return MyModelsStatusValueMap.Stopped;
|
return MyModelsStatusValueMap.Inactive;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.targets > 0 && !model.ready_targets) {
|
if (model.targets > 0 && !model.ready_targets) {
|
||||||
return MyModelsStatusValueMap.NotReady;
|
return MyModelsStatusValueMap.Degrade;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.ready_targets > 0 && model.targets > 0) {
|
if (model.ready_targets > 0 && model.targets > 0) {
|
||||||
return MyModelsStatusValueMap.Ready;
|
return MyModelsStatusValueMap.Active;
|
||||||
}
|
}
|
||||||
return MyModelsStatusValueMap.NotReady;
|
return MyModelsStatusValueMap.Degrade;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const dataList = useMemo(() => {
|
const dataList = useMemo(() => {
|
||||||
@@ -180,40 +151,7 @@ const UserModels: React.FC = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const { noResourceResult } = useNoResourceResult({
|
|
||||||
// Hold the empty state until infra queries resolve so managers don't
|
|
||||||
// see a "no clusters/workers" flash before the real data arrives.
|
|
||||||
loading: dataSource.loading || infraLoading,
|
|
||||||
loadend: dataSource.loadend,
|
|
||||||
dataSource: dataList,
|
|
||||||
// Preserve the original filters heuristic: only treat the current
|
|
||||||
// query as an active filter when there is data across pages, so a
|
|
||||||
// truly empty account still shows the full empty state (CTA).
|
|
||||||
queryParams: dataSource.totalPage > 0 ? queryParams : {},
|
|
||||||
iconType: 'icon-models',
|
|
||||||
title: intl.formatMessage({ id: 'noresult.mymodels.title' }),
|
|
||||||
noClusters: !!canManageResources && !clusterList.length,
|
|
||||||
noWorkers:
|
|
||||||
!!canManageResources && workerList.length === 0 && clusterList.length > 0,
|
|
||||||
defaultContent: {
|
|
||||||
// Infra is in place but no models yet: guide managers to deploy
|
|
||||||
// one, reusing the deployments-page copy so the two empty states
|
|
||||||
// read consistently. Plain users can't deploy (and skip the infra
|
|
||||||
// queries), so they keep the consumer-facing "ask an admin" copy
|
|
||||||
// and a button-less empty state.
|
|
||||||
subTitle: canManageResources
|
|
||||||
? intl.formatMessage({ id: 'noresult.deployments.subTitle' })
|
|
||||||
: intl.formatMessage({ id: 'noresult.mymodels.subTitle' }),
|
|
||||||
noFoundText: intl.formatMessage({ id: 'noresult.mymodels.nofound' }),
|
|
||||||
buttonText: canManageResources
|
|
||||||
? intl.formatMessage({ id: 'models.table.button.deploy' })
|
|
||||||
: '',
|
|
||||||
onClick: canManageResources ? () => navigate('/models/catalog') : () => {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<PageBox>
|
<PageBox>
|
||||||
<PageTools
|
<PageTools
|
||||||
marginBottom={22}
|
marginBottom={22}
|
||||||
@@ -237,9 +175,7 @@ const UserModels: React.FC = () => {
|
|||||||
<BaseSelect
|
<BaseSelect
|
||||||
allowClear
|
allowClear
|
||||||
showSearch={false}
|
showSearch={false}
|
||||||
placeholder={intl.formatMessage({
|
placeholder={intl.formatMessage({ id: 'models.filter.category' })}
|
||||||
id: 'models.filter.category'
|
|
||||||
})}
|
|
||||||
style={{ width: 180 }}
|
style={{ width: 180 }}
|
||||||
size="large"
|
size="large"
|
||||||
maxTagCount={1}
|
maxTagCount={1}
|
||||||
@@ -256,7 +192,6 @@ const UserModels: React.FC = () => {
|
|||||||
optionRender={optionRender}
|
optionRender={optionRender}
|
||||||
labelRender={labelRender}
|
labelRender={labelRender}
|
||||||
options={statusOptions}
|
options={statusOptions}
|
||||||
value={queryParams.state}
|
|
||||||
onChange={handleStatusChange}
|
onChange={handleStatusChange}
|
||||||
></BaseSelect>
|
></BaseSelect>
|
||||||
<Button
|
<Button
|
||||||
@@ -283,15 +218,20 @@ const UserModels: React.FC = () => {
|
|||||||
isFirst={!dataSource.loadend}
|
isFirst={!dataSource.loadend}
|
||||||
renderItem={renderCard}
|
renderItem={renderCard}
|
||||||
></TemplateCardList>
|
></TemplateCardList>
|
||||||
{noResourceResult}
|
<NoResult
|
||||||
|
loading={dataSource.loading}
|
||||||
|
loadend={dataSource.loadend}
|
||||||
|
dataSource={dataList}
|
||||||
|
image={<IconFont type="icon-models" />}
|
||||||
|
filters={{ ...queryParams }}
|
||||||
|
noFoundText={intl.formatMessage({
|
||||||
|
id: 'noresult.mymodels.nofound'
|
||||||
|
})}
|
||||||
|
title={intl.formatMessage({ id: 'noresult.mymodels.title' })}
|
||||||
|
subTitle={intl.formatMessage({ id: 'noresult.mymodels.subTitle' })}
|
||||||
|
></NoResult>
|
||||||
</InfiniteScrollerProvider>
|
</InfiniteScrollerProvider>
|
||||||
</PageBox>
|
</PageBox>
|
||||||
<APIAccessInfoModal
|
|
||||||
open={apiAccessInfo.show}
|
|
||||||
data={apiAccessInfo.data}
|
|
||||||
onClose={closeViewAPIInfo}
|
|
||||||
></APIAccessInfoModal>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const MODEL_ICON_RULES: [string, string][] = [
|
|||||||
['gemma', 'google'],
|
['gemma', 'google'],
|
||||||
['command-r', 'cohere'],
|
['command-r', 'cohere'],
|
||||||
['cohere', 'cohere'],
|
['cohere', 'cohere'],
|
||||||
['glm', 'zai'],
|
['glm', 'thudm'],
|
||||||
['yi-', '01ai'],
|
['yi-', '01ai'],
|
||||||
['ernie', 'ernie'],
|
['ernie', 'ernie'],
|
||||||
['hunyuan', 'hunyuan'],
|
['hunyuan', 'hunyuan'],
|
||||||
@@ -44,7 +44,6 @@ const MODEL_ICON_RULES: [string, string][] = [
|
|||||||
// 其余厂商/系列 → 对应 logo(按子串命中)
|
// 其余厂商/系列 → 对应 logo(按子串命中)
|
||||||
['gemini', 'google'],
|
['gemini', 'google'],
|
||||||
['gpt', 'openai'],
|
['gpt', 'openai'],
|
||||||
['hy', 'hunyuan'],
|
|
||||||
['openai', 'openai'],
|
['openai', 'openai'],
|
||||||
['internvl', 'opengvlab'],
|
['internvl', 'opengvlab'],
|
||||||
['minicpm', 'openbmb'],
|
['minicpm', 'openbmb'],
|
||||||
@@ -72,40 +71,13 @@ const MODEL_ICON_RULES: [string, string][] = [
|
|||||||
['alibaba', 'alibaba']
|
['alibaba', 'alibaba']
|
||||||
];
|
];
|
||||||
|
|
||||||
// Default llm logo, used as the ultimate fallback when neither a brand
|
|
||||||
// logo nor a model-category icon can be resolved.
|
|
||||||
export const defaultModelLogo = iconMap[DEFAULT_ICON];
|
|
||||||
|
|
||||||
// 模型类别 → model_icons 下的类别图标文件名。
|
|
||||||
const CATEGORY_ICON_MAP: Record<string, string> = {
|
|
||||||
llm: 'llm',
|
|
||||||
embedding: 'embedding',
|
|
||||||
reranker: 'reranker',
|
|
||||||
image: 'image',
|
|
||||||
text_to_speech: 'tts',
|
|
||||||
speech_to_text: 'stt'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a category icon (from model_icons) for a model's categories.
|
* Resolve a model logo from its name.
|
||||||
* Returns the first matching category icon url, or null when none match.
|
|
||||||
*/
|
|
||||||
export const getCategoryLogo = (categories?: string[]): string | null => {
|
|
||||||
const iconName = categories
|
|
||||||
?.map((category) => CATEGORY_ICON_MAP[category])
|
|
||||||
.find((name) => name && iconMap[name]);
|
|
||||||
|
|
||||||
return iconName ? iconMap[iconName] : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a brand logo from a model name.
|
|
||||||
* 1. Match against the keyword rules in order (first substring hit wins).
|
* 1. Match against the keyword rules in order (first substring hit wins).
|
||||||
* 2. Fall back to a direct match where the name contains an icon filename.
|
* 2. Fall back to a direct match where the name contains an icon filename.
|
||||||
* 3. Return null when nothing matches — the caller then falls back to a
|
* 3. Fall back to the default llm icon.
|
||||||
* category-based icon (see `categoryConfig`) rather than a default image.
|
|
||||||
*/
|
*/
|
||||||
export const getModelLogo = (modelName?: string): string | null => {
|
export const getModelLogo = (modelName?: string): string => {
|
||||||
const name = (modelName || '').toLowerCase();
|
const name = (modelName || '').toLowerCase();
|
||||||
|
|
||||||
const rule = MODEL_ICON_RULES.find(([keyword]) => name.includes(keyword));
|
const rule = MODEL_ICON_RULES.find(([keyword]) => name.includes(keyword));
|
||||||
@@ -118,5 +90,5 @@ export const getModelLogo = (modelName?: string): string | null => {
|
|||||||
.sort((a, b) => b.length - a.length)
|
.sort((a, b) => b.length - a.length)
|
||||||
.find((iconName) => name.includes(iconName));
|
.find((iconName) => name.includes(iconName));
|
||||||
|
|
||||||
return directHit ? iconMap[directHit] : null;
|
return iconMap[directHit || DEFAULT_ICON] || iconMap[DEFAULT_ICON];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ const Login = () => {
|
|||||||
[shouldUseCustomLogin, userSettings]
|
[shouldUseCustomLogin, userSettings]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log('useCustomLogin', useCustomLogin, userSettings);
|
||||||
|
|
||||||
const gotoDefaultPage = async (info: any) => {
|
const gotoDefaultPage = async (info: any) => {
|
||||||
if (!info || info?.require_password_change) {
|
if (!info || info?.require_password_change) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ const MaasProvider: React.FC = () => {
|
|||||||
if (type !== 'Table') return;
|
if (type !== 'Table') return;
|
||||||
return (
|
return (
|
||||||
<NoResult
|
<NoResult
|
||||||
minHeight="calc(100vh - 300px)"
|
|
||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
@@ -159,7 +158,6 @@ const MaasProvider: React.FC = () => {
|
|||||||
></FilterBar>
|
></FilterBar>
|
||||||
<ConfigProvider renderEmpty={renderEmpty}>
|
<ConfigProvider renderEmpty={renderEmpty}>
|
||||||
<Table
|
<Table
|
||||||
className={'scroll-table'}
|
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
sortDirections={TABLE_SORT_DIRECTIONS}
|
sortDirections={TABLE_SORT_DIRECTIONS}
|
||||||
|
|||||||
@@ -2,19 +2,24 @@ import ProviderLogo from '@/pages/maas-provider/components/provider-logo';
|
|||||||
import { DeleteOutlined } from '@ant-design/icons';
|
import { DeleteOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
AutoTooltip,
|
AutoTooltip,
|
||||||
ChildGridOptions,
|
|
||||||
DropdownButtons,
|
DropdownButtons,
|
||||||
ExpandedRowGrid,
|
RowChildren,
|
||||||
StatusTag
|
StatusTag
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Tag } from 'antd';
|
import { Col, Row, Tag } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { TargetStatus, TargetStatusLabelMap } from '../config';
|
import { TargetStatus, TargetStatusLabelMap } from '../config';
|
||||||
import { RouteTarget } from '../config/types';
|
import { RouteTarget } from '../config/types';
|
||||||
|
|
||||||
|
const CellContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
const FilesTag = styled(Tag)`
|
const FilesTag = styled(Tag)`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -24,33 +29,20 @@ const FilesTag = styled(Tag)`
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type SharedGrid = Pick<
|
interface ProviderModelProps {
|
||||||
ChildGridOptions,
|
|
||||||
'gridTemplate' | 'prefixWidth' | 'columns'
|
|
||||||
>;
|
|
||||||
|
|
||||||
interface ProviderModelProps extends SharedGrid {
|
|
||||||
dataList: RouteTarget[];
|
dataList: RouteTarget[];
|
||||||
onSelect: (val: any, record: any) => void;
|
onSelect: (val: any, record: any) => void;
|
||||||
sourceModels: any[];
|
sourceModels: any[];
|
||||||
modelList?: Global.BaseOption<number>[];
|
modelList?: Global.BaseOption<number>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TargetItemProps extends SharedGrid {
|
interface TargetItemProps {
|
||||||
onSelect: (val: any, record: any) => void;
|
onSelect: (val: any, record: any) => void;
|
||||||
data: any;
|
data: any;
|
||||||
sourceModels: any[];
|
sourceModels: any[];
|
||||||
modelList?: Global.BaseOption<number>[];
|
modelList?: Global.BaseOption<number>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sub-column inside the merged `targets` cell.
|
|
||||||
const subCellStyle: React.CSSProperties = {
|
|
||||||
minWidth: 0,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingInline: 'var(--ant-table-cell-padding-inline)'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const childActionList = [
|
export const childActionList = [
|
||||||
{
|
{
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
@@ -66,24 +58,10 @@ const RouteItem: React.FC<TargetItemProps> = ({
|
|||||||
onSelect,
|
onSelect,
|
||||||
data,
|
data,
|
||||||
sourceModels,
|
sourceModels,
|
||||||
modelList,
|
modelList
|
||||||
gridTemplate,
|
|
||||||
prefixWidth = 0,
|
|
||||||
columns
|
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
// The child row shares the parent's column grid. Cells flow left-to-right,
|
|
||||||
// so each cell only declares how many parent columns it spans. Parent layout
|
|
||||||
// is always: name (1) | middle plugin region | created_at (1) | operations (1).
|
|
||||||
// Enterprise inserts plugin columns (org, quota) into the middle region.
|
|
||||||
const columnCount = columns?.length ?? 0;
|
|
||||||
const middleSpan = Math.max(columnCount - 3, 1);
|
|
||||||
// With ≥3 middle tracks (enterprise: org / targets / quota) give source,
|
|
||||||
// weight and status their own tracks; source pins to the first middle track,
|
|
||||||
// status to the last, weight absorbs whatever is between.
|
|
||||||
const splitMiddle = middleSpan >= 3;
|
|
||||||
|
|
||||||
const renderProviderSource = () => {
|
const renderProviderSource = () => {
|
||||||
const model = sourceModels.find((item: any) => {
|
const model = sourceModels.find((item: any) => {
|
||||||
if (data.model_id) {
|
if (data.model_id) {
|
||||||
@@ -105,21 +83,56 @@ const RouteItem: React.FC<TargetItemProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
return (
|
||||||
const sourceNode = renderProviderSource();
|
<div style={{ borderRadius: 'var(--ant-table-header-border-radius)' }}>
|
||||||
const weightNode =
|
<RowChildren>
|
||||||
data.fallback_status_codes && data.fallback_status_codes?.length > 0 ? (
|
<Row
|
||||||
|
gutter={16}
|
||||||
|
style={{ width: '100%', color: 'var(--ant-color-text-secondary)' }}
|
||||||
|
>
|
||||||
|
<Col span={5}>
|
||||||
|
<CellContent
|
||||||
|
style={{
|
||||||
|
gap: 4,
|
||||||
|
paddingInline: 'var(--ant-table-cell-padding-inline)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AutoTooltip ghost>{data.name}</AutoTooltip>
|
||||||
|
{!!data.overridden_model_name && !!data.model_id && (
|
||||||
|
<FilesTag color="purple" variant="outlined">
|
||||||
|
<span style={{ opacity: 1 }}>LoRA</span>
|
||||||
|
</FilesTag>
|
||||||
|
)}
|
||||||
|
</CellContent>
|
||||||
|
</Col>
|
||||||
|
<Col span={5} style={{ paddingLeft: 64 }}>
|
||||||
|
<CellContent>{renderProviderSource()}</CellContent>
|
||||||
|
</Col>
|
||||||
|
<Col span={2}>
|
||||||
|
<CellContent>
|
||||||
|
{data.fallback_status_codes &&
|
||||||
|
data.fallback_status_codes?.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{data.weight > 0 && <span style={{ marginInline: 8 }}>/</span>}
|
{data.weight > 0 && (
|
||||||
<span>{intl.formatMessage({ id: 'routes.table.label.fallback' })}</span>
|
<span style={{ marginInline: 8 }}>/</span>
|
||||||
|
)}
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: 'routes.table.label.fallback'
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<AutoTooltip ghost>
|
<AutoTooltip ghost>
|
||||||
{intl.formatMessage({ id: 'routes.form.target.weight' })}:{' '}
|
{intl.formatMessage({ id: 'routes.form.target.weight' })}:{' '}
|
||||||
{data.weight || 0}
|
{data.weight || 0}
|
||||||
</AutoTooltip>
|
</AutoTooltip>
|
||||||
);
|
)}
|
||||||
const statusNode = (
|
</CellContent>
|
||||||
|
</Col>
|
||||||
|
<Col span={3}>
|
||||||
|
<CellContent>
|
||||||
|
<AutoTooltip ghost>
|
||||||
<StatusTag
|
<StatusTag
|
||||||
statusValue={{
|
statusValue={{
|
||||||
status: TargetStatus[data.state],
|
status: TargetStatus[data.state],
|
||||||
@@ -127,60 +140,31 @@ const RouteItem: React.FC<TargetItemProps> = ({
|
|||||||
message: ''
|
message: ''
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
</AutoTooltip>
|
||||||
|
</CellContent>
|
||||||
return (
|
</Col>
|
||||||
<ExpandedRowGrid
|
<Col span={5}>
|
||||||
gridTemplate={gridTemplate}
|
<CellContent style={{ paddingLeft: 45 }}>
|
||||||
prefixWidth={prefixWidth}
|
|
||||||
style={{ color: 'var(--ant-color-text-secondary)' }}
|
|
||||||
>
|
|
||||||
<ExpandedRowGrid.Cell span={1} style={{ height: '100%', gap: 4 }}>
|
|
||||||
<AutoTooltip ghost>{data.name}</AutoTooltip>
|
|
||||||
{!!data.overridden_model_name && !!data.model_id && (
|
|
||||||
<FilesTag color="purple" variant="outlined">
|
|
||||||
<span style={{ opacity: 1 }}>LoRA</span>
|
|
||||||
</FilesTag>
|
|
||||||
)}
|
|
||||||
</ExpandedRowGrid.Cell>
|
|
||||||
{splitMiddle ? (
|
|
||||||
// Enterprise: org / targets / quota → one track each.
|
|
||||||
<>
|
|
||||||
<ExpandedRowGrid.Cell span={1}>{sourceNode}</ExpandedRowGrid.Cell>
|
|
||||||
<ExpandedRowGrid.Cell span={middleSpan - 2}>
|
|
||||||
{weightNode}
|
|
||||||
</ExpandedRowGrid.Cell>
|
|
||||||
<ExpandedRowGrid.Cell span={1}>{statusNode}</ExpandedRowGrid.Cell>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
// Only `targets` exists: share the one track via a 5:2:3 sub-grid.
|
|
||||||
// A raw grid div (not <Cell>) because it needs `display: grid`.
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
gridColumn: `span ${middleSpan}`,
|
|
||||||
minWidth: 0,
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: 'minmax(0, 5fr) minmax(0, 2fr) minmax(0, 3fr)',
|
|
||||||
alignItems: 'center'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={subCellStyle}>{sourceNode}</div>
|
|
||||||
<div style={subCellStyle}>{weightNode}</div>
|
|
||||||
<div style={subCellStyle}>{statusNode}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<ExpandedRowGrid.Cell span={1} style={{ height: '100%' }}>
|
|
||||||
<AutoTooltip ghost minWidth={20}>
|
<AutoTooltip ghost minWidth={20}>
|
||||||
{dayjs(data.created_at).format('YYYY-MM-DD HH:mm:ss')}
|
{dayjs(data.created_at).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
</AutoTooltip>
|
</AutoTooltip>
|
||||||
</ExpandedRowGrid.Cell>
|
</CellContent>
|
||||||
<ExpandedRowGrid.Cell span={1} style={{ height: '100%' }}>
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<CellContent
|
||||||
|
style={{
|
||||||
|
paddingLeft: 38
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DropdownButtons
|
<DropdownButtons
|
||||||
items={childActionList}
|
items={childActionList}
|
||||||
onSelect={(val) => onSelect(val, data)}
|
onSelect={(val) => onSelect(val, data)}
|
||||||
></DropdownButtons>
|
></DropdownButtons>
|
||||||
</ExpandedRowGrid.Cell>
|
</CellContent>
|
||||||
</ExpandedRowGrid>
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</RowChildren>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -188,10 +172,7 @@ const RouteTargets: React.FC<ProviderModelProps> = ({
|
|||||||
dataList,
|
dataList,
|
||||||
onSelect,
|
onSelect,
|
||||||
modelList,
|
modelList,
|
||||||
sourceModels,
|
sourceModels
|
||||||
gridTemplate,
|
|
||||||
prefixWidth,
|
|
||||||
columns
|
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -202,9 +183,6 @@ const RouteTargets: React.FC<ProviderModelProps> = ({
|
|||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
sourceModels={sourceModels}
|
sourceModels={sourceModels}
|
||||||
modelList={modelList}
|
modelList={modelList}
|
||||||
gridTemplate={gridTemplate}
|
|
||||||
prefixWidth={prefixWidth}
|
|
||||||
columns={columns}
|
|
||||||
></RouteItem>
|
></RouteItem>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ const useAccessColumns = ({
|
|||||||
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
||||||
dataIndex: 'created_at',
|
dataIndex: 'created_at',
|
||||||
sorter: tableSorter(6),
|
sorter: tableSorter(6),
|
||||||
width: 180,
|
span: createTimeSpan,
|
||||||
render: (value: string) => (
|
render: (value: string) => (
|
||||||
<AutoTooltip ghost minWidth={20}>
|
<AutoTooltip ghost minWidth={20}>
|
||||||
{dayjs(value).format('YYYY-MM-DD HH:mm:ss')}
|
{dayjs(value).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
|||||||
@@ -291,9 +291,6 @@ const ModelRoutes: React.FC = () => {
|
|||||||
dataList={list}
|
dataList={list}
|
||||||
onSelect={onChildSelect}
|
onSelect={onChildSelect}
|
||||||
sourceModels={sourceModels}
|
sourceModels={sourceModels}
|
||||||
gridTemplate={options.gridTemplate}
|
|
||||||
prefixWidth={options.prefixWidth}
|
|
||||||
columns={options.columns}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -367,7 +364,6 @@ const ModelRoutes: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<SealTable
|
<SealTable
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
emptyMinHeight="calc(100vh - 300px)"
|
|
||||||
loadChildren={loadChildrenData}
|
loadChildren={loadChildrenData}
|
||||||
sortDirections={TABLE_SORT_DIRECTIONS}
|
sortDirections={TABLE_SORT_DIRECTIONS}
|
||||||
expandedRowKeys={expandedRowKeys}
|
expandedRowKeys={expandedRowKeys}
|
||||||
@@ -385,7 +381,6 @@ const ModelRoutes: React.FC = () => {
|
|||||||
expandable={true}
|
expandable={true}
|
||||||
empty={
|
empty={
|
||||||
<NoResult
|
<NoResult
|
||||||
minHeight="calc(100vh - 300px)"
|
|
||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ const GPUList: React.FC<GPUListProps> = ({ clusterId, source }) => {
|
|||||||
if (type !== 'Table') return;
|
if (type !== 'Table') return;
|
||||||
return (
|
return (
|
||||||
<NoResult
|
<NoResult
|
||||||
minHeight="calc(100vh - 300px)"
|
|
||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
@@ -123,14 +122,14 @@ const GPUList: React.FC<GPUListProps> = ({ clusterId, source }) => {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
sortDirections={TABLE_SORT_DIRECTIONS}
|
sortDirections={TABLE_SORT_DIRECTIONS}
|
||||||
showSorterTooltip={false}
|
showSorterTooltip={false}
|
||||||
scroll={{ x: 'max-content' }}
|
tableLayout={'auto'}
|
||||||
className={'scroll-table'}
|
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
loading={{
|
loading={{
|
||||||
spinning: dataSource.loading,
|
spinning: dataSource.loading,
|
||||||
size: 'middle'
|
size: 'middle'
|
||||||
}}
|
}}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
|
scroll={{ x: 900 }}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
pagination={{
|
pagination={{
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
|
|||||||
@@ -233,7 +233,6 @@ const ModelFiles = () => {
|
|||||||
if (type !== 'Table') return;
|
if (type !== 'Table') return;
|
||||||
return (
|
return (
|
||||||
<NoResult
|
<NoResult
|
||||||
minHeight="calc(100vh - 300px)"
|
|
||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
@@ -340,7 +339,6 @@ const ModelFiles = () => {
|
|||||||
></FilterBar>
|
></FilterBar>
|
||||||
<ConfigProvider renderEmpty={renderEmpty}>
|
<ConfigProvider renderEmpty={renderEmpty}>
|
||||||
<Table
|
<Table
|
||||||
className={'scroll-table'}
|
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
sortDirections={TABLE_SORT_DIRECTIONS}
|
sortDirections={TABLE_SORT_DIRECTIONS}
|
||||||
|
|||||||
@@ -103,14 +103,8 @@ const Workers: React.FC<WorkersProps> = ({ clusterId, source }) => {
|
|||||||
|
|
||||||
const getClusterList = async () => {
|
const getClusterList = async () => {
|
||||||
try {
|
try {
|
||||||
// Own-org clusters only (mine=true). A worker can only join a cluster
|
|
||||||
// its org owns, so another org's cluster (e.g. the Default org's
|
|
||||||
// "shared with everyone" clusters) must not be offered in the picker.
|
|
||||||
// The worker list is owner-scoped too, so this list also covers every
|
|
||||||
// cluster the table's name column can reference.
|
|
||||||
const params = {
|
const params = {
|
||||||
page: -1,
|
page: -1
|
||||||
mine: true
|
|
||||||
};
|
};
|
||||||
const items = await fetchClusterList(params);
|
const items = await fetchClusterList(params);
|
||||||
const clusterMap = items?.reduce(
|
const clusterMap = items?.reduce(
|
||||||
@@ -303,7 +297,7 @@ const Workers: React.FC<WorkersProps> = ({ clusterId, source }) => {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
sortDirections={TABLE_SORT_DIRECTIONS}
|
sortDirections={TABLE_SORT_DIRECTIONS}
|
||||||
showSorterTooltip={false}
|
showSorterTooltip={false}
|
||||||
scroll={{ x: 'max-content' }}
|
tableLayout={'auto'}
|
||||||
className={'scroll-table'}
|
className={'scroll-table'}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
loading={{
|
loading={{
|
||||||
@@ -311,6 +305,7 @@ const Workers: React.FC<WorkersProps> = ({ clusterId, source }) => {
|
|||||||
size: 'middle'
|
size: 'middle'
|
||||||
}}
|
}}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
|
scroll={{ x: 900 }}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
rowSelection={source === 'clusterDetail' ? undefined : rowSelection}
|
rowSelection={source === 'clusterDetail' ? undefined : rowSelection}
|
||||||
pagination={{
|
pagination={{
|
||||||
|
|||||||
@@ -10,8 +10,7 @@
|
|||||||
* (cpu/memory/ephemeral hours, dangling volumes) are left at 0 — the
|
* (cpu/memory/ephemeral hours, dangling volumes) are left at 0 — the
|
||||||
* whole-machine SKU model meters runtime, not decomposed components.
|
* whole-machine SKU model meters runtime, not decomposed components.
|
||||||
*/
|
*/
|
||||||
import { getIntl, request } from '@umijs/max';
|
import { request } from '@umijs/max';
|
||||||
import { withDeletedMark } from '../utils/deleted-label';
|
|
||||||
import { instanceTypeSeriesLabel } from '../utils/format-instance-type';
|
import { instanceTypeSeriesLabel } from '../utils/format-instance-type';
|
||||||
|
|
||||||
export interface ResourceUsageFilters {
|
export interface ResourceUsageFilters {
|
||||||
@@ -20,10 +19,6 @@ export interface ResourceUsageFilters {
|
|||||||
instance_ids?: number[];
|
instance_ids?: number[];
|
||||||
gpu_types?: string[];
|
gpu_types?: string[];
|
||||||
volume_ids?: number[];
|
volume_ids?: number[];
|
||||||
// Platform-wide "All" view only (backend-gated): consumer-Org ids and
|
|
||||||
// user-group ids (expanded server-side to the groups' direct members).
|
|
||||||
organization_ids?: number[];
|
|
||||||
user_group_ids?: number[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResourceBreakdownRequest {
|
export interface ResourceBreakdownRequest {
|
||||||
@@ -69,19 +64,6 @@ export interface ResourceBreakdownItem extends ResourceBreakdownSummary {
|
|||||||
volume_name?: string;
|
volume_name?: string;
|
||||||
user_id?: number;
|
user_id?: number;
|
||||||
user_name?: string;
|
user_name?: string;
|
||||||
// Organization grouping (platform-wide "All" view). ``organization_name``
|
|
||||||
// is resolved live server-side; a gone Org sets ``deleted``.
|
|
||||||
organization_id?: number;
|
|
||||||
organization_name?: string;
|
|
||||||
// The grouped entity (instance / volume / user) no longer exists. The name
|
|
||||||
// fields keep the clean (stale) name; the tables show a DeletedTag off this
|
|
||||||
// flag plus the id, matching the Tokens tab.
|
|
||||||
deleted?: boolean;
|
|
||||||
// Owner user of a per-instance / per-volume row (compound date+dim grouping),
|
|
||||||
// with its own deletion state — independent of the row's ``deleted`` (which
|
|
||||||
// refers to the grouped instance/volume). Lets the export mark the User
|
|
||||||
// column separately, matching the Tokens tab.
|
|
||||||
user_deleted?: boolean;
|
|
||||||
// Grouped-trend rows carry the sub-group label (sku / instance / user / …)
|
// Grouped-trend rows carry the sub-group label (sku / instance / user / …)
|
||||||
// alongside ``date`` so the chart can pivot one series per group.
|
// alongside ``date`` so the chart can pivot one series per group.
|
||||||
group?: string;
|
group?: string;
|
||||||
@@ -215,20 +197,11 @@ interface ServerMetrics {
|
|||||||
// gpu_type / type both mean the sku (Type) on the server.
|
// gpu_type / type both mean the sku (Type) on the server.
|
||||||
|
|
||||||
interface ServerBreakdownItem {
|
interface ServerBreakdownItem {
|
||||||
date?: string | null;
|
|
||||||
// Grouped entity: ``key`` is its display name, ``id`` its id, ``deleted`` its
|
|
||||||
// own lifecycle state (instance / volume / user / sku, per group_by).
|
|
||||||
key?: string | null;
|
key?: string | null;
|
||||||
id?: number | null;
|
id?: number | null;
|
||||||
|
date?: string | null;
|
||||||
sku?: string | null;
|
sku?: string | null;
|
||||||
deleted?: boolean | null;
|
deleted?: boolean | null;
|
||||||
// Owner (creator) of the instance/volume row (compound date+dim grouping),
|
|
||||||
// at the item root alongside the grouped entity, with its OWN deletion state
|
|
||||||
// — independent of ``deleted`` — so the export can show a User column that
|
|
||||||
// marks a deleted owner separately from a deleted instance/volume.
|
|
||||||
creator_id?: number | null;
|
|
||||||
creator_name?: string | null;
|
|
||||||
creator_deleted?: boolean | null;
|
|
||||||
dimensions?: {
|
dimensions?: {
|
||||||
product?: string | null;
|
product?: string | null;
|
||||||
unit_cpu_milli?: number | null;
|
unit_cpu_milli?: number | null;
|
||||||
@@ -269,7 +242,6 @@ const GROUP_BY_MAP: Record<string, string> = {
|
|||||||
instance: 'instance',
|
instance: 'instance',
|
||||||
volume: 'volume',
|
volume: 'volume',
|
||||||
user: 'user',
|
user: 'user',
|
||||||
organization: 'organization',
|
|
||||||
date: 'date'
|
date: 'date'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -305,24 +277,12 @@ function flattenItem(
|
|||||||
};
|
};
|
||||||
if (it.date) flat.date = it.date;
|
if (it.date) flat.date = it.date;
|
||||||
const id = it.id ?? undefined;
|
const id = it.id ?? undefined;
|
||||||
const deleted = !!it.deleted;
|
// Deleted entities get a "(Deleted)" suffix, matching the Token breakdown.
|
||||||
const rawKey = it.key ?? undefined;
|
const rawKey = it.key ?? undefined;
|
||||||
// The chart series legend can't render a tag, so it carries the deleted
|
const key = it.deleted && rawKey != null ? `${rawKey} (Deleted)` : rawKey;
|
||||||
// marker as text ("<name> [Deleted.<id>]"); the tables render a DeletedTag off
|
|
||||||
// ``flat.deleted`` + the id and so keep the clean name.
|
|
||||||
const key =
|
|
||||||
rawKey != null
|
|
||||||
? withDeletedMark(
|
|
||||||
rawKey,
|
|
||||||
deleted,
|
|
||||||
deleted ? getIntl().formatMessage({ id: 'usage.table.deleted' }) : '',
|
|
||||||
id
|
|
||||||
)
|
|
||||||
: rawKey;
|
|
||||||
// Generic group label — for a compound (date + dim) trend row the key is the
|
// Generic group label — for a compound (date + dim) trend row the key is the
|
||||||
// sub-group value (the switch below targets single-dimension table rows).
|
// sub-group value (the switch below targets single-dimension table rows).
|
||||||
if (rawKey != null) flat.group = key;
|
if (rawKey != null) flat.group = key;
|
||||||
flat.deleted = deleted;
|
|
||||||
switch (groupBy) {
|
switch (groupBy) {
|
||||||
case 'resource_type':
|
case 'resource_type':
|
||||||
flat.resource_type = key;
|
flat.resource_type = key;
|
||||||
@@ -332,21 +292,17 @@ function flattenItem(
|
|||||||
flat.gpu_type = key;
|
flat.gpu_type = key;
|
||||||
break;
|
break;
|
||||||
case 'instance':
|
case 'instance':
|
||||||
flat.instance_name = rawKey;
|
flat.instance_name = key;
|
||||||
flat.instance_id = id;
|
flat.instance_id = id;
|
||||||
break;
|
break;
|
||||||
case 'volume':
|
case 'volume':
|
||||||
flat.volume_name = rawKey;
|
flat.volume_name = key;
|
||||||
flat.volume_id = id;
|
flat.volume_id = id;
|
||||||
break;
|
break;
|
||||||
case 'user':
|
case 'user':
|
||||||
flat.user_name = rawKey;
|
flat.user_name = key;
|
||||||
flat.user_id = id;
|
flat.user_id = id;
|
||||||
break;
|
break;
|
||||||
case 'organization':
|
|
||||||
flat.organization_name = rawKey;
|
|
||||||
flat.organization_id = id;
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -374,12 +330,6 @@ function flattenItem(
|
|||||||
if (dims.storage_type) flat.storage_type = dims.storage_type;
|
if (dims.storage_type) flat.storage_type = dims.storage_type;
|
||||||
if (dims.capacity_mib != null) flat.capacity_mib = dims.capacity_mib;
|
if (dims.capacity_mib != null) flat.capacity_mib = dims.capacity_mib;
|
||||||
}
|
}
|
||||||
// Owner (creator) of a per-instance / per-volume row — the grouped entity is
|
|
||||||
// the instance/volume (``key``/``deleted``), so the owner sits at the item
|
|
||||||
// root with its own deleted flag for the export's User column.
|
|
||||||
if (it.creator_name != null) flat.user_name = it.creator_name;
|
|
||||||
if (it.creator_id != null) flat.user_id = it.creator_id;
|
|
||||||
if (it.creator_deleted != null) flat.user_deleted = !!it.creator_deleted;
|
|
||||||
// Instance-type grouped trend: the series label (``group``) defaults to the
|
// Instance-type grouped trend: the series label (``group``) defaults to the
|
||||||
// raw flavor slug. Instance Types are grouped by actual shape, so label each
|
// raw flavor slug. Instance Types are grouped by actual shape, so label each
|
||||||
// series by that shape — "<product> x <cards>" / "CPU Only · 3 vCPU · 6 GB" —
|
// series by that shape — "<product> x <cards>" / "CPU Only · 3 vCPU · 6 GB" —
|
||||||
@@ -406,13 +356,7 @@ function flattenResponse(
|
|||||||
|
|
||||||
function toServerRequest(data: ResourceBreakdownRequest) {
|
function toServerRequest(data: ResourceBreakdownRequest) {
|
||||||
const groupByList = data.group_by?.length ? data.group_by : ['resource_type'];
|
const groupByList = data.group_by?.length ? data.group_by : ['resource_type'];
|
||||||
const {
|
const { creator_ids, instance_ids, volume_ids } = data.filters ?? {};
|
||||||
creator_ids,
|
|
||||||
instance_ids,
|
|
||||||
volume_ids,
|
|
||||||
organization_ids,
|
|
||||||
user_group_ids
|
|
||||||
} = data.filters ?? {};
|
|
||||||
// The non-date dimension drives response flattening into the right field.
|
// The non-date dimension drives response flattening into the right field.
|
||||||
const dim = groupByList.find((g) => g !== 'date');
|
const dim = groupByList.find((g) => g !== 'date');
|
||||||
return {
|
return {
|
||||||
@@ -427,8 +371,6 @@ function toServerRequest(data: ResourceBreakdownRequest) {
|
|||||||
...(creator_ids?.length ? { creator_ids } : {}),
|
...(creator_ids?.length ? { creator_ids } : {}),
|
||||||
...(instance_ids?.length ? { instance_ids } : {}),
|
...(instance_ids?.length ? { instance_ids } : {}),
|
||||||
...(volume_ids?.length ? { volume_ids } : {}),
|
...(volume_ids?.length ? { volume_ids } : {}),
|
||||||
...(organization_ids?.length ? { organization_ids } : {}),
|
|
||||||
...(user_group_ids?.length ? { user_group_ids } : {}),
|
|
||||||
...(data.order_by ? { order_by: data.order_by } : {}),
|
...(data.order_by ? { order_by: data.order_by } : {}),
|
||||||
...(data.descending !== undefined ? { descending: data.descending } : {}),
|
...(data.descending !== undefined ? { descending: data.descending } : {}),
|
||||||
page: data.page ?? 1,
|
page: data.page ?? 1,
|
||||||
@@ -498,8 +440,6 @@ export async function queryResourceEvents(
|
|||||||
options?: { skipErrorHandler?: boolean; token?: any }
|
options?: { skipErrorHandler?: boolean; token?: any }
|
||||||
): Promise<ResourceEventsResponse> {
|
): Promise<ResourceEventsResponse> {
|
||||||
const creatorIds = data.filters?.creator_ids;
|
const creatorIds = data.filters?.creator_ids;
|
||||||
const organizationIds = data.filters?.organization_ids;
|
|
||||||
const userGroupIds = data.filters?.user_group_ids;
|
|
||||||
return request<ResourceEventsResponse>(URL.EVENTS, {
|
return request<ResourceEventsResponse>(URL.EVENTS, {
|
||||||
params: {
|
params: {
|
||||||
start_date: data.start_date,
|
start_date: data.start_date,
|
||||||
@@ -509,12 +449,6 @@ export async function queryResourceEvents(
|
|||||||
// GET endpoints take list params as CSV strings (avoids axios array
|
// GET endpoints take list params as CSV strings (avoids axios array
|
||||||
// serialization quirks); the server splits them back into lists.
|
// serialization quirks); the server splits them back into lists.
|
||||||
...(creatorIds?.length ? { creator_ids: creatorIds.join(',') } : {}),
|
...(creatorIds?.length ? { creator_ids: creatorIds.join(',') } : {}),
|
||||||
...(organizationIds?.length
|
|
||||||
? { organization_ids: organizationIds.join(',') }
|
|
||||||
: {}),
|
|
||||||
...(userGroupIds?.length
|
|
||||||
? { user_group_ids: userGroupIds.join(',') }
|
|
||||||
: {}),
|
|
||||||
...(data.event_types?.length
|
...(data.event_types?.length
|
||||||
? { event_types: data.event_types.join(',') }
|
? { event_types: data.event_types.join(',') }
|
||||||
: {}),
|
: {}),
|
||||||
@@ -532,18 +466,12 @@ export interface ResourceFilterOption {
|
|||||||
id: number;
|
id: number;
|
||||||
label: string;
|
label: string;
|
||||||
deleted?: boolean;
|
deleted?: boolean;
|
||||||
// ``org`` / ``user`` / ``group`` — only set on organization options so the
|
|
||||||
// filter dropdown can tag a personal (USER) consumer.
|
|
||||||
kind?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResourceFilterMeta {
|
export interface ResourceFilterMeta {
|
||||||
creators: ResourceFilterOption[];
|
creators: ResourceFilterOption[];
|
||||||
instances: ResourceFilterOption[];
|
instances: ResourceFilterOption[];
|
||||||
volumes: ResourceFilterOption[];
|
volumes: ResourceFilterOption[];
|
||||||
// Platform-wide "All" view only (backend returns them empty otherwise).
|
|
||||||
organizations: ResourceFilterOption[];
|
|
||||||
user_groups: ResourceFilterOption[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function queryResourceFilterMeta(
|
export async function queryResourceFilterMeta(
|
||||||
@@ -556,9 +484,7 @@ export async function queryResourceFilterMeta(
|
|||||||
return {
|
return {
|
||||||
creators: res.creators || [],
|
creators: res.creators || [],
|
||||||
instances: res.instances || [],
|
instances: res.instances || [],
|
||||||
volumes: res.volumes || [],
|
volumes: res.volumes || []
|
||||||
organizations: res.organizations || [],
|
|
||||||
user_groups: res.user_groups || []
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,12 +494,10 @@ export async function queryUsageSummary(
|
|||||||
end_date: string;
|
end_date: string;
|
||||||
scope?: 'self' | 'all';
|
scope?: 'self' | 'all';
|
||||||
creator_ids?: number[];
|
creator_ids?: number[];
|
||||||
organization_ids?: number[];
|
|
||||||
user_group_ids?: number[];
|
|
||||||
},
|
},
|
||||||
options?: { token?: any }
|
options?: { token?: any }
|
||||||
): Promise<UsageSummaryResponse> {
|
): Promise<UsageSummaryResponse> {
|
||||||
const { creator_ids, organization_ids, user_group_ids, ...rest } = params;
|
const { creator_ids, ...rest } = params;
|
||||||
const res = await request<{
|
const res = await request<{
|
||||||
total_tokens: number;
|
total_tokens: number;
|
||||||
input_tokens: number;
|
input_tokens: number;
|
||||||
@@ -587,13 +511,7 @@ export async function queryUsageSummary(
|
|||||||
params: {
|
params: {
|
||||||
...rest,
|
...rest,
|
||||||
scope: params.scope ?? 'all',
|
scope: params.scope ?? 'all',
|
||||||
...(creator_ids?.length ? { creator_ids: creator_ids.join(',') } : {}),
|
...(creator_ids?.length ? { creator_ids: creator_ids.join(',') } : {})
|
||||||
...(organization_ids?.length
|
|
||||||
? { organization_ids: organization_ids.join(',') }
|
|
||||||
: {}),
|
|
||||||
...(user_group_ids?.length
|
|
||||||
? { user_group_ids: user_group_ids.join(',') }
|
|
||||||
: {})
|
|
||||||
},
|
},
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
cancelToken: options?.token
|
cancelToken: options?.token
|
||||||
@@ -610,17 +528,7 @@ export async function queryUsageSummary(
|
|||||||
end_date: params.end_date,
|
end_date: params.end_date,
|
||||||
scope: params.scope ?? 'all',
|
scope: params.scope ?? 'all',
|
||||||
group_by: ['gpu_type'],
|
group_by: ['gpu_type'],
|
||||||
...(creator_ids?.length ||
|
...(creator_ids?.length ? { filters: { creator_ids } } : {}),
|
||||||
organization_ids?.length ||
|
|
||||||
user_group_ids?.length
|
|
||||||
? {
|
|
||||||
filters: {
|
|
||||||
...(creator_ids?.length ? { creator_ids } : {}),
|
|
||||||
...(organization_ids?.length ? { organization_ids } : {}),
|
|
||||||
...(user_group_ids?.length ? { user_group_ids } : {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
page: 1,
|
page: 1,
|
||||||
perPage: 100
|
perPage: 100
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,15 +12,6 @@ import {
|
|||||||
UsageBreakdownResponse,
|
UsageBreakdownResponse,
|
||||||
UsageFilterItem
|
UsageFilterItem
|
||||||
} from '../config/types';
|
} from '../config/types';
|
||||||
import { withDeletedMark } from '../utils/deleted-label';
|
|
||||||
|
|
||||||
// group dimension → the id field inside ``identity.current`` (the backend nulls
|
|
||||||
// it for deleted entities, so the marker falls back to just "[Deleted]").
|
|
||||||
const GROUP_ID_KEY: Record<string, 'route_id' | 'user_id' | 'api_key_id'> = {
|
|
||||||
route: 'route_id',
|
|
||||||
user: 'user_id',
|
|
||||||
api_key: 'api_key_id'
|
|
||||||
};
|
|
||||||
|
|
||||||
const ControlsWrapper = styled.div`
|
const ControlsWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -133,22 +124,12 @@ const DailyUsage: React.FC<DailyUsageProps> = (props) => {
|
|||||||
(a, b) => dayjs(a).valueOf() - dayjs(b).valueOf()
|
(a, b) => dayjs(a).valueOf() - dayjs(b).valueOf()
|
||||||
);
|
);
|
||||||
|
|
||||||
const deletedWord = intl.formatMessage({ id: 'usage.table.deleted' });
|
|
||||||
|
|
||||||
const groupOrder: string[] = [];
|
const groupOrder: string[] = [];
|
||||||
const groupItemsMap = new Map<string, Map<string, BreakdownItem>>();
|
const groupItemsMap = new Map<string, Map<string, BreakdownItem>>();
|
||||||
|
|
||||||
items.forEach((item) => {
|
items.forEach((item) => {
|
||||||
const groupEntity = groupDim
|
|
||||||
? (item[groupDim] as UsageFilterItem)
|
|
||||||
: undefined;
|
|
||||||
const groupLabel = groupDim
|
const groupLabel = groupDim
|
||||||
? withDeletedMark(
|
? ((item[groupDim] as UsageFilterItem)?.label ?? '-')
|
||||||
groupEntity?.label ?? '-',
|
|
||||||
groupEntity?.deleted,
|
|
||||||
deletedWord,
|
|
||||||
groupEntity?.identity?.current?.[GROUP_ID_KEY[groupDim]]
|
|
||||||
)
|
|
||||||
: '__total__';
|
: '__total__';
|
||||||
|
|
||||||
if (!groupItemsMap.has(groupLabel)) {
|
if (!groupItemsMap.has(groupLabel)) {
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
/**
|
|
||||||
* Deleted-entity marker for the usage filter dropdowns (model / user / api-key
|
|
||||||
* on the Tokens tab; user / instance / volume on the resource tabs). Renders an
|
|
||||||
* outlined transparent pill "Deleted·#{id}" — the id keeps two entries sharing
|
|
||||||
* a now-stale label distinguishable. Pairs with dimming the option label to the
|
|
||||||
* tertiary text color.
|
|
||||||
*
|
|
||||||
* Presentational only: each filter resolves the entity id from its own option
|
|
||||||
* shape (Tokens tab ← ``identity.current``; resource tabs ← option ``value``)
|
|
||||||
* and passes it in. When no id is available the tag falls back to plain
|
|
||||||
* "Deleted".
|
|
||||||
*/
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { Tag } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface DeletedTagProps {
|
|
||||||
id?: string | number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DeletedTag: React.FC<DeletedTagProps> = ({ id }) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const label = intl.formatMessage({ id: 'usage.table.deleted' });
|
|
||||||
return (
|
|
||||||
<Tag
|
|
||||||
variant="outlined"
|
|
||||||
style={{
|
|
||||||
margin: 0,
|
|
||||||
fontSize: 11,
|
|
||||||
borderRadius: 12,
|
|
||||||
background: 'transparent'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
{!!id && (
|
|
||||||
<span className="text-tertiary">
|
|
||||||
<span style={{ margin: '0 2px' }}>·</span>#{id}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DeletedTag;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user