Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e919b22d98 | ||
|
|
89a74ed26a | ||
|
|
fe4dbf087f | ||
|
|
cbd9fc4946 | ||
|
|
74f1399e3c | ||
|
|
96d194a4f4 | ||
|
|
e9d585cc90 | ||
|
|
3fff1c10f2 | ||
|
|
2763594ae3 | ||
|
|
1655549f31 | ||
|
|
35d5681008 | ||
|
|
15f457a0e1 | ||
|
|
9386f0872b | ||
|
|
fe0bab041b | ||
|
|
fe24f9b53d | ||
|
|
63ed6e777b |
@@ -97,6 +97,38 @@ const handleBChange = (b) => {
|
||||
};
|
||||
```
|
||||
|
||||
## 4. Controlled input with derived fields
|
||||
|
||||
When a controlled field's value comes from **both** user input and a programmatic default (e.g. a percentage picked on a slider, and a default seeded on select / mode-switch), funnel both through **one commit function** — don't duplicate "write field + recompute derived" per call site.
|
||||
|
||||
- The `Form.Item`-bound input's `onChange(value)` forwards the value to the commit fn (the field is antd-bound, but pass the value explicitly so the default path can reuse the same fn instead of reading the store).
|
||||
- Seed defaults by calling the **same** commit fn with the computed value.
|
||||
- Separate the **commit action** (write field + recompute dependents) from the **render-derive** (read the field → recompute dependents). Keeping the derive standalone lets it re-run on reload/edit where there's no user event.
|
||||
|
||||
```ts
|
||||
// commit action — slider onChange AND default both call this
|
||||
const commitRatio = (value: number) => {
|
||||
form.setFieldsValue({ spec: { resources: { ratio: value, cores: 100 } } });
|
||||
rescaleDerived(); // reads ratio from the form, sets the disabled cpu/ram
|
||||
};
|
||||
|
||||
// render-derive — also called from the edit/reload effect
|
||||
const rescaleDerived = () => {
|
||||
const ratio = form.getFieldValue(['spec', 'resources', 'ratio']);
|
||||
form.setFieldsValue({
|
||||
spec: {
|
||||
resources: {
|
||||
cpu: floorScale(unit.cpu, ratio),
|
||||
ram: floorScale(unit.ram, ratio)
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// default seeding reuses the commit fn — one path, not a second copy
|
||||
const applyDefaults = (item) => commitRatio(Math.min(10, item.maxRatio) || 10);
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- Module/file structure for forms lives in the **create-crud-page** skill (section 3).
|
||||
|
||||
@@ -3,55 +3,3 @@
|
||||
This project keeps a single source of truth for agent/contributor conventions in [`CLAUDE.md`](./CLAUDE.md). **Read [`CLAUDE.md`](./CLAUDE.md) and follow it.**
|
||||
|
||||
@CLAUDE.md
|
||||
|
||||
## Downstream fork workflow
|
||||
|
||||
This repo is a **downstream fork** that customizes the product appearance on top of upstream `gpustack/gpustack-ui`. The mirror chain is:
|
||||
|
||||
```
|
||||
upstream https://github.com/gpustack/gpustack-ui.git
|
||||
| (fetch)
|
||||
origin ssh://git@192.168.0.23:11022/root/gpustack-ui.git (private registry, also https://git.digiman.live)
|
||||
```
|
||||
|
||||
### Goal
|
||||
|
||||
Track upstream releases. When upstream changes, we pull it in, then apply our own appearance/customization changes on a dedicated branch so we keep our look-and-feel on top of the latest upstream product.
|
||||
|
||||
### Branch naming
|
||||
|
||||
Customization work lives on `v<upstream-version>-lofyer` branches (e.g. `v2.2.0-lofyer`). Each time upstream ships a new version we want to follow, create a new `v<version>-lofyer` branch from the corresponding upstream tag/branch and re-apply (or rebase) our customizations onto it.
|
||||
|
||||
### Syncing from upstream
|
||||
|
||||
`scripts/sync-github` mirrors upstream into the private `origin` (full mirror, force push of all branches + tags). It auto-configures the `upstream` remote on first run.
|
||||
|
||||
```bash
|
||||
# preview only, no push
|
||||
DRY_RUN=1 ./scripts/sync-github
|
||||
|
||||
# real sync (force-pushes every upstream branch + tag to origin)
|
||||
./scripts/sync-github
|
||||
|
||||
# also delete origin branches that no longer exist upstream (true mirror, destructive)
|
||||
PRUNE_BRANCHES=1 ./scripts/sync-github
|
||||
```
|
||||
|
||||
Env knobs: `UPSTREAM_URL`, `ORIGIN_REMOTE`, `UPSTREAM_REMOTE`, `PRUNE_BRANCHES`, `DRY_RUN`. After syncing, branch a fresh `v<version>-lofyer` off the updated upstream ref and apply the appearance changes there.
|
||||
|
||||
### Re-applying brand customizations
|
||||
|
||||
`scripts/rebrand` swaps the standalone brand word `GPUStack` for our brand (`ZStack AIOS`) across user-facing text. It is the first appearance change to re-apply on every new `v<version>-lofyer` branch.
|
||||
|
||||
```bash
|
||||
# preview hits, no writes
|
||||
DRY_RUN=1 ./scripts/rebrand
|
||||
|
||||
# apply (default GPUStack -> ZStack AIOS)
|
||||
./scripts/rebrand
|
||||
|
||||
# custom brand words
|
||||
FROM=GPUStack TO=AcmeStack ./scripts/rebrand
|
||||
```
|
||||
|
||||
It deliberately **does not** touch functional references — lowercase `gpustack` (npm pkg / URLs / paths / k8s namespace), ALL-CAPS `GPUSTACK_*` constants, JS identifiers like `getGPUStackPlugin`, and `X-*` HTTP headers — and carries a line-level skip list for backend-contract strings matched at runtime (see `SKIP_LINE_PATTERNS` in the script). Always review `git diff` afterwards. Logo images under `src/assets/images/` are NOT changed by the script — replace those PNGs separately when new brand assets are available.
|
||||
|
||||
@@ -98,6 +98,10 @@ Compose layout with Ant components, not hand-written `display: flex`.
|
||||
|
||||
Drive spacing with the theme scale (`Flex`/`Space` `gap`, or `var(--ant-*)` spacing tokens), not scattered `px` literals.
|
||||
|
||||
## Tables
|
||||
|
||||
- **Horizontally scrollable table**: set `scroll={{ x: 'max-content' }}` **and** add `className="scroll-table"` on the `Table`. The class styles the horizontal scroll to match the design; without it the scroll works but looks off.
|
||||
|
||||
# Naming conventions
|
||||
|
||||
A page module lives under `src/pages/{module}` with this sub-structure: `components/`, `config/`, `forms/`, `hooks/`, `services/`, `index.tsx`. File naming:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MesaStack UI
|
||||
# GPUStack UI
|
||||
|
||||
UI for [MesaStack](https://github.com/gpustack/gpustack).
|
||||
UI for [GPUStack](https://github.com/gpustack/gpustack).
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ export default defineConfig({
|
||||
antd: {
|
||||
style: 'less'
|
||||
},
|
||||
title: 'ZStack AIOS',
|
||||
title: 'GPUStack',
|
||||
hash: true,
|
||||
access: {},
|
||||
model: {},
|
||||
|
||||
@@ -211,6 +211,16 @@ const baseRoutes = [
|
||||
defaultIcon: 'icon-cloud-outlined',
|
||||
component: './gpu-service/instances'
|
||||
},
|
||||
{
|
||||
name: 'instanceTypes',
|
||||
path: '/gpu-service/instance-types',
|
||||
key: 'gpuServiceInstanceTypes',
|
||||
icon: 'icon-outline-gpu',
|
||||
access: 'canSeeOrgAdmin',
|
||||
selectedIcon: 'icon-filled-gpu',
|
||||
defaultIcon: 'icon-outline-gpu',
|
||||
component: './gpu-service/instance-types'
|
||||
},
|
||||
{
|
||||
name: 'templates',
|
||||
path: '/gpu-service/templates',
|
||||
|
||||
|
Before Width: | Height: | Size: 587 B After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 565 B After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 587 B After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 565 B After Width: | Height: | Size: 3.0 KiB |
@@ -1,102 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# rebrand: 把面向用户的品牌标识从 GPUStack 批量替换为 ZStack AIOS(或自定义品牌)。
|
||||
#
|
||||
# 设计目标:上游每次更新后,在新的 v<version>-lofyer 分支上跑一次即可重新应用品牌定制。
|
||||
#
|
||||
# 只替换「独立的品牌词」FROM,并刻意跳过所有功能性引用:
|
||||
# - 小写 `gpustack` —— npm 包名 (@gpustack/core-ui)、URL、路径、k8s 命名空间、author(大小写敏感,天然不匹配)
|
||||
# - 全大写 `GPUSTACK` —— 常量/环境变量/全局 (GPUSTACK_API_BASE_URL, __GPUSTACK_*__, GPUSTACK_UI_*)(大小写敏感,不匹配)
|
||||
# - 代码标识符 —— getGPUStackPlugin / GPUStackVersionAtom / GPUStackPluginManager / GPUStackLogo 等
|
||||
# (FROM 紧邻字母时视为标识符的一部分,跳过)
|
||||
# - HTTP 头 X-GPUStack-* —— 后端契约 (如 X-GPUStack-Model),跳过
|
||||
#
|
||||
# 匹配规则:FROM 前后都不是字母(独立单词),且不是 `X-` 前缀的头名。
|
||||
#
|
||||
# 环境变量:
|
||||
# FROM 源品牌词(默认 GPUStack)
|
||||
# TO 目标品牌词(默认 ZStack AIOS)
|
||||
# DRY_RUN 设为 1 时只预览将改动的行,不写文件
|
||||
#
|
||||
set -e
|
||||
|
||||
FROM="${FROM:-GPUStack}"
|
||||
TO="${TO:-ZStack AIOS}"
|
||||
DRY_RUN="${DRY_RUN:-0}"
|
||||
|
||||
log() { echo -e "\033[1;34m[rebrand]\033[0m $*"; }
|
||||
|
||||
# 大小写敏感、单词边界、排除 X- 头前缀的 Perl 正则。
|
||||
# (?<![A-Za-z]) 前面不是字母 (?<!X-) 不是 X- 头 (?![A-Za-z]) 后面不是字母
|
||||
# FROM 为纯字母品牌词(无正则元字符),故直接拼接,不用 \Q\E
|
||||
# —— \Q\E 在「经变量插值进正则」时不会被求值,反而会破坏匹配。
|
||||
PATTERN="(?<![A-Za-z])(?<!X-)${FROM}(?![A-Za-z])"
|
||||
|
||||
# 行级排除:某些 FROM 出现在「与后端契约绑定的字符串」里,改了会破坏运行时逻辑,
|
||||
# 即使是独立单词也必须整行跳过。命中下列任一正则的行不替换。
|
||||
# 已知例外:
|
||||
# - llmodels/hooks/index.ts 用 startsWith() 比对后端返回的英文兼容性消息
|
||||
# ("... does not exist on the GPUStack server ..."),后端仍发 GPUStack,前端不能改。
|
||||
SKIP_LINE_PATTERNS=(
|
||||
'does not exist on the .*server. It'"'"'s recommended'
|
||||
)
|
||||
skip_line() {
|
||||
local line="$1" pat
|
||||
for pat in "${SKIP_LINE_PATTERNS[@]}"; do
|
||||
if echo "$line" | grep -qP "$pat"; then return 0; fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# 待处理的已跟踪文本文件(排除 lockfile 与本脚本自身)。
|
||||
mapfile -t files < <(
|
||||
git ls-files -- \
|
||||
'*.ts' '*.tsx' '*.js' '*.jsx' '*.json' '*.less' '*.html' '*.md' \
|
||||
| grep -v 'pnpm-lock.yaml'
|
||||
)
|
||||
|
||||
log "品牌替换: '${FROM}' -> '${TO}'"
|
||||
log "候选文件: ${#files[@]}"
|
||||
|
||||
# 把行级排除合并成一个 perl 正则,供预览与替换共用(经环境变量传入,免去转义)。
|
||||
SKIP_RE=""
|
||||
for pat in "${SKIP_LINE_PATTERNS[@]}"; do
|
||||
SKIP_RE="${SKIP_RE:+${SKIP_RE}|}(?:${pat})"
|
||||
done
|
||||
export REBRAND_SKIP_RE="${SKIP_RE}"
|
||||
export REBRAND_PATTERN="${PATTERN}"
|
||||
export REBRAND_TO="${TO}"
|
||||
|
||||
# 统计命中行(预览/确认用),已扣除被行级排除的行。
|
||||
log "命中行预览(最多 40 行):"
|
||||
grep -rnP "${PATTERN}" "${files[@]}" 2>/dev/null \
|
||||
| { [[ -n "${SKIP_RE}" ]] && grep -vP "${SKIP_RE}" || cat; } \
|
||||
| head -40 || true
|
||||
total=$(grep -rnP "${PATTERN}" "${files[@]}" 2>/dev/null \
|
||||
| { [[ -n "${SKIP_RE}" ]] && grep -vP "${SKIP_RE}" || cat; } | wc -l)
|
||||
log "命中总行数: ${total}"
|
||||
if [[ -n "${SKIP_RE}" ]]; then
|
||||
skipped=$(grep -rnP "${PATTERN}" "${files[@]}" 2>/dev/null | grep -cP "${SKIP_RE}" || true)
|
||||
log "行级排除(后端契约,保留 ${FROM}): ${skipped} 行"
|
||||
fi
|
||||
|
||||
if [[ "${DRY_RUN}" == "1" ]]; then
|
||||
log "DRY_RUN=1:未写入任何文件。"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 实际替换(in-place)。命中行级排除的行整行跳过。
|
||||
changed=0
|
||||
for f in "${files[@]}"; do
|
||||
if grep -qP "${PATTERN}" "$f" 2>/dev/null; then
|
||||
perl -i -pe '
|
||||
my $skip = $ENV{REBRAND_SKIP_RE};
|
||||
next if length($skip) && /$skip/;
|
||||
s/$ENV{REBRAND_PATTERN}/$ENV{REBRAND_TO}/g;
|
||||
' "$f"
|
||||
changed=$((changed + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
log "已修改文件数: ${changed}"
|
||||
log "完成。请用 'git diff' 复核改动。"
|
||||
@@ -1,83 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# sync-github: 从上游 GitHub 拉取最新源码,并全量镜像同步到 192.168.0.23 私有源。
|
||||
#
|
||||
# 默认行为(全量镜像 / 强制):
|
||||
# 1. 确保存在 upstream remote 指向 GitHub(不存在则自动添加,URL 不一致则更新)。
|
||||
# 2. 从 upstream 抓取所有分支与 tag(--prune 清理已删除的远端引用)。
|
||||
# 3. 将上游每个分支强制推送到私有源同名分支(force push)。
|
||||
# 4. 将上游所有 tag 强制推送到私有源。
|
||||
#
|
||||
# 可用环境变量:
|
||||
# UPSTREAM_URL 上游 GitHub 仓库地址(默认 https://github.com/gpustack/gpustack-ui.git)
|
||||
# ORIGIN_REMOTE 私有源 remote 名称(默认 origin)
|
||||
# UPSTREAM_REMOTE 上游 remote 名称(默认 upstream)
|
||||
# PRUNE_BRANCHES 设为 1 时,删除私有源上「上游已不存在」的分支(真·镜像,破坏性,默认关闭)
|
||||
# DRY_RUN 设为 1 时,仅打印将要执行的推送动作,不实际推送
|
||||
#
|
||||
set -e
|
||||
|
||||
UPSTREAM_URL="${UPSTREAM_URL:-https://github.com/gpustack/gpustack-ui.git}"
|
||||
ORIGIN_REMOTE="${ORIGIN_REMOTE:-origin}"
|
||||
UPSTREAM_REMOTE="${UPSTREAM_REMOTE:-upstream}"
|
||||
PRUNE_BRANCHES="${PRUNE_BRANCHES:-0}"
|
||||
DRY_RUN="${DRY_RUN:-0}"
|
||||
|
||||
log() { echo -e "\033[1;34m[sync-github]\033[0m $*"; }
|
||||
warn() { echo -e "\033[1;33m[sync-github]\033[0m $*" >&2; }
|
||||
|
||||
run() {
|
||||
if [[ "${DRY_RUN}" == "1" ]]; then
|
||||
echo " (dry-run) git $*"
|
||||
else
|
||||
git "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# 1. 确保 upstream remote 指向 GitHub。
|
||||
if git remote get-url "${UPSTREAM_REMOTE}" >/dev/null 2>&1; then
|
||||
current_url=$(git remote get-url "${UPSTREAM_REMOTE}")
|
||||
if [[ "${current_url}" != "${UPSTREAM_URL}" ]]; then
|
||||
log "更新 ${UPSTREAM_REMOTE} 地址: ${current_url} -> ${UPSTREAM_URL}"
|
||||
git remote set-url "${UPSTREAM_REMOTE}" "${UPSTREAM_URL}"
|
||||
fi
|
||||
else
|
||||
log "添加 upstream remote: ${UPSTREAM_REMOTE} -> ${UPSTREAM_URL}"
|
||||
git remote add "${UPSTREAM_REMOTE}" "${UPSTREAM_URL}"
|
||||
fi
|
||||
|
||||
origin_url=$(git remote get-url "${ORIGIN_REMOTE}")
|
||||
log "上游 (拉取): ${UPSTREAM_URL}"
|
||||
log "私有源 (推送): ${origin_url}"
|
||||
|
||||
# 2. 抓取上游所有分支与 tag。
|
||||
log "抓取上游分支与 tag..."
|
||||
git fetch --prune --tags "${UPSTREAM_REMOTE}"
|
||||
|
||||
# 3. 逐个分支强制推送到私有源。
|
||||
log "强制同步分支到私有源..."
|
||||
upstream_branches=$(git for-each-ref --format='%(refname:strip=3)' "refs/remotes/${UPSTREAM_REMOTE}/" | grep -v '^HEAD$')
|
||||
|
||||
for branch in ${upstream_branches}; do
|
||||
log " -> ${branch}"
|
||||
run push --force "${ORIGIN_REMOTE}" \
|
||||
"refs/remotes/${UPSTREAM_REMOTE}/${branch}:refs/heads/${branch}"
|
||||
done
|
||||
|
||||
# 4. 强制同步所有 tag。
|
||||
log "强制同步 tag 到私有源..."
|
||||
run push --force --tags "${ORIGIN_REMOTE}"
|
||||
|
||||
# 5. 可选:删除私有源上、上游已不存在的分支(真·镜像)。
|
||||
if [[ "${PRUNE_BRANCHES}" == "1" ]]; then
|
||||
warn "PRUNE_BRANCHES=1:将删除私有源上上游已不存在的分支"
|
||||
origin_branches=$(git ls-remote --heads "${ORIGIN_REMOTE}" | sed 's@.*refs/heads/@@')
|
||||
for branch in ${origin_branches}; do
|
||||
if ! echo "${upstream_branches}" | grep -qx "${branch}"; then
|
||||
warn " 删除私有源分支: ${branch}"
|
||||
run push "${ORIGIN_REMOTE}" --delete "${branch}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
log "同步完成。"
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 26 KiB |
@@ -1,6 +1,8 @@
|
||||
import { GPUStackVersionAtom } from '@/atoms/user';
|
||||
import VersionInfo, { modalConfig } from '@/components/version-info';
|
||||
import externalLinks from '@/constants/external-links';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Divider, Typography } from 'antd';
|
||||
import { Button, Divider, Modal, Typography } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import styled from 'styled-components';
|
||||
@@ -31,11 +33,21 @@ const useStyles = createStyles(({ token, css }) => ({
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const { styles } = useStyles();
|
||||
const version = useAtomValue(GPUStackVersionAtom);
|
||||
|
||||
const showVersion = () => {
|
||||
modal.info({
|
||||
...modalConfig,
|
||||
width: 460,
|
||||
content: <VersionInfo intl={intl} />
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
<div className={styles.footer}>
|
||||
<div className="footer-content">
|
||||
<div className="footer-content-left">
|
||||
@@ -52,7 +64,18 @@ const Footer: React.FC = () => {
|
||||
</Typography.Link>
|
||||
</CompanyWrapper>
|
||||
<Divider orientation="vertical" />
|
||||
<span>{version?.version}</span>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
href={externalLinks.documentation}
|
||||
target="_blank"
|
||||
>
|
||||
{intl.formatMessage({ id: 'common.button.help' })}
|
||||
</Button>
|
||||
<Divider orientation="vertical" />
|
||||
<Button type="link" size="small" onClick={showVersion}>
|
||||
{version?.version}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,38 +8,24 @@ const findValidJSONStrings = (inputStr: string) => {
|
||||
const openingBraceIndex = inputStr.indexOf('{', startIndex);
|
||||
if (openingBraceIndex === -1) break; // No more opening braces
|
||||
|
||||
// find the matching closing brace, ignoring braces inside string
|
||||
// literals (e.g. a state_message containing `{`/`}`)
|
||||
let closingBraceIndex = -1;
|
||||
let closingBraceIndex = openingBraceIndex;
|
||||
let braceCount = 0;
|
||||
let inString = false;
|
||||
let escaped = false;
|
||||
|
||||
for (let i = openingBraceIndex; i < inputStr.length; i++) {
|
||||
const char = inputStr[i];
|
||||
if (inString) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else if (char === '\\') {
|
||||
escaped = true;
|
||||
} else if (char === '"') {
|
||||
inString = false;
|
||||
}
|
||||
} else if (char === '"') {
|
||||
inString = true;
|
||||
} else if (char === '{') {
|
||||
// find couple of braces
|
||||
while (closingBraceIndex < inputStr.length) {
|
||||
if (inputStr[closingBraceIndex] === '{') {
|
||||
braceCount++;
|
||||
} else if (char === '}') {
|
||||
} else if (inputStr[closingBraceIndex] === '}') {
|
||||
braceCount--;
|
||||
if (braceCount === 0) {
|
||||
closingBraceIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (braceCount === 0) {
|
||||
break;
|
||||
}
|
||||
closingBraceIndex++;
|
||||
}
|
||||
|
||||
if (closingBraceIndex === -1) {
|
||||
// no matching closing brace yet, wait for more data
|
||||
if (braceCount !== 0) {
|
||||
// no matching closing brace
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -51,11 +37,11 @@ const findValidJSONStrings = (inputStr: string) => {
|
||||
try {
|
||||
const parsedData = JSON.parse(jsonString);
|
||||
validJSONStrings.push(parsedData);
|
||||
startIndex = closingBraceIndex + 1;
|
||||
} catch (error) {
|
||||
// skip the malformed segment instead of breaking, otherwise it jams
|
||||
// the buffer and every later event on this stream is lost
|
||||
// mabye invalid JSON
|
||||
break;
|
||||
}
|
||||
startIndex = closingBraceIndex + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -43,11 +43,8 @@ export const createAxiosToken = (): CancelTokenSource => {
|
||||
};
|
||||
|
||||
export const sliceData = (data: string, loaded: number, loadedSize: any) => {
|
||||
// `loaded` is a byte count while `data` is a UTF-16 string; with any
|
||||
// non-ASCII payload the two drift apart, so track consumed characters by
|
||||
// string length only
|
||||
const result = data.slice(loadedSize.current);
|
||||
loadedSize.current = data.length;
|
||||
loadedSize.current = loaded;
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user';
|
||||
import PluginExtraField from '@/components/plugin-extra-fields';
|
||||
import VersionInfo, { modalConfig } from '@/components/version-info';
|
||||
import externalLinks from '@/constants/external-links';
|
||||
import useBodyScroll from '@/hooks/use-body-scroll';
|
||||
import { logout } from '@/pages/login/apis';
|
||||
import { getGPUStackPlugin } from '@/plugins';
|
||||
import { useModel } from '@@/plugin-model';
|
||||
@@ -12,11 +14,12 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { DropdownActions, IconFont } from '@gpustack/core-ui';
|
||||
import { history, useIntl, useNavigate } from '@umijs/max';
|
||||
import { Avatar, Divider } from 'antd';
|
||||
import { Avatar, Button, Divider, Modal } from 'antd';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { DEFAULT_ENTER_PAGE } from '../config/settings';
|
||||
import GithubStar from './github-star';
|
||||
|
||||
const NewLabel = styled.span`
|
||||
position: relative;
|
||||
@@ -97,9 +100,11 @@ const CustomItem = styled.div`
|
||||
export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
||||
const { isDarkTheme } = props;
|
||||
const plugin = getGPUStackPlugin();
|
||||
const intl = useIntl();
|
||||
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const [version] = useAtom(GPUStackVersionAtom);
|
||||
const [updateCheck] = useAtom(UpdateCheckAtom);
|
||||
const intl = useIntl();
|
||||
const initialInfo = useModel('@@initialState') || {
|
||||
initialState: undefined,
|
||||
loading: false,
|
||||
@@ -137,6 +142,16 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
||||
return {};
|
||||
}, [isDarkTheme]);
|
||||
|
||||
const showVersion = () => {
|
||||
saveScrollHeight();
|
||||
modal.info({
|
||||
...modalConfig,
|
||||
width: 460,
|
||||
content: <VersionInfo intl={intl} />,
|
||||
onCancel: restoreScrollHeight
|
||||
});
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
navigate(loginPath);
|
||||
@@ -146,7 +161,7 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
||||
{
|
||||
key: 'site',
|
||||
icon: <HomeOutlined />,
|
||||
label: 'ZStack AIOS',
|
||||
label: 'GPUStack',
|
||||
url: externalLinks.site
|
||||
},
|
||||
{
|
||||
@@ -246,20 +261,25 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
{contextHolder}
|
||||
<PluginExtraField name="OrgSwitcher" isDarkTheme={isDarkTheme} />
|
||||
{process.env.ENABLE_ENTERPRISE !== 'true' && <GithubStar />}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<span
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={showVersion}
|
||||
style={{
|
||||
color: 'var(--ant-color-text-tertiary)'
|
||||
}}
|
||||
>
|
||||
{version.version}
|
||||
</span>
|
||||
</Button>
|
||||
{showUpgrade && (
|
||||
<NewLabel>
|
||||
<span className="text">
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import externalLinks from '@/constants/external-links';
|
||||
import { GithubFilled } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Tooltip } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const REPO = 'gpustack/gpustack';
|
||||
const CACHE_KEY = 'gpustack:github-stars';
|
||||
const CACHE_TTL = 24 * 60 * 60 * 1000;
|
||||
const FETCH_TIMEOUT = 4000;
|
||||
|
||||
const StarLink = styled.a`
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
height: 24px;
|
||||
border-radius: var(--ant-border-radius);
|
||||
border: 1px solid var(--ant-color-border-secondary);
|
||||
background-color: var(--ant-color-bg-container);
|
||||
color: var(--ant-color-text-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--ant-color-border);
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
|
||||
.seg {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.seg + .seg {
|
||||
border-left: 1px solid var(--ant-color-border-secondary);
|
||||
background-color: var(--ant-color-fill-quaternary);
|
||||
}
|
||||
|
||||
.anticon {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.count {
|
||||
font-weight: 500;
|
||||
font-variant-numeric: tabular-nums;
|
||||
min-width: 1.5em;
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const formatCount = (n: number): string => {
|
||||
if (n >= 1000) {
|
||||
const k = n / 1000;
|
||||
return k >= 10 ? `${Math.round(k)}k` : `${k.toFixed(1)}k`;
|
||||
}
|
||||
return String(n);
|
||||
};
|
||||
|
||||
type CacheEntry = { value: number; time: number };
|
||||
|
||||
const readCache = (): CacheEntry | null => {
|
||||
try {
|
||||
const raw = localStorage.getItem(CACHE_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (typeof parsed?.value !== 'number' || typeof parsed?.time !== 'number') {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const writeCache = (value: number) => {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
CACHE_KEY,
|
||||
JSON.stringify({ value, time: Date.now() })
|
||||
);
|
||||
} catch {
|
||||
// ignore quota errors
|
||||
}
|
||||
};
|
||||
|
||||
const GithubStar = () => {
|
||||
const intl = useIntl();
|
||||
const [count, setCount] = useState<number | null>(
|
||||
() => readCache()?.value ?? null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const cached = readCache();
|
||||
const fresh = cached && Date.now() - cached.time < CACHE_TTL;
|
||||
if (fresh) return;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
||||
|
||||
fetch(`https://api.github.com/repos/${REPO}`, { signal: controller.signal })
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((data) => {
|
||||
if (!data || typeof data.stargazers_count !== 'number') return;
|
||||
setCount(data.stargazers_count);
|
||||
writeCache(data.stargazers_count);
|
||||
})
|
||||
.catch(() => {
|
||||
// offline, blocked, rate-limited — stay hidden if no cache
|
||||
})
|
||||
.finally(() => clearTimeout(timer));
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
controller.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Tooltip title={intl.formatMessage({ id: 'common.github.star.tooltip' })}>
|
||||
<StarLink href={externalLinks.github} target="_blank" rel="noreferrer">
|
||||
<span className="seg">
|
||||
<GithubFilled />
|
||||
</span>
|
||||
<span className="seg">
|
||||
<span className="count">
|
||||
{count != null ? formatCount(count) : 'Star'}
|
||||
</span>
|
||||
</span>
|
||||
</StarLink>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default GithubStar;
|
||||
@@ -1,128 +0,0 @@
|
||||
import { IconFont } from '@gpustack/core-ui';
|
||||
import { Link, useLocation, useNavigate } from '@umijs/max';
|
||||
import { Menu } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
interface MenuItem {
|
||||
icon?: string;
|
||||
selectedIcon?: string;
|
||||
defaultIcon?: string;
|
||||
children?: MenuItem[];
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface HeaderMenuProps {
|
||||
menuData: MenuItem[];
|
||||
initialState?: Global.InitialStateType;
|
||||
}
|
||||
|
||||
const useStyles = createStyles(({ css }) => {
|
||||
return {
|
||||
headerMenu: css`
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: transparent;
|
||||
border-bottom: none;
|
||||
line-height: inherit;
|
||||
|
||||
&.ant-menu-horizontal {
|
||||
border-bottom: none;
|
||||
}
|
||||
&.ant-menu-horizontal > .ant-menu-item::after,
|
||||
&.ant-menu-horizontal > .ant-menu-submenu::after {
|
||||
display: none;
|
||||
}
|
||||
.ant-menu-title-content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
}
|
||||
`
|
||||
};
|
||||
});
|
||||
|
||||
const isItemSelected = (item: MenuItem, pathname: string) => {
|
||||
return (
|
||||
pathname === item.path ||
|
||||
(Array.isArray(item.subMenu) && item.subMenu.includes(pathname))
|
||||
);
|
||||
};
|
||||
|
||||
const HeaderMenu: React.FC<HeaderMenuProps> = (props) => {
|
||||
const { menuData } = props;
|
||||
const { styles } = useStyles();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const buildLeaf = (item: MenuItem) => {
|
||||
const selected = isItemSelected(item, location.pathname);
|
||||
return {
|
||||
key: item.path as string,
|
||||
label: (
|
||||
<Link
|
||||
prefetch="intent"
|
||||
to={(item.path as string).replace('/*', '')}
|
||||
target={item.target}
|
||||
>
|
||||
<span className="flex-center gap-8">
|
||||
<IconFont
|
||||
type={selected ? item.selectedIcon || '' : item.defaultIcon || ''}
|
||||
/>
|
||||
<span>{item.name}</span>
|
||||
</span>
|
||||
</Link>
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
const items = useMemo(() => {
|
||||
return menuData.map((item) => {
|
||||
if (item.children && item.children.length > 0) {
|
||||
return {
|
||||
key: item.key,
|
||||
label: item.name,
|
||||
children: item.children.map((child) => buildLeaf(child))
|
||||
};
|
||||
}
|
||||
return buildLeaf(item);
|
||||
});
|
||||
}, [menuData, location.pathname]);
|
||||
|
||||
const selectedKeys = useMemo(() => {
|
||||
const keys: string[] = [];
|
||||
for (const item of menuData) {
|
||||
const leaves =
|
||||
item.children && item.children.length > 0 ? item.children : [item];
|
||||
for (const leaf of leaves) {
|
||||
if (isItemSelected(leaf, location.pathname)) {
|
||||
keys.push(leaf.path as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}, [menuData, location.pathname]);
|
||||
|
||||
const handleClick = ({ key }: { key: string }) => {
|
||||
if (key.startsWith('/')) {
|
||||
navigate(key.replace('/*', ''));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu
|
||||
className={styles.headerMenu}
|
||||
mode="horizontal"
|
||||
selectedKeys={selectedKeys}
|
||||
items={items}
|
||||
onClick={handleClick}
|
||||
triggerSubMenuAction="hover"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderMenu;
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import { useAccessMarkedRoutes } from '@@/plugin-access';
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import { ProLayout } from '@ant-design/pro-components';
|
||||
import { CoreUIProvider } from '@gpustack/core-ui';
|
||||
import { CoreUIProvider, IconFont } from '@gpustack/core-ui';
|
||||
import {
|
||||
Access,
|
||||
Outlet,
|
||||
@@ -39,18 +39,18 @@ import {
|
||||
useNavigate,
|
||||
type IRoute
|
||||
} from '@umijs/max';
|
||||
import { ConfigProvider, Modal, theme } from 'antd';
|
||||
import { Button, ConfigProvider, Modal, theme } from 'antd';
|
||||
import { useAtom } from 'jotai';
|
||||
import 'overlayscrollbars/overlayscrollbars.css';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { PageContainerInner } from '../pages/_components/page-box';
|
||||
import Exception from './Exception';
|
||||
import './Layout.css';
|
||||
import { LogoIcon } from './Logo';
|
||||
import { LogoIcon, SLogoIcon } from './Logo';
|
||||
import ErrorBoundary from './error-boundary';
|
||||
import { ExtraContent } from './extraRender';
|
||||
import HeaderMenu from './header-menu';
|
||||
import { patchRoutes } from './runtime';
|
||||
import SiderMenu from './sider-menu';
|
||||
|
||||
const CHECK_RESOURCE_PATH = [
|
||||
'/resources/workers',
|
||||
@@ -118,7 +118,7 @@ const mapRoutes = (routes: IRoute[], role: string) => {
|
||||
|
||||
export default (props: any) => {
|
||||
const [, contextHolder] = Modal.useModal();
|
||||
const { themeData, userSettings } = useUserSettings();
|
||||
const { themeData, setUserSettings, userSettings } = useUserSettings();
|
||||
const [userInfo] = useAtom(userAtom);
|
||||
const [routeCache] = useAtom(routeCacheAtom);
|
||||
const location = useLocation();
|
||||
@@ -223,6 +223,13 @@ export default (props: any) => {
|
||||
|
||||
const coreUISlots = useMemo(() => ({ ExtraContent }), []);
|
||||
|
||||
const handleToggleCollapse = (e: any) => {
|
||||
e.stopPropagation();
|
||||
setUserSettings({
|
||||
...userSettings,
|
||||
collapsed: !userSettings.collapsed
|
||||
});
|
||||
};
|
||||
const newRoutes = filterRoutes(
|
||||
// @ts-ignore
|
||||
clientRoutes.filter((route) => route.id === 'max-tabs'),
|
||||
@@ -247,19 +254,16 @@ export default (props: any) => {
|
||||
[location.pathname]
|
||||
);
|
||||
|
||||
const collapsed = useMemo(() => {
|
||||
return userSettings.collapsed || false;
|
||||
}, [userSettings.collapsed]);
|
||||
|
||||
const renderMenuHeader = (logo: React.ReactNode, title: React.ReactNode) => {
|
||||
return <>{logo}</>;
|
||||
};
|
||||
|
||||
const headerContentRender = (
|
||||
headerProps: any,
|
||||
defaultDom: React.ReactNode
|
||||
) => {
|
||||
return <HeaderMenu {...headerProps}></HeaderMenu>;
|
||||
};
|
||||
|
||||
const actionsRender = () => {
|
||||
return <ExtraContent isDarkTheme={userSettings.isDarkTheme} />;
|
||||
const menuContentRender = (menuProps: any, defaultDom: React.ReactNode) => {
|
||||
return <SiderMenu {...menuProps}></SiderMenu>;
|
||||
};
|
||||
|
||||
const onPageChange = async (route: any) => {
|
||||
@@ -313,6 +317,17 @@ export default (props: any) => {
|
||||
navigate(pagepath);
|
||||
};
|
||||
|
||||
const onCollapse = (value: boolean) => {
|
||||
// only trigger by window resize
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
setUserSettings({
|
||||
...userSettings,
|
||||
collapsed: value
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
componentSize="large"
|
||||
@@ -373,27 +388,48 @@ export default (props: any) => {
|
||||
<DarkMask></DarkMask>
|
||||
<ProLayout
|
||||
fixSiderbar
|
||||
fixedHeader
|
||||
fixedHeader={false}
|
||||
headerRender={false}
|
||||
breadcrumbRender={false}
|
||||
route={route}
|
||||
location={location}
|
||||
title={userConfig.title}
|
||||
navTheme={userSettings.theme}
|
||||
layout="top"
|
||||
layout="side"
|
||||
contentStyle={{
|
||||
paddingBlock: 0,
|
||||
paddingInline: 0
|
||||
}}
|
||||
openKeys={false}
|
||||
disableMobile={true}
|
||||
siderWidth={220}
|
||||
menuFooterRender={() => (
|
||||
<Button
|
||||
style={{
|
||||
border: 'none'
|
||||
}}
|
||||
size="small"
|
||||
type={'text'}
|
||||
onClick={handleToggleCollapse}
|
||||
>
|
||||
<IconFont
|
||||
type={collapsed ? 'icon-expand-left' : 'icon-expand-right'}
|
||||
className="font-size-18"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
onCollapse={onCollapse}
|
||||
onMenuHeaderClick={onMenuHeaderClick}
|
||||
collapsed={userSettings.collapsed}
|
||||
onPageChange={onPageChange}
|
||||
formatMessage={formatMessage}
|
||||
menu={{
|
||||
locale: true
|
||||
locale: true,
|
||||
type: 'group'
|
||||
}}
|
||||
logo={<LogoIcon />}
|
||||
headerContentRender={headerContentRender}
|
||||
actionsRender={actionsRender}
|
||||
splitMenus={true}
|
||||
logo={userSettings.collapsed ? <SLogoIcon /> : <LogoIcon />}
|
||||
menuContentRender={menuContentRender}
|
||||
{...runtimeConfig}
|
||||
ErrorBoundary={ErrorBoundary}
|
||||
>
|
||||
@@ -401,7 +437,7 @@ export default (props: any) => {
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
height: '100vh',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -82,7 +82,7 @@ export const getRightRenderContent = (opts: {
|
||||
{
|
||||
key: 'site',
|
||||
icon: <HomeOutlined />,
|
||||
label: 'ZStack AIOS',
|
||||
label: 'GPUStack',
|
||||
url: externalLinks.site
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default {
|
||||
'billing.upsell.title': 'Billing is an Enterprise feature',
|
||||
'billing.upsell.subtitle':
|
||||
'Track spend, generate invoices, and enforce budgets across teams. Upgrade to ZStack AIOS Enterprise to manage billing.',
|
||||
'Track spend, generate invoices, and enforce budgets across teams. Upgrade to GPUStack Enterprise to manage billing.',
|
||||
'billing.upsell.featuresTitle': 'What you get in Enterprise',
|
||||
'billing.upsell.feature.usage':
|
||||
'See cost breakdowns by organization, user, and model',
|
||||
|
||||
@@ -87,7 +87,7 @@ export default {
|
||||
'clusters.addworker.detectWorkerAddress.tips':
|
||||
'Defaults to Worker IP if not specified.',
|
||||
'clusters.addworker.externalIP.tips':
|
||||
'If running in a VPC or private network, please specify the Worker external address reachable by the ZStack AIOS Server.',
|
||||
'If running in a VPC or private network, please specify the Worker external address reachable by the GPUStack Server.',
|
||||
'clusters.addworker.enterWorkerIP': 'Enter worker IP',
|
||||
'clusters.addworker.enterWorkerIP.error': 'Please enter the worker IP.',
|
||||
'clusters.addworker.enterWorkerAddress': 'Enter worker external address',
|
||||
@@ -115,20 +115,20 @@ export default {
|
||||
'{count} new worker has been added to the cluster.',
|
||||
'clusters.addworker.message.success_multiple':
|
||||
'{count} new workers have been added to the cluster.',
|
||||
'clusters.create.serverUrl': 'ZStack AIOS Server URL',
|
||||
'clusters.create.serverUrl': 'GPUStack Server URL',
|
||||
'clusters.create.workerConfig': 'Worker Configuration',
|
||||
'clusters.edit.k8sOptions.changed.tip':
|
||||
'You have changed the Kubernetes options. Re-run the registration command on the target cluster for the changes to take effect.',
|
||||
'clusters.addworker.containerName': 'Worker Container Name',
|
||||
'clusters.addworker.containerName.tips':
|
||||
'Specify a name for the worker container.',
|
||||
'clusters.addworker.dataVolume': 'ZStack AIOS Data Volume',
|
||||
'clusters.addworker.dataVolume': 'GPUStack Data Volume',
|
||||
'clusters.addworker.dataVolume.tips':
|
||||
'Specify a data storage path for ZStack AIOS.',
|
||||
'Specify a data storage path for GPUStack.',
|
||||
'clusters.table.ip.internal': 'Internal',
|
||||
'clusters.table.ip.external': 'External',
|
||||
'clusters.form.serverUrl.tips':
|
||||
'Specify an externally accessible ZStack AIOS service URL if the worker cannot access ZStack AIOS Server directly.',
|
||||
'Specify an externally accessible GPUStack service URL if the worker cannot access GPUStack Server directly.',
|
||||
'clusters.form.setDefault': 'Set as Default',
|
||||
'clusters.form.setDefault.tips': 'Default for deployment.',
|
||||
'clusters.addworker.noClusters': 'No available Docker clusters found',
|
||||
@@ -146,7 +146,7 @@ export default {
|
||||
'clusters.addworker.theadNotes-02':
|
||||
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
||||
'clusters.addworker.nvidiaNotes':
|
||||
'The built-in inference backends in ZStack AIOS require <span class="bold-text">CUDA 12.8+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">570</span> or newer.',
|
||||
'The built-in inference backends in GPUStack require <span class="bold-text">CUDA 12.8+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">570</span> or newer.',
|
||||
'clusters.volume.title': 'Volume Mounts',
|
||||
'clusters.volume.name': 'Volume Name',
|
||||
'clusters.volume.mountPath': 'Container Path',
|
||||
@@ -173,7 +173,7 @@ export default {
|
||||
'clusters.volume.add': 'Add Volume Mount',
|
||||
'clusters.systemDefaultContainerRegistry.title': 'Default Container Registry',
|
||||
'clusters.systemDefaultContainerRegistry.tip':
|
||||
'Default registry used to resolve ZStack AIOS images for this cluster. Falls back to the server default when unset.',
|
||||
'Default registry used to resolve GPUStack images for this cluster. Falls back to the server default when unset.',
|
||||
'clusters.k8sOptions.title': 'Kubernetes Deployment Options',
|
||||
'clusters.imageCredentials.title': 'Image Credentials',
|
||||
'clusters.imageCredentials.add': 'Add Credential',
|
||||
@@ -185,7 +185,7 @@ export default {
|
||||
'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.',
|
||||
'clusters.operatorImage.title': 'Operator Image',
|
||||
'clusters.operatorImage.tip':
|
||||
'Override for the ZStack AIOS Operator container image. Leave empty to use the server default.',
|
||||
'Override for the GPUStack Operator container image. Leave empty to use the server default.',
|
||||
'clusters.namespace.title': 'Namespace',
|
||||
'clusters.namespace.tip':
|
||||
'Kubernetes namespace the cluster’s manifests render into. Leave empty to use gpustack-system.',
|
||||
|
||||
@@ -46,7 +46,7 @@ export default {
|
||||
'common.button.enabled': 'Enabled',
|
||||
'common.button.disabled': 'Disabled',
|
||||
'common.button.upgrade': 'Upgrade',
|
||||
'common.enterprise.feature': 'Available in ZStack AIOS Enterprise',
|
||||
'common.enterprise.feature': 'Available in GPUStack Enterprise',
|
||||
'common.input.holder': 'Please enter',
|
||||
'common.validate.value': '{name} value is required',
|
||||
'common.button.edit': 'Edit',
|
||||
@@ -214,7 +214,7 @@ export default {
|
||||
'common.form.password': 'Password',
|
||||
'common.form.username': 'Username',
|
||||
'common.login.rember': 'Remember me',
|
||||
'settings.company': 'ZStack AIOS',
|
||||
'settings.company': 'GPUStack.ai',
|
||||
'common.button.help': 'Help',
|
||||
'common.button.feedback': 'Feedback',
|
||||
'common.button.docs': 'Documentation',
|
||||
@@ -222,6 +222,12 @@ export default {
|
||||
'common.title.delete.confirm': 'Confirm deletion',
|
||||
'common.title.stop.confirm': 'Confirm stop',
|
||||
'common.title.start.confirm': 'Confirm start',
|
||||
'common.title.activate.confirm': 'Confirm activate',
|
||||
'common.title.deactivate.confirm': 'Confirm deactivate',
|
||||
'common.activate.single.confirm':
|
||||
'Are you sure you want to activate? \n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||
'common.deactivate.single.confirm':
|
||||
'Are you sure you want to deactivate? \n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||
'common.title.recreate.confirm': 'Confirm recreate',
|
||||
'common.button.addLabel': 'Add Label',
|
||||
'common.button.addSelector': 'Add Selector',
|
||||
|
||||
@@ -119,12 +119,44 @@ export default {
|
||||
'No available GPU resources, please choose another instance type.',
|
||||
'gpuservice.instance.gpuCount.zero':
|
||||
'CPU-only setup for environment preparation.',
|
||||
'gpuservice.instance.mode.whole': 'Full GPU',
|
||||
'gpuservice.instance.mode.sliced': 'By Ratio',
|
||||
'gpuservice.instance.slice.memoryPercentage': 'VRAM Percentage (%)',
|
||||
'gpuservice.instance.slice.percentage': 'Percentage (%)',
|
||||
'gpuservice.instance.slice.coresPercentage': 'Compute Percentage (%)',
|
||||
'gpuservice.instance.slice.cores.min':
|
||||
'The compute ratio must be no less than the VRAM ratio ({count}%)',
|
||||
'gpuservice.instance.slice.fullCores': '100% Compute',
|
||||
'gpuservice.instance.slice.percentage.required':
|
||||
'Please select or enter a percentage',
|
||||
'gpuservice.instance.slice.percentage.max':
|
||||
'The ratio must be between 1% and {count}%',
|
||||
'gpuservice.instance.stock': 'Stock',
|
||||
'gpuservice.instance.sliced': 'Sliced',
|
||||
'gpuservice.instance.sliceable': 'Sliceable',
|
||||
'gpuservice.instance.memory': 'VRAM',
|
||||
'gpuservice.instance.ram': 'RAM',
|
||||
'gpuservice.instance.os': 'OS',
|
||||
'gpuservice.instance.arch': 'Arch',
|
||||
'gpuservice.instanceType': 'GPU Instance Type',
|
||||
'gpuservice.instanceType.add': 'Add Instance Type',
|
||||
'gpuservice.instanceType.flavor': 'Flavor',
|
||||
'gpuservice.instanceType.flavor.required':
|
||||
'Please select an instance type flavor',
|
||||
'gpuservice.instanceType.flavor.gpuGroup': 'GPU Compute',
|
||||
'gpuservice.instanceType.flavor.cpuGroup': 'CPU Compute',
|
||||
'gpuservice.instanceType.activate': 'Activate',
|
||||
'gpuservice.instanceType.deactivate': 'Deactivate',
|
||||
'gpuservice.instanceType.platform': 'Platform',
|
||||
'gpuservice.instanceType.product': 'Product',
|
||||
'gpuservice.instanceType.unitCpu': 'Unit CPU',
|
||||
'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU',
|
||||
'gpuservice.instanceType.unitRam': 'Unit RAM',
|
||||
'gpuservice.instanceType.unitRam.tip': 'RAM allocated per GPU',
|
||||
'gpuservice.instanceType.localStorage': 'Storage',
|
||||
'gpuservice.instanceType.localStorage.tip': 'Maximum available disk',
|
||||
'gpuservice.instanceType.notSliceable': 'Not Sliceable',
|
||||
'gpuservice.instanceType.filter.name': 'Search by name',
|
||||
'gpuservice.instance.disk': 'Disk',
|
||||
'gpuservice.table.count': 'Count',
|
||||
'gpuservice.instance.disk.system': 'System Disk',
|
||||
@@ -146,9 +178,6 @@ export default {
|
||||
'Only events from the last hour are shown',
|
||||
'gpuservice.instance.event.tab.instance': 'Instance Events',
|
||||
'gpuservice.instance.event.tab.volume': 'Volume Events',
|
||||
'gpuservice.instance.recreate.confirm.title': 'Confirm recreation',
|
||||
'gpuservice.instance.recreate.confirm.content':
|
||||
'The current instance will be deleted first, then recreated with the current configuration.\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||
'gpuservice.storage': 'Storage',
|
||||
'gpuservice.storage.add': 'Add Storage',
|
||||
'gpuservice.storage.edit': 'Edit Storage',
|
||||
|
||||
@@ -47,6 +47,7 @@ export default {
|
||||
'menu.models.backendsList': 'Inference Backends',
|
||||
'menu.gpuService': 'GPU Service',
|
||||
'menu.gpuService.instances': 'GPU Instances',
|
||||
'menu.gpuService.instanceTypes': 'Instance Types',
|
||||
'menu.gpuService.templates': 'Instance Templates',
|
||||
'menu.gpuService.storage': 'Storage',
|
||||
'menu.gpuService.storageTypes': 'Storage Types',
|
||||
|
||||
@@ -14,7 +14,7 @@ export default {
|
||||
'models.form.env': 'Environment Variables',
|
||||
'models.form.configurations': 'Configurations',
|
||||
'models.form.s3address': 'S3 Address',
|
||||
'models.form.partialoffload.tips': `When CPU offloading is enabled, ZStack AIOS will allocate CPU memory if GPU resources are insufficient. You must correctly configure the inference backend to use hybrid CPU+GPU or full CPU inference.`,
|
||||
'models.form.partialoffload.tips': `When CPU offloading is enabled, GPUStack will allocate CPU memory if GPU resources are insufficient. You must correctly configure the inference backend to use hybrid CPU+GPU or full CPU inference.`,
|
||||
'models.form.distribution.tips': `Allows for offloading part of the model's layers to single or multiple remote workers when the resources of a worker are insufficient.`,
|
||||
'models.openinplayground': 'Open in Playground',
|
||||
'models.instances': 'instances',
|
||||
@@ -24,7 +24,7 @@ export default {
|
||||
'model.deploy.sort': 'Sort',
|
||||
'model.deploy.search.placeholder': 'Type <kbd>/</kbd> to search models',
|
||||
'model.form.ollamatips':
|
||||
'Tip: The following are the preconfigured Ollama models in ZStack AIOS. Please select the model you want, or directly enter the model you wish to deploy in the 【{name}】 input box on the right.',
|
||||
'Tip: The following are the preconfigured Ollama models in GPUStack. Please select the model you want, or directly enter the model you wish to deploy in the 【{name}】 input box on the right.',
|
||||
'models.sort.name': 'Name',
|
||||
'models.sort.size': 'Size',
|
||||
'models.sort.likes': 'Likes',
|
||||
@@ -87,7 +87,7 @@ export default {
|
||||
'models.form.filePath': 'Model Path',
|
||||
'models.form.backendVersion': 'Backend Version',
|
||||
'models.form.backendVersion.tips':
|
||||
'To use the desired version of {backend}{version}, the system will automatically create a virtual environment in the online environment to install the corresponding version. After a ZStack AIOS upgrade, the backend version will remain fixed. {link}',
|
||||
'To use the desired version of {backend}{version}, the system will automatically create a virtual environment in the online environment to install the corresponding version. After a GPUStack upgrade, the backend version will remain fixed. {link}',
|
||||
'models.form.gpuselector': 'GPU Selector',
|
||||
'models.form.backend.llamabox':
|
||||
'For GGUF format models, supports Linux, macOS, and Windows.',
|
||||
@@ -277,7 +277,7 @@ export default {
|
||||
'models.form.backendVersions.tips': `To use more versions, go to the {link} page and edit the backend to add versions.`,
|
||||
'models.catalog.nogpus.tips':
|
||||
'No compatible GPUs are available in the selected cluster for this model.',
|
||||
'models.form.modelfile.notfound': `The model file path you specified does not exist on the ZStack AIOS server. It's recommended to place the model file at the same path on both the ZStack AIOS server and ZStack AIOS workers. This helps ZStack AIOS make better decisions.`,
|
||||
'models.form.modelfile.notfound': `The model file path you specified does not exist on the GPUStack server. It's recommended to place the model file at the same path on both the GPUStack server and GPUStack workers. This helps GPUStack make better decisions.`,
|
||||
'models.form.readyWorkers': 'workers ready',
|
||||
'models.form.maxContextLength': 'Maximum Context Length',
|
||||
'models.form.backend.helperText':
|
||||
|
||||
@@ -68,6 +68,10 @@ export default {
|
||||
'noresult.gpuservice.storage.title': 'No Storage',
|
||||
'noresult.gpuservice.storage.subTitle': 'No storage has been added yet.',
|
||||
'noresult.gpuservice.storage.nofound': 'No matching storage found.',
|
||||
'noresult.gpuservice.instanceType.title': 'No Instance Types',
|
||||
'noresult.gpuservice.instanceType.subTitle':
|
||||
'Create an instance type to get started',
|
||||
'noresult.gpuservice.instanceType.nofound': 'No instance types found',
|
||||
'noresult.gpuservice.storageType.title': 'No Storage Types',
|
||||
'noresult.gpuservice.storageType.subTitle':
|
||||
'No storage types have been added yet.',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default {
|
||||
'organizations.upsell.title': 'Organizations are an Enterprise feature',
|
||||
'organizations.upsell.subtitle':
|
||||
'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to ZStack AIOS Enterprise to manage organizations.',
|
||||
'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to GPUStack Enterprise to manage organizations.',
|
||||
'organizations.upsell.featuresTitle': 'What you get in Enterprise',
|
||||
'organizations.upsell.feature.orgs':
|
||||
'Create organizations to group users and isolate workloads',
|
||||
|
||||
@@ -52,7 +52,7 @@ export default {
|
||||
'resources.worker.container.supported': 'Do not support macOS or Windows.',
|
||||
'resources.worker.current.version': 'Current version is {version}.',
|
||||
'resources.worker.driver.install':
|
||||
'Install <a href="https://docs.gpustack.ai/latest/installation/installation-requirements/" target="_blank">required drivers and libraries</a> prior to ZStack AIOS installation.',
|
||||
'Install <a href="https://docs.gpustack.ai/latest/installation/installation-requirements/" target="_blank">required drivers and libraries</a> prior to GPUStack installation.',
|
||||
'resources.worker.select.command':
|
||||
'Select a label to generate the command and copy it using the copy button.',
|
||||
'resources.worker.script.install': 'Script Installation',
|
||||
@@ -89,7 +89,7 @@ export default {
|
||||
'Paste the <span class="bold-text">Token</span>.',
|
||||
'resources.register.worker.step7':
|
||||
'Click <span class="bold-text">Restart</span> to apply the settings.',
|
||||
'resources.register.install.title': 'Install ZStack AIOS on {os}',
|
||||
'resources.register.install.title': 'Install GPUStack on {os}',
|
||||
'resources.register.download':
|
||||
'Download and install the <a href={url} target="_blank">installer</a>. Only supported: {versions}.',
|
||||
'resource.register.maos.support': 'Apple Silicon (M series), macOS 14+',
|
||||
@@ -111,7 +111,7 @@ export default {
|
||||
'No available clusters. Please create a cluster before adding a node.',
|
||||
'resources.metrics.details': 'Monitoring',
|
||||
'resoureces.worker.upgrade.tips':
|
||||
'Please upgrade to match the ZStack AIOS Server version.',
|
||||
'Please upgrade to match the GPUStack Server version.',
|
||||
'resources.worker.version': 'Worker Version: {version}',
|
||||
'resources.server.version': 'Server Version: {version}',
|
||||
'resources.worker.currentVersion': 'Current Version: {version}',
|
||||
|
||||
@@ -39,12 +39,12 @@ export default {
|
||||
'users.password.confirm.empty': 'Please confirm the new password.',
|
||||
'users.password.confirm.error': 'The two passwords entered do not match.',
|
||||
'users.login.title': 'Log in to',
|
||||
'users.version.islatest': 'ZStack AIOS {version} is the latest version',
|
||||
'users.version.update': 'ZStack AIOS {version} is available',
|
||||
'users.version.islatest': 'GPUStack {version} is the latest version',
|
||||
'users.version.update': 'GPUStack {version} is available',
|
||||
'users.settings.title': 'User Settings',
|
||||
'users.status.activate': 'Activate Account',
|
||||
'users.status.deactivate': 'Deactivate Account',
|
||||
'users.status.inactiveAccount': 'Inactive Account',
|
||||
'users.login.getInitialPassword':
|
||||
'Run the following command on your ZStack AIOS Server to retrieve the initial admin password.'
|
||||
'Run the following command on your GPUStack Server to retrieve the initial admin password.'
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default {
|
||||
'billing.upsell.title': 'Billing is an Enterprise feature',
|
||||
'billing.upsell.subtitle':
|
||||
'Track spend, generate invoices, and enforce budgets across teams. Upgrade to ZStack AIOS Enterprise to manage billing.',
|
||||
'Track spend, generate invoices, and enforce budgets across teams. Upgrade to GPUStack Enterprise to manage billing.',
|
||||
'billing.upsell.featuresTitle': 'What you get in Enterprise',
|
||||
'billing.upsell.feature.usage':
|
||||
'See cost breakdowns by organization, user, and model',
|
||||
|
||||
@@ -115,20 +115,20 @@ export default {
|
||||
'{count} new worker has been added to the cluster.',
|
||||
'clusters.addworker.message.success_multiple':
|
||||
'{count} new workers have been added to the cluster.',
|
||||
'clusters.create.serverUrl': 'ZStack AIOS Server URL',
|
||||
'clusters.create.serverUrl': 'GPUStack Server URL',
|
||||
'clusters.create.workerConfig': 'Worker Configuration',
|
||||
'clusters.edit.k8sOptions.changed.tip':
|
||||
'Kubernetes オプションを変更しました。変更を有効にするには、対象クラスターで登録コマンドを再実行してください。',
|
||||
'clusters.addworker.containerName': 'Worker Container Name',
|
||||
'clusters.addworker.containerName.tips':
|
||||
'Specify a name for the worker container.',
|
||||
'clusters.addworker.dataVolume': 'ZStack AIOS Data Volume',
|
||||
'clusters.addworker.dataVolume': 'GPUStack Data Volume',
|
||||
'clusters.addworker.dataVolume.tips':
|
||||
'Specify a data storage path for ZStack AIOS.',
|
||||
'Specify a data storage path for GPUStack.',
|
||||
'clusters.table.ip.internal': 'Internal',
|
||||
'clusters.table.ip.external': 'External',
|
||||
'clusters.form.serverUrl.tips':
|
||||
'Specify an externally accessible ZStack AIOS service URL if the worker cannot access ZStack AIOS Server directly.',
|
||||
'Specify an externally accessible GPUStack service URL if the worker cannot access GPUStack Server directly.',
|
||||
'clusters.form.setDefault': 'Set as Default',
|
||||
'clusters.form.setDefault.tips': 'Default for deployment.',
|
||||
'clusters.addworker.noClusters': 'No available Docker clusters found',
|
||||
@@ -146,7 +146,7 @@ export default {
|
||||
'clusters.addworker.theadNotes-02':
|
||||
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
||||
'clusters.addworker.nvidiaNotes':
|
||||
'The built-in inference backends in ZStack AIOS require <span class="bold-text">CUDA 12.8+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">570</span> or newer.',
|
||||
'The built-in inference backends in GPUStack require <span class="bold-text">CUDA 12.8+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">570</span> or newer.',
|
||||
'clusters.volume.title': 'Volume Mounts',
|
||||
'clusters.volume.name': 'Volume Name',
|
||||
'clusters.volume.mountPath': 'Container Path',
|
||||
@@ -173,7 +173,7 @@ export default {
|
||||
'clusters.volume.add': 'Add Volume Mount',
|
||||
'clusters.systemDefaultContainerRegistry.title': 'Default Container Registry',
|
||||
'clusters.systemDefaultContainerRegistry.tip':
|
||||
'Default registry used to resolve ZStack AIOS images for this cluster. Falls back to the server default when unset.',
|
||||
'Default registry used to resolve GPUStack images for this cluster. Falls back to the server default when unset.',
|
||||
'clusters.k8sOptions.title': 'Kubernetes Deployment Options',
|
||||
'clusters.imageCredentials.title': 'Image Credentials',
|
||||
'clusters.imageCredentials.add': 'Add Credential',
|
||||
@@ -185,7 +185,7 @@ export default {
|
||||
'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.',
|
||||
'clusters.operatorImage.title': 'Operator Image',
|
||||
'clusters.operatorImage.tip':
|
||||
'Override for the ZStack AIOS Operator container image. Leave empty to use the server default.',
|
||||
'Override for the GPUStack Operator container image. Leave empty to use the server default.',
|
||||
'clusters.namespace.title': 'Namespace',
|
||||
'clusters.namespace.tip':
|
||||
'Kubernetes namespace the cluster’s manifests render into. Leave empty to use gpustack-system.',
|
||||
@@ -278,15 +278,15 @@ export default {
|
||||
// 73. 'clusters.addworker.cacheVolume.holder': 'e.g. /data/cache (path must start with /)',
|
||||
// 74. 'clusters.addworker.message.success_single': '{count} new worker has been added to the cluster.',
|
||||
// 75. 'clusters.addworker.message.success_multiple': '{count} new workers have been added to the cluster.',
|
||||
// 76. 'clusters.create.serverUrl': 'ZStack AIOS Server URL',
|
||||
// 76. 'clusters.create.serverUrl': 'GPUStack Server URL',
|
||||
// 77. 'clusters.create.workerConfig': 'Worker Configuration'
|
||||
// 78. 'clusters.addworker.containerName': 'Worker Container Name',
|
||||
// 79. 'clusters.addworker.containerName.tips':'Specify a name for the worker container.',
|
||||
// 77. 'clusters.addworker.dataVolume': 'ZStack AIOS Data Volume',
|
||||
// 78. 'clusters.addworker.dataVolume.tips': 'Specify a data storage path for ZStack AIOS.',
|
||||
// 77. 'clusters.addworker.dataVolume': 'GPUStack Data Volume',
|
||||
// 78. 'clusters.addworker.dataVolume.tips': 'Specify a data storage path for GPUStack.',
|
||||
// 79. 'clusters.table.ip.internal': 'Internal',
|
||||
// 80. 'clusters.table.ip.external': 'External',
|
||||
// 81. 'clusters.form.serverUrl.tips': 'Specify an externally accessible ZStack AIOS service URL if the worker cannot access ZStack AIOS Server directly.',
|
||||
// 81. 'clusters.form.serverUrl.tips': 'Specify an externally accessible GPUStack service URL if the worker cannot access GPUStack Server directly.',
|
||||
// 82. 'clusters.addworker.externalIP.tips': 'Specify an external IP if the worker is in a VPC or private network.',
|
||||
// 83. 'clusters.form.setDefault': 'Set as Default',
|
||||
// 84. 'clusters.form.setDefault.tips': 'Default for deployment',
|
||||
@@ -302,5 +302,5 @@ export default {
|
||||
// 94. 'clusters.create.steps.configure': 'Configure',
|
||||
// 99. 'clusters.addworker.theadNotes': 'If the <span class="bold-text>/usr/local/PPU_SDK</span> directory does not exist, please create a symbolic link pointing to the T-Head PPU SDK installed path: <span class="bold-text>ln -s /path/to/PPU_SDK /usr/local/PPU_SDK</span>',
|
||||
// 100. 'clusters.addworker.theadNotes-02': 'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
||||
// 101. 'clusters.addworker.nvidiaNotes': 'The built-in inference backends in ZStack AIOS v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.'
|
||||
// 101. 'clusters.addworker.nvidiaNotes': 'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.'
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -46,7 +46,7 @@ export default {
|
||||
'common.button.enabled': '有効',
|
||||
'common.button.disabled': '無効',
|
||||
'common.button.upgrade': 'アップグレード',
|
||||
'common.enterprise.feature': 'Available in ZStack AIOS Enterprise',
|
||||
'common.enterprise.feature': 'Available in GPUStack Enterprise',
|
||||
'common.input.holder': '入力してください',
|
||||
'common.validate.value': '{name} の値は必須です',
|
||||
'common.button.edit': '編集',
|
||||
@@ -213,7 +213,7 @@ export default {
|
||||
'common.form.password': 'パスワード',
|
||||
'common.form.username': 'ユーザー名',
|
||||
'common.login.rember': 'ログイン状態を保持',
|
||||
'settings.company': 'ZStack AIOS',
|
||||
'settings.company': 'GPUStack.ai',
|
||||
'common.button.help': 'ヘルプ',
|
||||
'common.button.feedback': 'フィードバック',
|
||||
'common.button.docs': 'ドキュメント',
|
||||
@@ -221,6 +221,12 @@ export default {
|
||||
'common.title.delete.confirm': '削除を確認',
|
||||
'common.title.stop.confirm': '停止を確認',
|
||||
'common.title.start.confirm': '開始を確認',
|
||||
'common.title.activate.confirm': '有効化を確認',
|
||||
'common.title.deactivate.confirm': '無効化を確認',
|
||||
'common.activate.single.confirm':
|
||||
'<span style="font-size: 13px;font-weight: 700">{name}</span> を有効化してもよろしいですか?',
|
||||
'common.deactivate.single.confirm':
|
||||
'<span style="font-size: 13px;font-weight: 700">{name}</span> を無効化してもよろしいですか?',
|
||||
'common.title.recreate.confirm': '再作成を確認',
|
||||
'common.button.addLabel': 'ラベルを追加',
|
||||
'common.button.addSelector': 'セレクターを追加',
|
||||
|
||||
@@ -118,12 +118,44 @@ export default {
|
||||
'gpuservice.instance.gpuCount.noAvailable':
|
||||
'利用可能な GPU リソースがありません。別のインスタンスタイプを選択してください。',
|
||||
'gpuservice.instance.gpuCount.zero': 'CPU のみを使用し、環境準備用です。',
|
||||
'gpuservice.instance.mode.whole': 'GPU 全体',
|
||||
'gpuservice.instance.mode.sliced': '比率で',
|
||||
'gpuservice.instance.slice.memoryPercentage': 'VRAM の割合(%)',
|
||||
'gpuservice.instance.slice.percentage': '割合(%)',
|
||||
'gpuservice.instance.slice.coresPercentage': '演算能力の割合(%)',
|
||||
'gpuservice.instance.slice.cores.min':
|
||||
'演算能力の割合は VRAM の割合({count}%)以上である必要があります',
|
||||
'gpuservice.instance.slice.fullCores': '100% コンピュート',
|
||||
'gpuservice.instance.slice.percentage.required':
|
||||
'パーセンテージを選択または入力してください',
|
||||
'gpuservice.instance.slice.percentage.max':
|
||||
'比率は 1% から {count}% の間で指定してください',
|
||||
'gpuservice.instance.stock': '在庫',
|
||||
'gpuservice.instance.sliced': '分割',
|
||||
'gpuservice.instance.sliceable': '分割可能',
|
||||
'gpuservice.instance.memory': 'VRAM',
|
||||
'gpuservice.instance.ram': 'RAM',
|
||||
'gpuservice.instance.os': 'OS',
|
||||
'gpuservice.instance.arch': 'アーキテクチャ',
|
||||
'gpuservice.instanceType': 'GPU Instance Type',
|
||||
'gpuservice.instanceType.add': 'Add Instance Type',
|
||||
'gpuservice.instanceType.flavor': 'Flavor',
|
||||
'gpuservice.instanceType.flavor.required':
|
||||
'Please select an instance type flavor',
|
||||
'gpuservice.instanceType.flavor.gpuGroup': 'GPU Compute',
|
||||
'gpuservice.instanceType.flavor.cpuGroup': 'CPU Compute',
|
||||
'gpuservice.instanceType.activate': 'Activate',
|
||||
'gpuservice.instanceType.deactivate': 'Deactivate',
|
||||
'gpuservice.instanceType.platform': 'Platform',
|
||||
'gpuservice.instanceType.product': 'Product',
|
||||
'gpuservice.instanceType.unitCpu': 'Unit CPU',
|
||||
'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU',
|
||||
'gpuservice.instanceType.unitRam': 'Unit RAM',
|
||||
'gpuservice.instanceType.unitRam.tip': 'RAM allocated per GPU',
|
||||
'gpuservice.instanceType.localStorage': 'Storage',
|
||||
'gpuservice.instanceType.localStorage.tip': 'Maximum available disk',
|
||||
'gpuservice.instanceType.notSliceable': 'Not Sliceable',
|
||||
'gpuservice.instanceType.filter.name': 'Search by name',
|
||||
'gpuservice.instance.disk': 'ディスク',
|
||||
'gpuservice.table.count': '数量',
|
||||
'gpuservice.instance.disk.system': 'システムディスク',
|
||||
@@ -145,9 +177,6 @@ export default {
|
||||
'直近 1 時間のイベントのみ表示されます',
|
||||
'gpuservice.instance.event.tab.instance': 'インスタンスイベント',
|
||||
'gpuservice.instance.event.tab.volume': 'ボリュームイベント',
|
||||
'gpuservice.instance.recreate.confirm.title': '再作成を確認しますか',
|
||||
'gpuservice.instance.recreate.confirm.content':
|
||||
'現在のインスタンスを削除した後、現在の構成で再作成します。\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||
'gpuservice.storage': 'ストレージ',
|
||||
'gpuservice.storage.add': 'ストレージを追加',
|
||||
'gpuservice.storage.edit': 'ストレージを編集',
|
||||
|
||||
@@ -47,6 +47,7 @@ export default {
|
||||
'menu.models.backendsList': 'Inference Backends',
|
||||
'menu.gpuService': 'GPU Service',
|
||||
'menu.gpuService.instances': 'GPU Instances',
|
||||
'menu.gpuService.instanceTypes': 'Instance Types',
|
||||
'menu.gpuService.templates': 'Instance Templates',
|
||||
'menu.gpuService.storage': 'Storage',
|
||||
'menu.gpuService.storageTypes': 'ストレージタイプ',
|
||||
|
||||
@@ -14,7 +14,7 @@ export default {
|
||||
'models.form.env': '環境変数',
|
||||
'models.form.configurations': '設定',
|
||||
'models.form.s3address': 'S3アドレス',
|
||||
'models.form.partialoffload.tips': `When CPU offloading is enabled, ZStack AIOS will allocate CPU memory if GPU resources are insufficient. You must correctly configure the inference backend to use hybrid CPU+GPU or full CPU inference.`,
|
||||
'models.form.partialoffload.tips': `When CPU offloading is enabled, GPUStack will allocate CPU memory if GPU resources are insufficient. You must correctly configure the inference backend to use hybrid CPU+GPU or full CPU inference.`,
|
||||
'models.form.distribution.tips':
|
||||
'ワーカーのリソースが不足している場合、モデルの一部のレイヤーを単一または複数のリモートワーカーにオフロードすることができます。',
|
||||
'models.openinplayground': 'プレイグラウンドで開く',
|
||||
@@ -25,7 +25,7 @@ export default {
|
||||
'model.deploy.sort': '並び替え',
|
||||
'model.deploy.search.placeholder': '<kbd>/</kbd>を入力してモデルを検索',
|
||||
'model.form.ollamatips':
|
||||
'ヒント: 以下はZStack AIOSで事前設定されたOllamaモデルです。希望するモデルを選択するか、右側の【{name}】入力ボックスにデプロイしたいモデルを直接入力してください。',
|
||||
'ヒント: 以下はGPUStackで事前設定されたOllamaモデルです。希望するモデルを選択するか、右側の【{name}】入力ボックスにデプロイしたいモデルを直接入力してください。',
|
||||
'models.sort.name': '名前',
|
||||
'models.sort.size': 'サイズ',
|
||||
'models.sort.likes': 'いいね',
|
||||
@@ -88,7 +88,7 @@ export default {
|
||||
'models.form.filePath': 'モデルパス',
|
||||
'models.form.backendVersion': 'バックエンドバージョン',
|
||||
'models.form.backendVersion.tips':
|
||||
'希望する{backend}{version}バージョンを使用するには、システムがオンライン環境で対応するバージョンをインストールする仮想環境を自動的に作成します。ZStack AIOSのアップグレード後もバックエンドバージョンは固定されます。{link}',
|
||||
'希望する{backend}{version}バージョンを使用するには、システムがオンライン環境で対応するバージョンをインストールする仮想環境を自動的に作成します。GPUStackのアップグレード後もバックエンドバージョンは固定されます。{link}',
|
||||
'models.form.gpuselector': 'GPUセレクター',
|
||||
'models.form.backend.llamabox':
|
||||
'GGUF形式のモデル用(Linux、macOS、Windowsをサポート)。',
|
||||
@@ -277,7 +277,7 @@ export default {
|
||||
'models.form.backendVersions.tips': `To use more versions, go to the {link} page and edit the backend to add versions.`,
|
||||
'models.catalog.nogpus.tips':
|
||||
'No compatible GPUs are available in the selected cluster for this model.',
|
||||
'models.form.modelfile.notfound': `The model file path you specified does not exist on the ZStack AIOS server. It's recommended to place the model file at the same path on both the ZStack AIOS server and ZStack AIOS workers. This helps ZStack AIOS make better decisions.`,
|
||||
'models.form.modelfile.notfound': `The model file path you specified does not exist on the GPUStack server. It's recommended to place the model file at the same path on both the GPUStack server and GPUStack workers. This helps GPUStack make better decisions.`,
|
||||
'models.form.readyWorkers': 'workers ready',
|
||||
'models.form.maxContextLength': 'Maximum Context Length',
|
||||
'models.form.backend.helperText':
|
||||
@@ -381,7 +381,7 @@ export default {
|
||||
// 62. 'models.form.backend_parameters.vllm.tips': 'For more details about {backend} parameters, see <a href={link} target="_blank">here</a>.',
|
||||
// 63. 'models.button.accessSettings.tips': 'Changes to access settings take effect after one minute.',
|
||||
// 64. 'models.table.userSelection.tips': 'Admin users can access all models by default.',
|
||||
// 65. 'models.form.partialoffload.tips': `When CPU offloading is enabled, ZStack AIOS will allocate CPU memory if GPU resources are insufficient. You must correctly configure the inference backend to use hybrid CPU+GPU or full CPU inference.`,
|
||||
// 65. 'models.form.partialoffload.tips': `When CPU offloading is enabled, GPUStack will allocate CPU memory if GPU resources are insufficient. You must correctly configure the inference backend to use hybrid CPU+GPU or full CPU inference.`,
|
||||
// 66. 'models.form.backend.warning': 'The selected backend does not support GGUF models. Please add a backend with GGUF support in the Inference Backend.',
|
||||
// 67. 'models.form.backend.warning.gguf': 'Please ensure that the selected custom backend supports GGUF models.',,
|
||||
// 68. 'models.form.backendVersion.deprecated': 'Deprecated',
|
||||
@@ -390,7 +390,7 @@ export default {
|
||||
// 71.'models.accessSettings.allowedUsers.tips': 'Only designated users can access the model.',
|
||||
// 72. 'models.form.backendVersions.tips': `To use more versions, go to the {link} page and edit the backend to add versions.`,
|
||||
// 73. 'models.catalog.nogpus.tips': 'No compatible GPUs are available in the selected cluster for this model.',
|
||||
// 74. 'models.form.modelfile.notfound': `The model file path you specified does not exist on the ZStack AIOS server. It's recommended to place the model file at the same path on both the ZStack AIOS server and ZStack AIOS workers. This helps ZStack AIOS make better decisions.`,
|
||||
// 74. 'models.form.modelfile.notfound': `The model file path you specified does not exist on the GPUStack server. It's recommended to place the model file at the same path on both the GPUStack server and GPUStack workers. This helps GPUStack make better decisions.`,
|
||||
// 75. 'models.form.readyWorkers': 'workers ready',
|
||||
// 76. 'models.form.maxContextLength': 'Maximum Context Length',
|
||||
// 77. 'models.form.backend.helperText': 'Not enabled yet. Will be enabled after deployment. ',
|
||||
|
||||
@@ -70,6 +70,10 @@ export default {
|
||||
'noresult.gpuservice.storage.subTitle':
|
||||
'ストレージはまだ追加されていません。',
|
||||
'noresult.gpuservice.storage.nofound': '一致するストレージが見つかりません。',
|
||||
'noresult.gpuservice.instanceType.title': 'No Instance Types',
|
||||
'noresult.gpuservice.instanceType.subTitle':
|
||||
'Create an instance type to get started',
|
||||
'noresult.gpuservice.instanceType.nofound': 'No instance types found',
|
||||
'noresult.gpuservice.storageType.title': 'ストレージタイプなし',
|
||||
'noresult.gpuservice.storageType.subTitle':
|
||||
'ストレージタイプはまだ追加されていません。',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default {
|
||||
'organizations.upsell.title': 'Organizations are an Enterprise feature',
|
||||
'organizations.upsell.subtitle':
|
||||
'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to ZStack AIOS Enterprise to manage organizations.',
|
||||
'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to GPUStack Enterprise to manage organizations.',
|
||||
'organizations.upsell.featuresTitle': 'What you get in Enterprise',
|
||||
'organizations.upsell.feature.orgs':
|
||||
'Create organizations to group users and isolate workloads',
|
||||
|
||||
@@ -52,7 +52,7 @@ export default {
|
||||
'MacOSまたはWindowsはサポートされていません。',
|
||||
'resources.worker.current.version': '現在のバージョンは {version} です。',
|
||||
'resources.worker.driver.install':
|
||||
'<a href="https://docs.gpustack.ai/latest/installation/installation-requirements/" target="_blank">必要なドライバとライブラリ</a> をZStack AIOSのインストール前にインストールしてください。',
|
||||
'<a href="https://docs.gpustack.ai/latest/installation/installation-requirements/" target="_blank">必要なドライバとライブラリ</a> をGPUStackのインストール前にインストールしてください。',
|
||||
'resources.worker.select.command':
|
||||
'ラベルを選択してコマンドを生成し、コピーを使用してコマンドをコピーします。',
|
||||
'resources.worker.script.install': 'スクリプトインストール',
|
||||
@@ -89,7 +89,7 @@ export default {
|
||||
'Paste the <span class="bold-text">Token</span>.',
|
||||
'resources.register.worker.step7':
|
||||
'Click <span class="bold-text">Restart</span> to apply the settings.',
|
||||
'resources.register.install.title': 'Install ZStack AIOS on {os}',
|
||||
'resources.register.install.title': 'Install GPUStack on {os}',
|
||||
'resources.register.download':
|
||||
'Download and install the <a href={url} target="_blank">installer</a>. Only supported: {versions}.',
|
||||
'resource.register.maos.support': 'Apple Silicon (M series), macOS 14+',
|
||||
@@ -112,7 +112,7 @@ export default {
|
||||
'No available clusters. Please create a cluster before adding a node.',
|
||||
'resources.metrics.details': 'Monitoring',
|
||||
'resoureces.worker.upgrade.tips':
|
||||
'Please upgrade to match the ZStack AIOS Server version.',
|
||||
'Please upgrade to match the GPUStack Server version.',
|
||||
'resources.worker.version': 'Worker Version: {version}',
|
||||
'resources.server.version': 'Server Version: {version}',
|
||||
'resources.worker.currentVersion': 'Current Version: {version}',
|
||||
@@ -128,7 +128,7 @@ export default {
|
||||
// 5. 'resources.register.worker.step5': 'Enter the <span class="bold-text">Server URL</span>: {url}.',
|
||||
// 6. 'resources.register.worker.step6': 'Paste the <span class="bold-text">Token</span>.',
|
||||
// 7. 'resources.register.worker.step7': 'Click <span class="bold-text">Restart</span> to apply the settings.',
|
||||
// 8. 'resources.register.install.title': 'Install ZStack AIOS on {os}',
|
||||
// 8. 'resources.register.install.title': 'Install GPUStack on {os}',
|
||||
// 9. 'resources.register.download':'Download and install the <a>installer</a>. Only supported: {versions}.',
|
||||
// 10. 'resource.register.maos.support': 'Apple Silicon (M series), macOS 14+',
|
||||
// 11. 'resource.register.windows.support': 'win 10, win 11',
|
||||
@@ -145,5 +145,5 @@ export default {
|
||||
// 22. 'resources.worker.maintenance.remark.rules': 'Please enter maintenance remarks',
|
||||
// 23. 'resources.worker.maintenance.tips': 'When maintenance mode is enabled, the node will stop scheduling new model deployment tasks. Running instances will not be affected.',
|
||||
// 24. 'resources.worker.noCluster.tips': 'No available clusters. Please create a cluster before adding a node.'
|
||||
// 25. 'resoureces.worker.upgrade.tips': 'Please upgrade to match the ZStack AIOS Server version.'
|
||||
// 25. 'resoureces.worker.upgrade.tips': 'Please upgrade to match the GPUStack Server version.'
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -40,14 +40,14 @@ export default {
|
||||
'users.password.confirm.empty': '新しいパスワードを確認してください。',
|
||||
'users.password.confirm.error': '入力された2つのパスワードが一致しません。',
|
||||
'users.login.title': 'ログイン',
|
||||
'users.version.islatest': 'ZStack AIOS {version} は最新バージョンです',
|
||||
'users.version.update': 'ZStack AIOS {version} が利用可能です',
|
||||
'users.version.islatest': 'GPUStack {version} は最新バージョンです',
|
||||
'users.version.update': 'GPUStack {version} が利用可能です',
|
||||
'users.settings.title': 'User Settings',
|
||||
'users.status.activate': 'Activate Account',
|
||||
'users.status.deactivate': 'Deactivate Account',
|
||||
'users.status.inactiveAccount': 'Inactive Account',
|
||||
'users.login.getInitialPassword':
|
||||
'Run the following command on your ZStack AIOS Server to retrieve the initial admin password.'
|
||||
'Run the following command on your GPUStack Server to retrieve the initial admin password.'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
@@ -55,5 +55,5 @@ export default {
|
||||
// 2. 'users.status.activate': 'Activate Account',
|
||||
// 3. 'users.status.deactivate': 'Deactivate Account',
|
||||
// 4. 'users.status.inactiveAccount': 'Inactive Account',
|
||||
// 5. 'users.login.getInitialPassword': 'Run the following command on your ZStack AIOS Server to retrieve the initial admin password.'
|
||||
// 5. 'users.login.getInitialPassword': 'Run the following command on your GPUStack Server to retrieve the initial admin password.'
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default {
|
||||
'billing.upsell.title': 'Billing is an Enterprise feature',
|
||||
'billing.upsell.subtitle':
|
||||
'Track spend, generate invoices, and enforce budgets across teams. Upgrade to ZStack AIOS Enterprise to manage billing.',
|
||||
'Track spend, generate invoices, and enforce budgets across teams. Upgrade to GPUStack Enterprise to manage billing.',
|
||||
'billing.upsell.featuresTitle': 'What you get in Enterprise',
|
||||
'billing.upsell.feature.usage':
|
||||
'See cost breakdowns by organization, user, and model',
|
||||
|
||||
@@ -115,20 +115,20 @@ export default {
|
||||
'{count} новый воркер был добавлен в кластер.',
|
||||
'clusters.addworker.message.success_multiple':
|
||||
'{count} новых воркеров были добавлены в кластер.',
|
||||
'clusters.create.serverUrl': 'URL сервера ZStack AIOS',
|
||||
'clusters.create.serverUrl': 'URL сервера GPUStack',
|
||||
'clusters.create.workerConfig': 'Конфигурация воркера',
|
||||
'clusters.edit.k8sOptions.changed.tip':
|
||||
'Вы изменили параметры Kubernetes. Чтобы изменения вступили в силу, повторно выполните команду регистрации в целевом кластере.',
|
||||
'clusters.addworker.containerName': 'Имя контейнера воркера',
|
||||
'clusters.addworker.containerName.tips':
|
||||
'Укажите имя для контейнера воркера.',
|
||||
'clusters.addworker.dataVolume': 'Том данных ZStack AIOS',
|
||||
'clusters.addworker.dataVolume': 'Том данных GPUStack',
|
||||
'clusters.addworker.dataVolume.tips':
|
||||
'Укажите путь для хранения данных ZStack AIOS.',
|
||||
'Укажите путь для хранения данных GPUStack.',
|
||||
'clusters.table.ip.internal': 'Внутренний',
|
||||
'clusters.table.ip.external': 'Внешний',
|
||||
'clusters.form.serverUrl.tips':
|
||||
'Если рабочий узел не может напрямую получить доступ к ZStack AIOS Server, укажите внешний URL службы ZStack AIOS Server.',
|
||||
'Если рабочий узел не может напрямую получить доступ к GPUStack Server, укажите внешний URL службы GPUStack Server.',
|
||||
'clusters.form.setDefault': 'Установить по умолчанию',
|
||||
'clusters.form.setDefault.tips':
|
||||
'Использовать по умолчанию для развертывания.',
|
||||
@@ -147,7 +147,7 @@ export default {
|
||||
'clusters.addworker.theadNotes-02':
|
||||
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
||||
'clusters.addworker.nvidiaNotes':
|
||||
'The built-in inference backends in ZStack AIOS require <span class="bold-text">CUDA 12.8+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">570</span> or newer.',
|
||||
'The built-in inference backends in GPUStack require <span class="bold-text">CUDA 12.8+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">570</span> or newer.',
|
||||
'clusters.volume.title': 'Volume Mounts',
|
||||
'clusters.volume.name': 'Volume Name',
|
||||
'clusters.volume.mountPath': 'Container Path',
|
||||
@@ -174,7 +174,7 @@ export default {
|
||||
'clusters.volume.add': 'Add Volume Mount',
|
||||
'clusters.systemDefaultContainerRegistry.title': 'Default Container Registry',
|
||||
'clusters.systemDefaultContainerRegistry.tip':
|
||||
'Default registry used to resolve ZStack AIOS images for this cluster. Falls back to the server default when unset.',
|
||||
'Default registry used to resolve GPUStack images for this cluster. Falls back to the server default when unset.',
|
||||
'clusters.k8sOptions.title': 'Kubernetes Deployment Options',
|
||||
'clusters.imageCredentials.title': 'Image Credentials',
|
||||
'clusters.imageCredentials.add': 'Add Credential',
|
||||
@@ -186,7 +186,7 @@ export default {
|
||||
'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.',
|
||||
'clusters.operatorImage.title': 'Operator Image',
|
||||
'clusters.operatorImage.tip':
|
||||
'Override for the ZStack AIOS Operator container image. Leave empty to use the server default.',
|
||||
'Override for the GPUStack Operator container image. Leave empty to use the server default.',
|
||||
'clusters.namespace.title': 'Namespace',
|
||||
'clusters.namespace.tip':
|
||||
'Kubernetes namespace the cluster’s manifests render into. Leave empty to use gpustack-system.',
|
||||
@@ -215,5 +215,5 @@ export default {
|
||||
// 10. 'clusters.addworker.theadNotes': 'If the <span class="bold-text>/usr/local/PPU_SDK</span> directory does not exist, please create a symbolic link pointing to the T-Head PPU SDK installed path: <span class="bold-text>ln -s /path/to/PPU_SDK /usr/local/PPU_SDK</span>',
|
||||
// 11. 'clusters.addworker.theadNotes-02': 'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.'
|
||||
// 12. 'clusters.addworker.metaxNotes': `If the <span class="bold-text">/opt/mxdriver</span> or <span class="bold-text">/opt/maca</span> directory does not exist, create a symbolic link to the MetaX driver and SDK installation path: <span class="desc-fill">ln -s /path/to/mxdriver /opt/mxdriver</span><span class="desc-fill">ln -s /path/to/maca /opt/maca</span>.`,
|
||||
// 13. 'clusters.addworker.nvidiaNotes': 'The built-in inference backends in ZStack AIOS v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.'
|
||||
// 13. 'clusters.addworker.nvidiaNotes': 'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.'
|
||||
// ================================================================
|
||||
|
||||
@@ -46,7 +46,7 @@ export default {
|
||||
'common.button.enabled': 'Активно',
|
||||
'common.button.disabled': 'Отключено',
|
||||
'common.button.upgrade': 'Обновить',
|
||||
'common.enterprise.feature': 'Available in ZStack AIOS Enterprise',
|
||||
'common.enterprise.feature': 'Available in GPUStack Enterprise',
|
||||
'common.input.holder': 'Введите значение',
|
||||
'common.validate.value': 'Поле {name} обязательно',
|
||||
'common.button.edit': 'Редактировать',
|
||||
@@ -211,7 +211,7 @@ export default {
|
||||
'common.form.password': 'Пароль',
|
||||
'common.form.username': 'Имя пользователя',
|
||||
'common.login.rember': 'Запомнить меня',
|
||||
'settings.company': 'ZStack AIOS',
|
||||
'settings.company': 'GPUStack.ai',
|
||||
'common.button.help': 'Помощь',
|
||||
'common.button.feedback': 'Обратная связь',
|
||||
'common.button.docs': 'Документация',
|
||||
@@ -219,6 +219,12 @@ export default {
|
||||
'common.title.delete.confirm': 'Подтверждение удаления',
|
||||
'common.title.stop.confirm': 'Подтверждение остановки',
|
||||
'common.title.start.confirm': 'Подтверждение запуска',
|
||||
'common.title.activate.confirm': 'Подтверждение активации',
|
||||
'common.title.deactivate.confirm': 'Подтверждение деактивации',
|
||||
'common.activate.single.confirm':
|
||||
'Вы уверены, что хотите активировать <span style="font-size: 13px;font-weight: 700">{name}</span>?',
|
||||
'common.deactivate.single.confirm':
|
||||
'Вы уверены, что хотите деактивировать <span style="font-size: 13px;font-weight: 700">{name}</span>?',
|
||||
'common.title.recreate.confirm': 'Подтверждение пересоздания',
|
||||
'common.button.addLabel': 'Добавить метку',
|
||||
'common.button.addSelector': 'Добавить селектор',
|
||||
|
||||
@@ -117,12 +117,44 @@ export default {
|
||||
'gpuservice.instance.gpuCount.noAvailable':
|
||||
'Нет доступных ресурсов GPU, выберите другой тип экземпляра.',
|
||||
'gpuservice.instance.gpuCount.zero': 'Только CPU, для подготовки окружения.',
|
||||
'gpuservice.instance.mode.whole': 'Весь GPU',
|
||||
'gpuservice.instance.mode.sliced': 'По доле',
|
||||
'gpuservice.instance.slice.memoryPercentage': 'Доля VRAM (%)',
|
||||
'gpuservice.instance.slice.percentage': 'Доля (%)',
|
||||
'gpuservice.instance.slice.coresPercentage': 'Доля вычислений (%)',
|
||||
'gpuservice.instance.slice.cores.min':
|
||||
'Доля вычислений должна быть не меньше доли VRAM ({count}%)',
|
||||
'gpuservice.instance.slice.fullCores': '100% вычислений',
|
||||
'gpuservice.instance.slice.percentage.required':
|
||||
'Выберите или введите процент',
|
||||
'gpuservice.instance.slice.percentage.max':
|
||||
'Доля должна быть от 1% до {count}%',
|
||||
'gpuservice.instance.stock': 'Остаток',
|
||||
'gpuservice.instance.sliced': 'Разделено',
|
||||
'gpuservice.instance.sliceable': 'Делимый',
|
||||
'gpuservice.instance.memory': 'VRAM',
|
||||
'gpuservice.instance.ram': 'RAM',
|
||||
'gpuservice.instance.os': 'ОС',
|
||||
'gpuservice.instance.arch': 'Архитектура',
|
||||
'gpuservice.instanceType': 'GPU Instance Type',
|
||||
'gpuservice.instanceType.add': 'Add Instance Type',
|
||||
'gpuservice.instanceType.flavor': 'Flavor',
|
||||
'gpuservice.instanceType.flavor.required':
|
||||
'Please select an instance type flavor',
|
||||
'gpuservice.instanceType.flavor.gpuGroup': 'GPU Compute',
|
||||
'gpuservice.instanceType.flavor.cpuGroup': 'CPU Compute',
|
||||
'gpuservice.instanceType.activate': 'Activate',
|
||||
'gpuservice.instanceType.deactivate': 'Deactivate',
|
||||
'gpuservice.instanceType.platform': 'Platform',
|
||||
'gpuservice.instanceType.product': 'Product',
|
||||
'gpuservice.instanceType.unitCpu': 'Unit CPU',
|
||||
'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU',
|
||||
'gpuservice.instanceType.unitRam': 'Unit RAM',
|
||||
'gpuservice.instanceType.unitRam.tip': 'RAM allocated per GPU',
|
||||
'gpuservice.instanceType.localStorage': 'Storage',
|
||||
'gpuservice.instanceType.localStorage.tip': 'Maximum available disk',
|
||||
'gpuservice.instanceType.notSliceable': 'Not Sliceable',
|
||||
'gpuservice.instanceType.filter.name': 'Search by name',
|
||||
'gpuservice.instance.disk': 'Диск',
|
||||
'gpuservice.table.count': 'Количество',
|
||||
'gpuservice.instance.disk.system': 'Системный диск',
|
||||
@@ -144,9 +176,6 @@ export default {
|
||||
'Отображаются только события за последний час',
|
||||
'gpuservice.instance.event.tab.instance': 'События экземпляра',
|
||||
'gpuservice.instance.event.tab.volume': 'События тома',
|
||||
'gpuservice.instance.recreate.confirm.title': 'Подтвердить пересоздание',
|
||||
'gpuservice.instance.recreate.confirm.content':
|
||||
'Текущий экземпляр будет сначала удалён, а затем пересоздан с текущей конфигурацией.\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||
'gpuservice.storage': 'Хранилище',
|
||||
'gpuservice.storage.add': 'Добавить хранилище',
|
||||
'gpuservice.storage.edit': 'Редактировать хранилище',
|
||||
|
||||
@@ -47,6 +47,7 @@ export default {
|
||||
'menu.settings': 'Settings',
|
||||
'menu.gpuService': 'GPU Service',
|
||||
'menu.gpuService.instances': 'GPU Instances',
|
||||
'menu.gpuService.instanceTypes': 'Instance Types',
|
||||
'menu.gpuService.templates': 'Instance Templates',
|
||||
'menu.gpuService.storage': 'Storage',
|
||||
'menu.gpuService.storageTypes': 'Типы хранилищ',
|
||||
|
||||
@@ -15,7 +15,7 @@ export default {
|
||||
'models.form.configurations': 'Конфигурации',
|
||||
'models.form.s3address': 'S3-адрес',
|
||||
'models.form.partialoffload.tips':
|
||||
'При включении CPU оффлоудинга ZStack AIOS будет выделять оперативную память, если ресурсов GPU недостаточно. Вы должны правильно настроить бэкенд вывода для использования гибридного CPU+GPU или полного CPU вывода.',
|
||||
'При включении CPU оффлоудинга GPUStack будет выделять оперативную память, если ресурсов GPU недостаточно. Вы должны правильно настроить бэкенд вывода для использования гибридного CPU+GPU или полного CPU вывода.',
|
||||
'models.form.distribution.tips':
|
||||
'Позволяет переносить часть слоёв модели на один или несколько удалённых воркеров, когда ресурсов текущего воркера недостаточно.',
|
||||
'models.openinplayground': 'Открыть в Песочнице',
|
||||
@@ -26,7 +26,7 @@ export default {
|
||||
'model.deploy.sort': 'Сортировка',
|
||||
'model.deploy.search.placeholder': 'Введите <kbd>/</kbd> для поиска моделей',
|
||||
'model.form.ollamatips':
|
||||
'Подсказка: ниже представлены предустановленные модели Ollama в ZStack AIOS. Выберите нужную или введите модель для развертывания в поле 【{name}】 справа.',
|
||||
'Подсказка: ниже представлены предустановленные модели Ollama в GPUStack. Выберите нужную или введите модель для развертывания в поле 【{name}】 справа.',
|
||||
'models.sort.name': 'По имени',
|
||||
'models.sort.size': 'По размеру',
|
||||
'models.sort.likes': 'По лайкам',
|
||||
@@ -89,7 +89,7 @@ export default {
|
||||
'models.form.filePath': 'Путь к модели',
|
||||
'models.form.backendVersion': 'Версия бэкенда',
|
||||
'models.form.backendVersion.tips':
|
||||
'Чтобы использовать желаемую версию {backend} {version}, система автоматически создаст виртуальную среду в онлайн-окружении для установки соответствующей версии. После обновления ZStack AIOS версия бэкенда останется зафиксированной. {link}',
|
||||
'Чтобы использовать желаемую версию {backend} {version}, система автоматически создаст виртуальную среду в онлайн-окружении для установки соответствующей версии. После обновления GPUStack версия бэкенда останется зафиксированной. {link}',
|
||||
'models.form.gpuselector': 'Селектор GPU',
|
||||
'models.form.backend.llamabox':
|
||||
'Для моделей формата GGUF. Поддержка Linux, macOS и Windows.',
|
||||
@@ -281,7 +281,7 @@ export default {
|
||||
'models.form.backendVersions.tips': `Чтобы использовать больше версий, перейдите на страницу {link} и отредактируйте бэкенд для добавления версий.`,
|
||||
'models.catalog.nogpus.tips':
|
||||
'В выбранном кластере нет доступных GPU, совместимых с этой моделью.',
|
||||
'models.form.modelfile.notfound': `Указанный путь к файлу модели не существует на сервере ZStack AIOS. Рекомендуется размещать файл модели по одному и тому же пути как на сервере ZStack AIOS, так и на воркерах ZStack AIOS. Это поможет системе принимать лучшие решения по распределению ресурсов.`,
|
||||
'models.form.modelfile.notfound': `Указанный путь к файлу модели не существует на сервере GPUStack. Рекомендуется размещать файл модели по одному и тому же пути как на сервере GPUStack, так и на воркерах GPUStack. Это поможет системе принимать лучшие решения по распределению ресурсов.`,
|
||||
'models.form.readyWorkers': 'воркеров готово',
|
||||
'models.form.maxContextLength': 'Maximum Context Length',
|
||||
'models.form.backend.helperText':
|
||||
|
||||
@@ -69,6 +69,10 @@ export default {
|
||||
'noresult.gpuservice.storage.title': 'Нет хранилищ',
|
||||
'noresult.gpuservice.storage.subTitle': 'Хранилища ещё не добавлены.',
|
||||
'noresult.gpuservice.storage.nofound': 'Подходящие хранилища не найдены.',
|
||||
'noresult.gpuservice.instanceType.title': 'No Instance Types',
|
||||
'noresult.gpuservice.instanceType.subTitle':
|
||||
'Create an instance type to get started',
|
||||
'noresult.gpuservice.instanceType.nofound': 'No instance types found',
|
||||
'noresult.gpuservice.storageType.title': 'Нет типов хранилищ',
|
||||
'noresult.gpuservice.storageType.subTitle': 'Типы хранилищ ещё не добавлены.',
|
||||
'noresult.gpuservice.storageType.nofound':
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default {
|
||||
'organizations.upsell.title': 'Organizations are an Enterprise feature',
|
||||
'organizations.upsell.subtitle':
|
||||
'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to ZStack AIOS Enterprise to manage organizations.',
|
||||
'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to GPUStack Enterprise to manage organizations.',
|
||||
'organizations.upsell.featuresTitle': 'What you get in Enterprise',
|
||||
'organizations.upsell.feature.orgs':
|
||||
'Create organizations to group users and isolate workloads',
|
||||
|
||||
@@ -51,7 +51,7 @@ export default {
|
||||
'resources.worker.container.supported': 'Только для Linux.',
|
||||
'resources.worker.current.version': 'Текущая версия: {version}',
|
||||
'resources.worker.driver.install':
|
||||
'Установите <a href="https://docs.gpustack.ai/latest/installation/installation-requirements/" target="_blank">необходимые драйверы и библиотеки</a> перед установкой ZStack AIOS.', // Translated
|
||||
'Установите <a href="https://docs.gpustack.ai/latest/installation/installation-requirements/" target="_blank">необходимые драйверы и библиотеки</a> перед установкой GPUStack.', // Translated
|
||||
'resources.worker.select.command':
|
||||
'Выберите метку для генерации команды и скопируйте её.',
|
||||
'resources.worker.script.install': 'Установка скриптом',
|
||||
@@ -87,7 +87,7 @@ export default {
|
||||
'Вставьте <span class="bold-text">Токен</span>.',
|
||||
'resources.register.worker.step7':
|
||||
'Нажмите <span class="bold-text">Перезапуск</span> для применения настроек.',
|
||||
'resources.register.install.title': 'Установка ZStack AIOS на {os}',
|
||||
'resources.register.install.title': 'Установка GPUStack на {os}',
|
||||
'resources.register.download':
|
||||
'Скачайте и установите <a href={url} target="_blank">инсталлятор</a>. Поддерживаемые версии: {versions}.',
|
||||
'resource.register.maos.support': 'Apple Silicon (серия M), macOS 14+',
|
||||
@@ -110,7 +110,7 @@ export default {
|
||||
'No available clusters. Please create a cluster before adding a node.',
|
||||
'resources.metrics.details': 'Monitoring',
|
||||
'resoureces.worker.upgrade.tips':
|
||||
'Please upgrade to match the ZStack AIOS Server version.',
|
||||
'Please upgrade to match the GPUStack Server version.',
|
||||
'resources.worker.version': 'Worker Version: {version}',
|
||||
'resources.server.version': 'Server Version: {version}',
|
||||
'resources.worker.currentVersion': 'Current Version: {version}',
|
||||
@@ -127,5 +127,5 @@ export default {
|
||||
// 7. 'resources.worker.maintenance.tips': 'When maintenance mode is enabled, the node will stop scheduling new model deployment tasks. Running instances will not be affected.',
|
||||
// 8. 'resources.worker.noCluster.tips': 'No available clusters. Please create a cluster before adding a node.',
|
||||
// 9. 'resources.metrics.details': 'Monitoring',
|
||||
// 10. 'resoureces.worker.upgrade.tips': 'Please upgrade to match the ZStack AIOS Server version.'
|
||||
// 10. 'resoureces.worker.upgrade.tips': 'Please upgrade to match the GPUStack Server version.'
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -40,8 +40,8 @@ export default {
|
||||
'users.password.confirm.empty': 'Подтвердите новый пароль',
|
||||
'users.password.confirm.error': 'Пароли не совпадают',
|
||||
'users.login.title': 'Вход в',
|
||||
'users.version.islatest': 'ZStack AIOS {version} — последняя версия',
|
||||
'users.version.update': 'Доступно обновление ZStack AIOS {version}',
|
||||
'users.version.islatest': 'GPUStack {version} — последняя версия',
|
||||
'users.version.update': 'Доступно обновление GPUStack {version}',
|
||||
'users.settings.title': 'Настройки пользователя',
|
||||
'users.status.activate': 'Активировать аккаунт',
|
||||
'users.status.deactivate': 'Деактивировать аккаунт',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default {
|
||||
'billing.upsell.title': 'Billing is an Enterprise feature',
|
||||
'billing.upsell.subtitle':
|
||||
'Track spend, generate invoices, and enforce budgets across teams. Upgrade to ZStack AIOS Enterprise to manage billing.',
|
||||
'Track spend, generate invoices, and enforce budgets across teams. Upgrade to GPUStack Enterprise to manage billing.',
|
||||
'billing.upsell.featuresTitle': 'What you get in Enterprise',
|
||||
'billing.upsell.feature.usage':
|
||||
'See cost breakdowns by organization, user, and model',
|
||||
|
||||
@@ -87,7 +87,7 @@ export default {
|
||||
'clusters.addworker.detectWorkerAddress.tips':
|
||||
"Belirtilmezse İşçi Düğüm IP'si varsayılır.",
|
||||
'clusters.addworker.externalIP.tips':
|
||||
'VPC veya özel ağda çalıştırılıyorsa, lütfen ZStack AIOS Sunucusuna erişilebilir İşçi Düğüm harici adresini belirtin.',
|
||||
'VPC veya özel ağda çalıştırılıyorsa, lütfen GPUStack Sunucusuna erişilebilir İşçi Düğüm harici adresini belirtin.',
|
||||
'clusters.addworker.enterWorkerIP': "İşçi düğüm IP'sini girin",
|
||||
'clusters.addworker.enterWorkerIP.error': "Lütfen işçi düğüm IP'sini girin.",
|
||||
'clusters.addworker.enterWorkerAddress': 'İşçi düğüm harici adresini girin',
|
||||
@@ -115,20 +115,20 @@ export default {
|
||||
'{count} yeni işçi düğüm kümeye eklendi.',
|
||||
'clusters.addworker.message.success_multiple':
|
||||
'{count} yeni işçi düğüm kümeye eklendi.',
|
||||
'clusters.create.serverUrl': "ZStack AIOS Sunucu URL'si",
|
||||
'clusters.create.serverUrl': "GPUStack Sunucu URL'si",
|
||||
'clusters.create.workerConfig': 'İşçi Düğüm Yapılandırması',
|
||||
'clusters.edit.k8sOptions.changed.tip':
|
||||
'Kubernetes seçeneklerini değiştirdiniz. Değişikliklerin etkili olması için kayıt komutunu hedef kümede yeniden çalıştırın.',
|
||||
'clusters.addworker.containerName': 'İşçi Düğüm Konteyner Adı',
|
||||
'clusters.addworker.containerName.tips':
|
||||
'İşçi düğüm konteyneri için bir ad belirtin.',
|
||||
'clusters.addworker.dataVolume': 'ZStack AIOS Veri Birimi',
|
||||
'clusters.addworker.dataVolume': 'GPUStack Veri Birimi',
|
||||
'clusters.addworker.dataVolume.tips':
|
||||
'ZStack AIOS için veri depolama yolu belirtin.',
|
||||
'GPUStack için veri depolama yolu belirtin.',
|
||||
'clusters.table.ip.internal': 'Dahili',
|
||||
'clusters.table.ip.external': 'Harici',
|
||||
'clusters.form.serverUrl.tips':
|
||||
"İşçi düğüm ZStack AIOS Sunucusuna doğrudan erişemiyorsa, harici olarak erişilebilir bir ZStack AIOS hizmet URL'si belirtin.",
|
||||
"İşçi düğüm GPUStack Sunucusuna doğrudan erişemiyorsa, harici olarak erişilebilir bir GPUStack hizmet URL'si belirtin.",
|
||||
'clusters.form.setDefault': 'Varsayılan Olarak Ayarla',
|
||||
'clusters.form.setDefault.tips': 'Dağıtım için varsayılan.',
|
||||
'clusters.addworker.noClusters': 'Kullanılabilir Docker kümesi bulunamadı',
|
||||
@@ -147,7 +147,7 @@ export default {
|
||||
'clusters.addworker.theadNotes-02':
|
||||
'T-Head PPU, cihaz enjeksiyonu için Container Device Interface (CDI) kullanır ve CDI oluşturma için <span class="bold-text">/var/run/cdi</span> dizininin kullanılabilir olmasını gerektirir.',
|
||||
'clusters.addworker.nvidiaNotes':
|
||||
'ZStack AIOS\'teki yerleşik çıkarım altyapıları <span class="bold-text">CUDA 12.8+</span> gerektirir. Lütfen NVIDIA sürücü sürümünüzün <span class="bold-text">570</span> veya daha yeni olduğundan emin olun.',
|
||||
'GPUStack\'teki yerleşik çıkarım altyapıları <span class="bold-text">CUDA 12.8+</span> gerektirir. Lütfen NVIDIA sürücü sürümünüzün <span class="bold-text">570</span> veya daha yeni olduğundan emin olun.',
|
||||
'clusters.volume.title': 'Volume Mounts',
|
||||
'clusters.volume.name': 'Volume Name',
|
||||
'clusters.volume.mountPath': 'Container Path',
|
||||
@@ -174,7 +174,7 @@ export default {
|
||||
'clusters.volume.add': 'Add Volume Mount',
|
||||
'clusters.systemDefaultContainerRegistry.title': 'Default Container Registry',
|
||||
'clusters.systemDefaultContainerRegistry.tip':
|
||||
'Default registry used to resolve ZStack AIOS images for this cluster. Falls back to the server default when unset.',
|
||||
'Default registry used to resolve GPUStack images for this cluster. Falls back to the server default when unset.',
|
||||
'clusters.k8sOptions.title': 'Kubernetes Deployment Options',
|
||||
'clusters.imageCredentials.title': 'Image Credentials',
|
||||
'clusters.imageCredentials.add': 'Add Credential',
|
||||
@@ -186,7 +186,7 @@ export default {
|
||||
'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.',
|
||||
'clusters.operatorImage.title': 'Operator Image',
|
||||
'clusters.operatorImage.tip':
|
||||
'Override for the ZStack AIOS Operator container image. Leave empty to use the server default.',
|
||||
'Override for the GPUStack Operator container image. Leave empty to use the server default.',
|
||||
'clusters.namespace.title': 'Namespace',
|
||||
'clusters.namespace.tip':
|
||||
'Kubernetes namespace the cluster’s manifests render into. Leave empty to use gpustack-system.',
|
||||
|
||||
@@ -46,7 +46,7 @@ export default {
|
||||
'common.button.enabled': 'Etkin',
|
||||
'common.button.disabled': 'Devre dışı',
|
||||
'common.button.upgrade': 'Yükselt',
|
||||
'common.enterprise.feature': 'Available in ZStack AIOS Enterprise',
|
||||
'common.enterprise.feature': 'Available in GPUStack Enterprise',
|
||||
'common.input.holder': 'Lütfen girin',
|
||||
'common.validate.value': '{name} değeri gereklidir',
|
||||
'common.button.edit': 'Düzenle',
|
||||
@@ -216,7 +216,7 @@ export default {
|
||||
'common.form.password': 'Şifre',
|
||||
'common.form.username': 'Kullanıcı adı',
|
||||
'common.login.rember': 'Beni hatırla',
|
||||
'settings.company': 'ZStack AIOS',
|
||||
'settings.company': 'GPUStack.ai',
|
||||
'common.button.help': 'Yardım',
|
||||
'common.button.feedback': 'Geri Bildirim',
|
||||
'common.button.docs': 'Dokümantasyon',
|
||||
@@ -224,6 +224,12 @@ export default {
|
||||
'common.title.delete.confirm': 'Silme onayı',
|
||||
'common.title.stop.confirm': 'Durdurma onayı',
|
||||
'common.title.start.confirm': 'Başlatma onayı',
|
||||
'common.title.activate.confirm': 'Etkinleştirme onayı',
|
||||
'common.title.deactivate.confirm': 'Devre dışı bırakma onayı',
|
||||
'common.activate.single.confirm':
|
||||
'Etkinleştirmek istediğinizden emin misiniz? \n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||
'common.deactivate.single.confirm':
|
||||
'Devre dışı bırakmak istediğinizden emin misiniz? \n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||
'common.title.recreate.confirm': 'Yeniden oluşturma onayı',
|
||||
'common.button.addLabel': 'Etiket Ekle',
|
||||
'common.button.addSelector': 'Seçici Ekle',
|
||||
|
||||
@@ -113,12 +113,44 @@ export default {
|
||||
'gpuservice.instance.gpuCount.noAvailable':
|
||||
'Kullanılabilir GPU kaynağı yok, lütfen başka bir örnek türü seçin.',
|
||||
'gpuservice.instance.gpuCount.zero': 'Yalnızca CPU, ortam hazırlığı için.',
|
||||
'gpuservice.instance.mode.whole': 'Tam GPU',
|
||||
'gpuservice.instance.mode.sliced': 'Orana Göre',
|
||||
'gpuservice.instance.slice.memoryPercentage': 'VRAM Yüzdesi (%)',
|
||||
'gpuservice.instance.slice.percentage': 'Yüzde (%)',
|
||||
'gpuservice.instance.slice.coresPercentage': 'İşlem Gücü Yüzdesi (%)',
|
||||
'gpuservice.instance.slice.cores.min':
|
||||
'İşlem gücü oranı VRAM oranından ({count}%) küçük olamaz',
|
||||
'gpuservice.instance.slice.fullCores': '%100 İşlem Gücü',
|
||||
'gpuservice.instance.slice.percentage.required':
|
||||
'Lütfen bir yüzde seçin veya girin',
|
||||
'gpuservice.instance.slice.percentage.max':
|
||||
'Oran %1 ile %{count} arasında olmalıdır',
|
||||
'gpuservice.instance.stock': 'Stok',
|
||||
'gpuservice.instance.sliced': 'Bölünmüş',
|
||||
'gpuservice.instance.sliceable': 'Bölünebilir',
|
||||
'gpuservice.instance.memory': 'VRAM',
|
||||
'gpuservice.instance.ram': 'RAM',
|
||||
'gpuservice.instance.os': 'OS',
|
||||
'gpuservice.instance.arch': 'Mimari',
|
||||
'gpuservice.instanceType': 'GPU Instance Type',
|
||||
'gpuservice.instanceType.add': 'Add Instance Type',
|
||||
'gpuservice.instanceType.flavor': 'Flavor',
|
||||
'gpuservice.instanceType.flavor.required':
|
||||
'Please select an instance type flavor',
|
||||
'gpuservice.instanceType.flavor.gpuGroup': 'GPU Compute',
|
||||
'gpuservice.instanceType.flavor.cpuGroup': 'CPU Compute',
|
||||
'gpuservice.instanceType.activate': 'Activate',
|
||||
'gpuservice.instanceType.deactivate': 'Deactivate',
|
||||
'gpuservice.instanceType.platform': 'Platform',
|
||||
'gpuservice.instanceType.product': 'Product',
|
||||
'gpuservice.instanceType.unitCpu': 'Unit CPU',
|
||||
'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU',
|
||||
'gpuservice.instanceType.unitRam': 'Unit RAM',
|
||||
'gpuservice.instanceType.unitRam.tip': 'RAM allocated per GPU',
|
||||
'gpuservice.instanceType.localStorage': 'Storage',
|
||||
'gpuservice.instanceType.localStorage.tip': 'Maximum available disk',
|
||||
'gpuservice.instanceType.notSliceable': 'Not Sliceable',
|
||||
'gpuservice.instanceType.filter.name': 'Search by name',
|
||||
'gpuservice.instance.disk': 'Disk',
|
||||
'gpuservice.table.count': 'Sayı',
|
||||
'gpuservice.instance.disk.system': 'Sistem Diski',
|
||||
@@ -140,10 +172,6 @@ export default {
|
||||
'Yalnızca son bir saatteki olaylar gösterilir',
|
||||
'gpuservice.instance.event.tab.instance': 'Örnek Olayları',
|
||||
'gpuservice.instance.event.tab.volume': 'Birim Olayları',
|
||||
'gpuservice.instance.recreate.confirm.title':
|
||||
'Yeniden oluşturma onaylansın mı',
|
||||
'gpuservice.instance.recreate.confirm.content':
|
||||
'Mevcut örnek önce silinecek, ardından mevcut yapılandırmayla yeniden oluşturulacaktır.\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||
'gpuservice.storage': 'Depolama',
|
||||
'gpuservice.storage.add': 'Depolama Ekle',
|
||||
'gpuservice.storage.edit': 'Depolamayı Düzenle',
|
||||
|
||||
@@ -47,6 +47,7 @@ export default {
|
||||
'menu.settings': 'Settings',
|
||||
'menu.gpuService': 'GPU Service',
|
||||
'menu.gpuService.instances': 'GPU Instances',
|
||||
'menu.gpuService.instanceTypes': 'Instance Types',
|
||||
'menu.gpuService.templates': 'Instance Templates',
|
||||
'menu.gpuService.storage': 'Storage',
|
||||
'menu.gpuService.storageTypes': 'Depolama Türleri',
|
||||
|
||||
@@ -14,7 +14,7 @@ export default {
|
||||
'models.form.env': 'Ortam Değişkenleri',
|
||||
'models.form.configurations': 'Yapılandırmalar',
|
||||
'models.form.s3address': 'S3 Adresi',
|
||||
'models.form.partialoffload.tips': `CPU aktarımı etkinleştirildiğinde, GPU kaynakları yetersiz olduğunda ZStack AIOS CPU belleği ayırır. Hibrit CPU+GPU veya tam CPU çıkarımı kullanmak için çıkarım altyapısını doğru şekilde yapılandırmanız gerekir.`,
|
||||
'models.form.partialoffload.tips': `CPU aktarımı etkinleştirildiğinde, GPU kaynakları yetersiz olduğunda GPUStack CPU belleği ayırır. Hibrit CPU+GPU veya tam CPU çıkarımı kullanmak için çıkarım altyapısını doğru şekilde yapılandırmanız gerekir.`,
|
||||
'models.form.distribution.tips': `Bir işçi düğümün kaynakları yetersiz olduğunda, modelin katmanlarının bir kısmının tekli veya çoklu uzak işçi düğümlere aktarılmasına olanak tanır.`,
|
||||
'models.openinplayground': 'Deneme Alanında Aç',
|
||||
'models.instances': 'örnekler',
|
||||
@@ -24,7 +24,7 @@ export default {
|
||||
'model.deploy.sort': 'Sırala',
|
||||
'model.deploy.search.placeholder': 'Modelleri aramak için <kbd>/</kbd> yazın',
|
||||
'model.form.ollamatips':
|
||||
"İpucu: Aşağıdakiler ZStack AIOS'te önceden yapılandırılmış Ollama modelleridir. İstediğiniz modeli seçin veya dağıtmak istediğiniz modeli doğrudan sağdaki 【{name}】 giriş kutusuna yazın.",
|
||||
"İpucu: Aşağıdakiler GPUStack'te önceden yapılandırılmış Ollama modelleridir. İstediğiniz modeli seçin veya dağıtmak istediğiniz modeli doğrudan sağdaki 【{name}】 giriş kutusuna yazın.",
|
||||
'models.sort.name': 'Ad',
|
||||
'models.sort.size': 'Boyut',
|
||||
'models.sort.likes': 'Beğeniler',
|
||||
@@ -87,7 +87,7 @@ export default {
|
||||
'models.form.filePath': 'Model Yolu',
|
||||
'models.form.backendVersion': 'Altyapı Sürümü',
|
||||
'models.form.backendVersion.tips':
|
||||
'{backend}{version} sürümünü kullanmak için sistem, ilgili sürümü yüklemek üzere çevrimiçi ortamda otomatik olarak sanal ortam oluşturur. ZStack AIOS yükseltmesinden sonra altyapı sürümü sabit kalır. {link}',
|
||||
'{backend}{version} sürümünü kullanmak için sistem, ilgili sürümü yüklemek üzere çevrimiçi ortamda otomatik olarak sanal ortam oluşturur. GPUStack yükseltmesinden sonra altyapı sürümü sabit kalır. {link}',
|
||||
'models.form.gpuselector': 'GPU Seçici',
|
||||
'models.form.backend.llamabox':
|
||||
'GGUF format modeller için, Linux, macOS ve Windows destekler.',
|
||||
@@ -277,7 +277,7 @@ export default {
|
||||
'models.form.backendVersions.tips': `Daha fazla sürüm kullanmak için {link} sayfasına gidin ve sürüm eklemek üzere altyapıyı düzenleyin.`,
|
||||
'models.catalog.nogpus.tips':
|
||||
'Seçili kümede bu model için uyumlu GPU bulunmuyor.',
|
||||
'models.form.modelfile.notfound': `Belirttiğiniz model dosyası yolu ZStack AIOS sunucusunda mevcut değil. Model dosyasını hem ZStack AIOS sunucusunda hem de ZStack AIOS işçi düğümlerinde aynı yola yerleştirmeniz önerilir. Bu, ZStack AIOS'in daha iyi kararlar almasına yardımcı olur.`,
|
||||
'models.form.modelfile.notfound': `Belirttiğiniz model dosyası yolu GPUStack sunucusunda mevcut değil. Model dosyasını hem GPUStack sunucusunda hem de GPUStack işçi düğümlerinde aynı yola yerleştirmeniz önerilir. Bu, GPUStack'in daha iyi kararlar almasına yardımcı olur.`,
|
||||
'models.form.readyWorkers': 'hazır işçi düğüm',
|
||||
'models.form.maxContextLength': 'Maksimum Bağlam Uzunluğu',
|
||||
'models.form.backend.helperText':
|
||||
|
||||
@@ -67,6 +67,10 @@ export default {
|
||||
'noresult.gpuservice.storage.title': 'Depolama Yok',
|
||||
'noresult.gpuservice.storage.subTitle': 'Henüz depolama eklenmedi.',
|
||||
'noresult.gpuservice.storage.nofound': 'Eşleşen depolama bulunamadı.',
|
||||
'noresult.gpuservice.instanceType.title': 'No Instance Types',
|
||||
'noresult.gpuservice.instanceType.subTitle':
|
||||
'Create an instance type to get started',
|
||||
'noresult.gpuservice.instanceType.nofound': 'No instance types found',
|
||||
'noresult.gpuservice.storageType.title': 'Depolama Türü Yok',
|
||||
'noresult.gpuservice.storageType.subTitle': 'Henüz depolama türü eklenmedi.',
|
||||
'noresult.gpuservice.storageType.nofound':
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default {
|
||||
'organizations.upsell.title': 'Organizations are an Enterprise feature',
|
||||
'organizations.upsell.subtitle':
|
||||
'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to ZStack AIOS Enterprise to manage organizations.',
|
||||
'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to GPUStack Enterprise to manage organizations.',
|
||||
'organizations.upsell.featuresTitle': 'What you get in Enterprise',
|
||||
'organizations.upsell.feature.orgs':
|
||||
'Create organizations to group users and isolate workloads',
|
||||
|
||||
@@ -52,7 +52,7 @@ export default {
|
||||
'resources.worker.container.supported': 'macOS veya Windows desteklenmez.',
|
||||
'resources.worker.current.version': 'Mevcut sürüm: {version}.',
|
||||
'resources.worker.driver.install':
|
||||
'ZStack AIOS kurulumundan önce <a href="https://docs.gpustack.ai/latest/installation/installation-requirements/" target="_blank">gerekli sürücüleri ve kütüphaneleri</a> yükleyin.',
|
||||
'GPUStack kurulumundan önce <a href="https://docs.gpustack.ai/latest/installation/installation-requirements/" target="_blank">gerekli sürücüleri ve kütüphaneleri</a> yükleyin.',
|
||||
'resources.worker.select.command':
|
||||
'Komutu oluşturmak için bir etiket seçin ve kopyala düğmesiyle kopyalayın.',
|
||||
'resources.worker.script.install': 'Betik Kurulumu',
|
||||
@@ -89,7 +89,7 @@ export default {
|
||||
'<span class="bold-text">Token</span>\'ı yapıştırın.',
|
||||
'resources.register.worker.step7':
|
||||
'Ayarları uygulamak için <span class="bold-text">Yeniden Başlat</span>\'a tıklayın.',
|
||||
'resources.register.install.title': '{os} üzerine ZStack AIOS kur',
|
||||
'resources.register.install.title': '{os} üzerine GPUStack kur',
|
||||
'resources.register.download':
|
||||
'<a href={url} target="_blank">Yükleyiciyi</a> indirip kurun. Yalnızca desteklenen: {versions}.',
|
||||
'resource.register.maos.support': 'Apple Silicon (M serisi), macOS 14+',
|
||||
@@ -110,7 +110,7 @@ export default {
|
||||
'Kullanılabilir küme yok. Lütfen düğüm eklemeden önce bir küme oluşturun.',
|
||||
'resources.metrics.details': 'İzleme',
|
||||
'resoureces.worker.upgrade.tips':
|
||||
'Please upgrade to match the ZStack AIOS Server version.',
|
||||
'Please upgrade to match the GPUStack Server version.',
|
||||
'resources.worker.version': 'Worker Version: {version}',
|
||||
'resources.server.version': 'Server Version: {version}',
|
||||
'resources.worker.currentVersion': 'Current Version: {version}',
|
||||
@@ -119,5 +119,5 @@ export default {
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
// 1. 'resoureces.worker.upgrade.tips': 'Please upgrade to match the ZStack AIOS Server version.'
|
||||
// 1. 'resoureces.worker.upgrade.tips': 'Please upgrade to match the GPUStack Server version.'
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -40,12 +40,12 @@ export default {
|
||||
'users.password.confirm.empty': 'Lütfen yeni şifreyi tekrar girin.',
|
||||
'users.password.confirm.error': 'Girilen iki şifre eşleşmiyor.',
|
||||
'users.login.title': 'Giriş yap:',
|
||||
'users.version.islatest': 'ZStack AIOS {version} en güncel sürümdür',
|
||||
'users.version.update': 'ZStack AIOS {version} kullanılabilir',
|
||||
'users.version.islatest': 'GPUStack {version} en güncel sürümdür',
|
||||
'users.version.update': 'GPUStack {version} kullanılabilir',
|
||||
'users.settings.title': 'Kullanıcı Ayarları',
|
||||
'users.status.activate': 'Hesabı Etkinleştir',
|
||||
'users.status.deactivate': 'Hesabı Devre Dışı Bırak',
|
||||
'users.status.inactiveAccount': 'Pasif Hesap',
|
||||
'users.login.getInitialPassword':
|
||||
'Başlangıç yönetici şifresini almak için ZStack AIOS Sunucunuzda aşağıdaki komutu çalıştırın.'
|
||||
'Başlangıç yönetici şifresini almak için GPUStack Sunucunuzda aşağıdaki komutu çalıştırın.'
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default {
|
||||
'billing.upsell.title': '计费是企业版功能',
|
||||
'billing.upsell.subtitle':
|
||||
'在团队间跟踪花费、生成账单并执行预算。升级到 ZStack AIOS 企业版即可管理计费。',
|
||||
'在团队间跟踪花费、生成账单并执行预算。升级到 GPUStack 企业版即可管理计费。',
|
||||
'billing.upsell.featuresTitle': '企业版包含的能力',
|
||||
'billing.upsell.feature.usage': '按组织、用户与模型查看成本明细',
|
||||
'billing.upsell.feature.invoices': '生成账单并导出计费报表',
|
||||
|
||||
@@ -85,7 +85,7 @@ export default {
|
||||
'clusters.addworker.detectWorkerAddress.tips':
|
||||
'如果未指定,则默认为节点 IP。',
|
||||
'clusters.addworker.externalIP.tips':
|
||||
'如运行在 VPC 或私有网络时,请指定 ZStack AIOS Server 可达的节点外部地址。',
|
||||
'如运行在 VPC 或私有网络时,请指定 GPUStack Server 可达的节点外部地址。',
|
||||
'clusters.addworker.enterWorkerIP': '输入节点 IP',
|
||||
'clusters.addworker.enterWorkerIP.error': '请输入节点 IP',
|
||||
'clusters.addworker.enterWorkerAddress': '输入节点外部地址',
|
||||
@@ -113,18 +113,18 @@ export default {
|
||||
'已将 {count} 个新节点添加到集群中。',
|
||||
'clusters.addworker.message.success_multiple':
|
||||
'已将 {count} 个新节点添加到集群中。',
|
||||
'clusters.create.serverUrl': 'ZStack AIOS Server 节点地址',
|
||||
'clusters.create.serverUrl': 'GPUStack Server 节点地址',
|
||||
'clusters.create.workerConfig': '节点配置',
|
||||
'clusters.edit.k8sOptions.changed.tip':
|
||||
'您已修改 Kubernetes 选项,需要在目标集群上重新运行注册命令才会生效。',
|
||||
'clusters.addworker.containerName': '节点容器名称',
|
||||
'clusters.addworker.containerName.tips': '为节点容器指定一个名称。',
|
||||
'clusters.addworker.dataVolume': 'ZStack AIOS 数据卷',
|
||||
'clusters.addworker.dataVolume.tips': '为 ZStack AIOS 指定数据存储路径。',
|
||||
'clusters.addworker.dataVolume': 'GPUStack 数据卷',
|
||||
'clusters.addworker.dataVolume.tips': '为 GPUStack 指定数据存储路径。',
|
||||
'clusters.table.ip.internal': '内',
|
||||
'clusters.table.ip.external': '外',
|
||||
'clusters.form.serverUrl.tips':
|
||||
'如果节点无法直接访问 ZStack AIOS Server,则指定一个可访问的外部 ZStack AIOS Server 地址。',
|
||||
'如果节点无法直接访问 GPUStack Server,则指定一个可访问的外部 GPUStack Server 地址。',
|
||||
'clusters.form.setDefault': '设为默认',
|
||||
'clusters.form.setDefault.tips': '部署时的默认集群。',
|
||||
'clusters.addworker.noClusters': '无可用的 Docker 集群',
|
||||
@@ -140,7 +140,7 @@ export default {
|
||||
'clusters.addworker.theadNotes-02':
|
||||
'平头哥(T-Head)PPU 使用容器设备接口(CDI)进行设备注入,因此需要确保 <span class="bold-text">/var/run/cdi</span> 目录可用以生成 CDI。',
|
||||
'clusters.addworker.nvidiaNotes':
|
||||
'ZStack AIOS 内置推理后端依赖 <span class="bold-text">CUDA 12.8</span> 及以上版本,请确保 NVIDIA 驱动版本为 <span class="bold-text">570</span> 或以上。',
|
||||
'GPUStack 内置推理后端依赖 <span class="bold-text">CUDA 12.8</span> 及以上版本,请确保 NVIDIA 驱动版本为 <span class="bold-text">570</span> 或以上。',
|
||||
'clusters.volume.title': '卷挂载',
|
||||
'clusters.volume.name': '卷名称',
|
||||
'clusters.volume.mountPath': '容器内路径',
|
||||
@@ -166,7 +166,7 @@ export default {
|
||||
'clusters.volume.add': '添加卷挂载',
|
||||
'clusters.systemDefaultContainerRegistry.title': '默认容器镜像仓库',
|
||||
'clusters.systemDefaultContainerRegistry.tip':
|
||||
'用于解析该集群 ZStack AIOS 镜像的默认镜像仓库。未设置时回退到服务端默认值。',
|
||||
'用于解析该集群 GPUStack 镜像的默认镜像仓库。未设置时回退到服务端默认值。',
|
||||
'clusters.k8sOptions.title': 'Kubernetes 部署选项',
|
||||
'clusters.imageCredentials.title': '镜像仓库凭证',
|
||||
'clusters.imageCredentials.add': '添加凭证',
|
||||
@@ -178,7 +178,7 @@ export default {
|
||||
'应用到每个 worker DaemonSet 的 Pod nodeSelector,只有标签匹配的节点才会被调度运行 worker。',
|
||||
'clusters.operatorImage.title': 'Operator 镜像',
|
||||
'clusters.operatorImage.tip':
|
||||
'ZStack AIOS Operator 容器镜像的覆盖值。留空则使用服务端默认值。',
|
||||
'GPUStack Operator 容器镜像的覆盖值。留空则使用服务端默认值。',
|
||||
'clusters.namespace.title': '命名空间',
|
||||
'clusters.namespace.tip':
|
||||
'集群清单渲染所使用的 Kubernetes 命名空间。留空则使用 gpustack-system。',
|
||||
|
||||
@@ -44,7 +44,7 @@ export default {
|
||||
'common.button.rollback': '回滚',
|
||||
'common.button.new': '新建{ text }',
|
||||
'common.button.upgrade': '升级',
|
||||
'common.enterprise.feature': 'ZStack AIOS 企业版可用',
|
||||
'common.enterprise.feature': 'GPUStack 企业版可用',
|
||||
'common.input.holder': '请输入',
|
||||
'common.holder.search': '搜索',
|
||||
'common.button.edit': '编辑',
|
||||
@@ -204,7 +204,7 @@ export default {
|
||||
'common.form.password': '密码',
|
||||
'common.form.username': '用户名',
|
||||
'common.login.rember': '记住我',
|
||||
'settings.company': 'ZStack AIOS',
|
||||
'settings.company': '数澈软件',
|
||||
'common.button.help': '帮助',
|
||||
'common.button.feedback': '反馈',
|
||||
'common.button.docs': '文档',
|
||||
@@ -212,6 +212,12 @@ export default {
|
||||
'common.title.delete.confirm': '确认删除',
|
||||
'common.title.stop.confirm': '确认停止',
|
||||
'common.title.start.confirm': '确认启动',
|
||||
'common.title.activate.confirm': '确认启用',
|
||||
'common.title.deactivate.confirm': '确认停用',
|
||||
'common.activate.single.confirm':
|
||||
'确定启用 <span style="font-size: 13px;font-weight: 700">{name}?</span>',
|
||||
'common.deactivate.single.confirm':
|
||||
'确定停用 <span style="font-size: 13px;font-weight: 700">{name}?</span>',
|
||||
'common.title.recreate.confirm': '确认重新创建',
|
||||
'common.button.addLabel': '添加标签',
|
||||
'common.button.addSelector': '添加选择器',
|
||||
|
||||
@@ -108,12 +108,40 @@ export default {
|
||||
'gpuservice.instance.gpuCount.noAvailable':
|
||||
'没有可用的 GPU 资源,请选择其他实例类型。',
|
||||
'gpuservice.instance.gpuCount.zero': '仅使用 CPU,用于环境准备。',
|
||||
'gpuservice.instance.mode.whole': '整卡',
|
||||
'gpuservice.instance.mode.sliced': '按比例',
|
||||
'gpuservice.instance.slice.memoryPercentage': '显存占比(%)',
|
||||
'gpuservice.instance.slice.percentage': '占比(%)',
|
||||
'gpuservice.instance.slice.coresPercentage': '算力占比(%)',
|
||||
'gpuservice.instance.slice.cores.min': '算力占比需不小于显存占比 {count}%',
|
||||
'gpuservice.instance.slice.fullCores': '100% 算力',
|
||||
'gpuservice.instance.slice.percentage.required': '请选择或输入百分比',
|
||||
'gpuservice.instance.slice.percentage.max': '比例需在 1% 到 {count}% 之间',
|
||||
'gpuservice.instance.stock': '库存',
|
||||
'gpuservice.instance.sliced': '切分',
|
||||
'gpuservice.instance.sliceable': '可切分',
|
||||
'gpuservice.instance.memory': '显存',
|
||||
'gpuservice.instance.ram': '内存',
|
||||
'gpuservice.instance.os': '系统',
|
||||
'gpuservice.instance.arch': '架构',
|
||||
'gpuservice.instanceType': 'GPU 实例类型',
|
||||
'gpuservice.instanceType.add': '添加实例类型',
|
||||
'gpuservice.instanceType.flavor': '规格',
|
||||
'gpuservice.instanceType.flavor.required': '请选择实例类型规格',
|
||||
'gpuservice.instanceType.flavor.gpuGroup': 'GPU 算力',
|
||||
'gpuservice.instanceType.flavor.cpuGroup': 'CPU 算力',
|
||||
'gpuservice.instanceType.activate': '启用',
|
||||
'gpuservice.instanceType.deactivate': '停用',
|
||||
'gpuservice.instanceType.platform': '平台',
|
||||
'gpuservice.instanceType.product': '商品',
|
||||
'gpuservice.instanceType.unitCpu': '单位 CPU',
|
||||
'gpuservice.instanceType.unitCpu.tip': '每 GPU 对应多少 CPU',
|
||||
'gpuservice.instanceType.unitRam': '单位内存',
|
||||
'gpuservice.instanceType.unitRam.tip': '每 GPU 对应多少内存',
|
||||
'gpuservice.instanceType.localStorage': '存储',
|
||||
'gpuservice.instanceType.localStorage.tip': '最大可用磁盘',
|
||||
'gpuservice.instanceType.notSliceable': '不可切分',
|
||||
'gpuservice.instanceType.filter.name': '按名称搜索',
|
||||
'gpuservice.instance.disk': '磁盘',
|
||||
'gpuservice.table.count': '数量',
|
||||
'gpuservice.instance.disk.system': '系统盘',
|
||||
@@ -134,9 +162,6 @@ export default {
|
||||
'gpuservice.instance.event.recentHourTip': '仅显示最近一小时的事件。',
|
||||
'gpuservice.instance.event.tab.instance': '实例事件',
|
||||
'gpuservice.instance.event.tab.volume': '存储卷事件',
|
||||
'gpuservice.instance.recreate.confirm.title': '确认重新创建',
|
||||
'gpuservice.instance.recreate.confirm.content':
|
||||
'系统将先删除当前实例,然后使用当前配置重新创建。\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||
'gpuservice.storage': '存储',
|
||||
'gpuservice.storage.add': '添加存储',
|
||||
'gpuservice.storage.edit': '编辑存储',
|
||||
|
||||
@@ -47,6 +47,7 @@ export default {
|
||||
'menu.models.backendsList': '推理后端',
|
||||
'menu.gpuService': 'GPU 服务',
|
||||
'menu.gpuService.instances': 'GPU 实例',
|
||||
'menu.gpuService.instanceTypes': '实例类型',
|
||||
'menu.gpuService.templates': '实例模板',
|
||||
'menu.gpuService.storage': '存储',
|
||||
'menu.gpuService.storageTypes': '存储类型',
|
||||
|
||||
@@ -15,7 +15,7 @@ export default {
|
||||
'models.form.configurations': '配置',
|
||||
'models.form.s3address': 'S3 地址',
|
||||
'models.form.partialoffload.tips':
|
||||
'启用 CPU 卸载后,GPU 不足时 ZStack AIOS 会自动使用 CPU 内存。请确保推理后端已正确配置为混合 CPU+GPU 或纯 CPU 推理。',
|
||||
'启用 CPU 卸载后,GPU 不足时 GPUStack 会自动使用 CPU 内存。请确保推理后端已正确配置为混合 CPU+GPU 或纯 CPU 推理。',
|
||||
'models.form.distribution.tips':
|
||||
'允许在单个节点资源不足时,将部分计算卸载到一个或多个远程节点。',
|
||||
'models.openinplayground': '在 Playground 中打开',
|
||||
@@ -26,7 +26,7 @@ export default {
|
||||
'model.deploy.sort': '排序',
|
||||
'model.deploy.search.placeholder': '按 <kbd>/</kbd> 开始搜索模型',
|
||||
'model.form.ollamatips':
|
||||
'提示:以下为 ZStack AIOS 预设的 Ollama 模型,请选择你想要的模型或者直接在右侧表单 【{name}】 输入框中输入你要部署的模型。',
|
||||
'提示:以下为 GPUStack 预设的 Ollama 模型,请选择你想要的模型或者直接在右侧表单 【{name}】 输入框中输入你要部署的模型。',
|
||||
'models.sort.name': '名称',
|
||||
'models.sort.size': '大小',
|
||||
'models.sort.likes': '点赞量',
|
||||
@@ -86,7 +86,7 @@ export default {
|
||||
'models.form.filePath': '模型路径',
|
||||
'models.form.backendVersion': '后端版本',
|
||||
'models.form.backendVersion.tips':
|
||||
'固定以使用期望的 {backend} 版本 {version},在线环境会自动创建虚拟环境安装对应版本的 {backend}。在 ZStack AIOS 升级后也将保持固定的后端版本。{link}',
|
||||
'固定以使用期望的 {backend} 版本 {version},在线环境会自动创建虚拟环境安装对应版本的 {backend}。在 GPUStack 升级后也将保持固定的后端版本。{link}',
|
||||
'models.form.gpuselector': 'GPU 选择器',
|
||||
'models.form.backend.llamabox':
|
||||
'用于 GGUF 格式模型,支持 Linux, macOS 和 Windows。',
|
||||
@@ -262,7 +262,7 @@ export default {
|
||||
'models.form.backendVersions.tips': `如需使用更多版本,请前往{link}页面并编辑对应的后端以添加版本。`,
|
||||
'models.catalog.nogpus.tips': '所选集群中没有兼容该模型的 GPU。',
|
||||
'models.form.modelfile.notfound':
|
||||
'你指定的模型文件路径在 ZStack AIOS Server 节点上不存在。建议在 ZStack AIOS Server 节点和 ZStack AIOS 节点上使用相同的模型文件路径,这有助于 ZStack AIOS 做出更优的调度与决策。',
|
||||
'你指定的模型文件路径在 GPUStack Server 节点上不存在。建议在 GPUStack Server 节点和 GPUStack 节点上使用相同的模型文件路径,这有助于 GPUStack 做出更优的调度与决策。',
|
||||
'models.form.readyWorkers': '节点就绪',
|
||||
'models.form.maxContextLength': '最大上下文长度',
|
||||
'models.form.backend.helperText': '该社区后端暂未启用,部署后将自动启用',
|
||||
|
||||
@@ -63,6 +63,9 @@ export default {
|
||||
'noresult.gpuservice.storage.title': '暂无存储',
|
||||
'noresult.gpuservice.storage.subTitle': '尚未添加任何存储。',
|
||||
'noresult.gpuservice.storage.nofound': '未找到匹配的存储',
|
||||
'noresult.gpuservice.instanceType.title': '暂无实例类型',
|
||||
'noresult.gpuservice.instanceType.subTitle': '创建一个实例类型以开始使用',
|
||||
'noresult.gpuservice.instanceType.nofound': '未找到实例类型',
|
||||
'noresult.gpuservice.storageType.title': '暂无存储类型',
|
||||
'noresult.gpuservice.storageType.subTitle': '尚未添加任何存储类型。',
|
||||
'noresult.gpuservice.storageType.nofound': '未找到匹配的存储类型',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default {
|
||||
'organizations.upsell.title': '组织是企业版功能',
|
||||
'organizations.upsell.subtitle':
|
||||
'多租户可在团队间隔离用户、资源与配额。升级到 ZStack AIOS 企业版即可管理组织。',
|
||||
'多租户可在团队间隔离用户、资源与配额。升级到 GPUStack 企业版即可管理组织。',
|
||||
'organizations.upsell.featuresTitle': '企业版包含的能力',
|
||||
'organizations.upsell.feature.orgs': '创建组织来分组用户并隔离工作负载',
|
||||
'organizations.upsell.feature.members': '为每个组织管理成员与角色',
|
||||
|
||||
@@ -52,7 +52,7 @@ export default {
|
||||
'resources.worker.current.version': '当前版本为 {version}',
|
||||
'resources.worker.select.command': '选择一个标签生成命令并使用复制按钮复制',
|
||||
'resources.worker.driver.install':
|
||||
'在安装 ZStack AIOS 之前,请安装<a href="https://docs.gpustack.ai/latest/installation/installation-requirements/" target="_blank">所需的驱动程序和库</a>。',
|
||||
'在安装 GPUStack 之前,请安装<a href="https://docs.gpustack.ai/latest/installation/installation-requirements/" target="_blank">所需的驱动程序和库</a>。',
|
||||
'resources.worker.script.install': '脚本安装',
|
||||
'resources.worker.container.install': '容器安装(仅支持 Linux)',
|
||||
'resources.worker.cann.tips':
|
||||
@@ -87,7 +87,7 @@ export default {
|
||||
'粘贴 <span class="bold-text">Token</span>。',
|
||||
'resources.register.worker.step7':
|
||||
'点击<span class="bold-text">重启</span>,应用设置。',
|
||||
'resources.register.install.title': '在 {os} 上安装 ZStack AIOS',
|
||||
'resources.register.install.title': '在 {os} 上安装 GPUStack',
|
||||
'resources.register.download':
|
||||
'下载并安装<a href={url} target="_blank">安装包</a>,仅支持 {versions}。',
|
||||
'resource.register.maos.support': 'M 芯片,macOS 14+',
|
||||
@@ -106,7 +106,7 @@ export default {
|
||||
'进入维护模式后,节点将停止调度新的模型实例部署任务,正在运行的实例不会受到影响。',
|
||||
'resources.worker.noCluster.tips': '当前无可用集群,请先创建集群再添加节点。',
|
||||
'resources.metrics.details': '监控',
|
||||
'resoureces.worker.upgrade.tips': '请升级到与 ZStack AIOS Server 版本一致。',
|
||||
'resoureces.worker.upgrade.tips': '请升级到与 GPUStack Server 版本一致。',
|
||||
'resources.worker.version': '节点版本:{version}',
|
||||
'resources.server.version': 'Server 版本:{version}',
|
||||
'resources.worker.currentVersion': '当前版本:{version}',
|
||||
|
||||
@@ -37,12 +37,12 @@ export default {
|
||||
'users.password.confirm.empty': '请确认新密码',
|
||||
'users.password.confirm.error': '两次输入的密码不一致',
|
||||
'users.login.title': '登录',
|
||||
'users.version.islatest': 'ZStack AIOS {version} 已是最新版本',
|
||||
'users.version.update': 'ZStack AIOS {version} 版本可供更新',
|
||||
'users.version.islatest': 'GPUStack {version} 已是最新版本',
|
||||
'users.version.update': 'GPUStack {version} 版本可供更新',
|
||||
'users.settings.title': '用户设置',
|
||||
'users.status.activate': '启用账户',
|
||||
'users.status.deactivate': '停用账户',
|
||||
'users.status.inactiveAccount': '停用账户',
|
||||
'users.login.getInitialPassword':
|
||||
'在 ZStack AIOS Server 节点运行以下命令以获取初始管理员密码。'
|
||||
'在 GPUStack Server 节点运行以下命令以获取初始管理员密码。'
|
||||
};
|
||||
|
||||
@@ -23,6 +23,11 @@ interface NumberSelectionProps {
|
||||
labelExtra?: React.ReactNode;
|
||||
maxCount?: number;
|
||||
tips?: string;
|
||||
// Explicit preset tick values (e.g. [10,20,...,100] for percentage slicing).
|
||||
// Overrides the default 1..maxCount sequence.
|
||||
presetValues?: number[];
|
||||
// Force the free-input box to show regardless of max/maxCount.
|
||||
alwaysShowInput?: boolean;
|
||||
onChange?: (value: number) => void;
|
||||
}
|
||||
|
||||
@@ -39,17 +44,18 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
|
||||
className,
|
||||
maxCount = 8,
|
||||
tips,
|
||||
presetValues,
|
||||
alwaysShowInput,
|
||||
style,
|
||||
onChange
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const showCustomInput = max > maxCount;
|
||||
const presetItems = Array.from(
|
||||
{ length: Math.max(0, maxCount) },
|
||||
(_, i) => i + 1
|
||||
);
|
||||
if (min <= 0) {
|
||||
const showCustomInput = alwaysShowInput || max > maxCount;
|
||||
const presetItems =
|
||||
presetValues ??
|
||||
Array.from({ length: Math.max(0, maxCount) }, (_, i) => i + 1);
|
||||
if (!presetValues && min <= 0) {
|
||||
presetItems.unshift(0);
|
||||
}
|
||||
const items = presetItems;
|
||||
|
||||
@@ -39,9 +39,20 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
padding-right: 2px;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
white-space: nowrap;
|
||||
padding-block: 6px;
|
||||
width: 100%;
|
||||
|
||||
// Spread the label and its labelExtra (e.g. the whole/sliced Segmented)
|
||||
// to opposite ends of the row.
|
||||
:global(.label-text) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.contentWrapper {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ExtraContent } from '@/layouts/extraRender';
|
||||
import {
|
||||
PageContainer,
|
||||
RouteContext,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
useOverlayScroller,
|
||||
type HeaderSlotContextValue
|
||||
} from '@gpustack/core-ui';
|
||||
import { Divider } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
useCallback,
|
||||
@@ -118,6 +120,12 @@ export const PageContainerInner: React.FC<
|
||||
</div>
|
||||
<div className={pageBoxCss.right}>
|
||||
<div ref={setRightEl} className={pageBoxCss.rightSlot} />
|
||||
<Divider
|
||||
className={pageBoxCss.divider}
|
||||
orientation="vertical"
|
||||
style={{ margin: '0 16px' }}
|
||||
/>
|
||||
<ExtraContent />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -163,11 +163,7 @@ const PoolRows: React.FC<PoolRowsProps> = ({
|
||||
key={col.dataIndex || col.key}
|
||||
span={spanFor(col.dataIndex)}
|
||||
style={{
|
||||
color: 'var(--ant-color-text-secondary)',
|
||||
// CellContent shrinks to its content inside the flex
|
||||
// cell, so its own align class can't center it —
|
||||
// center at the cell level instead.
|
||||
justifyContent: col.align
|
||||
color: 'var(--ant-color-text-secondary)'
|
||||
}}
|
||||
>
|
||||
<CellContent
|
||||
|
||||
@@ -116,7 +116,6 @@ const usePoolsColumns = (
|
||||
dataIndex: 'replicas',
|
||||
span: 6,
|
||||
key: 'replicas',
|
||||
align: 'center',
|
||||
editable: {
|
||||
valueType: 'number',
|
||||
title: intl.formatMessage({ id: 'models.table.replicas.edit' })
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { request } from '@umijs/max';
|
||||
import { FlavorItem, FormData, ListItem } from '../config/types';
|
||||
|
||||
export const GPU_INSTANCE_TYPES_API = '/gpu-instance-types';
|
||||
|
||||
export const GPU_INSTANCE_TYPE_FLAVORS_API = '/gpu-instance-type-flavors';
|
||||
|
||||
// GET /gpu-instance-types?cluster_id — instance types defined on a cluster.
|
||||
export async function queryGPUInstanceTypes(
|
||||
params: { cluster_id: number },
|
||||
options?: any
|
||||
) {
|
||||
return request<{ items: ListItem[] }>(GPU_INSTANCE_TYPES_API, {
|
||||
method: 'GET',
|
||||
params,
|
||||
cancelToken: options?.token
|
||||
});
|
||||
}
|
||||
|
||||
// GET /gpu-instance-type-flavors?cluster_id — the hardware flavors a new
|
||||
// instance type can be based on.
|
||||
export async function queryGPUInstanceTypeFlavors(
|
||||
params: { cluster_id: number },
|
||||
options?: any
|
||||
) {
|
||||
return request<{ items: FlavorItem[] }>(GPU_INSTANCE_TYPE_FLAVORS_API, {
|
||||
method: 'GET',
|
||||
params,
|
||||
cancelToken: options?.token
|
||||
});
|
||||
}
|
||||
|
||||
// POST /gpu-instance-types?cluster_id (GPUInstanceTypeCreate).
|
||||
export async function createGPUInstanceType(params: {
|
||||
cluster_id: number;
|
||||
data: FormData;
|
||||
}) {
|
||||
return request<ListItem>(GPU_INSTANCE_TYPES_API, {
|
||||
method: 'POST',
|
||||
params: { cluster_id: params.cluster_id },
|
||||
data: params.data
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE /gpu-instance-types/{name}?cluster_id.
|
||||
export async function deleteGPUInstanceType(params: {
|
||||
name: string;
|
||||
cluster_id: number;
|
||||
}) {
|
||||
return request(`${GPU_INSTANCE_TYPES_API}/${params.name}`, {
|
||||
method: 'DELETE',
|
||||
params: { cluster_id: params.cluster_id }
|
||||
});
|
||||
}
|
||||
|
||||
// PUT /gpu-instance-types/{name}/activate?cluster_id — activate an instance type.
|
||||
export async function activateGPUInstanceType(params: {
|
||||
name: string;
|
||||
cluster_id: number;
|
||||
}) {
|
||||
return request(`${GPU_INSTANCE_TYPES_API}/${params.name}/activate`, {
|
||||
method: 'PUT',
|
||||
params: { cluster_id: params.cluster_id }
|
||||
});
|
||||
}
|
||||
|
||||
// PUT /gpu-instance-types/{name}/deactivate?cluster_id — deactivate an instance type.
|
||||
export async function deactivateGPUInstanceType(params: {
|
||||
name: string;
|
||||
cluster_id: number;
|
||||
}) {
|
||||
return request(`${GPU_INSTANCE_TYPES_API}/${params.name}/deactivate`, {
|
||||
method: 'PUT',
|
||||
params: { cluster_id: params.cluster_id }
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import useSubmitLock from '@/hooks/use-submit-lock';
|
||||
import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { message } from 'antd';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { FlavorItem, FormData } from '../config/types';
|
||||
import GPUServiceInstanceTypeForm from '../forms';
|
||||
import useQueryFlavors from '../services/use-query-flavors';
|
||||
|
||||
type AddInstanceTypeModalProps = {
|
||||
title: string;
|
||||
open: boolean;
|
||||
clusterId?: number;
|
||||
onOk: (values: FormData) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const AddInstanceTypeModal: React.FC<AddInstanceTypeModalProps> = ({
|
||||
title,
|
||||
open,
|
||||
clusterId,
|
||||
onOk,
|
||||
onCancel
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const form = useRef<any>(null);
|
||||
const { loading, guard, run, release } = useSubmitLock();
|
||||
const [selectedFlavor, setSelectedFlavor] = useState<FlavorItem | null>(null);
|
||||
const {
|
||||
dataList: flavorList,
|
||||
loading: flavorLoading,
|
||||
fetchFlavors
|
||||
} = useQueryFlavors();
|
||||
|
||||
// Fetch flavors when the drawer opens and auto-select the first one, so the
|
||||
// form's flavor-derived fields (group / acceleratable) are always set.
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setSelectedFlavor(null);
|
||||
return;
|
||||
}
|
||||
if (!clusterId) return;
|
||||
const load = async () => {
|
||||
const list = await fetchFlavors(clusterId);
|
||||
setSelectedFlavor(list?.[0] ?? null);
|
||||
};
|
||||
load();
|
||||
}, [open, clusterId]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!selectedFlavor) {
|
||||
message.warning(
|
||||
intl.formatMessage({ id: 'gpuservice.instanceType.flavor.required' })
|
||||
);
|
||||
return;
|
||||
}
|
||||
guard(() => form.current?.submit());
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.current?.resetFields();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const onFinish = async (values: FormData) => {
|
||||
await run(() => onOk({ ...values }));
|
||||
};
|
||||
|
||||
return (
|
||||
<GSDrawer
|
||||
title={title}
|
||||
open={open}
|
||||
onClose={handleCancel}
|
||||
destroyOnHidden
|
||||
closeIcon={false}
|
||||
mask={{ closable: false }}
|
||||
keyboard={false}
|
||||
styles={{
|
||||
wrapper: { width: 'min(600px, calc(100vw - 220px))' },
|
||||
body: { overflowY: 'hidden' }
|
||||
}}
|
||||
footer={false}
|
||||
>
|
||||
<ColumnWrapper
|
||||
styles={{ container: { paddingBlock: 0 } }}
|
||||
footer={
|
||||
<ModalFooter
|
||||
onOk={handleSubmit}
|
||||
onCancel={handleCancel}
|
||||
loading={loading}
|
||||
style={{
|
||||
padding: '16px 24px 8px',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end'
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<GPUServiceInstanceTypeForm
|
||||
ref={form}
|
||||
open={open}
|
||||
selectedFlavor={selectedFlavor}
|
||||
flavorList={flavorList}
|
||||
flavorLoading={flavorLoading}
|
||||
onFlavorChange={setSelectedFlavor}
|
||||
onFinish={onFinish}
|
||||
onFinishFailed={release}
|
||||
/>
|
||||
</ColumnWrapper>
|
||||
</GSDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddInstanceTypeModal;
|
||||
@@ -0,0 +1,118 @@
|
||||
import { AutoTooltip, ThemeTag } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Flex } from 'antd';
|
||||
import { formatMemoryDisplay } from '../../instances/config';
|
||||
import { manufactureColorMap } from '../../templates/config';
|
||||
import { formatManufacturer } from '../../utils';
|
||||
|
||||
// The subset of a flavor / instance-type display shape the flavor renderers
|
||||
// read. Flavor specs satisfy it directly (minus sliceable, which the API
|
||||
// removed from flavors); the management list builds it from spec.acceleratable
|
||||
// + status.detail, deriving sliceable from slicedDetail.
|
||||
interface FlavorSpecLike {
|
||||
manufacturer?: string | null;
|
||||
product?: string | null;
|
||||
memory?: string | null;
|
||||
sliceable?: boolean;
|
||||
acceleratable?: boolean;
|
||||
}
|
||||
|
||||
// A flavor's title mirrors the flavor card: a generic (no product, no/`generic`
|
||||
// manufacturer, non-acceleratable) flavor reads as "CPU-only".
|
||||
export const getFlavorTitle = (
|
||||
spec: FlavorSpecLike = {},
|
||||
fallbackName?: string | null
|
||||
) => {
|
||||
const manufacturer = spec.manufacturer || '';
|
||||
const isCpuOnly =
|
||||
!spec.acceleratable &&
|
||||
!spec.product &&
|
||||
(!manufacturer || manufacturer.toLowerCase() === 'generic');
|
||||
return isCpuOnly ? 'CPU-only' : spec.product || fallbackName || '-';
|
||||
};
|
||||
|
||||
// Secondary line, dot-separated: manufacturer · memory · sliceable. memory and
|
||||
// sliceable apply to accelerator (GPU) flavors only; sliceable stays a tag.
|
||||
// Returns null when a (generic) flavor has nothing to show.
|
||||
export const FlavorMeta: React.FC<{ spec?: FlavorSpecLike }> = ({
|
||||
spec = {}
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const manufacturer = spec.manufacturer || '';
|
||||
const color = manufactureColorMap[manufacturer] ?? 'purple';
|
||||
const memory = spec.acceleratable
|
||||
? formatMemoryDisplay(spec.memory ?? undefined)
|
||||
: '';
|
||||
|
||||
const pieces: React.ReactNode[] = [];
|
||||
if (manufacturer) {
|
||||
pieces.push(
|
||||
<ThemeTag
|
||||
key="vendor"
|
||||
color={color}
|
||||
style={{ fontWeight: 400, marginInlineEnd: 0 }}
|
||||
>
|
||||
{formatManufacturer(manufacturer)}
|
||||
</ThemeTag>
|
||||
);
|
||||
}
|
||||
if (memory) {
|
||||
pieces.push(<span key="memory">{memory}</span>);
|
||||
}
|
||||
if (!pieces.length) return null;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
align="center"
|
||||
gap={8}
|
||||
style={{
|
||||
minWidth: 0,
|
||||
color: 'var(--ant-color-text-tertiary)',
|
||||
fontSize: 12
|
||||
}}
|
||||
>
|
||||
{pieces.flatMap((piece, index) =>
|
||||
index === 0
|
||||
? [piece]
|
||||
: [
|
||||
<span
|
||||
key={`dot-${index}`}
|
||||
style={{ color: 'var(--ant-color-text-quaternary)' }}
|
||||
>
|
||||
·
|
||||
</span>,
|
||||
piece
|
||||
]
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
// Two-line flavor display: title on top, meta row below. Shared by the create
|
||||
// drawer's dropdown option and the management list's flavor cell.
|
||||
export const FlavorOption: React.FC<{
|
||||
spec?: FlavorSpecLike;
|
||||
fallbackName?: string | null;
|
||||
maxWidth?: number | string;
|
||||
}> = ({ spec = {}, fallbackName, maxWidth = '100%' }) => (
|
||||
<Flex vertical gap={4} style={{ minWidth: 0, padding: '2px 0' }}>
|
||||
<AutoTooltip ghost minWidth={20} maxWidth={maxWidth}>
|
||||
{getFlavorTitle(spec, fallbackName)}
|
||||
</AutoTooltip>
|
||||
<FlavorMeta spec={spec} />
|
||||
</Flex>
|
||||
);
|
||||
|
||||
// Single-line flavor display: title then meta inline. Used for the collapsed
|
||||
// selected value in the create drawer's Select.
|
||||
export const FlavorSelected: React.FC<{
|
||||
spec?: FlavorSpecLike;
|
||||
fallbackName?: string | null;
|
||||
}> = ({ spec = {}, fallbackName }) => (
|
||||
<Flex align="center" gap={8} style={{ minWidth: 0 }}>
|
||||
<AutoTooltip ghost minWidth={20} maxWidth={200}>
|
||||
{getFlavorTitle(spec, fallbackName)}
|
||||
</AutoTooltip>
|
||||
<FlavorMeta spec={spec} />
|
||||
</Flex>
|
||||
);
|
||||
@@ -0,0 +1,120 @@
|
||||
import { FileSkeletonRows } from '@/pages/llmodels/components/model-source/file-skeleton';
|
||||
import {
|
||||
AutoTooltip,
|
||||
IconFont,
|
||||
TemplateCard,
|
||||
ThemeTag
|
||||
} from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Empty, Flex, Spin, Typography } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { formatMemoryDisplay } from '../../instances/config';
|
||||
import { manufactureColorMap } from '../../templates/config';
|
||||
import { FlavorItem } from '../config/types';
|
||||
import styles from '../styles/instance-types.module.less';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface FlavorListProps {
|
||||
value?: string;
|
||||
dataList: FlavorItem[];
|
||||
loading?: boolean;
|
||||
onChange?: (item: FlavorItem) => void;
|
||||
}
|
||||
|
||||
const MetaItem: React.FC<{
|
||||
icon: string;
|
||||
label: string;
|
||||
value?: React.ReactNode;
|
||||
}> = ({ icon, label, value }) => {
|
||||
return (
|
||||
<span className={styles.metaLabel}>
|
||||
<IconFont className="icon" type={icon} />
|
||||
{label}: <span className={styles.metaValue}>{value ?? '-'}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const FlavorList: React.FC<FlavorListProps> = ({
|
||||
value,
|
||||
dataList,
|
||||
loading,
|
||||
onChange
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
if (!dataList.length) {
|
||||
if (loading) {
|
||||
return (
|
||||
<Spin spinning size="middle">
|
||||
<Flex vertical gap={16} style={{ minHeight: 200 }}>
|
||||
{_.times(6, (index: number) => (
|
||||
<FileSkeletonRows key={index} counts={2} itemHeight={96} />
|
||||
))}
|
||||
</Flex>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex vertical gap={16}>
|
||||
{dataList.map((item) => {
|
||||
const spec = item.spec || {};
|
||||
const manufacturer = spec.manufacturer || '';
|
||||
const color = manufactureColorMap[manufacturer] ?? 'purple';
|
||||
// A generic (no product, no/`generic` manufacturer) flavor is shown as
|
||||
// "CPU-only" instead of falling back to the raw flavor name.
|
||||
const isCpuOnly =
|
||||
!spec.acceleratable &&
|
||||
!spec.product &&
|
||||
(!manufacturer || manufacturer.toLowerCase() === 'generic');
|
||||
const title = isCpuOnly ? 'CPU-only' : spec.product || item.name || '-';
|
||||
return (
|
||||
<TemplateCard
|
||||
key={item.name}
|
||||
className={styles.flavorCard}
|
||||
clickable
|
||||
ghost
|
||||
hoverable
|
||||
active={value === item.name}
|
||||
onClick={() => onChange?.(item)}
|
||||
>
|
||||
<Flex vertical gap={12} style={{ width: '100%' }}>
|
||||
<Flex align="center" justify="space-between" gap={8}>
|
||||
<div style={{ minWidth: 0, fontWeight: 500 }}>
|
||||
<AutoTooltip ghost minWidth={20}>
|
||||
<Text>{title}</Text>
|
||||
</AutoTooltip>
|
||||
</div>
|
||||
{manufacturer && (
|
||||
<ThemeTag color={color} style={{ fontWeight: 400 }}>
|
||||
{manufacturer.toUpperCase()}
|
||||
</ThemeTag>
|
||||
)}
|
||||
</Flex>
|
||||
{/* Memory only applies to accelerator (GPU) flavors; a
|
||||
non-acceleratable (generic) flavor has none. (Sliceable is no
|
||||
longer a flavor field — it is observed per instance type on
|
||||
status.detail.slicedDetail.) */}
|
||||
{spec.acceleratable && (
|
||||
<Flex wrap gap={16}>
|
||||
<MetaItem
|
||||
icon="icon-ram-02"
|
||||
label={intl.formatMessage({
|
||||
id: 'gpuservice.instance.memory'
|
||||
})}
|
||||
value={formatMemoryDisplay(spec.memory ?? undefined) ?? '-'}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</TemplateCard>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default FlavorList;
|
||||
@@ -0,0 +1,151 @@
|
||||
import {
|
||||
AutoTooltip,
|
||||
DropdownActions,
|
||||
IconFont,
|
||||
StatusTag,
|
||||
TemplateCard,
|
||||
ThemeTag
|
||||
} from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { formatMemoryDisplay, isSliceableDetail } from '../../instances/config';
|
||||
import { manufactureColorMap } from '../../templates/config';
|
||||
import { ceilMilliToCore, parseQuantityToGi } from '../../utils';
|
||||
import {
|
||||
InstanceTypePhaseLabelMap,
|
||||
status as phaseStatusMap,
|
||||
rowActionList
|
||||
} from '../config';
|
||||
import { ListItem } from '../config/types';
|
||||
import styles from '../styles/instance-types.module.less';
|
||||
|
||||
interface InstanceTypeCardProps {
|
||||
data: ListItem;
|
||||
onDelete?: (record: ListItem) => void;
|
||||
}
|
||||
|
||||
const InstanceTypeCard: React.FC<InstanceTypeCardProps> = ({
|
||||
data,
|
||||
onDelete
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const spec = data.spec || {};
|
||||
// Observed hardware (manufacturer / memory / sliced capability, …) comes
|
||||
// from status.detail and may be absent until the operator backfills status.
|
||||
const detail = data.status?.detail || {};
|
||||
const unit = spec.unitResources || {};
|
||||
const phase = data.status?.phase || '';
|
||||
const manufacturer = detail.manufacturer || '';
|
||||
const manufacturerColor = manufactureColorMap[manufacturer] ?? 'purple';
|
||||
const sliceable = isSliceableDetail(detail.slicedDetail);
|
||||
|
||||
const memoryText = formatMemoryDisplay(detail.memory ?? undefined);
|
||||
|
||||
// Base resources, formatted into a single "·"-separated line. Falsy parts
|
||||
// (e.g. a CPU-only type without VRAM) drop out rather than showing "-".
|
||||
const cpuCores = ceilMilliToCore(unit.cpu ?? null)?.cores;
|
||||
const ramGi = parseQuantityToGi(unit.ram ?? null)?.value;
|
||||
const storageGi = parseQuantityToGi(spec.localStorage ?? null)?.value;
|
||||
const osLabel = _.capitalize(spec.os || '');
|
||||
const archLabel = _.toUpper(spec.arch || '');
|
||||
const storageWord = intl.formatMessage({
|
||||
id: 'gpuservice.instanceType.localStorage'
|
||||
});
|
||||
const footerParts = [
|
||||
cpuCores != null ? `${cpuCores} vCPU` : null,
|
||||
ramGi != null ? `${ramGi} GiB RAM` : null,
|
||||
storageGi != null ? `${storageGi} GiB ${storageWord}` : null,
|
||||
osLabel ? `${osLabel}${archLabel ? ` (${archLabel})` : ''}` : null
|
||||
].filter(Boolean) as string[];
|
||||
|
||||
const handleAction = (item: any) => {
|
||||
if (item.key === 'delete') {
|
||||
onDelete?.(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TemplateCard
|
||||
className={styles.listCard}
|
||||
clickable={false}
|
||||
hoverable
|
||||
ghost
|
||||
header={
|
||||
<div className={styles.header}>
|
||||
<span className={styles.product}>
|
||||
<AutoTooltip ghost minWidth={20}>
|
||||
{detail.product || data.name || '-'}
|
||||
</AutoTooltip>
|
||||
</span>
|
||||
<span className={styles.headerRight}>
|
||||
<span onClick={(e) => e.stopPropagation()}>
|
||||
<DropdownActions
|
||||
menu={{ items: rowActionList, onClick: handleAction }}
|
||||
>
|
||||
<Button
|
||||
icon={<IconFont type="icon-more" />}
|
||||
size="small"
|
||||
type="text"
|
||||
/>
|
||||
</DropdownActions>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={styles.card}>
|
||||
<div className={styles.hero}>
|
||||
<span className={styles.name}>
|
||||
<AutoTooltip ghost minWidth={20}>
|
||||
{data.name || '-'}
|
||||
</AutoTooltip>
|
||||
</span>
|
||||
<span className={styles.memory}>{memoryText || '—'}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.subline}>
|
||||
{manufacturer && (
|
||||
<ThemeTag color={manufacturerColor} style={{ fontWeight: 400 }}>
|
||||
{manufacturer.toUpperCase()}
|
||||
</ThemeTag>
|
||||
)}
|
||||
{phase ? (
|
||||
<StatusTag
|
||||
statusValue={{
|
||||
status: phaseStatusMap[phase],
|
||||
text: InstanceTypePhaseLabelMap[phase] || phase,
|
||||
message: data.status?.phaseMessage || ''
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{detail.clockSpeed ? <span>{detail.clockSpeed}</span> : null}
|
||||
<span
|
||||
className={`${styles.tag} ${
|
||||
sliceable ? styles.tagSliceable : styles.tagPlain
|
||||
}`}
|
||||
>
|
||||
{sliceable
|
||||
? intl.formatMessage({ id: 'gpuservice.instance.sliceable' })
|
||||
: intl.formatMessage({
|
||||
id: 'gpuservice.instanceType.notSliceable'
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.divider} />
|
||||
|
||||
<div className={styles.footer}>
|
||||
{footerParts.map((part, index) => (
|
||||
<span key={part}>
|
||||
{index > 0 && <span className={styles.dotSep}>·</span>}
|
||||
{part}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</TemplateCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstanceTypeCard;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ResizeContainer } from '@gpustack/core-ui';
|
||||
import { Spin } from 'antd';
|
||||
import { ListItem } from '../config/types';
|
||||
import InstanceTypeCard from './instance-type-card';
|
||||
|
||||
interface InstanceTypeListProps {
|
||||
dataList: ListItem[];
|
||||
loading: boolean;
|
||||
onDelete?: (record: ListItem) => void;
|
||||
}
|
||||
|
||||
const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
|
||||
dataList,
|
||||
loading,
|
||||
onDelete
|
||||
}) => {
|
||||
return (
|
||||
<Spin spinning={loading} size="middle">
|
||||
<ResizeContainer
|
||||
defaultSpan={8}
|
||||
resizable
|
||||
dataList={dataList}
|
||||
renderItem={(item: ListItem) => (
|
||||
<InstanceTypeCard data={item} onDelete={onDelete} />
|
||||
)}
|
||||
/>
|
||||
</Spin>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstanceTypeList;
|
||||
@@ -0,0 +1,47 @@
|
||||
import { StatusMaps } from '@/config';
|
||||
import { StatusType } from '@/config/types';
|
||||
import { icons } from '@gpustack/core-ui';
|
||||
|
||||
// os is fixed to lowercase "linux" on the wire; the form only ever shows Linux.
|
||||
export const GPU_INSTANCE_TYPE_OS = 'linux';
|
||||
|
||||
export const InstanceTypePhaseValueMap = {
|
||||
Active: 'Active',
|
||||
Inactive: 'Inactive',
|
||||
Draining: 'Draining'
|
||||
};
|
||||
|
||||
export const InstanceTypePhaseLabelMap: Record<string, string> = {
|
||||
[InstanceTypePhaseValueMap.Active]: 'Active',
|
||||
[InstanceTypePhaseValueMap.Inactive]: 'Inactive',
|
||||
[InstanceTypePhaseValueMap.Draining]: 'Draining'
|
||||
};
|
||||
|
||||
export const status: Record<string, StatusType> = {
|
||||
[InstanceTypePhaseValueMap.Active]: StatusMaps.success,
|
||||
[InstanceTypePhaseValueMap.Inactive]: StatusMaps.inactive,
|
||||
[InstanceTypePhaseValueMap.Draining]: StatusMaps.transitioning
|
||||
};
|
||||
|
||||
export const ArchOptions = [
|
||||
{ label: 'AMD64', value: 'amd64' },
|
||||
{ label: 'ARM64', value: 'arm64' }
|
||||
];
|
||||
|
||||
// ``icon`` is narrowed to ``any`` so the inferred type doesn't reach into
|
||||
// the antd icon component's internal path.
|
||||
export const rowActionList: Array<{
|
||||
label: string;
|
||||
key: string;
|
||||
locale: boolean;
|
||||
icon: any;
|
||||
danger?: boolean;
|
||||
}> = [
|
||||
{
|
||||
label: 'common.button.delete',
|
||||
key: 'delete',
|
||||
locale: true,
|
||||
icon: icons.DeleteOutlined,
|
||||
danger: true
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
InstanceTypeDetail,
|
||||
InstanceTypeResource
|
||||
} from '../../instances/config/types';
|
||||
|
||||
export interface UnitResources {
|
||||
cpu?: string | null;
|
||||
ram?: string | null;
|
||||
}
|
||||
|
||||
// spec carries user-defined fields only; observed hardware (manufacturer,
|
||||
// memory, sliced capability, …) lives on status.detail.
|
||||
export interface InstanceTypeSpec {
|
||||
displayName?: string | null;
|
||||
os?: string | null;
|
||||
arch?: string | null;
|
||||
acceleratable?: boolean;
|
||||
acceleratorGroup?: string | null;
|
||||
generalGroup?: string | null;
|
||||
unitResources?: UnitResources | null;
|
||||
localStorage?: string | null;
|
||||
}
|
||||
|
||||
export interface InstanceTypeStatus {
|
||||
// Observed hardware descriptor; absent until the operator backfills status.
|
||||
detail?: InstanceTypeDetail | null;
|
||||
phase?: string | null;
|
||||
phaseMessage?: string | null;
|
||||
// Per-mode resource accounting ({onceMaxRequest, remaining, capacity}).
|
||||
accelerator?: InstanceTypeResource | null;
|
||||
acceleratorShared?: InstanceTypeResource | null;
|
||||
acceleratorSliced?: InstanceTypeResource | null;
|
||||
cpu?: InstanceTypeResource | null;
|
||||
}
|
||||
|
||||
// Row shape for the management list (GET /gpu-instance-types).
|
||||
export interface ListItem {
|
||||
name: string;
|
||||
spec: InstanceTypeSpec;
|
||||
status?: InstanceTypeStatus;
|
||||
}
|
||||
|
||||
// Selectable flavor shown in the create drawer's first column
|
||||
// (GET /gpu-instance-type-flavors). Its acceleratorGroup / generalGroup /
|
||||
// acceleratable are copied into the created instance type.
|
||||
export interface FlavorItem {
|
||||
name: string;
|
||||
spec: {
|
||||
manufacturer?: string | null;
|
||||
product?: string | null;
|
||||
family?: string | null;
|
||||
memory?: string | null;
|
||||
cores?: string | null;
|
||||
acceleratable?: boolean;
|
||||
acceleratorGroup?: string | null;
|
||||
generalGroup?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
// Body for POST /gpu-instance-types (GPUInstanceTypeCreate).
|
||||
export interface FormData {
|
||||
name: string;
|
||||
spec: {
|
||||
displayName?: string | null;
|
||||
acceleratorGroup?: string | null;
|
||||
generalGroup?: string | null;
|
||||
acceleratable?: boolean;
|
||||
os: string;
|
||||
arch?: string | null;
|
||||
unitResources?: UnitResources;
|
||||
localStorage?: string | null;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
import { validateLabelNameRegxFor63 } from '@/config';
|
||||
import {
|
||||
Input as CInput,
|
||||
InputNumber,
|
||||
Select as SealSelect,
|
||||
useAppUtils
|
||||
} from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form } from 'antd';
|
||||
import { forwardRef, useEffect, useImperativeHandle } from 'react';
|
||||
import {
|
||||
FlavorOption,
|
||||
FlavorSelected,
|
||||
getFlavorTitle
|
||||
} from '../components/flavor-display';
|
||||
import { ArchOptions, GPU_INSTANCE_TYPE_OS } from '../config';
|
||||
import { FlavorItem, FormData } from '../config/types';
|
||||
import styles from '../styles/instance-types.module.less';
|
||||
|
||||
// RAM / storage are entered as a plain number in GB but stored/submitted as a
|
||||
// "Gi" quantity string. These drive the FormItem's submit (`normalize`) and
|
||||
// display (`getValueProps`) conversions.
|
||||
const giNormalize = (value?: number | string | null) =>
|
||||
value ? `${value}Gi` : undefined;
|
||||
const giValueProps = (value?: string | null) => ({
|
||||
value: value ? String(value).replace(/Gi$/i, '') : ''
|
||||
});
|
||||
|
||||
interface InstanceTypeFormProps {
|
||||
ref?: any;
|
||||
open: boolean;
|
||||
// The flavor picked from the flavor Select. Its acceleratorGroup /
|
||||
// generalGroup / acceleratable are copied into the created instance type.
|
||||
selectedFlavor?: FlavorItem | null;
|
||||
flavorList: FlavorItem[];
|
||||
flavorLoading?: boolean;
|
||||
onFlavorChange: (flavor: FlavorItem | null) => void;
|
||||
onFinish: (values: FormData) => Promise<void>;
|
||||
onFinishFailed?: (errorInfo: any) => void;
|
||||
}
|
||||
|
||||
const GPUServiceInstanceTypeForm: React.FC<InstanceTypeFormProps> = forwardRef(
|
||||
(props, ref) => {
|
||||
const {
|
||||
open,
|
||||
selectedFlavor,
|
||||
flavorList,
|
||||
flavorLoading,
|
||||
onFlavorChange,
|
||||
onFinish,
|
||||
onFinishFailed
|
||||
} = props;
|
||||
const intl = useIntl();
|
||||
const { getRuleMessage } = useAppUtils();
|
||||
const [form] = Form.useForm<FormData>();
|
||||
|
||||
// A non-acceleratable (generic) flavor has no per-GPU concept, so unit CPU
|
||||
// is fixed to 1 and the field is disabled.
|
||||
const acceleratable = !!selectedFlavor?.spec?.acceleratable;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
form.resetFields();
|
||||
return;
|
||||
}
|
||||
form.setFieldsValue({
|
||||
spec: {
|
||||
arch: ArchOptions[0].value
|
||||
}
|
||||
} as any);
|
||||
}, [open, form]);
|
||||
|
||||
// Force unit CPU to 1 whenever the picked flavor is not acceleratable.
|
||||
useEffect(() => {
|
||||
if (!open || acceleratable) return;
|
||||
form.setFieldValue(['spec', 'unitResources', 'cpu'], 1);
|
||||
}, [open, acceleratable, form]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
submit: () => {
|
||||
form.submit();
|
||||
},
|
||||
resetFields: () => {
|
||||
form.resetFields();
|
||||
}
|
||||
}));
|
||||
|
||||
// Split flavors into two groups: CPU compute (generic) and GPU compute
|
||||
// (accelerator). Groups render as labeled sections in the dropdown.
|
||||
const toFlavorOption = (flavor: FlavorItem) => ({
|
||||
value: flavor.name,
|
||||
label: getFlavorTitle(flavor.spec, flavor.name),
|
||||
flavor
|
||||
});
|
||||
const cpuFlavors = flavorList.filter(
|
||||
(flavor) => !flavor.spec?.acceleratable
|
||||
);
|
||||
const gpuFlavors = flavorList.filter(
|
||||
(flavor) => flavor.spec?.acceleratable
|
||||
);
|
||||
const flavorOptions = [
|
||||
cpuFlavors.length && {
|
||||
label: intl.formatMessage({
|
||||
id: 'gpuservice.instanceType.flavor.cpuGroup'
|
||||
}),
|
||||
title: 'cpu',
|
||||
options: cpuFlavors.map(toFlavorOption)
|
||||
},
|
||||
gpuFlavors.length && {
|
||||
label: intl.formatMessage({
|
||||
id: 'gpuservice.instanceType.flavor.gpuGroup'
|
||||
}),
|
||||
title: 'gpu',
|
||||
options: gpuFlavors.map(toFlavorOption)
|
||||
}
|
||||
].filter(Boolean) as any;
|
||||
|
||||
const handleFinish = async (values: FormData) => {
|
||||
// The hardware group / acceleratable flags are not user-editable; they
|
||||
// come from the chosen flavor. os is fixed to lowercase "linux". ram /
|
||||
// localStorage already carry the "Gi" suffix from the FormItem normalize.
|
||||
const cpu = values.spec?.unitResources?.cpu;
|
||||
await onFinish({
|
||||
name: values.name,
|
||||
spec: {
|
||||
displayName: values.spec?.displayName?.trim() || null,
|
||||
acceleratorGroup: selectedFlavor?.spec?.acceleratorGroup ?? null,
|
||||
generalGroup: selectedFlavor?.spec?.generalGroup ?? null,
|
||||
acceleratable: selectedFlavor?.spec?.acceleratable ?? false,
|
||||
os: GPU_INSTANCE_TYPE_OS,
|
||||
arch: values.spec?.arch ?? null,
|
||||
unitResources: {
|
||||
cpu: cpu != null && cpu !== '' ? String(cpu) : null,
|
||||
ram: values.spec?.unitResources?.ram ?? null
|
||||
},
|
||||
localStorage: values.spec?.localStorage ?? null
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
name="gpuServiceInstanceTypeForm"
|
||||
form={form}
|
||||
onFinish={handleFinish}
|
||||
onFinishFailed={onFinishFailed}
|
||||
preserve={false}
|
||||
>
|
||||
<Form.Item<FormData>
|
||||
name="name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage('input', 'common.table.name')
|
||||
},
|
||||
{
|
||||
pattern: validateLabelNameRegxFor63,
|
||||
message: intl.formatMessage({ id: 'gpuservice.form.rule.name' })
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CInput.Input
|
||||
label={intl.formatMessage({ id: 'common.table.name' })}
|
||||
required
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FormData>
|
||||
name={['spec', 'displayName']}
|
||||
rules={[
|
||||
{
|
||||
max: 63,
|
||||
message: intl.formatMessage({
|
||||
id: 'gpuservice.template.displayName.max'
|
||||
})
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CInput.Input
|
||||
trim={false}
|
||||
label={intl.formatMessage({
|
||||
id: 'gpuservice.template.displayName'
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<SealSelect
|
||||
label={intl.formatMessage({ id: 'gpuservice.instanceType.flavor' })}
|
||||
required
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
classNames={{ popup: { root: styles.flavorDropdown } }}
|
||||
loading={flavorLoading}
|
||||
value={selectedFlavor?.name}
|
||||
options={flavorOptions}
|
||||
onChange={(val: string) =>
|
||||
onFlavorChange(
|
||||
flavorList.find((flavor) => flavor.name === val) ?? null
|
||||
)
|
||||
}
|
||||
optionRender={(option: any) => {
|
||||
const flavor: FlavorItem = option.data.flavor;
|
||||
return (
|
||||
<FlavorOption spec={flavor.spec} fallbackName={flavor.name} />
|
||||
);
|
||||
}}
|
||||
labelRender={({ value }) => {
|
||||
const flavor = flavorList.find((item) => item.name === value);
|
||||
return flavor ? (
|
||||
<FlavorSelected spec={flavor.spec} fallbackName={flavor.name} />
|
||||
) : (
|
||||
((value ?? '') as React.ReactNode)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<CInput.Input
|
||||
disabled
|
||||
value="Linux"
|
||||
label={intl.formatMessage({ id: 'gpuservice.instance.os' })}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FormData>
|
||||
name={['spec', 'arch']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage('select', 'gpuservice.instance.arch')
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealSelect
|
||||
label={intl.formatMessage({ id: 'gpuservice.instance.arch' })}
|
||||
required
|
||||
options={ArchOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FormData>
|
||||
name={['spec', 'unitResources', 'cpu']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage(
|
||||
'input',
|
||||
'gpuservice.instanceType.unitCpu'
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
disabled={!acceleratable}
|
||||
style={{ width: '100%' }}
|
||||
label={intl.formatMessage({
|
||||
id: 'gpuservice.instanceType.unitCpu'
|
||||
})}
|
||||
description={intl.formatMessage({
|
||||
id: 'gpuservice.instanceType.unitCpu.tip'
|
||||
})}
|
||||
required
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FormData>
|
||||
name={['spec', 'unitResources', 'ram']}
|
||||
normalize={giNormalize}
|
||||
getValueProps={giValueProps}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage(
|
||||
'input',
|
||||
'gpuservice.instanceType.unitRam'
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
style={{ width: '100%' }}
|
||||
label={`${intl.formatMessage({
|
||||
id: 'gpuservice.instanceType.unitRam'
|
||||
})} (GB)`}
|
||||
description={intl.formatMessage({
|
||||
id: 'gpuservice.instanceType.unitRam.tip'
|
||||
})}
|
||||
required
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FormData>
|
||||
name={['spec', 'localStorage']}
|
||||
normalize={giNormalize}
|
||||
getValueProps={giValueProps}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage(
|
||||
'input',
|
||||
'gpuservice.instanceType.localStorage'
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
style={{ width: '100%' }}
|
||||
label={`${intl.formatMessage({
|
||||
id: 'gpuservice.instanceType.localStorage'
|
||||
})} (GB)`}
|
||||
description={intl.formatMessage({
|
||||
id: 'gpuservice.instanceType.localStorage.tip'
|
||||
})}
|
||||
required
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default GPUServiceInstanceTypeForm;
|
||||
@@ -0,0 +1,37 @@
|
||||
import useBodyScroll from '@/hooks/use-body-scroll';
|
||||
import { useState } from 'react';
|
||||
|
||||
const useCreateInstanceTypeModal = () => {
|
||||
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
|
||||
const [openModalStatus, setOpenModalStatus] = useState<{
|
||||
open: boolean;
|
||||
title: string;
|
||||
}>({
|
||||
open: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
const openModal = (title: string) => {
|
||||
setOpenModalStatus({
|
||||
open: true,
|
||||
title
|
||||
});
|
||||
saveScrollHeight();
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setOpenModalStatus({
|
||||
open: false,
|
||||
title: ''
|
||||
});
|
||||
restoreScrollHeight();
|
||||
};
|
||||
|
||||
return {
|
||||
openInstanceTypeModalStatus: openModalStatus,
|
||||
openInstanceTypeModal: openModal,
|
||||
closeInstanceTypeModal: closeModal
|
||||
};
|
||||
};
|
||||
|
||||
export default useCreateInstanceTypeModal;
|
||||
@@ -0,0 +1,232 @@
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
AutoTooltip,
|
||||
DropdownButtons,
|
||||
icons,
|
||||
StatusTag
|
||||
} from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Space, Tooltip } from 'antd';
|
||||
import type { ColumnsType } from 'antd/lib/table';
|
||||
import _ from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { isSliceableDetail } from '../../instances/config';
|
||||
import { ceilMilliToCore, parseQuantityToGi } from '../../utils';
|
||||
import { FlavorOption } from '../components/flavor-display';
|
||||
import {
|
||||
InstanceTypePhaseLabelMap,
|
||||
InstanceTypePhaseValueMap,
|
||||
status as phaseStatusMap
|
||||
} from '../config';
|
||||
import { ListItem } from '../config/types';
|
||||
|
||||
interface ColumnsHookProps {
|
||||
handleSelect: (val: string, record: ListItem) => void;
|
||||
}
|
||||
|
||||
// DropdownButtons reads `locale` / `props` at runtime; its `items` prop is
|
||||
// typed as antd's MenuProps['items'], so cast the config to satisfy it. The
|
||||
// activate / deactivate action is chosen from the row's current phase: Active
|
||||
// types can be deactivated, Inactive ones activated (none while Preparing).
|
||||
const buildRowActions = (record: ListItem) => {
|
||||
const phase = record.status?.phase;
|
||||
const actions: any[] = [];
|
||||
if (phase === InstanceTypePhaseValueMap.Active) {
|
||||
actions.push({
|
||||
label: 'gpuservice.instanceType.deactivate',
|
||||
key: 'deactivate',
|
||||
locale: true,
|
||||
icon: icons.Disabled
|
||||
});
|
||||
} else if (phase === InstanceTypePhaseValueMap.Inactive) {
|
||||
actions.push({
|
||||
label: 'gpuservice.instanceType.activate',
|
||||
key: 'activate',
|
||||
locale: true,
|
||||
icon: icons.Charger
|
||||
});
|
||||
}
|
||||
actions.push({
|
||||
label: 'common.button.delete',
|
||||
key: 'delete',
|
||||
locale: true,
|
||||
icon: icons.DeleteOutlined,
|
||||
props: { danger: true }
|
||||
});
|
||||
return actions;
|
||||
};
|
||||
|
||||
// Column header with an info tooltip (used for the per-GPU resource columns).
|
||||
const TitleWithTip: React.FC<{ title: string; tip: string }> = ({
|
||||
title,
|
||||
tip
|
||||
}) => (
|
||||
<Space size={4}>
|
||||
<span>{title}</span>
|
||||
<Tooltip title={tip}>
|
||||
<QuestionCircleOutlined
|
||||
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
);
|
||||
|
||||
const useInstanceTypeColumns = ({
|
||||
handleSelect
|
||||
}: ColumnsHookProps): ColumnsType<ListItem> => {
|
||||
const intl = useIntl();
|
||||
|
||||
return useMemo(() => {
|
||||
return [
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.name' }),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
ellipsis: { showTitle: false },
|
||||
// Prefer the friendly display name, fall back to the resource name.
|
||||
render: (text: string, record: ListItem) => {
|
||||
const label = record.spec?.displayName || text;
|
||||
return (
|
||||
<AutoTooltip ghost minWidth={20} maxWidth={200} title={label}>
|
||||
<span className="text-primary">{label || '-'}</span>
|
||||
</AutoTooltip>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
// Flavor cell mirrors the create drawer's dropdown: product name on
|
||||
// top, manufacturer · memory · sliceable on the meta line below.
|
||||
// Observed hardware comes from status.detail (absent until the
|
||||
// operator backfills status); sliceable is derived from slicedDetail.
|
||||
title: intl.formatMessage({ id: 'gpuservice.instanceType.flavor' }),
|
||||
dataIndex: ['status', 'detail', 'product'],
|
||||
key: 'product',
|
||||
ellipsis: { showTitle: false },
|
||||
render: (_text: string, record: ListItem) => {
|
||||
const detail = record.status?.detail;
|
||||
return (
|
||||
<FlavorOption
|
||||
spec={{
|
||||
acceleratable: record.spec?.acceleratable,
|
||||
manufacturer: detail?.manufacturer,
|
||||
product: detail?.product,
|
||||
memory: detail?.memory,
|
||||
sliceable: isSliceableDetail(detail?.slicedDetail)
|
||||
}}
|
||||
fallbackName={record.name}
|
||||
maxWidth={200}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<TitleWithTip
|
||||
title={intl.formatMessage({
|
||||
id: 'gpuservice.instanceType.unitCpu'
|
||||
})}
|
||||
tip={intl.formatMessage({
|
||||
id: 'gpuservice.instanceType.unitCpu.tip'
|
||||
})}
|
||||
/>
|
||||
),
|
||||
dataIndex: ['spec', 'unitResources', 'cpu'],
|
||||
key: 'cpu',
|
||||
ellipsis: { showTitle: false },
|
||||
render: (value: string) => {
|
||||
const cores = ceilMilliToCore(value ?? null)?.cores;
|
||||
return cores != null ? `${cores} vCPU` : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<TitleWithTip
|
||||
title={intl.formatMessage({
|
||||
id: 'gpuservice.instanceType.unitRam'
|
||||
})}
|
||||
tip={intl.formatMessage({
|
||||
id: 'gpuservice.instanceType.unitRam.tip'
|
||||
})}
|
||||
/>
|
||||
),
|
||||
dataIndex: ['spec', 'unitResources', 'ram'],
|
||||
key: 'ram',
|
||||
ellipsis: { showTitle: false },
|
||||
render: (value: string) => {
|
||||
const gi = parseQuantityToGi(value ?? null)?.value;
|
||||
return gi != null ? `${gi} GB` : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<TitleWithTip
|
||||
title={intl.formatMessage({
|
||||
id: 'gpuservice.instanceType.localStorage'
|
||||
})}
|
||||
tip={intl.formatMessage({
|
||||
id: 'gpuservice.instanceType.localStorage.tip'
|
||||
})}
|
||||
/>
|
||||
),
|
||||
dataIndex: ['spec', 'localStorage'],
|
||||
key: 'localStorage',
|
||||
ellipsis: { showTitle: false },
|
||||
render: (value: string) => {
|
||||
const gi = parseQuantityToGi(value ?? null)?.value;
|
||||
return gi != null ? `${gi} GB` : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'gpuservice.instanceType.platform' }),
|
||||
key: 'os',
|
||||
ellipsis: { showTitle: false },
|
||||
render: (_text, record: ListItem) => {
|
||||
const os = _.capitalize(record.spec?.os || '');
|
||||
const arch = _.toUpper(record.spec?.arch || '');
|
||||
if (!os) return '-';
|
||||
return (
|
||||
<AutoTooltip
|
||||
ghost
|
||||
maxWidth={240}
|
||||
title={arch ? `${os}/${arch}` : os}
|
||||
>
|
||||
{arch ? `${os}/${arch}` : os}
|
||||
</AutoTooltip>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.status' }),
|
||||
dataIndex: ['status', 'phase'],
|
||||
key: 'status',
|
||||
ellipsis: { showTitle: false },
|
||||
render: (value: string, record: ListItem) =>
|
||||
value ? (
|
||||
<StatusTag
|
||||
statusValue={{
|
||||
status: phaseStatusMap[value],
|
||||
text: InstanceTypePhaseLabelMap[value] || value,
|
||||
message: record.status?.phaseMessage || ''
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
'-'
|
||||
)
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.operation' }),
|
||||
key: 'operation',
|
||||
dataIndex: 'operation',
|
||||
ellipsis: { showTitle: false },
|
||||
render: (_text, record: ListItem) => (
|
||||
<DropdownButtons
|
||||
items={buildRowActions(record)}
|
||||
onSelect={(val: string) => handleSelect(val, record)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
}, [handleSelect, intl]);
|
||||
};
|
||||
|
||||
export default useInstanceTypeColumns;
|
||||
@@ -0,0 +1,271 @@
|
||||
import { ProviderValueMap } from '@/pages/cluster-management/config';
|
||||
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
|
||||
import {
|
||||
BaseSelect,
|
||||
DeleteModal,
|
||||
FilterBar,
|
||||
IconFont,
|
||||
NoResult
|
||||
} from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { ConfigProvider, Divider, Flex, Table, message } from 'antd';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import PageBox, { HeaderLeft } from '../../_components/page-box';
|
||||
import {
|
||||
activateGPUInstanceType,
|
||||
deactivateGPUInstanceType,
|
||||
deleteGPUInstanceType
|
||||
} from './apis';
|
||||
import AddInstanceTypeModal from './components/add-instance-type-modal';
|
||||
import { FormData, ListItem } from './config/types';
|
||||
import useCreateInstanceTypeModal from './hooks/use-create-instance-type-modal';
|
||||
import useInstanceTypeColumns from './hooks/use-instance-type-columns';
|
||||
import useCreateInstanceType from './services/use-create-instance-type';
|
||||
import useQueryInstanceTypes from './services/use-query-instance-types';
|
||||
|
||||
const GPUServiceInstanceTypes: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const deleteModalRef = useRef<any>(null);
|
||||
const [clusterId, setClusterId] = useState<number | undefined>();
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
const {
|
||||
clusterList,
|
||||
fetchClusterList,
|
||||
loading: clusterLoading
|
||||
} = useQueryClusterList();
|
||||
const {
|
||||
dataList,
|
||||
loading: instanceTypesLoading,
|
||||
fetchInstanceTypes,
|
||||
startWatch
|
||||
} = useQueryInstanceTypes();
|
||||
const { fetchData: createInstanceType } = useCreateInstanceType();
|
||||
const {
|
||||
openInstanceTypeModalStatus,
|
||||
openInstanceTypeModal,
|
||||
closeInstanceTypeModal
|
||||
} = useCreateInstanceTypeModal();
|
||||
|
||||
// Only Kubernetes clusters own GPU instance types.
|
||||
const k8sClusters = useMemo(
|
||||
() => clusterList.filter((c) => c.provider === ProviderValueMap.Kubernetes),
|
||||
[clusterList]
|
||||
);
|
||||
|
||||
// Fetch the visible clusters, default to the first Kubernetes one, then load
|
||||
// its instance types. Action-driven: subsequent loads fire from the cluster
|
||||
// picker / refresh, never from an effect dependency.
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
const items = await fetchClusterList({ page: -1 });
|
||||
const firstK8s = (items || []).find(
|
||||
(c: any) => c.provider === ProviderValueMap.Kubernetes
|
||||
);
|
||||
if (firstK8s?.id != null) {
|
||||
setClusterId(firstK8s.id);
|
||||
await fetchInstanceTypes(firstK8s.id);
|
||||
startWatch(firstK8s.id);
|
||||
}
|
||||
setLoaded(true);
|
||||
};
|
||||
init();
|
||||
}, []);
|
||||
|
||||
const handleClusterChange = useMemoizedFn(async (value: number) => {
|
||||
setClusterId(value);
|
||||
setKeyword('');
|
||||
await fetchInstanceTypes(value);
|
||||
startWatch(value);
|
||||
});
|
||||
|
||||
const handleRefresh = useMemoizedFn(() => {
|
||||
if (clusterId != null) {
|
||||
fetchInstanceTypes(clusterId);
|
||||
}
|
||||
});
|
||||
|
||||
const handleNameChange = useMemoizedFn(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setKeyword(e.target.value);
|
||||
}
|
||||
);
|
||||
|
||||
const handleAdd = useMemoizedFn(() => {
|
||||
openInstanceTypeModal(
|
||||
intl.formatMessage({ id: 'gpuservice.instanceType.add' })
|
||||
);
|
||||
});
|
||||
|
||||
const handleModalOk = useMemoizedFn(async (data: FormData) => {
|
||||
if (clusterId == null) return;
|
||||
try {
|
||||
await createInstanceType({ cluster_id: clusterId, data });
|
||||
closeInstanceTypeModal();
|
||||
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||
fetchInstanceTypes(clusterId);
|
||||
} catch (error) {
|
||||
// handled by the request interceptor
|
||||
}
|
||||
});
|
||||
|
||||
const handleDelete = useMemoizedFn((record: ListItem) => {
|
||||
if (clusterId == null) return;
|
||||
deleteModalRef.current?.show({
|
||||
content: intl.formatMessage({ id: 'gpuservice.instanceType' }),
|
||||
operation: 'common.delete.single.confirm',
|
||||
name: record.name,
|
||||
async onOk() {
|
||||
await deleteGPUInstanceType({
|
||||
name: record.name,
|
||||
cluster_id: clusterId
|
||||
});
|
||||
fetchInstanceTypes(clusterId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const handleToggleActive = useMemoizedFn(
|
||||
(record: ListItem, activate: boolean) => {
|
||||
if (clusterId == null) return;
|
||||
const action = activate
|
||||
? activateGPUInstanceType
|
||||
: deactivateGPUInstanceType;
|
||||
deleteModalRef.current?.show({
|
||||
content: intl.formatMessage({ id: 'gpuservice.instanceType' }),
|
||||
title: activate
|
||||
? 'common.title.activate.confirm'
|
||||
: 'common.title.deactivate.confirm',
|
||||
okText: activate
|
||||
? 'gpuservice.instanceType.activate'
|
||||
: 'gpuservice.instanceType.deactivate',
|
||||
operation: activate
|
||||
? 'common.activate.single.confirm'
|
||||
: 'common.deactivate.single.confirm',
|
||||
name: record.spec?.displayName || record.name,
|
||||
async onOk() {
|
||||
await action({ name: record.name, cluster_id: clusterId });
|
||||
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||
fetchInstanceTypes(clusterId);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const handleSelect = useMemoizedFn((val: string, record: ListItem) => {
|
||||
if (val === 'delete') {
|
||||
handleDelete(record);
|
||||
} else if (val === 'activate') {
|
||||
handleToggleActive(record, true);
|
||||
} else if (val === 'deactivate') {
|
||||
handleToggleActive(record, false);
|
||||
}
|
||||
});
|
||||
|
||||
const columns = useInstanceTypeColumns({ handleSelect });
|
||||
|
||||
const filteredList = useMemo(() => {
|
||||
const trimmed = keyword.trim().toLowerCase();
|
||||
if (!trimmed) return dataList;
|
||||
return dataList.filter((item) => item.name.toLowerCase().includes(trimmed));
|
||||
}, [dataList, keyword]);
|
||||
|
||||
const hasK8sCluster = k8sClusters.length > 0;
|
||||
|
||||
const renderEmpty = (type?: string) => {
|
||||
if (type !== 'Table') return;
|
||||
return (
|
||||
<NoResult
|
||||
loading={instanceTypesLoading || clusterLoading}
|
||||
loadend={loaded}
|
||||
dataSource={filteredList}
|
||||
image={<IconFont type="icon-gpu1" />}
|
||||
filters={keyword ? { search: keyword } : undefined}
|
||||
noFoundText={intl.formatMessage({
|
||||
id: 'noresult.gpuservice.instanceType.nofound'
|
||||
})}
|
||||
title={intl.formatMessage({
|
||||
id: 'noresult.gpuservice.instanceType.title'
|
||||
})}
|
||||
subTitle={
|
||||
hasK8sCluster
|
||||
? intl.formatMessage({
|
||||
id: 'noresult.gpuservice.instanceType.subTitle'
|
||||
})
|
||||
: intl.formatMessage({ id: 'noresult.resources.k8sCluster' })
|
||||
}
|
||||
{...(hasK8sCluster
|
||||
? {
|
||||
onClick: handleAdd,
|
||||
buttonText: intl.formatMessage({ id: 'noresult.button.add' })
|
||||
}
|
||||
: {})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderLeft>
|
||||
<Flex align="center">
|
||||
<span className="font-600">
|
||||
{intl.formatMessage({ id: 'gpuservice.instance.types' })}
|
||||
</span>
|
||||
<Divider orientation="vertical" style={{ margin: '0 16px' }} />
|
||||
<BaseSelect
|
||||
size="small"
|
||||
variant="borderless"
|
||||
style={{ minWidth: 160 }}
|
||||
popupMatchSelectWidth={false}
|
||||
options={k8sClusters}
|
||||
value={clusterId}
|
||||
onChange={handleClusterChange}
|
||||
/>
|
||||
</Flex>
|
||||
</HeaderLeft>
|
||||
<PageBox>
|
||||
<FilterBar
|
||||
marginBottom={22}
|
||||
marginTop={30}
|
||||
showSelect={false}
|
||||
inputHolder={intl.formatMessage({
|
||||
id: 'gpuservice.instanceType.filter.name'
|
||||
})}
|
||||
buttonText={intl.formatMessage({
|
||||
id: 'gpuservice.instanceType.add'
|
||||
})}
|
||||
handleSearch={handleRefresh}
|
||||
handleClickPrimary={hasK8sCluster ? handleAdd : undefined}
|
||||
handleInputChange={handleNameChange}
|
||||
widths={{ input: 300 }}
|
||||
/>
|
||||
<ConfigProvider renderEmpty={renderEmpty}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={filteredList}
|
||||
scroll={{ x: 'max-content' }}
|
||||
className={'scroll-table'}
|
||||
loading={{
|
||||
spinning: instanceTypesLoading || clusterLoading,
|
||||
size: 'middle'
|
||||
}}
|
||||
rowKey={(record) => record.name}
|
||||
pagination={false}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
</PageBox>
|
||||
<AddInstanceTypeModal
|
||||
open={openInstanceTypeModalStatus.open}
|
||||
title={openInstanceTypeModalStatus.title}
|
||||
clusterId={clusterId}
|
||||
onCancel={closeInstanceTypeModal}
|
||||
onOk={handleModalOk}
|
||||
/>
|
||||
<DeleteModal ref={deleteModalRef} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GPUServiceInstanceTypes;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useQueryData } from '@gpustack/core-ui';
|
||||
import { useCallback } from 'react';
|
||||
import { createGPUInstanceType } from '../apis';
|
||||
import { FormData, ListItem } from '../config/types';
|
||||
|
||||
interface CreateInstanceTypeParams {
|
||||
cluster_id: number;
|
||||
data: FormData;
|
||||
}
|
||||
|
||||
export default function useCreateInstanceType() {
|
||||
const fetchDetail = useCallback(
|
||||
(params: CreateInstanceTypeParams) =>
|
||||
createGPUInstanceType({
|
||||
cluster_id: params.cluster_id,
|
||||
data: params.data
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
|
||||
ListItem,
|
||||
CreateInstanceTypeParams
|
||||
>({
|
||||
fetchDetail,
|
||||
key: 'createInstanceType'
|
||||
});
|
||||
|
||||
return {
|
||||
detailData,
|
||||
loading,
|
||||
cancelRequest,
|
||||
fetchData
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { createAxiosToken } from '@/hooks/use-chunk-request';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { CancelTokenSource } from 'axios';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { queryGPUInstanceTypeFlavors } from '../apis';
|
||||
import { FlavorItem } from '../config/types';
|
||||
|
||||
// Cluster-scoped flavors for the create drawer's first column. Fetched when
|
||||
// the drawer opens (and on cluster change), never via an effect dependency.
|
||||
export default function useQueryFlavors() {
|
||||
const tokenRef = useRef<CancelTokenSource | null>(null);
|
||||
const [dataList, setDataList] = useState<FlavorItem[]>([]);
|
||||
|
||||
const {
|
||||
runAsync: fetchFlavors,
|
||||
loading,
|
||||
cancel
|
||||
} = useRequest(
|
||||
async (clusterId: number) => {
|
||||
tokenRef.current?.cancel();
|
||||
tokenRef.current = createAxiosToken();
|
||||
const res = await queryGPUInstanceTypeFlavors(
|
||||
{ cluster_id: clusterId },
|
||||
{ token: tokenRef.current.token }
|
||||
);
|
||||
const list = res?.items || [];
|
||||
setDataList(list);
|
||||
return list;
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
onError: (error: any) => {
|
||||
if (error?.message === 'CANCEL_PREVIOUS_REQUEST') return;
|
||||
setDataList([]);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const cancelRequest = () => {
|
||||
cancel();
|
||||
tokenRef.current?.cancel('CANCEL_PREVIOUS_REQUEST');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cancel();
|
||||
tokenRef.current?.cancel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
dataList,
|
||||
loading,
|
||||
fetchFlavors,
|
||||
cancelRequest,
|
||||
setDataList
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { WatchEventType } from '@/config';
|
||||
import useSetChunkRequest, {
|
||||
createAxiosToken
|
||||
} from '@/hooks/use-chunk-request';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { CancelTokenSource } from 'axios';
|
||||
import qs from 'query-string';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { GPU_INSTANCE_TYPES_API, queryGPUInstanceTypes } from '../apis';
|
||||
import { ListItem } from '../config/types';
|
||||
|
||||
// Merge a batch of watch events into the current name-keyed list. Instance
|
||||
// types have no numeric id, so we upsert / remove by `name` rather than reuse
|
||||
// the shared id-based chunked-list helper.
|
||||
const mergeWatchEvents = (current: ListItem[], events: any[]) => {
|
||||
let list = [...current];
|
||||
events.forEach((event: any) => {
|
||||
const collection: ListItem[] = event?.collection || [];
|
||||
if (event?.type === WatchEventType.DELETE) {
|
||||
const names = collection.map((item) => item.name);
|
||||
list = list.filter((item) => !names.includes(item.name));
|
||||
} else if (
|
||||
event?.type === WatchEventType.CREATE ||
|
||||
event?.type === WatchEventType.UPDATE
|
||||
) {
|
||||
collection.forEach((item) => {
|
||||
const index = list.findIndex((it) => it.name === item.name);
|
||||
if (index > -1) {
|
||||
list[index] = item;
|
||||
} else {
|
||||
list = [item, ...list];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return list;
|
||||
};
|
||||
|
||||
// Cluster-scoped instance types for the management list. Action-driven:
|
||||
// call fetchInstanceTypes(clusterId) from the cluster picker / refresh, not
|
||||
// via an effect dependency. startWatch(clusterId) opens a chunked watch that
|
||||
// keeps the list in sync with live create/update/delete events.
|
||||
export default function useQueryInstanceTypes() {
|
||||
const tokenRef = useRef<CancelTokenSource | null>(null);
|
||||
const chunkRequestRef = useRef<any>(null);
|
||||
const { setChunkRequest } = useSetChunkRequest();
|
||||
const [dataList, setDataList] = useState<ListItem[]>([]);
|
||||
|
||||
const {
|
||||
runAsync: fetchInstanceTypes,
|
||||
loading,
|
||||
cancel
|
||||
} = useRequest(
|
||||
async (clusterId: number) => {
|
||||
tokenRef.current?.cancel();
|
||||
tokenRef.current = createAxiosToken();
|
||||
const res = await queryGPUInstanceTypes(
|
||||
{ cluster_id: clusterId },
|
||||
{ token: tokenRef.current.token }
|
||||
);
|
||||
const list = res?.items || [];
|
||||
setDataList(list);
|
||||
return list;
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
onError: (error: any) => {
|
||||
// Ignore the synthetic cancel error from switching clusters quickly.
|
||||
if (error?.message === 'CANCEL_PREVIOUS_REQUEST') return;
|
||||
setDataList([]);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const cancelRequest = () => {
|
||||
cancel();
|
||||
tokenRef.current?.cancel('CANCEL_PREVIOUS_REQUEST');
|
||||
};
|
||||
|
||||
const stopWatch = () => {
|
||||
chunkRequestRef.current?.current?.cancel?.();
|
||||
};
|
||||
|
||||
const startWatch = (clusterId: number) => {
|
||||
stopWatch();
|
||||
chunkRequestRef.current = setChunkRequest({
|
||||
url: `${GPU_INSTANCE_TYPES_API}?${qs.stringify({
|
||||
cluster_id: clusterId
|
||||
})}`,
|
||||
handler: (events: any[]) => {
|
||||
setDataList((pre) => mergeWatchEvents(pre, events));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cancel();
|
||||
tokenRef.current?.cancel();
|
||||
stopWatch();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
dataList,
|
||||
loading,
|
||||
fetchInstanceTypes,
|
||||
cancelRequest,
|
||||
startWatch,
|
||||
stopWatch,
|
||||
setDataList
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// ============ create drawer (two-column: flavors | form) ============
|
||||
.container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.colWrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.formWrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.panelBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.stickyHead {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background-color: var(--ant-color-bg-elevated);
|
||||
}
|
||||
|
||||
.flavorCard {
|
||||
height: auto !important;
|
||||
min-height: 96px;
|
||||
}
|
||||
|
||||
// ============ flavor select dropdown ============
|
||||
.flavorDropdown {
|
||||
:global {
|
||||
// tighten the indent of grouped options
|
||||
.ant-select-item-option-grouped {
|
||||
padding-inline-start: 12px;
|
||||
}
|
||||
// divider between options
|
||||
.ant-select-item-option {
|
||||
border-block-end: 1px solid var(--ant-color-border-secondary);
|
||||
|
||||
&:last-child {
|
||||
border-block-end: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ list card (Linear-style, minimal) ============
|
||||
.listCard {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// --- level 1: identity + status + actions ---
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.name {
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
}
|
||||
|
||||
.headerRight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// --- level 2: hero (model + memory) ---
|
||||
.hero {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.product {
|
||||
min-width: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
|
||||
.memory {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
|
||||
.subline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
min-height: 22px;
|
||||
font-size: 13px;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 22px;
|
||||
padding: 0 8px;
|
||||
border-radius: var(--ant-border-radius-sm);
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tagSliceable {
|
||||
color: var(--ant-color-primary);
|
||||
background-color: var(--ant-color-primary-bg);
|
||||
}
|
||||
|
||||
.tagPlain {
|
||||
color: var(--ant-color-text-tertiary);
|
||||
background-color: var(--ant-color-fill-tertiary);
|
||||
}
|
||||
|
||||
// --- level 3: base resources ---
|
||||
.divider {
|
||||
height: 1px;
|
||||
margin: 16px 0;
|
||||
background-color: var(--ant-color-border-secondary);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 14px;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
}
|
||||
|
||||
.dotSep {
|
||||
margin: 0 8px;
|
||||
color: var(--ant-color-text-quaternary);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
|
||||
export const GPU_SERVICE_INSTANCES_API = '/gpu-instances';
|
||||
|
||||
export const GPU_SERVICE_INSTANCES_TYPE_API = '/gpu-instance-types';
|
||||
export const GPU_SERVICE_INSTANCES_TYPE_API = '/gpu-instance-types/aggregated';
|
||||
|
||||
// View logs / events still go through the K8s proxy until the /v2
|
||||
// /gpu-instances API exposes equivalents. clusterID and namespace come
|
||||
|
||||
@@ -5,23 +5,19 @@ import useUserDirectory from '@/pages/gpu-service/hooks/use-user-directory';
|
||||
import Separator from '@/pages/llmodels/components/separator';
|
||||
import { getGPUStackPlugin } from '@/plugins';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
AlertBlockInfo,
|
||||
ColumnWrapper,
|
||||
GSDrawer,
|
||||
ModalFooter
|
||||
} from '@gpustack/core-ui';
|
||||
import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui';
|
||||
import { useIntl, useModel } from '@umijs/max';
|
||||
import { Input, Typography } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ListItem as TemplateItem } from '../../templates/config/types';
|
||||
import useQueryTemplates from '../../templates/services/use-query-templates';
|
||||
import { InstanceStatusValueMap } from '../config';
|
||||
import { FormData, InstanceTypeItem, ListItem } from '../config/types';
|
||||
import GPUServiceInstanceForm from '../forms';
|
||||
import TemplateSelector, { TemplateGroup } from '../forms/template-selector';
|
||||
import useQueryInstanceTypes from '../services/use-query-instance-types';
|
||||
import styles from '../styles/instances.module.less';
|
||||
import { saveInstanceDataInDescription } from '../utils/instance-description';
|
||||
import InstanceTypeList from './instance-type-list';
|
||||
|
||||
type AddModalProps = {
|
||||
@@ -29,7 +25,6 @@ type AddModalProps = {
|
||||
action: PageActionType;
|
||||
open: boolean;
|
||||
width?: number | string;
|
||||
realAction?: string;
|
||||
clusterList?: Array<{
|
||||
label: string;
|
||||
value: number;
|
||||
@@ -83,8 +78,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
data,
|
||||
onCancel,
|
||||
width,
|
||||
clusterList = [],
|
||||
realAction
|
||||
clusterList = []
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { initialState } = useModel('@@initialState') || {};
|
||||
@@ -103,6 +97,12 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
manufacturer: undefined
|
||||
});
|
||||
const [templateId, setTemplateId] = useState<number | undefined>();
|
||||
// Re-selected instance type on a stopped-instance edit. Kept separate from
|
||||
// `instanceTypeSelection` (the create card selection) so the two flows don't
|
||||
// couple; starts empty each open (no default highlight).
|
||||
const [editSelectedType, setEditSelectedType] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [instanceKeyword, setInstanceKeyword] = useState('');
|
||||
const [templateKeyword, setTemplateKeyword] = useState('');
|
||||
const { loading, guard, run, release } = useSubmitLock();
|
||||
@@ -166,9 +166,18 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
);
|
||||
// const readonly = action === PageAction.VIEW;
|
||||
const readonly = false;
|
||||
const isRecreate = realAction === PageAction.CREATE;
|
||||
const showResourceSelectors = action === PageAction.CREATE || isRecreate;
|
||||
const shouldAutoSelectResource = action === PageAction.CREATE && !isRecreate;
|
||||
const showResourceSelectors = action === PageAction.CREATE;
|
||||
// Only a stopped instance can be re-typed on edit. It shows the instance-type
|
||||
// column (but not the template column) beside the form; the create card
|
||||
// columns render for CREATE.
|
||||
const isStoppedEdit =
|
||||
action === PageAction.EDIT &&
|
||||
data?.status?.phase === InstanceStatusValueMap.Stopped;
|
||||
const showInstanceTypeColumn = showResourceSelectors || isStoppedEdit;
|
||||
// Editing a non-stopped instance is restricted: only displayName and the
|
||||
// SSH public keys stay editable; the type / template / storage sections
|
||||
// render disabled. A stopped instance edits everything.
|
||||
const isRestrictedEdit = action === PageAction.EDIT && !isStoppedEdit;
|
||||
|
||||
const findTemplateByManufacturer = (
|
||||
manufacturer: string | undefined,
|
||||
@@ -179,24 +188,13 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const saveInstanceDataInDescription = (instanceType: InstanceTypeItem) => {
|
||||
return JSON.stringify({
|
||||
name: instanceType.name,
|
||||
spec: {
|
||||
..._.omit(instanceType.spec, ['cache', 'cpu']),
|
||||
cpu: _.pick(instanceType.spec?.cpu, [
|
||||
'manufacturer',
|
||||
'product',
|
||||
'family'
|
||||
])
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// GPU types carry their accelerator vendor; non-acceleratable (CPU) types
|
||||
// all map to the single 'cpu' bucket used to match templates.
|
||||
// GPU types carry their accelerator vendor on status.detail (observed — may
|
||||
// be absent until the operator backfills status); non-acceleratable (CPU)
|
||||
// types all map to the single 'cpu' bucket used to match templates.
|
||||
const manufacturerOf = (instanceType: InstanceTypeItem) =>
|
||||
instanceType.spec.acceleratable ? instanceType.spec?.manufacturer : 'cpu';
|
||||
instanceType.spec.acceleratable
|
||||
? (instanceType.status?.detail?.manufacturer ?? undefined)
|
||||
: 'cpu';
|
||||
|
||||
// apply the selection of instance type and template
|
||||
const applySelection = (
|
||||
@@ -263,43 +261,12 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const findAggregateOf = (
|
||||
candidateName: string | undefined,
|
||||
clusterId: number | null | undefined,
|
||||
instanceTypes: InstanceTypeItem[]
|
||||
): InstanceTypeItem | undefined => {
|
||||
if (!candidateName) return undefined;
|
||||
return instanceTypes.find((item) =>
|
||||
(item.status?.tiers ?? []).some((tier) =>
|
||||
(tier.candidates ?? []).some(
|
||||
(c) => c.name === candidateName && Number(c.cluster) === clusterId
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// initial for first
|
||||
const applyAutoSelection = (
|
||||
instanceTypes: InstanceTypeItem[],
|
||||
templates: TemplateItem[],
|
||||
orgId?: number | null
|
||||
) => {
|
||||
// On edit / view, surface the persisted selection in the card list.
|
||||
if (!shouldAutoSelectResource) {
|
||||
const aggregate = findAggregateOf(
|
||||
data?.spec?.type,
|
||||
data?.clusterId,
|
||||
instanceTypes
|
||||
);
|
||||
if (aggregate) {
|
||||
setInstanceTypeSelection({
|
||||
instanceType: aggregate.name,
|
||||
manufacturer: manufacturerOf(aggregate)
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Scope to clusters the chosen org owns (admin "All" view).
|
||||
const owned = filterTypesByOwner(instanceTypes, orgId);
|
||||
|
||||
@@ -359,6 +326,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
manufacturer: undefined
|
||||
});
|
||||
setTemplateId(undefined);
|
||||
setEditSelectedType(undefined);
|
||||
setInstanceKeyword('');
|
||||
setTemplateKeyword('');
|
||||
setScopeOrgId(undefined);
|
||||
@@ -367,8 +335,12 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
|
||||
if (action === PageAction.CREATE) {
|
||||
loadCreateResources();
|
||||
} else if (action === PageAction.EDIT) {
|
||||
// Edit has no card columns, but the change-type overlay still needs the
|
||||
// full instance-type list to re-type a stopped instance.
|
||||
fetchData({ page: -1 });
|
||||
}
|
||||
}, [open, shouldAutoSelectResource, action]);
|
||||
}, [open, action]);
|
||||
|
||||
// filter instance types (already scoped to the chosen org's clusters)
|
||||
const filteredInstanceTypes = ownedInstanceTypes.filter((item) =>
|
||||
@@ -506,6 +478,17 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
applySelection(item, template);
|
||||
};
|
||||
|
||||
// Stopped-edit re-type. Decoupled from applySelection (the create flow): it
|
||||
// only snapshots the type into `description` and applies it to the form — no
|
||||
// template selection or filtering.
|
||||
const handleEditInstanceTypeChange = (item: InstanceTypeItem) => {
|
||||
setEditSelectedType(item.name);
|
||||
form.current?.setFieldsValue({
|
||||
description: saveInstanceDataInDescription(item)
|
||||
});
|
||||
form.current?.applyInstanceType?.(item);
|
||||
};
|
||||
|
||||
const handleTemplateChange = (id: number, item: TemplateItem) => {
|
||||
setTemplateId(id);
|
||||
const formValues = form.current?.getFieldsValue();
|
||||
@@ -543,104 +526,108 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
footer={false}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
{showInstanceTypeColumn && (
|
||||
<div
|
||||
className={styles.colWrapper}
|
||||
// The 33% cap suits the 3-column create layout; in the 2-column
|
||||
// stopped-edit layout, split the space evenly with the form column.
|
||||
style={isStoppedEdit ? { flex: 1, maxWidth: 'none' } : undefined}
|
||||
>
|
||||
<ColumnWrapper styles={{ container: { paddingBlock: 0 } }}>
|
||||
<div className={styles.panelBody}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
backgroundColor: 'var(--ant-color-bg-elevated)'
|
||||
}}
|
||||
>
|
||||
<ColTitle style={{ paddingBottom: 0 }}>
|
||||
{intl.formatMessage({
|
||||
id: 'gpuservice.instance.types'
|
||||
})}
|
||||
</ColTitle>
|
||||
<Input
|
||||
allowClear
|
||||
prefix={<SearchOutlined className="text-tertiary" />}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'gpuservice.instance.search.type.placeholder'
|
||||
})}
|
||||
value={instanceKeyword}
|
||||
onChange={(e) => setInstanceKeyword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<InstanceTypeList
|
||||
// Edit (stopped) re-selection is decoupled from create's
|
||||
// card selection: separate highlight state + apply handler.
|
||||
value={
|
||||
isStoppedEdit
|
||||
? editSelectedType
|
||||
: instanceTypeSelection.instanceType
|
||||
}
|
||||
dataList={filteredInstanceTypes}
|
||||
loading={instanceTypesLoading}
|
||||
onChange={
|
||||
isStoppedEdit
|
||||
? handleEditInstanceTypeChange
|
||||
: handleInstanceTypeChange
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</ColumnWrapper>
|
||||
<Separator></Separator>
|
||||
</div>
|
||||
)}
|
||||
{showResourceSelectors && (
|
||||
<>
|
||||
<div className={styles.colWrapper}>
|
||||
<ColumnWrapper styles={{ container: { paddingBlock: 0 } }}>
|
||||
<div className={styles.panelBody}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
backgroundColor: 'var(--ant-color-bg-elevated)'
|
||||
}}
|
||||
>
|
||||
<ColTitle style={{ paddingBottom: 0 }}>
|
||||
{intl.formatMessage({
|
||||
id: 'gpuservice.instance.types'
|
||||
})}
|
||||
</ColTitle>
|
||||
<Input
|
||||
allowClear
|
||||
prefix={<SearchOutlined className="text-tertiary" />}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'gpuservice.instance.search.type.placeholder'
|
||||
})}
|
||||
value={instanceKeyword}
|
||||
onChange={(e) => setInstanceKeyword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<InstanceTypeList
|
||||
value={instanceTypeSelection.instanceType}
|
||||
dataList={filteredInstanceTypes}
|
||||
loading={instanceTypesLoading}
|
||||
onChange={handleInstanceTypeChange}
|
||||
<div className={styles.colWrapper}>
|
||||
<ColumnWrapper styles={{ container: { paddingBlock: 0 } }}>
|
||||
<div className={styles.panelBody}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
backgroundColor: 'var(--ant-color-bg-elevated)'
|
||||
}}
|
||||
>
|
||||
<ColTitle style={{ paddingBottom: 0 }}>
|
||||
{intl.formatMessage({
|
||||
id: 'gpuservice.instance.templates'
|
||||
})}
|
||||
</ColTitle>
|
||||
<Input
|
||||
allowClear
|
||||
prefix={<SearchOutlined className="text-tertiary" />}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'gpuservice.instance.search.template.placeholder'
|
||||
})}
|
||||
value={templateKeyword}
|
||||
onChange={(e) => setTemplateKeyword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</ColumnWrapper>
|
||||
<Separator></Separator>
|
||||
</div>
|
||||
<div className={styles.colWrapper}>
|
||||
<ColumnWrapper styles={{ container: { paddingBlock: 0 } }}>
|
||||
<div className={styles.panelBody}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
backgroundColor: 'var(--ant-color-bg-elevated)'
|
||||
}}
|
||||
>
|
||||
<ColTitle style={{ paddingBottom: 0 }}>
|
||||
{intl.formatMessage({
|
||||
id: 'gpuservice.instance.templates'
|
||||
})}
|
||||
</ColTitle>
|
||||
<Input
|
||||
allowClear
|
||||
prefix={<SearchOutlined className="text-tertiary" />}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'gpuservice.instance.search.template.placeholder'
|
||||
})}
|
||||
value={templateKeyword}
|
||||
onChange={(e) => setTemplateKeyword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<TemplateSelector
|
||||
value={templateId}
|
||||
loading={templateLoading || !initialized}
|
||||
groups={templateGroups}
|
||||
onChange={handleTemplateChange}
|
||||
/>
|
||||
</div>
|
||||
</ColumnWrapper>
|
||||
<Separator></Separator>
|
||||
</div>
|
||||
</>
|
||||
<TemplateSelector
|
||||
value={templateId}
|
||||
loading={templateLoading || !initialized}
|
||||
groups={templateGroups}
|
||||
onChange={handleTemplateChange}
|
||||
/>
|
||||
</div>
|
||||
</ColumnWrapper>
|
||||
<Separator></Separator>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.formWrapper}>
|
||||
<ColumnWrapper
|
||||
styles={{ container: { paddingBlock: 0 } }}
|
||||
footer={
|
||||
<>
|
||||
{isRecreate && open && (
|
||||
<div style={{ marginInline: 24, paddingTop: 8 }}>
|
||||
<AlertBlockInfo
|
||||
type="warning"
|
||||
contentStyle={{ paddingInline: 0 }}
|
||||
message={intl.formatMessage({
|
||||
id: 'gpuservice.instance.recreate.confirm.content'
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ModalFooter
|
||||
onOk={handleSubmit}
|
||||
onCancel={handleCancel}
|
||||
@@ -664,9 +651,9 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
<GPUServiceInstanceForm
|
||||
ref={form}
|
||||
action={action}
|
||||
realAction={realAction}
|
||||
currentData={data}
|
||||
disabled={readonly}
|
||||
restrictedEdit={isRestrictedEdit}
|
||||
onFinish={onFinish}
|
||||
onFinishFailed={release}
|
||||
onScopeChange={handleScopeChange}
|
||||
|
||||
@@ -5,10 +5,13 @@ import { Flex, Tag } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
import { manufactureColorMap } from '../../templates/config';
|
||||
import { formatManufacturer } from '../../utils';
|
||||
import { formatMemoryDisplay } from '../config';
|
||||
import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types';
|
||||
|
||||
const Vendors = ['intel'] as const;
|
||||
import {
|
||||
InstanceTypeItem as InstanceTypeItemModel,
|
||||
InstanceTypeSnapshotSpec
|
||||
} from '../config/types';
|
||||
import { buildInstanceTypeSnapshotSpec } from '../utils/instance-description';
|
||||
|
||||
const Title = styled.div`
|
||||
display: flex;
|
||||
@@ -55,10 +58,17 @@ const Meta = styled.div<{ $columns?: number }>`
|
||||
|
||||
interface InstanceTypeItemProps {
|
||||
item: InstanceTypeItemModel;
|
||||
action?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface MetadataSectionProps {
|
||||
spec: InstanceTypeItemModel['spec'];
|
||||
// The flat snapshot / display model — built from a live item with
|
||||
// buildInstanceTypeSnapshotSpec, or parsed back from a persisted
|
||||
// `description` snapshot (readonly edit card).
|
||||
spec: InstanceTypeSnapshotSpec;
|
||||
// status.onceMaxRequest.acceleratorSliced (max sliceable percentage). Shown
|
||||
// next to Max for sliceable types.
|
||||
slicedMaxPercentage?: number;
|
||||
}
|
||||
|
||||
const MetaItem: React.FC<{
|
||||
@@ -101,8 +111,14 @@ const CPUManufacturerTag: React.FC<{ manufacturer?: string }> = ({
|
||||
);
|
||||
};
|
||||
|
||||
function getInstanceDerived(item: InstanceTypeItemModel) {
|
||||
const spec = item.spec || {};
|
||||
// Derives the display fields from the flat snapshot spec (the UI document
|
||||
// format — built from a live item with buildInstanceTypeSnapshotSpec, or
|
||||
// parsed back from a persisted `description` snapshot). Observed hardware
|
||||
// (manufacturer / product / memory / cpu) originates from status.detail.
|
||||
function getInstanceDerived(
|
||||
spec: InstanceTypeSnapshotSpec = {},
|
||||
fallbackName?: string
|
||||
) {
|
||||
const acceleratable = spec.acceleratable;
|
||||
|
||||
const cpuManufacturer = acceleratable
|
||||
@@ -113,122 +129,113 @@ function getInstanceDerived(item: InstanceTypeItemModel) {
|
||||
acceleratable,
|
||||
isGPU: acceleratable,
|
||||
manufacturer: acceleratable ? spec.manufacturer || '' : 'cpu', // GPU manufacturer or 'cpu' for non-acceleratable types
|
||||
displayName: acceleratable ? spec.product || item.name : 'CPU Only',
|
||||
displayName: acceleratable
|
||||
? spec.displayName || spec.product || fallbackName
|
||||
: spec.displayName || 'CPU-only',
|
||||
ramUnit: spec.unitResourcesParsed?.ram?.value,
|
||||
os: _.capitalize(spec.os) || '',
|
||||
arch: spec.arch,
|
||||
cpuManufacturer: Vendors.includes(cpuManufacturer as any)
|
||||
? _.capitalize(cpuManufacturer)
|
||||
: _.toUpper(cpuManufacturer),
|
||||
cpuManufacturer: formatManufacturer(cpuManufacturer),
|
||||
cpuUnitCores: spec.unitResourcesParsed?.cpu?.cores
|
||||
};
|
||||
}
|
||||
|
||||
type MetaEntry = { icon: string; label?: string; value: React.ReactNode };
|
||||
|
||||
// All rows share a single grid so columns — and therefore icons — line up
|
||||
// vertically. Each item is 3 cells (icon/label/value); every item past the
|
||||
// first adds a leading dot cell, so a row of k items spans 4k-1 cells. A short
|
||||
// row is padded with a spanning spacer so the next row restarts at column 1.
|
||||
const renderMetaRow = (items: MetaEntry[], columns: number, rowKey: string) => {
|
||||
const cells = items.map((item, index) => (
|
||||
<MetaItem
|
||||
key={`${rowKey}-${item.icon}`}
|
||||
showDot={index > 0}
|
||||
icon={item.icon}
|
||||
label={item.label}
|
||||
value={item.value}
|
||||
/>
|
||||
));
|
||||
const remaining = columns - (4 * items.length - 1);
|
||||
if (remaining > 0) {
|
||||
cells.push(
|
||||
<span
|
||||
key={`${rowKey}-spacer`}
|
||||
style={{ gridColumn: `span ${remaining}` }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return cells;
|
||||
};
|
||||
|
||||
export const InstanceMetadataSection: React.FC<MetadataSectionProps> = ({
|
||||
spec
|
||||
spec,
|
||||
slicedMaxPercentage
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { ramUnit, cpuUnitCores, isGPU, os, arch } = getInstanceDerived({
|
||||
spec
|
||||
} as InstanceTypeItemModel);
|
||||
const { ramUnit, cpuUnitCores, isGPU, arch } = getInstanceDerived(spec);
|
||||
|
||||
// Sliceable types append a "Sliceable {n}%" cell to the second row.
|
||||
const showSliceable = !!spec.sliceable && (slicedMaxPercentage ?? 0) > 0;
|
||||
|
||||
const cpuItem: MetaEntry = {
|
||||
icon: 'icon-cpu',
|
||||
label: 'CPU',
|
||||
value: cpuUnitCores || '-'
|
||||
};
|
||||
const ramItem: MetaEntry = {
|
||||
icon: 'icon-ram-02',
|
||||
label: intl.formatMessage({ id: 'gpuservice.instance.ram' }),
|
||||
value: ramUnit ? `${ramUnit} GB` : '-'
|
||||
};
|
||||
const memoryItem: MetaEntry = {
|
||||
icon: 'icon-gpu1',
|
||||
label: intl.formatMessage({ id: 'gpuservice.instance.memory' }),
|
||||
value: formatMemoryDisplay(spec?.memory ?? undefined) ?? '-'
|
||||
};
|
||||
const archItem: MetaEntry = {
|
||||
icon: 'icon-cube',
|
||||
label: intl.formatMessage({ id: 'gpuservice.instance.arch' }),
|
||||
value: _.toUpper(arch) || '-'
|
||||
};
|
||||
const maxItem: MetaEntry = {
|
||||
icon: 'icon-database',
|
||||
label: intl.formatMessage({ id: 'common.max' }, { count: '' }),
|
||||
value: `${spec.maxComputeUnitCount || 0}`
|
||||
};
|
||||
const slicedItem: MetaEntry = {
|
||||
icon: 'icon-sliced',
|
||||
label: intl.formatMessage({ id: 'gpuservice.instance.sliceable' }),
|
||||
value: `${slicedMaxPercentage}%`
|
||||
};
|
||||
|
||||
// GPU: 3 items/row → 11 cols. CPU: 2 items/row → 7 cols.
|
||||
const columns = isGPU ? 11 : 7;
|
||||
const rows: MetaEntry[][] = isGPU
|
||||
? [
|
||||
[ramItem, memoryItem, cpuItem],
|
||||
showSliceable ? [archItem, maxItem, slicedItem] : [archItem, maxItem]
|
||||
]
|
||||
: [[ramItem], [archItem, maxItem]];
|
||||
|
||||
return (
|
||||
<Meta $columns={isGPU ? 11 : 7}>
|
||||
{isGPU && (
|
||||
<>
|
||||
{/* row 1: Memory | Max | RAM */}
|
||||
<MetaItem
|
||||
show={isGPU}
|
||||
showDot={false}
|
||||
icon="icon-gpu1"
|
||||
label={intl.formatMessage({ id: 'gpuservice.instance.memory' })}
|
||||
value={formatMemoryDisplay(spec?.memory ?? undefined) ?? '-'}
|
||||
/>
|
||||
<MetaItem
|
||||
showDot={true}
|
||||
icon="icon-ram-02"
|
||||
label={intl.formatMessage({ id: 'gpuservice.instance.ram' })}
|
||||
value={ramUnit ? `${ramUnit} GB` : '-'}
|
||||
/>
|
||||
<MetaItem
|
||||
icon="icon-database"
|
||||
label={intl.formatMessage(
|
||||
{
|
||||
id: 'common.max'
|
||||
},
|
||||
{ count: '' }
|
||||
)}
|
||||
value={`${spec.maxComputeUnitCount || 0}`}
|
||||
/>
|
||||
{/* row 2: OS | Arch | CPU */}
|
||||
<MetaItem
|
||||
showDot={false}
|
||||
icon="icon-server02"
|
||||
label={intl.formatMessage({ id: 'gpuservice.instance.os' })}
|
||||
value={os || '-'}
|
||||
/>
|
||||
<MetaItem
|
||||
icon="icon-cube"
|
||||
label={intl.formatMessage({ id: 'gpuservice.instance.arch' })}
|
||||
value={_.toUpper(arch) || '-'}
|
||||
/>
|
||||
<MetaItem
|
||||
show={isGPU}
|
||||
showDot={true}
|
||||
icon="icon-cpu"
|
||||
label="CPU"
|
||||
value={
|
||||
<Flex gap={4} align="center">
|
||||
<span>{cpuUnitCores || '-'}</span>
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isGPU && (
|
||||
<>
|
||||
{/* row 1: RAM | Max */}
|
||||
<MetaItem
|
||||
showDot={false}
|
||||
icon="icon-ram-02"
|
||||
label={intl.formatMessage({ id: 'gpuservice.instance.ram' })}
|
||||
value={ramUnit ? `${ramUnit} GB` : '-'}
|
||||
/>
|
||||
<MetaItem
|
||||
icon="icon-database"
|
||||
label={intl.formatMessage(
|
||||
{
|
||||
id: 'common.max'
|
||||
},
|
||||
{ count: '' }
|
||||
)}
|
||||
value={`${spec.maxComputeUnitCount || 0}`}
|
||||
/>
|
||||
{/* row 2: OS | Arch */}
|
||||
<MetaItem
|
||||
showDot={false}
|
||||
icon="icon-server02"
|
||||
label={intl.formatMessage({ id: 'gpuservice.instance.os' })}
|
||||
value={os || '-'}
|
||||
/>
|
||||
<MetaItem
|
||||
icon="icon-cube"
|
||||
label={intl.formatMessage({ id: 'gpuservice.instance.arch' })}
|
||||
value={_.toUpper(arch) || '-'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Meta $columns={columns}>
|
||||
{rows.map((row, index) => renderMetaRow(row, columns, `row-${index}`))}
|
||||
</Meta>
|
||||
);
|
||||
};
|
||||
|
||||
const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
|
||||
const specData = item.spec || {};
|
||||
const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({
|
||||
item,
|
||||
action
|
||||
}) => {
|
||||
// Fold the live (API-shaped) item into the flat display model: definition
|
||||
// fields from spec, observed hardware from status.detail.
|
||||
const specData = buildInstanceTypeSnapshotSpec(item);
|
||||
|
||||
const { acceleratable, manufacturer, displayName, cpuManufacturer } =
|
||||
getInstanceDerived(item);
|
||||
getInstanceDerived(specData, item.name);
|
||||
|
||||
const manufacturerColor = manufactureColorMap[manufacturer] ?? 'purple';
|
||||
const showManufacturerTag = acceleratable && !!manufacturer;
|
||||
@@ -260,7 +267,7 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
|
||||
disabled={false}
|
||||
style={{ fontWeight: 400 }}
|
||||
>
|
||||
{manufacturer?.toUpperCase()}
|
||||
{formatManufacturer(manufacturer)}
|
||||
</ThemeTag>
|
||||
)}
|
||||
{showCpuManufacturerTag && (
|
||||
@@ -276,9 +283,15 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
|
||||
name="InstanceTypeBillingBadge"
|
||||
context={{ instanceType: item }}
|
||||
/>
|
||||
{action && <div style={{ marginLeft: 8 }}>{action}</div>}
|
||||
</Flex>
|
||||
</Title>
|
||||
<InstanceMetadataSection spec={specData}></InstanceMetadataSection>
|
||||
<InstanceMetadataSection
|
||||
spec={specData}
|
||||
slicedMaxPercentage={
|
||||
Number(item.status?.onceMaxRequest?.acceleratorSliced) || 0
|
||||
}
|
||||
></InstanceMetadataSection>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TemplateCard } from '@gpustack/core-ui';
|
||||
import { Empty, Flex, Spin } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types';
|
||||
import styles from '../styles/instances.module.less';
|
||||
import InstanceTypeItem from './instance-type-item';
|
||||
|
||||
interface InstanceTypeListProps {
|
||||
@@ -50,10 +51,10 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
|
||||
return (
|
||||
<TemplateCard
|
||||
key={name}
|
||||
className={styles.instanceTypeCard}
|
||||
clickable
|
||||
ghost
|
||||
hoverable
|
||||
height={106}
|
||||
active={value === name}
|
||||
disabled={item.disabled}
|
||||
onClick={() => handleSelect(item)}
|
||||
|
||||
@@ -3,7 +3,15 @@ import { StatusType } from '@/config/types';
|
||||
import { IconFont, icons } from '@gpustack/core-ui';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { ListItem } from '../config/types';
|
||||
import { AcceleratorSlicedDetail, ListItem } from '../config/types';
|
||||
|
||||
// Whether a type can be sliced, per the API contract (replaces the removed
|
||||
// `spec.sliceable` boolean): logical (soft) slicing reports per-card capacity
|
||||
// or physical (e.g. MIG) profiles exist. Every level of slicedDetail may be
|
||||
// absent (exclude_none responses).
|
||||
export const isSliceableDetail = (detail?: AcceleratorSlicedDetail | null) =>
|
||||
(detail?.logical?.count ?? 0) > 0 ||
|
||||
(detail?.physical?.profiles?.length ?? 0) > 0;
|
||||
|
||||
export const InstanceStatusValueMap = {
|
||||
Scheduling: 'Scheduling',
|
||||
@@ -48,7 +56,7 @@ export const GPUStackFailedStatuses = [
|
||||
export const InstanceStatusLabelMap: Record<string, string> = {
|
||||
// === K8s Statuses ===
|
||||
...Object.fromEntries(K8SStatuses.map((status) => [status, status])),
|
||||
// === ZStack AIOS Statuses no logs and events===
|
||||
// === GPUStack Statuses no logs and events===
|
||||
[InstanceStatusValueMap.Deleting]: 'Deleting',
|
||||
[InstanceStatusValueMap.Stopping]: 'Stopping',
|
||||
[InstanceStatusValueMap.Stopped]: 'Stopped',
|
||||
@@ -251,7 +259,7 @@ const parseQuantity = (value?: string | null): number => {
|
||||
// Returns the slider max for the accelerator count: the largest
|
||||
// tier.onceMaxRequest.accelerator across all tiers (not from candidates).
|
||||
export const getAcceleratorMax = (
|
||||
tiers?: { onceMaxRequest: { accelerator?: string } }[] | null
|
||||
tiers?: { onceMaxRequest: { accelerator?: string | null } }[] | null
|
||||
) => {
|
||||
if (!tiers?.length) return 0;
|
||||
return tiers.reduce((acc, tier) => {
|
||||
@@ -262,32 +270,46 @@ export const getAcceleratorMax = (
|
||||
|
||||
// Picks the candidate (cluster + type name) that should fulfill a requested
|
||||
// accelerator count: the first candidate of the smallest tier whose
|
||||
// onceMaxRequest.accelerator is >= the requested count and whose cpu/ram/localStorage
|
||||
// remaining are all > 0.
|
||||
// onceMaxRequest.accelerator is >= the requested count. Only Active candidates
|
||||
// are eligible. Accelerated types are not gated on CPU remaining (only CPU-only
|
||||
// types are); in sliced mode the candidate's acceleratorSliced remaining must
|
||||
// also be > 0.
|
||||
export const pickCandidateForAccelerator = <
|
||||
C extends {
|
||||
cluster: string;
|
||||
name: string;
|
||||
phase?: string | null;
|
||||
cpu?: { remaining?: string | null } | null;
|
||||
ram?: { remaining?: string | null } | null;
|
||||
localStorage?: { remaining?: string | null } | null;
|
||||
acceleratorSliced?: { remaining?: string | null } | null;
|
||||
}
|
||||
>(
|
||||
tiers:
|
||||
| {
|
||||
onceMaxRequest: { accelerator?: string };
|
||||
onceMaxRequest: {
|
||||
accelerator?: string | null;
|
||||
acceleratorSliced?: string | null;
|
||||
};
|
||||
candidates?: C[] | null;
|
||||
}[]
|
||||
| undefined
|
||||
| null,
|
||||
{ count, acceleratable }: { count: number; acceleratable?: boolean }
|
||||
{
|
||||
count,
|
||||
acceleratable,
|
||||
sliced
|
||||
}: { count: number; acceleratable?: boolean; sliced?: boolean }
|
||||
): C | null => {
|
||||
if (!tiers?.length) return null;
|
||||
|
||||
const hasResources = (c: C) =>
|
||||
parseQuantity(c.cpu?.remaining) > 0 &&
|
||||
parseQuantity(c.ram?.remaining) > 0 &&
|
||||
parseQuantity(c.localStorage?.remaining) > 0;
|
||||
const hasResources = (c: C) => {
|
||||
// Only Active candidates can serve new instances.
|
||||
if (c.phase !== InstanceTypePhaseValueMap.Active) return false;
|
||||
// Accelerated types are not gated on CPU remaining; CPU-only types are.
|
||||
if (!acceleratable && parseQuantity(c.cpu?.remaining) <= 0) return false;
|
||||
if (sliced && parseQuantity(c.acceleratorSliced?.remaining) <= 0)
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const sorted = [...tiers].sort(
|
||||
(a, b) =>
|
||||
@@ -298,9 +320,14 @@ export const pickCandidateForAccelerator = <
|
||||
// count === 0 ? parseQuantity(tier.onceMaxRequest.accelerator) > count; this is CPU-only case.
|
||||
for (const tier of sorted) {
|
||||
const acceleratorCount = parseQuantity(tier.onceMaxRequest?.accelerator);
|
||||
const fits = acceleratable
|
||||
? acceleratorCount >= count
|
||||
: acceleratorCount === 0;
|
||||
// Sliced mode requests a fraction of a single card, so the tier's
|
||||
// whole-card accelerator count (0 for a slice-only type) can't gate it;
|
||||
// fit on the tier's sliced capacity instead.
|
||||
const fits = sliced
|
||||
? parseQuantity(tier.onceMaxRequest?.acceleratorSliced) > 0
|
||||
: acceleratable
|
||||
? acceleratorCount >= count
|
||||
: acceleratorCount === 0;
|
||||
if (!fits) continue;
|
||||
const candidate = tier.candidates?.find(hasResources);
|
||||
if (candidate) return candidate;
|
||||
|
||||
@@ -45,6 +45,11 @@ export interface FormData {
|
||||
ram: string | null | number;
|
||||
localStorage: string | null | number;
|
||||
accelerator: number | string | null;
|
||||
// Sliced (percentage) mode only. Memory (VRAM) percentage bound to the
|
||||
// 10-100 selector + free input; cores (compute) percentage bound to the
|
||||
// "100% compute" checkbox (100 when checked, mirrors memory otherwise).
|
||||
acceleratorSlicedMemoryPercentage?: number;
|
||||
acceleratorSlicedCoresPercentage?: number;
|
||||
};
|
||||
volume: {
|
||||
ephemeral?: {
|
||||
@@ -126,70 +131,129 @@ export interface InstanceTypeResource {
|
||||
export interface InstanceTypeCandidate {
|
||||
cluster: string;
|
||||
name: string;
|
||||
accelerator: InstanceTypeResource;
|
||||
cpu: InstanceTypeResource;
|
||||
ram: InstanceTypeResource;
|
||||
localStorage: InstanceTypeResource;
|
||||
accelerator?: InstanceTypeResource | null;
|
||||
cpu?: InstanceTypeResource | null;
|
||||
// Shared-mode available resource (not shown in the GPU Instance form).
|
||||
acceleratorShared?: InstanceTypeResource | null;
|
||||
// Sliced-mode available resource.
|
||||
acceleratorSliced?: InstanceTypeResource | null;
|
||||
// This candidate's sliced (partitioning) capability.
|
||||
acceleratorSlicedDetail?: AcceleratorSlicedDetail | null;
|
||||
phase?: 'Active' | 'Inactive' | 'Draining' | null;
|
||||
}
|
||||
|
||||
export interface InstanceTypeTierOnceMaxRequestResource {
|
||||
accelerator?: string;
|
||||
cpu: QuanityCPU;
|
||||
ram: QuanityMemory;
|
||||
localStorage: QuanityLocalStorage;
|
||||
// Per-mode maxima as plain number strings — the shape of the aggregated
|
||||
// status.onceMaxRequest / status.remaining AND of tier onceMaxRequest /
|
||||
// remaining (they are identical in the API). accelerator counts whole cards,
|
||||
// acceleratorShared / acceleratorSliced are percentages, cpu is cores. The
|
||||
// API carries no ram / localStorage here — RAM caps derive from
|
||||
// spec.unitResources, disk from spec.localStorage.
|
||||
export interface InstanceTypeOverviewResource {
|
||||
accelerator?: `${number}` | null;
|
||||
acceleratorShared?: `${number}` | null;
|
||||
acceleratorSliced?: `${number}` | null;
|
||||
cpu?: QuanityCPU | null;
|
||||
}
|
||||
|
||||
export interface InstanceTypeTier {
|
||||
onceMaxRequest: InstanceTypeTierOnceMaxRequestResource;
|
||||
onceMaxRequest: InstanceTypeOverviewResource;
|
||||
remaining?: InstanceTypeOverviewResource | null;
|
||||
// The tier's aggregated sliced (partitioning) capability.
|
||||
acceleratorSlicedDetail?: AcceleratorSlicedDetail | null;
|
||||
candidates?: InstanceTypeCandidate[] | null;
|
||||
}
|
||||
|
||||
export interface InstanceTypeOnceMaxRequestResource {
|
||||
accelerator?: `${number}` | null;
|
||||
cpu: QuanityCPU;
|
||||
ram: QuanityMemory;
|
||||
localStorage: QuanityLocalStorage;
|
||||
}
|
||||
|
||||
export interface CPUCache {
|
||||
l1i: string;
|
||||
l1d: string;
|
||||
l2: string;
|
||||
l3: string;
|
||||
l1i?: string | null;
|
||||
l1d?: string | null;
|
||||
l2?: string | null;
|
||||
l3?: string | null;
|
||||
}
|
||||
|
||||
export interface CPUInfo {
|
||||
physicalCores: string;
|
||||
threadsPerPhysicalCore: string;
|
||||
logicalCores: string;
|
||||
stepping: string | null;
|
||||
clockSpeed: string | null;
|
||||
maxClockSpeed: string | null;
|
||||
cacheLine: string;
|
||||
cache: CPUCache;
|
||||
manufacturer: string;
|
||||
product: string;
|
||||
family: string;
|
||||
physicalCores?: string | null;
|
||||
threadsPerPhysicalCore?: string | null;
|
||||
logicalCores?: string | null;
|
||||
stepping?: string | null;
|
||||
clockSpeed?: string | null;
|
||||
maxClockSpeed?: string | null;
|
||||
cacheLine?: string | null;
|
||||
cache?: CPUCache | null;
|
||||
manufacturer?: string | null;
|
||||
product?: string | null;
|
||||
family?: string | null;
|
||||
}
|
||||
|
||||
export interface InstanceTypeSpec {
|
||||
group: string;
|
||||
acceleratable: boolean;
|
||||
manufacturer: string;
|
||||
// Sliced (partitioning) capability descriptor. Replaces the removed
|
||||
// `spec.sliceable` boolean: a type is sliceable when logical (soft) slicing
|
||||
// reports capacity or physical (e.g. MIG) profiles exist — see
|
||||
// isSliceableDetail in ./index. Appears as status.detail.slicedDetail and as
|
||||
// tier / candidate `acceleratorSlicedDetail` in the aggregated view.
|
||||
export interface AcceleratorSlicedLogicalDetail {
|
||||
coresPercentageOvercommit?: boolean;
|
||||
// Max soft slices per card; 0 → soft slicing unsupported.
|
||||
count?: number | null;
|
||||
}
|
||||
|
||||
export interface AcceleratorSlicedPhysicalDetailProfile {
|
||||
name?: string | null;
|
||||
count?: number | null;
|
||||
}
|
||||
|
||||
export interface AcceleratorSlicedPhysicalDetail {
|
||||
profiles?: AcceleratorSlicedPhysicalDetailProfile[] | null;
|
||||
count?: number | null;
|
||||
}
|
||||
|
||||
export interface AcceleratorSlicedDetail {
|
||||
logical?: AcceleratorSlicedLogicalDetail | null;
|
||||
physical?: AcceleratorSlicedPhysicalDetail | null;
|
||||
}
|
||||
|
||||
// status.detail — the observed hardware descriptor. The API moved these off
|
||||
// spec (spec keeps user-defined fields only). The whole object is absent until
|
||||
// the operator backfills status, and every response is exclude_none — treat
|
||||
// every key as possibly missing.
|
||||
export interface InstanceTypeDetail {
|
||||
// Device identity.
|
||||
manufacturer?: string | null;
|
||||
product?: string | null;
|
||||
memory?: string | null;
|
||||
family?: string | null;
|
||||
// Host node CPU (flat fields, as opposed to the nested `cpu` below).
|
||||
physicalCores?: string | null;
|
||||
threadsPerPhysicalCore?: string | null;
|
||||
logicalCores?: string | null;
|
||||
stepping?: string | null;
|
||||
clockSpeed?: string | null;
|
||||
maxClockSpeed?: string | null;
|
||||
cacheLine?: string | null;
|
||||
cache?: CPUCache | null;
|
||||
// Accelerator hardware.
|
||||
memory?: string | null;
|
||||
cores?: string | null;
|
||||
computeCapability?: string | null;
|
||||
sliced?: string | null;
|
||||
maxComputeUnitCount?: number;
|
||||
slicedDetail?: AcceleratorSlicedDetail | null;
|
||||
// The accelerator's own CPU (distinct from the flat host CPU fields above).
|
||||
cpu?: CPUInfo | null;
|
||||
}
|
||||
|
||||
// Mirrors the API spec object exactly (user-defined fields only — observed
|
||||
// hardware lives on status.detail), plus two UI-computed enrichments filled by
|
||||
// use-query-instance-types whose names exist nowhere in the API.
|
||||
export interface InstanceTypeSpec {
|
||||
displayName?: string | null;
|
||||
acceleratorGroup?: string | null;
|
||||
generalGroup?: string | null;
|
||||
acceleratable?: boolean;
|
||||
os?: string;
|
||||
arch?: string;
|
||||
localStorage?: QuanityLocalStorage;
|
||||
unitResources?: {
|
||||
cpu: QuanityCPU;
|
||||
ram: QuanityMemory;
|
||||
};
|
||||
os?: string;
|
||||
arch?: string;
|
||||
cpu?: CPUInfo;
|
||||
cache?: Record<string, string>;
|
||||
// ---- UI-computed (not part of the API contract) ----
|
||||
// spec.unitResources parsed to numbers.
|
||||
unitResourcesParsed?: {
|
||||
cpu: {
|
||||
cores?: number;
|
||||
@@ -202,10 +266,31 @@ export interface InstanceTypeSpec {
|
||||
num: number;
|
||||
} | null;
|
||||
};
|
||||
// Max requestable unit (card / core) count, derived from status.
|
||||
maxComputeUnitCount?: number;
|
||||
}
|
||||
|
||||
// Flat spec snapshot persisted in a GPU instance's `description` field at
|
||||
// create time (see utils/instance-description.ts) and reused as the display
|
||||
// model of the type card / metadata section. It merges the definition spec
|
||||
// with the observed hardware from status.detail and the derived `sliceable`.
|
||||
// The flat shape is a UI document format — do NOT confuse it with the API
|
||||
// InstanceTypeSpec; it stays flat for compatibility with snapshots persisted
|
||||
// by older instances.
|
||||
export interface InstanceTypeSnapshotSpec extends InstanceTypeSpec {
|
||||
manufacturer?: string | null;
|
||||
product?: string | null;
|
||||
family?: string | null;
|
||||
memory?: string | null;
|
||||
sliceable?: boolean;
|
||||
// Accelerator CPU identity only (from status.detail.cpu).
|
||||
cpu?: Pick<CPUInfo, 'manufacturer' | 'product' | 'family'> | null;
|
||||
}
|
||||
|
||||
export interface InstanceTypeStatus {
|
||||
onceMaxRequest: InstanceTypeOnceMaxRequestResource;
|
||||
detail?: InstanceTypeDetail | null;
|
||||
onceMaxRequest: InstanceTypeOverviewResource;
|
||||
remaining?: InstanceTypeOverviewResource | null;
|
||||
tiers?: InstanceTypeTier[] | null;
|
||||
}
|
||||
|
||||
|
||||