Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a90e02a04c | ||
|
|
9a223f9565 | ||
|
|
4150070829 | ||
|
|
62351a781f | ||
|
|
eb2561ff1e | ||
|
|
c6643f2bc9 | ||
|
|
8901118bd2 |
@@ -3,3 +3,72 @@
|
||||
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
|
||||
(`MesaStack`) 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 -> MesaStack)
|
||||
./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.
|
||||
|
||||
@@ -88,16 +88,6 @@ Prefer action-driven updates, explicit handlers, and localized state transitions
|
||||
|
||||
Existing `styled-components` usage is legacy tech debt — do not migrate it wholesale, but do not add new `styled-components` either. Theme tokens (`var(--ant-color-*)`) work in all three approaches.
|
||||
|
||||
## Layout
|
||||
|
||||
Compose layout with Ant components, not hand-written `display: flex`.
|
||||
|
||||
- **1D flex** (row/column with `gap`, `align`, `justify`) → `Flex`. Do not write raw `display: flex` in new code.
|
||||
- **Inline sequence** of a few elements with uniform spacing → `Space`.
|
||||
- **Page/grid columns** → `Row` / `Col`.
|
||||
|
||||
Drive spacing with the theme scale (`Flex`/`Space` `gap`, or `var(--ant-*)` spacing tokens), not scattered `px` literals.
|
||||
|
||||
# Naming conventions
|
||||
|
||||
A page module lives under `src/pages/{module}` with this sub-structure: `components/`, `config/`, `forms/`, `hooks/`, `services/`, `index.tsx`. File naming:
|
||||
@@ -111,7 +101,6 @@ A page module lives under `src/pages/{module}` with this sub-structure: `compone
|
||||
|
||||
- `config/types.ts` — TypeScript types. Form shape → `FormData`; table/list row → `ListItem`.
|
||||
- `config/index.ts` — static constants, enums, and value/label maps (e.g. `XxxStatusValueMap`, `XxxStatusLabelMap`). Keep constants out of `types.ts`.
|
||||
- **`Select` options that need i18n**: set `label` to the message key and add `locale: true` on the option — the field translates it at render. Omit `locale` for options whose label is already final text. Ref `src/pages/benchmark/config/index.ts`.
|
||||
|
||||
# Common components
|
||||
|
||||
@@ -124,24 +113,13 @@ Always check `@gpustack/core-ui` first. Frequently reused:
|
||||
- **Form fields**: `BaseSelect`, `Input` (labeled).
|
||||
- **Text overflow**: `AutoTooltip`.
|
||||
- **Icons**: `IconFont`.
|
||||
- **Tags & status** (4 variants): see the section below.
|
||||
- **Status display** (success/failed/processing/warning): `StatusTag`.
|
||||
- **Permission-gated visibility**: `Access` / `useAccess`.
|
||||
- **Request hooks**: `useRequest` / `useQueryData` / `useQueryDataList`.
|
||||
- **Table data fetching**: `useTableFetch`.
|
||||
- **Submit guard** (prevent double-submit): `useSubmitLock`.
|
||||
- **Tabbed forms**: `ScrollSpyTabs`.
|
||||
|
||||
# Tags & status indicators
|
||||
|
||||
Four core-ui components cover tag/status display in tables and lists. Pick by **what the value means**, not by how it looks — don't reach for a generic antd `Tag`:
|
||||
|
||||
- **`StatusTag`** — semantic status with a **dynamic message/detail** (tooltip, download, extra content). Use when a row's status carries variable text, e.g. a failed job with an error message. Colors come from `StatusColorMap` (error/warning/transitioning/success/inactive).
|
||||
- **`StatusDot`** — colored dot + short label, **no message**. Use for a plain status/type cell where the value is a fixed enum (e.g. an event-type or log column). Same `StatusColorMap` palette; `inactive` dot is quaternary. If the status needs dynamic text, use `StatusTag` instead.
|
||||
- **`ThemeTag`** — a **standalone category label** (independent content, e.g. a permission scope or a model name). Default neutral; wraps antd `Tag`.
|
||||
- **`TextAttribute`** — a small neutral pill that is a **subordinate annotation following a primary text** (e.g. `key-name [custom]`), not a standalone tag. Manages its own leading margin. Two variants: `filled` (default) and `outlined`. Ref the name column in `src/pages/api-keys/hooks/use-keys-columns.tsx`.
|
||||
|
||||
Rule of thumb: semantic + dynamic text → `StatusTag`; semantic + fixed enum → `StatusDot`; independent category → `ThemeTag`; annotation of nearby text → `TextAttribute`.
|
||||
|
||||
# Dynamic add-item form fields
|
||||
|
||||
When building a form, select the add-item component from the **shape of the field's data** (its schema). Match the schema, don't hand-roll a list UI:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# GPUStack UI
|
||||
# MesaStack UI
|
||||
|
||||
UI for [GPUStack](https://github.com/gpustack/gpustack).
|
||||
UI for [MesaStack](https://github.com/gpustack/gpustack).
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ export default defineConfig({
|
||||
antd: {
|
||||
style: 'less'
|
||||
},
|
||||
title: 'GPUStack',
|
||||
title: 'MesaStack',
|
||||
hash: true,
|
||||
access: {},
|
||||
model: {},
|
||||
|
||||
@@ -100,15 +100,6 @@ const baseRoutes = [
|
||||
path: '/models',
|
||||
redirect: '/models/deployments'
|
||||
},
|
||||
{
|
||||
name: 'userModels',
|
||||
path: '/models/user-models',
|
||||
key: 'userModels',
|
||||
icon: 'icon-models',
|
||||
selectedIcon: 'icon-models-filled',
|
||||
defaultIcon: 'icon-models',
|
||||
component: './llmodels/user-models'
|
||||
},
|
||||
{
|
||||
name: 'modelCatalog',
|
||||
path: '/models/catalog',
|
||||
@@ -119,6 +110,16 @@ const baseRoutes = [
|
||||
access: 'canSeeOrgAdmin',
|
||||
component: './llmodels/catalog'
|
||||
},
|
||||
{
|
||||
name: 'userModels',
|
||||
path: '/models/user-models',
|
||||
key: 'userModels',
|
||||
icon: 'icon-models',
|
||||
selectedIcon: 'icon-models-filled',
|
||||
defaultIcon: 'icon-models',
|
||||
access: 'canSeeUser',
|
||||
component: './llmodels/user-models'
|
||||
},
|
||||
{
|
||||
name: 'deployment',
|
||||
path: '/models/deployments',
|
||||
@@ -273,7 +274,7 @@ const baseRoutes = [
|
||||
selectedIcon: 'icon-cluster2-filled',
|
||||
defaultIcon: 'icon-cluster2-outline',
|
||||
component: './cluster-management/clusters',
|
||||
subMenu: ['/resources/clusters/create']
|
||||
subMenu: ['/resources/clusters/detail', '/resources/clusters/create']
|
||||
},
|
||||
{
|
||||
name: 'workers',
|
||||
@@ -301,6 +302,16 @@ const baseRoutes = [
|
||||
selectedIcon: 'icon-credential-filled',
|
||||
defaultIcon: 'icon-credential-outline',
|
||||
component: './cluster-management/credentials'
|
||||
},
|
||||
{
|
||||
name: 'clusterDetail',
|
||||
path: '/resources/clusters/detail',
|
||||
key: 'clusterDetail',
|
||||
icon: 'icon-cluster2-outline',
|
||||
selectedIcon: 'icon-cluster2-filled',
|
||||
defaultIcon: 'icon-cluster2-outline',
|
||||
hideInMenu: true,
|
||||
component: './cluster-management/cluster-detail'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"@ant-design/pro-components": "3.1.0-0",
|
||||
"@antv/g6": "^5.0.51",
|
||||
"@braintree/sanitize-url": "^7.1.1",
|
||||
"@gpustack/core-ui": "^1.0.39",
|
||||
"@gpustack/core-ui": "^1.0.32",
|
||||
"@huggingface/gguf": "^0.1.7",
|
||||
"@huggingface/hub": "^0.15.1",
|
||||
"@huggingface/tasks": "^0.11.6",
|
||||
@@ -39,6 +39,7 @@
|
||||
"culori": "^4.0.2",
|
||||
"dayjs": "^1.11.11",
|
||||
"dompurify": "^3.2.6",
|
||||
"driver.js": "^1.3.1",
|
||||
"echarts": "^5.5.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"has-ansi": "^5.0.1",
|
||||
|
||||
@@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^7.1.1
|
||||
version: 7.1.2
|
||||
'@gpustack/core-ui':
|
||||
specifier: ^1.0.39
|
||||
version: 1.0.39(czdvzceysqw7iv6pct2ucnb23e)
|
||||
specifier: ^1.0.32
|
||||
version: 1.0.32(czdvzceysqw7iv6pct2ucnb23e)
|
||||
'@huggingface/gguf':
|
||||
specifier: ^0.1.7
|
||||
version: 0.1.18
|
||||
@@ -89,6 +89,9 @@ importers:
|
||||
dompurify:
|
||||
specifier: ^3.2.6
|
||||
version: 3.4.2
|
||||
driver.js:
|
||||
specifier: ^1.3.1
|
||||
version: 1.4.0
|
||||
echarts:
|
||||
specifier: ^5.5.1
|
||||
version: 5.6.0
|
||||
@@ -1481,8 +1484,8 @@ packages:
|
||||
resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz}
|
||||
deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package
|
||||
|
||||
'@gpustack/core-ui@1.0.39':
|
||||
resolution: {integrity: sha512-6NS0TDkBFHK6RipfvLwvDnLZlAoa/yFbHV+xeERAnUb+4t0mDyIQFqvYAeDgrUW/JIZ6WScrm31FDZe2sKiuaw==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.39.tgz}
|
||||
'@gpustack/core-ui@1.0.32':
|
||||
resolution: {integrity: sha512-kGTazoqbK2KyZgOP6gmQaRxTiQVfF2IKLGDXjJq6w6BbmJgALXFJA2v2ROjAbjEVyfTdBzyYeXfPo/JgISpMNw==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.32.tgz}
|
||||
peerDependencies:
|
||||
'@ant-design/icons': ^6.1.0
|
||||
'@ant-design/pro-components': 3.1.0-0
|
||||
@@ -4317,6 +4320,9 @@ packages:
|
||||
dot-case@3.0.4:
|
||||
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==, tarball: https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz}
|
||||
|
||||
driver.js@1.4.0:
|
||||
resolution: {integrity: sha512-Gm64jm6PmcU+si21sQhBrTAM1JvUrR0QhNmjkprNLxohOBzul9+pNHXgQaT9lW84gwg9GMLB3NZGuGolsz5uew==, tarball: https://registry.npmjs.org/driver.js/-/driver.js-1.4.0.tgz}
|
||||
|
||||
duck@0.1.12:
|
||||
resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==, tarball: https://registry.npmjs.org/duck/-/duck-0.1.12.tgz}
|
||||
|
||||
@@ -10802,7 +10808,7 @@ snapshots:
|
||||
|
||||
'@formatjs/intl-utils@2.3.0': {}
|
||||
|
||||
'@gpustack/core-ui@1.0.39(czdvzceysqw7iv6pct2ucnb23e)':
|
||||
'@gpustack/core-ui@1.0.32(czdvzceysqw7iv6pct2ucnb23e)':
|
||||
dependencies:
|
||||
'@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@ant-design/pro-components': 3.1.0-0(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -14548,6 +14554,8 @@ snapshots:
|
||||
no-case: 3.0.4
|
||||
tslib: 2.8.1
|
||||
|
||||
driver.js@1.4.0: {}
|
||||
|
||||
duck@0.1.12:
|
||||
dependencies:
|
||||
underscore: 1.13.8
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# rebrand: 把面向用户的品牌标识从 GPUStack 批量替换为 MesaStack(或自定义品牌)。
|
||||
#
|
||||
# 设计目标:上游每次更新后,在新的 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 目标品牌词(默认 MesaStack)
|
||||
# DRY_RUN 设为 1 时只预览将改动的行,不写文件
|
||||
#
|
||||
set -e
|
||||
|
||||
FROM="${FROM:-GPUStack}"
|
||||
TO="${TO:-MesaStack}"
|
||||
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' 复核改动。"
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/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: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
@@ -1,24 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<defs>
|
||||
<!-- 顶面填充(亮面,高透明度) -->
|
||||
<linearGradient id="cube-top-grad" x1="12" y1="2" x2="12" y2="12" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#f759ab" stop-opacity="0.18"/>
|
||||
<stop offset="100%" stop-color="#c41d7f" stop-opacity="0.1"/>
|
||||
</linearGradient>
|
||||
<!-- 左侧面填充(暗面,低透明度) -->
|
||||
<linearGradient id="cube-left-grad" x1="2" y1="7" x2="12" y2="17" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#c41d7f" stop-opacity="0.12"/>
|
||||
<stop offset="100%" stop-color="#c41d7f" stop-opacity="0.04"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="cube-stroke-grad" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#f759ab"/>
|
||||
<stop offset="100%" stop-color="#c41d7f"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- 顶面填充 -->
|
||||
<polygon points="12,2 21.5,6.7 12,11.5 2.5,6.7" fill="url(#cube-top-grad)" />
|
||||
<!-- 左侧面填充 -->
|
||||
<polygon points="2.5,6.7 12,11.5 12,21.3 2.5,16.5" fill="url(#cube-left-grad)" />
|
||||
<!-- 立方体全纯线外骨架(细化为圆角衔接) -->
|
||||
<path d="M12 2L2.5 6.7M12 2l9.5 4.7M21.5 6.7L12 11.5M2.5 6.7L12 11.5M2.5 6.7v9.8l9.5 4.8M21.5 6.7v9.8l-9.5 4.8M12 11.5v9.8" stroke="url(#cube-stroke-grad)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,22 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<defs>
|
||||
<!-- 1. 定义专属微通透渐变填充 -->
|
||||
<linearGradient id="img-fill-grad" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#d46b08" stop-opacity="0.12"/>
|
||||
<stop offset="100%" stop-color="#d46b08" stop-opacity="0.04"/>
|
||||
</linearGradient>
|
||||
<!-- 2. 定义边框高精度渐变(亮橙到深橙,拉开层次) -->
|
||||
<linearGradient id="img-stroke-grad" x1="4" y1="4" x2="20" y2="20" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#fa8c16"/>
|
||||
<stop offset="100%" stop-color="#d46b08"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- 3. 精装底色充填层 -->
|
||||
<rect x="3" y="3" width="18" height="18" rx="4" fill="url(#img-fill-grad)" />
|
||||
<!-- 4. 高级柔和微圆角边框层 -->
|
||||
<rect x="3" y="3" width="18" height="18" rx="4" stroke="url(#img-stroke-grad)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<!-- 内部几何现代山脉线条 -->
|
||||
<path d="M3 16l4-4a2 2 0 0 1 2.8 0l5.2 5.2M13 15l2.5-2.5a2 2 0 0 1 2.8 0l2.7 2.7" stroke="url(#img-stroke-grad)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<!-- 标志性通透小钻石 -->
|
||||
<rect x="14" y="6" width="4" height="4" rx="1.5" transform="rotate(45 16 8)" fill="#fa8c16" fill-opacity="0.3" stroke="url(#img-stroke-grad)" stroke-width="1"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 68 KiB |
@@ -1,17 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="chat-fill-grad" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#389e0d" stop-opacity="0.1"/>
|
||||
<stop offset="100%" stop-color="#389e0d" stop-opacity="0.02"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="chat-stroke-grad" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#73d13d"/>
|
||||
<stop offset="100%" stop-color="#389e0d"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- 主现代对话框体(全部改为圆润R角,精装填充) -->
|
||||
<path d="M18 4H6a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h7.5l3.5 3.5a1 1 0 0 0 1.5-.5V17a3 3 0 0 0 3-3V7a3 3 0 0 0-3-3z" fill="url(#chat-fill-grad)" stroke="url(#chat-stroke-grad)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<!-- 内部通透对话线条(细化为圆角代码采样块感) -->
|
||||
<rect x="7" y="8" width="8" height="1.5" rx="0.75" fill="#73d13d" fill-opacity="0.3"/>
|
||||
<rect x="7" y="11.5" width="10" height="1.5" rx="0.75" fill="#73d13d" fill-opacity="0.2"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 640 B |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,19 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="rank-fill-grad" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#08979c" stop-opacity="0.1"/>
|
||||
<stop offset="100%" stop-color="#08979c" stop-opacity="0.01"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="rank-stroke-grad" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#36cfc9"/>
|
||||
<stop offset="100%" stop-color="#08979c"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- 数据权重条(全部改为高级圆角) -->
|
||||
<rect x="11" y="4" width="10" height="2.5" rx="1.25" fill="#36cfc9" fill-opacity="0.1" stroke="url(#rank-stroke-grad)" stroke-width="1.5"/>
|
||||
<rect x="11" y="9" width="7.5" height="2.5" rx="1.25" fill="#36cfc9" fill-opacity="0.05" stroke="url(#rank-stroke-grad)" stroke-width="1.5"/>
|
||||
<rect x="11" y="14" width="5" height="2.5" rx="1.25" stroke="url(#rank-stroke-grad)" stroke-width="1.5"/>
|
||||
<!-- 基准线与立体双向指引箭头(优化为圆角) -->
|
||||
<path d="M3 17l3 3 3-3M6 4v16" stroke="url(#rank-stroke-grad)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.5 5.5L6 4l1.5 1.5" stroke="url(#rank-stroke-grad)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 801 B |
@@ -1,18 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="stt-fill-grad" x1="12" y1="3" x2="12" y2="14" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#40a9ff" stop-opacity="0.2"/>
|
||||
<stop offset="100%" stop-color="#1677ff" stop-opacity="0.05"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="stt-stroke-grad" x1="12" y1="3" x2="12" y2="21" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#40a9ff"/>
|
||||
<stop offset="100%" stop-color="#1677ff"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- 麦克风核心体:精装通透填充 -->
|
||||
<rect x="8.5" y="3" width="7" height="11" rx="3.5" fill="url(#stt-fill-grad)" stroke="url(#stt-stroke-grad)" stroke-width="1.8" />
|
||||
<!-- 内部音膜立体结构线(细化为点状) -->
|
||||
<line x1="10" y1="8" x2="14" y2="8" stroke="url(#stt-stroke-grad)" stroke-width="1" stroke-dasharray="1 2"/>
|
||||
<!-- 悬挂外托架与底座(全部改为高级圆角) -->
|
||||
<path d="M5 10a7 7 0 0 0 14 0M12 17v4M8 21h8" stroke="url(#stt-stroke-grad)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1,23 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<defs>
|
||||
<!-- 1. 核心高通透深蓝渐变充填 -->
|
||||
<linearGradient id="tts-v2-fill" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#2f54eb" stop-opacity="0.15"/>
|
||||
<stop offset="100%" stop-color="#1d39c4" stop-opacity="0.03"/>
|
||||
</linearGradient>
|
||||
<!-- 2. 精准调校的专属蓝色渐变边框 -->
|
||||
<linearGradient id="tts-v2-stroke" x1="4" y1="4" x2="20" y2="20" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#2f54eb"/>
|
||||
<stop offset="100%" stop-color="#1d39c4"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- 左侧:低频辅助声波(大间距,带现代圆角) -->
|
||||
<rect x="4" y="8" width="2.2" height="8" rx="1.1" stroke="url(#tts-v2-stroke)" stroke-width="1.8" stroke-linecap="round"/>
|
||||
|
||||
<!-- 中央:核心高频声波主体(拉大宽度,注入通透水晶质感) -->
|
||||
<rect x="10.4" y="2" width="3.2" height="20" rx="1.6" fill="url(#tts-v2-fill)" stroke="url(#tts-v2-stroke)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
<!-- 右侧:高频衰减声波(保持几何对称与呼吸感) -->
|
||||
<rect x="17.8" y="5" width="2.2" height="14" rx="1.1" stroke="url(#tts-v2-stroke)" stroke-width="1.8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -9,15 +9,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tableEmptyFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-table {
|
||||
.ant-table {
|
||||
.ant-table-container {
|
||||
@@ -27,27 +18,5 @@
|
||||
scrollbar-color: var(--color-scrollbar-thumb) transparent;
|
||||
}
|
||||
}
|
||||
|
||||
// Reserve a stable block for the empty/loading state so the first-load
|
||||
// spinner and the empty result occupy the same height as eventual data —
|
||||
// this removes the layout jump when entering the page. Scoped to
|
||||
// `.ant-table-content` so it only targets x-scroll tables (whose empty
|
||||
// row lives here) and leaves fixed-height `scroll.y` tables untouched.
|
||||
// Height must match the `minHeight` passed to <NoResult> in
|
||||
// use-no-resource-result.
|
||||
.ant-table-content {
|
||||
.ant-table-placeholder {
|
||||
> .ant-table-cell {
|
||||
height: calc(100vh - 300px);
|
||||
}
|
||||
|
||||
// NoResult renders nothing while loading and mounts an <Empty> only
|
||||
// once the request settles, so this fires exactly when the empty
|
||||
// state appears — a seamless fade-in instead of a hard pop.
|
||||
.ant-empty {
|
||||
animation: tableEmptyFadeIn 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { GPUStackVersionAtom } from '@/atoms/user';
|
||||
import VersionInfo, { modalConfig } from '@/components/version-info';
|
||||
import externalLinks from '@/constants/external-links';
|
||||
import { getAtomStorage } from '@/atoms/utils';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Divider, Modal, Typography } from 'antd';
|
||||
import { Divider, Typography } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const CompanyWrapper = styled.div`
|
||||
@@ -33,21 +31,10 @@ 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">
|
||||
@@ -64,18 +51,7 @@ const Footer: React.FC = () => {
|
||||
</Typography.Link>
|
||||
</CompanyWrapper>
|
||||
<Divider orientation="vertical" />
|
||||
<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>
|
||||
<span>{getAtomStorage(GPUStackVersionAtom)?.version}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
// Wrapper around core-ui's FullMarkdown that co-locates the KaTeX stylesheet.
|
||||
//
|
||||
// core-ui deliberately does NOT bundle katex.min.css (importing it there
|
||||
// base64-inlines ~1.4MB of fonts into the shared, render-blocking index.css).
|
||||
// Importing it here keeps the KaTeX CSS in the route chunk that actually
|
||||
// renders math, so it loads lazily and never blocks first paint.
|
||||
//
|
||||
// Always import FullMarkdown from this module, not from '@gpustack/core-ui/markdown'.
|
||||
import { FullMarkdown } from '@gpustack/core-ui/markdown';
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
export default FullMarkdown;
|
||||
@@ -1,7 +1,6 @@
|
||||
import Logo from '@/assets/images/gpustack-logo.png';
|
||||
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
|
||||
import externalLinks from '@/constants/external-links';
|
||||
import { useLogo } from '@/hooks/use-logo';
|
||||
import { Button } from 'antd';
|
||||
import { useAtom } from 'jotai';
|
||||
import React from 'react';
|
||||
@@ -19,7 +18,7 @@ const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
|
||||
isProd,
|
||||
isDev
|
||||
} = gpuStackVersionAtom;
|
||||
const { sidebarLogo } = useLogo();
|
||||
|
||||
// user info
|
||||
const { is_admin } = userDataAtom || {};
|
||||
|
||||
@@ -31,7 +30,7 @@ const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
|
||||
return (
|
||||
<div className="version-box">
|
||||
<div className="img">
|
||||
<img src={sidebarLogo || Logo} alt="logo" />
|
||||
<img src={Logo} alt="logo" />
|
||||
</div>
|
||||
|
||||
<div className="ver">
|
||||
|
||||
@@ -28,7 +28,6 @@ export default {
|
||||
rowSelectedBg: 'transparent',
|
||||
headerSortActiveBg: 'transparent',
|
||||
headerSortHoverBg: 'transparent',
|
||||
bodySortBg: 'transparent',
|
||||
headerBg: 'none'
|
||||
},
|
||||
Button: {
|
||||
|
||||
@@ -31,7 +31,6 @@ export default {
|
||||
rowSelectedBg: 'transparent',
|
||||
headerSortActiveBg: 'transparent',
|
||||
headerSortHoverBg: 'transparent',
|
||||
bodySortBg: 'transparent',
|
||||
headerSplitColor: '#e8e8e8',
|
||||
headerBg: 'none'
|
||||
},
|
||||
|
||||
@@ -91,7 +91,6 @@ html {
|
||||
// ======== container ============
|
||||
--color-border-container: #ededed;
|
||||
--color-text-table-header: #71717a;
|
||||
--seal-table-row-min-height: 68px;
|
||||
}
|
||||
|
||||
html[data-theme='realDark'] {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function useActions<T>(actions: Global.ActionItem<T>[], ctx: T) {
|
||||
return actions
|
||||
.filter((action) => {
|
||||
return action.visible ? action.visible(ctx) : true;
|
||||
})
|
||||
.map((action) => ({
|
||||
...action,
|
||||
disabled: action.disabled?.(ctx)
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { message } from 'antd';
|
||||
|
||||
type MessageType = 'input' | 'select';
|
||||
|
||||
const useAppUtils = () => {
|
||||
const intl = useIntl();
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
|
||||
/**
|
||||
*
|
||||
* @param type Array<'input' | 'select'>
|
||||
* @param name
|
||||
* @param locale boolean
|
||||
* @returns
|
||||
*/
|
||||
const getRuleMessage = (
|
||||
type: MessageType | MessageType[],
|
||||
name: string,
|
||||
locale = true
|
||||
) => {
|
||||
const nameStr = locale ? intl.formatMessage({ id: name }) : name;
|
||||
// transform type to array
|
||||
const typeList = Array.isArray(type) ? type : [type];
|
||||
|
||||
if (typeList.includes('select') && typeList.includes('input')) {
|
||||
return intl.formatMessage(
|
||||
{ id: 'common.form.rule.selectInput' },
|
||||
{ name: nameStr }
|
||||
);
|
||||
}
|
||||
|
||||
if (typeList.includes('input')) {
|
||||
return intl.formatMessage(
|
||||
{ id: 'common.form.rule.input' },
|
||||
{ name: nameStr }
|
||||
);
|
||||
}
|
||||
|
||||
return intl.formatMessage(
|
||||
{ id: 'common.form.rule.select' },
|
||||
{ name: nameStr }
|
||||
);
|
||||
};
|
||||
|
||||
const showSuccess = (msg?: string) => {
|
||||
messageApi.success(
|
||||
msg || intl.formatMessage({ id: 'common.message.success' })
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
getRuleMessage,
|
||||
showSuccess
|
||||
};
|
||||
};
|
||||
|
||||
export default useAppUtils;
|
||||
@@ -0,0 +1,18 @@
|
||||
// broadcast channel hook
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export const useBroadcast = () => {
|
||||
const broadcastChannel = useRef<BroadcastChannel | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
broadcastChannel.current = new BroadcastChannel('broadcast_channel');
|
||||
|
||||
return () => {
|
||||
broadcastChannel.current?.close();
|
||||
console.log('broadcast channel closed');
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { broadcastChannel };
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import _ from 'lodash';
|
||||
import { useRef } from 'react';
|
||||
|
||||
export default function useContainerScroll(
|
||||
container: any,
|
||||
options?: { toBottom?: boolean }
|
||||
) {
|
||||
const isWheeled = useRef(false);
|
||||
const scroller = useRef(container);
|
||||
const optionsRef = useRef(options);
|
||||
const toBottomFlag = useRef(options?.toBottom);
|
||||
const timerRef = useRef<any>(null);
|
||||
|
||||
const debunceResetWheeled = _.debounce(() => {
|
||||
isWheeled.current = false;
|
||||
}, 5000);
|
||||
|
||||
const handleContentWheel = (e: any) => {
|
||||
isWheeled.current = true;
|
||||
debunceResetWheeled.cancel?.();
|
||||
debunceResetWheeled();
|
||||
};
|
||||
|
||||
const scrollerRun = () => {
|
||||
const scrollerContainer = scroller.current?.current || {};
|
||||
const { scrollHeight, clientHeight, scrollTop } = scrollerContainer;
|
||||
if (
|
||||
optionsRef.current?.toBottom &&
|
||||
toBottomFlag.current &&
|
||||
scrollHeight > clientHeight + scrollTop
|
||||
) {
|
||||
scroller.current.current.scrollTop = scrollHeight;
|
||||
// toBottomFlag.current = false;
|
||||
isWheeled.current = false;
|
||||
} else if (
|
||||
!isWheeled.current &&
|
||||
scrollHeight > clientHeight + scrollTop &&
|
||||
scroller.current?.current
|
||||
) {
|
||||
scroller.current.current.scrollTop += 10;
|
||||
window.requestAnimationFrame(scrollerRun);
|
||||
}
|
||||
};
|
||||
|
||||
const updateScrollerPosition = () => {
|
||||
if (!isWheeled.current) {
|
||||
window.requestAnimationFrame(scrollerRun);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
handleContentWheel,
|
||||
updateScrollerPosition,
|
||||
scroller
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
const useCopyToClipboard = () => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copyToClipboard = useCallback(async (text: string) => {
|
||||
try {
|
||||
if (navigator.clipboard) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 3000);
|
||||
}
|
||||
} catch (error) {
|
||||
setCopied(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { copied, copyToClipboard };
|
||||
};
|
||||
|
||||
export default useCopyToClipboard;
|
||||
@@ -0,0 +1,71 @@
|
||||
import { HandlerOptions } from '@/hooks/use-chunk-fetch';
|
||||
import { useDownloadStream } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Progress, notification } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const renderMessage = (title: string) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 280,
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const createFileName = (name: string) => {
|
||||
const timestamp = dayjs().format('YYYY-MM-DD_HH-mm-ss');
|
||||
const fileName = `${name}_${timestamp}.txt`;
|
||||
return fileName;
|
||||
};
|
||||
|
||||
const useDownloadLogs = () => {
|
||||
const { downloadStream } = useDownloadStream();
|
||||
const intl = useIntl();
|
||||
const [api, contextHolder] = notification.useNotification({
|
||||
stack: { threshold: 1 }
|
||||
});
|
||||
|
||||
const downloadNotification = (
|
||||
data: HandlerOptions & {
|
||||
filename: string;
|
||||
duration?: number;
|
||||
chunkRequestRef: any;
|
||||
}
|
||||
) => {
|
||||
api.open({
|
||||
duration: data.duration,
|
||||
message: renderMessage(data.filename),
|
||||
key: data.filename,
|
||||
closeIcon: (
|
||||
<span>{intl.formatMessage({ id: 'common.button.cancel' })}</span>
|
||||
),
|
||||
description: <Progress percent={data.percent} size="small"></Progress>,
|
||||
onClose() {
|
||||
data.chunkRequestRef?.current?.abort();
|
||||
notification.destroy?.(data.filename);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownloadLog = async (params: { url: string; name: string }) => {
|
||||
downloadStream({
|
||||
url: params.url,
|
||||
filename: createFileName(params.name),
|
||||
downloadNotification
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
onDownloadLog: handleDownloadLog,
|
||||
contextHolder
|
||||
};
|
||||
};
|
||||
|
||||
export default useDownloadLogs;
|
||||
@@ -0,0 +1,136 @@
|
||||
import useSetChunkFetch, { HandlerOptions } from '@/hooks/use-chunk-fetch';
|
||||
import { message } from 'antd';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export default function useDownloadStream() {
|
||||
const chunkRequestRef = useRef<any>(null);
|
||||
const logParseWorker = useRef<any>(null);
|
||||
const clearScreen = useRef(false);
|
||||
const filename = useRef('log');
|
||||
const downloadNotificationRef = useRef<any>(null);
|
||||
|
||||
const { setChunkFetch } = useSetChunkFetch();
|
||||
|
||||
const downloadFile = (content: string) => {
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename.current;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const updateContent = (data: string, options?: HandlerOptions) => {
|
||||
const { isComplete, percent } = options || {};
|
||||
|
||||
logParseWorker.current?.postMessage({
|
||||
inputStr: data,
|
||||
reset: clearScreen.current,
|
||||
isComplete: isComplete,
|
||||
percent: percent,
|
||||
chunked: false
|
||||
});
|
||||
clearScreen.current = false;
|
||||
};
|
||||
|
||||
const handleError = (error: any) => {
|
||||
const errorMsg = error?.message || error;
|
||||
const msg =
|
||||
typeof errorMsg === 'string' ? errorMsg : JSON.stringify(errorMsg);
|
||||
message.error(msg);
|
||||
downloadNotificationRef.current?.({
|
||||
duration: 1,
|
||||
percent: 0,
|
||||
filename: filename.current
|
||||
});
|
||||
};
|
||||
|
||||
const downloadStream = async (props: {
|
||||
data?: any;
|
||||
url: string;
|
||||
params?: any;
|
||||
signal?: AbortSignal;
|
||||
method?: string;
|
||||
headers?: any;
|
||||
filename?: string;
|
||||
downloadNotification?: (data: any) => void;
|
||||
}) => {
|
||||
try {
|
||||
clearScreen.current = true;
|
||||
filename.current = props.filename || 'log';
|
||||
downloadNotificationRef.current = props.downloadNotification;
|
||||
const { params, url } = props;
|
||||
|
||||
chunkRequestRef.current?.current?.abort?.();
|
||||
|
||||
chunkRequestRef.current = setChunkFetch({
|
||||
url,
|
||||
params,
|
||||
watch: false,
|
||||
contentType: 'text',
|
||||
errorHandler: handleError,
|
||||
handler: updateContent
|
||||
});
|
||||
downloadNotificationRef.current?.({
|
||||
filename: filename.current,
|
||||
duration: null,
|
||||
chunkRequestRef: chunkRequestRef.current
|
||||
});
|
||||
} catch (error) {
|
||||
//
|
||||
downloadNotificationRef.current?.({
|
||||
duration: 1,
|
||||
percent: 0,
|
||||
filename: filename.current
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
logParseWorker.current?.terminate?.();
|
||||
|
||||
logParseWorker.current = new Worker(
|
||||
// @ts-ignore
|
||||
new URL('@/components/logs-viewer/parse-worker.ts', import.meta.url),
|
||||
{
|
||||
type: 'module'
|
||||
}
|
||||
);
|
||||
|
||||
logParseWorker.current.onmessage = (event: any) => {
|
||||
const { result, isComplete, percent } = event.data;
|
||||
|
||||
const isAborted = chunkRequestRef.current?.current?.signal?.aborted;
|
||||
if (!isComplete && !isAborted) {
|
||||
downloadNotificationRef.current?.({
|
||||
percent: percent,
|
||||
duration: null,
|
||||
filename: filename.current,
|
||||
chunkRequestRef: chunkRequestRef.current
|
||||
});
|
||||
} else if (isComplete && !isAborted) {
|
||||
downloadNotificationRef.current?.({
|
||||
duration: 1,
|
||||
percent: 100,
|
||||
filename: filename.current
|
||||
});
|
||||
downloadFile(result);
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
if (logParseWorker.current) {
|
||||
logParseWorker.current.terminate();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
downloadStream
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { driver, type Config } from 'driver.js';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export const useDriver = (config?: Config & { id: string }) => {
|
||||
const intl = useIntl();
|
||||
const driverRef = useRef<any>(null);
|
||||
|
||||
const handleDoNotShowAgain = () => {};
|
||||
|
||||
const init = () => {
|
||||
driverRef.current = driver({
|
||||
overlayOpacity: 0.2,
|
||||
animate: false,
|
||||
...config
|
||||
});
|
||||
};
|
||||
|
||||
const start = () => {
|
||||
if (!driverRef.current) {
|
||||
init();
|
||||
}
|
||||
driverRef.current.drive();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
driverRef.current?.destroy();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { start, initDriver: init, driver: driverRef.current };
|
||||
};
|
||||
|
||||
export default useDriver;
|
||||
@@ -0,0 +1,106 @@
|
||||
import HotKeys from '@/config/hotkeys';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { throttle } from 'lodash';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
hintOverlay: css`
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--color-esc-hint-bg);
|
||||
color: ${token.colorTextLightSolid};
|
||||
padding: 16px 24px;
|
||||
border-radius: 4px;
|
||||
z-index: 2000;
|
||||
font-size: 14px;
|
||||
pointer-events: none;
|
||||
animation: fadeInOut 2s ease-in-out;
|
||||
@keyframes fadeInOut {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 1;
|
||||
}
|
||||
90% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`
|
||||
}));
|
||||
|
||||
export function useEscHint(options?: {
|
||||
enabled?: boolean;
|
||||
message?: string;
|
||||
throttleDelay?: number;
|
||||
}) {
|
||||
const { enabled = true, message, throttleDelay = 3000 } = options || {};
|
||||
const intl = useIntl();
|
||||
const { styles } = useStyles();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const timeoutRef = useRef<any>(null);
|
||||
const isHintActiveRef = useRef(false);
|
||||
|
||||
const showHintThrottled = useMemo(
|
||||
() =>
|
||||
throttle(
|
||||
() => {
|
||||
if (isHintActiveRef.current) return;
|
||||
|
||||
isHintActiveRef.current = true;
|
||||
|
||||
setVisible(true);
|
||||
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setVisible(false);
|
||||
isHintActiveRef.current = false;
|
||||
}, 2000);
|
||||
},
|
||||
throttleDelay,
|
||||
{
|
||||
leading: true,
|
||||
trailing: false
|
||||
}
|
||||
),
|
||||
[throttleDelay]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
HotKeys.ESC,
|
||||
() => {
|
||||
if (!enabled) return;
|
||||
showHintThrottled();
|
||||
},
|
||||
{
|
||||
enabled: enabled
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
showHintThrottled.cancel();
|
||||
};
|
||||
}, [showHintThrottled]);
|
||||
|
||||
const EscHint = visible ? (
|
||||
<div className={styles.hintOverlay}>
|
||||
{message || intl.formatMessage({ id: 'common.tips.escape.disable' })}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return { EscHint };
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import qs from 'query-string';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export const createEventSourceURL = (url: string) => {
|
||||
const { host, protocol } = window.location;
|
||||
return `${protocol}://${host}${url}`;
|
||||
};
|
||||
|
||||
/*
|
||||
0: connecting
|
||||
1: connect successfully
|
||||
2: closed
|
||||
*/
|
||||
|
||||
export default function useEventSource() {
|
||||
const eventSourceRef = useRef<any>(null);
|
||||
|
||||
const createEventSourceConnection = (query: {
|
||||
url: string;
|
||||
params: any;
|
||||
onmessage?: (data: any) => void;
|
||||
}) => {
|
||||
eventSourceRef.current?.close?.();
|
||||
|
||||
const { url, params, onmessage = () => {} } = query;
|
||||
|
||||
const sseurl = createEventSourceURL(url);
|
||||
|
||||
eventSourceRef.current = new EventSource(
|
||||
`${url}?${qs.stringify({
|
||||
...params
|
||||
})}`,
|
||||
{
|
||||
withCredentials: true
|
||||
}
|
||||
);
|
||||
|
||||
eventSourceRef.current.onmessage = (res: any) => {
|
||||
try {
|
||||
console.log('event source message: ', { res, resData: res });
|
||||
const data = JSON.parse(res.data);
|
||||
onmessage(data);
|
||||
} catch (error) {
|
||||
// error
|
||||
console.log('event source error: ', error);
|
||||
}
|
||||
};
|
||||
|
||||
eventSourceRef.current.onclose = () => {
|
||||
console.log('event source closed...');
|
||||
};
|
||||
|
||||
eventSourceRef.current.onopen = () => {
|
||||
console.log('event source connected...');
|
||||
};
|
||||
|
||||
eventSourceRef.current.onerror = (error: any) => {
|
||||
console.log('event source error: ', error);
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
eventSourceRef.current?.close?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
eventSourceRef: eventSourceRef,
|
||||
createEventSourceConnection
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { throttle } from 'lodash';
|
||||
import {
|
||||
UseOverlayScrollbarsParams,
|
||||
useOverlayScrollbars
|
||||
} from 'overlayscrollbars-react';
|
||||
import React, { useEffect } from 'react';
|
||||
import useUserSettings from './use-user-settings';
|
||||
|
||||
type OverflowBehavior =
|
||||
| 'hidden'
|
||||
| 'scroll'
|
||||
| 'visible'
|
||||
| 'visible-hidden'
|
||||
| 'visible-scroll';
|
||||
export interface OverlayScrollerOptions {
|
||||
oppositeTheme?: boolean;
|
||||
overflow?: {
|
||||
x?: OverflowBehavior;
|
||||
y?: OverflowBehavior;
|
||||
};
|
||||
scrollbars?: {
|
||||
theme?: 'os-theme-light' | 'os-theme-dark';
|
||||
autoHide?: 'never' | 'scroll' | 'leave' | 'move';
|
||||
autoHideDelay?: number;
|
||||
clickScroll?: boolean | 'instant';
|
||||
};
|
||||
}
|
||||
|
||||
export const overlaySollerOptions: UseOverlayScrollbarsParams = {
|
||||
options: {
|
||||
update: {
|
||||
debounce: 0
|
||||
},
|
||||
overflow: {
|
||||
x: 'hidden'
|
||||
},
|
||||
scrollbars: {
|
||||
theme: 'os-theme-light',
|
||||
autoHide: 'scroll',
|
||||
autoHideDelay: 600,
|
||||
clickScroll: 'instant'
|
||||
}
|
||||
},
|
||||
defer: true
|
||||
};
|
||||
|
||||
const RESETSCROLLDELAY = 5000;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param options.theme: if set theme, it will fix the theme
|
||||
* @returns
|
||||
*/
|
||||
export default function useOverlayScroller(data?: {
|
||||
options?: OverlayScrollerOptions;
|
||||
events?: any;
|
||||
defer?: boolean;
|
||||
}) {
|
||||
const { userSettings } = useUserSettings();
|
||||
const { options, events, defer = true } = data || {};
|
||||
const { scrollbars, overflow, oppositeTheme } = options || {};
|
||||
const scrollEventElement = React.useRef<any>(null);
|
||||
const instanceRef = React.useRef<any>(null);
|
||||
const initialized = React.useRef(false);
|
||||
const scrollElementRef = React.useRef<any>(null);
|
||||
const stopUpdatePosition = React.useRef(false);
|
||||
const timerRef = React.useRef<any>(null);
|
||||
const [initialize, instance] = useOverlayScrollbars({
|
||||
options: {
|
||||
update: {
|
||||
debounce: 0
|
||||
},
|
||||
overflow: {
|
||||
x: 'hidden',
|
||||
...overflow
|
||||
},
|
||||
scrollbars: {
|
||||
autoHide: 'scroll',
|
||||
autoHideDelay: 600,
|
||||
clickScroll: 'instant',
|
||||
...scrollbars,
|
||||
theme:
|
||||
scrollbars?.theme ||
|
||||
(userSettings.theme === 'light' || !userSettings.theme
|
||||
? 'os-theme-dark'
|
||||
: 'os-theme-light')
|
||||
}
|
||||
},
|
||||
events: {
|
||||
...events
|
||||
},
|
||||
defer: defer
|
||||
});
|
||||
|
||||
instanceRef.current = instance?.();
|
||||
scrollEventElement.current =
|
||||
instanceRef.current?.elements()?.scrollEventElement;
|
||||
|
||||
const handleOnScroll = () => {
|
||||
const scrollTop = scrollEventElement.current?.scrollTop;
|
||||
const scrollHeight = scrollEventElement.current?.scrollHeight;
|
||||
const clientHeight = scrollEventElement.current?.clientHeight;
|
||||
|
||||
const isBottom = scrollTop + clientHeight + 20 >= scrollHeight;
|
||||
|
||||
if (isBottom) {
|
||||
stopUpdatePosition.current = false;
|
||||
} else {
|
||||
stopUpdatePosition.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
const throttledScroll = useMemoizedFn(
|
||||
throttle(() => {
|
||||
scrollEventElement.current?.scrollTo?.({
|
||||
top: scrollEventElement.current?.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
instanceRef.current?.update?.();
|
||||
}, 100)
|
||||
);
|
||||
|
||||
const scrollauto = useMemoizedFn(() => {
|
||||
scrollEventElement.current?.scrollTo?.({
|
||||
top: scrollEventElement.current.scrollHeight,
|
||||
behavior: 'auto'
|
||||
});
|
||||
instanceRef.current?.update?.();
|
||||
});
|
||||
|
||||
// scroll to bottom
|
||||
const throttledUpdateScrollerPosition = useMemoizedFn((delay?: number) => {
|
||||
if (stopUpdatePosition.current) {
|
||||
return;
|
||||
}
|
||||
if (delay === 0) {
|
||||
scrollauto();
|
||||
} else {
|
||||
throttledScroll();
|
||||
}
|
||||
});
|
||||
|
||||
// scroll to top
|
||||
const updateScrollerPositionToTop = useMemoizedFn(() => {
|
||||
scrollEventElement.current?.scrollTo?.({
|
||||
top: 0,
|
||||
behavior: 'auto'
|
||||
});
|
||||
instanceRef.current?.update?.();
|
||||
});
|
||||
|
||||
const generateInstance = () => {
|
||||
instanceRef.current = instance?.();
|
||||
scrollEventElement.current =
|
||||
instanceRef.current?.elements()?.scrollEventElement;
|
||||
};
|
||||
|
||||
const handleWheelCallback = useMemoizedFn((e: any) => {
|
||||
handleOnScroll();
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
timerRef.current = setTimeout(() => {
|
||||
stopUpdatePosition.current = false;
|
||||
}, RESETSCROLLDELAY);
|
||||
});
|
||||
|
||||
// add wheel event
|
||||
const handleWheelEvent = () => {
|
||||
scrollElementRef.current?.addEventListener?.('wheel', handleWheelCallback, {
|
||||
passive: true
|
||||
});
|
||||
};
|
||||
|
||||
// remove wheel event
|
||||
const removeWheelEvent = () => {
|
||||
scrollElementRef.current?.removeEventListener?.(
|
||||
'wheel',
|
||||
handleWheelCallback,
|
||||
{ passive: true }
|
||||
);
|
||||
};
|
||||
|
||||
const createInstance = useMemoizedFn((el: any) => {
|
||||
if (instanceRef.current) {
|
||||
return instanceRef.current;
|
||||
}
|
||||
if (el) {
|
||||
initialize(el);
|
||||
scrollElementRef.current = el;
|
||||
initialized.current = true;
|
||||
instanceRef.current = instance?.();
|
||||
scrollEventElement.current =
|
||||
instanceRef.current?.elements()?.scrollEventElement;
|
||||
handleWheelEvent();
|
||||
}
|
||||
return instanceRef.current;
|
||||
});
|
||||
|
||||
const destroyInstance = () => {
|
||||
instanceRef.current?.destroy?.();
|
||||
removeWheelEvent();
|
||||
instanceRef.current = null;
|
||||
};
|
||||
|
||||
const scrollToTarget = (target: any, offset = 100) => {
|
||||
if (!target) return;
|
||||
if (!instanceRef.current || !scrollEventElement.current) {
|
||||
instanceRef.current = instance?.();
|
||||
scrollEventElement.current =
|
||||
instanceRef.current?.elements()?.scrollEventElement;
|
||||
}
|
||||
|
||||
const viewport = instanceRef.current?.elements().viewport;
|
||||
const containerRect = viewport.getBoundingClientRect();
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
|
||||
const scrollerState = instanceRef.current?.state();
|
||||
|
||||
const currentScroll = scrollerState.current?.overflowAmount?.y;
|
||||
|
||||
// const currentScroll = instanceRef.current?.scroll().position.y;
|
||||
const targetPos = targetRect.top - containerRect.top + currentScroll;
|
||||
|
||||
scrollEventElement.current.scroll({
|
||||
y: targetPos - offset,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
instanceRef.current?.update?.();
|
||||
};
|
||||
|
||||
const getScrollElementScrollableHeight = () => {
|
||||
if (!instanceRef.current || !scrollEventElement.current) {
|
||||
instanceRef.current = instance?.();
|
||||
scrollEventElement.current =
|
||||
instanceRef.current?.elements()?.scrollEventElement;
|
||||
}
|
||||
|
||||
const scrollOffsetElement = instanceRef.current?.elements().viewport;
|
||||
|
||||
return {
|
||||
scrollTop: scrollOffsetElement?.scrollTop,
|
||||
scrollHeight:
|
||||
scrollOffsetElement?.scrollHeight - scrollOffsetElement?.clientHeight
|
||||
};
|
||||
};
|
||||
|
||||
const getScrollElement = () => {
|
||||
if (!instanceRef.current || !scrollEventElement.current) {
|
||||
instanceRef.current = instance?.();
|
||||
scrollEventElement.current =
|
||||
instanceRef.current?.elements()?.scrollEventElement;
|
||||
}
|
||||
return scrollEventElement;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
instanceRef.current?.destroy?.();
|
||||
removeWheelEvent();
|
||||
};
|
||||
}, [instance]);
|
||||
|
||||
return {
|
||||
initialize: createInstance,
|
||||
instance: instanceRef,
|
||||
scrollEventElement: scrollEventElement,
|
||||
initialized: initialized.current,
|
||||
getScrollElementScrollableHeight,
|
||||
getScrollElement,
|
||||
generateInstance,
|
||||
destroyInstance: destroyInstance,
|
||||
updateScrollerPosition: throttledUpdateScrollerPosition,
|
||||
updateScrollerPositionToTop: updateScrollerPositionToTop,
|
||||
scrollToBottom: scrollauto,
|
||||
scrollToTop: updateScrollerPositionToTop,
|
||||
scrollToTarget
|
||||
};
|
||||
}
|
||||
@@ -40,11 +40,6 @@ export default function useWatchList<T = Record<string, any>>(API: string) {
|
||||
}
|
||||
});
|
||||
|
||||
const cancelWatch = useMemoizedFn(() => {
|
||||
chunkRequestRef.current?.current?.cancel?.();
|
||||
listRequestTokenRef.current?.cancel?.();
|
||||
});
|
||||
|
||||
const queryAllDataList = async (
|
||||
params: Global.SearchParams,
|
||||
options?: any
|
||||
@@ -83,15 +78,14 @@ export default function useWatchList<T = Record<string, any>>(API: string) {
|
||||
useEffect(() => {
|
||||
createWatchChunkRequest();
|
||||
return () => {
|
||||
cancelWatch();
|
||||
chunkRequestRef.current?.cancel?.();
|
||||
listRequestTokenRef.current?.cancel?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
watchDataList,
|
||||
setWatchDataList,
|
||||
startWatch: createWatchChunkRequest,
|
||||
cancelWatch,
|
||||
deleteItemFromCache: handleDeleteItemFromCache
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
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';
|
||||
import {
|
||||
DiscordOutlined,
|
||||
@@ -14,12 +11,11 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { DropdownActions, IconFont } from '@gpustack/core-ui';
|
||||
import { history, useIntl, useNavigate } from '@umijs/max';
|
||||
import { Avatar, Button, Divider, Modal } from 'antd';
|
||||
import { Avatar, Divider } 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;
|
||||
@@ -99,12 +95,9 @@ const CustomItem = styled.div`
|
||||
|
||||
export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
||||
const { isDarkTheme } = props;
|
||||
const plugin = getGPUStackPlugin();
|
||||
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const intl = useIntl();
|
||||
const [version] = useAtom(GPUStackVersionAtom);
|
||||
const [updateCheck] = useAtom(UpdateCheckAtom);
|
||||
const intl = useIntl();
|
||||
const initialInfo = useModel('@@initialState') || {
|
||||
initialState: undefined,
|
||||
loading: false,
|
||||
@@ -142,16 +135,6 @@ 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);
|
||||
@@ -161,7 +144,7 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
||||
{
|
||||
key: 'site',
|
||||
icon: <HomeOutlined />,
|
||||
label: 'GPUStack',
|
||||
label: 'MesaStack',
|
||||
url: externalLinks.site
|
||||
},
|
||||
{
|
||||
@@ -261,25 +244,20 @@ 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'
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={showVersion}
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--ant-color-text-tertiary)'
|
||||
}}
|
||||
>
|
||||
{version.version}
|
||||
</Button>
|
||||
</span>
|
||||
{showUpgrade && (
|
||||
<NewLabel>
|
||||
<span className="text">
|
||||
@@ -288,17 +266,15 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
||||
</NewLabel>
|
||||
)}
|
||||
</div>
|
||||
{!plugin && (
|
||||
<DropdownActions menu={{ ...helpMenu }} popupRender={helpPopupRender}>
|
||||
<IconWrapper>
|
||||
<IconFont
|
||||
type="icon-help"
|
||||
className="font-size-20"
|
||||
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
||||
/>
|
||||
</IconWrapper>
|
||||
</DropdownActions>
|
||||
)}
|
||||
<DropdownActions menu={{ ...helpMenu }} popupRender={helpPopupRender}>
|
||||
<IconWrapper>
|
||||
<IconFont
|
||||
type="icon-help"
|
||||
className="font-size-20"
|
||||
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
||||
/>
|
||||
</IconWrapper>
|
||||
</DropdownActions>
|
||||
<PluginExtraField name="GlobalSettings" />
|
||||
<DropdownActions menu={{ ...userMenu }} popupRender={userPopupRender}>
|
||||
<IconWrapper>
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
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;
|
||||
@@ -0,0 +1,128 @@
|
||||
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, IconFont } from '@gpustack/core-ui';
|
||||
import { CoreUIProvider } from '@gpustack/core-ui';
|
||||
import {
|
||||
Access,
|
||||
Outlet,
|
||||
@@ -39,18 +39,33 @@ import {
|
||||
useNavigate,
|
||||
type IRoute
|
||||
} from '@umijs/max';
|
||||
import { Button, ConfigProvider, Modal, theme } from 'antd';
|
||||
import { 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, SLogoIcon } from './Logo';
|
||||
import { LogoIcon } 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';
|
||||
|
||||
// Pages that use the page container in the page
|
||||
const NO_CONTAINER_PAGES = [
|
||||
'chat',
|
||||
'rerank',
|
||||
'embedding',
|
||||
'speech',
|
||||
'image',
|
||||
'text2images',
|
||||
'clusterDetail',
|
||||
'clusterCreate',
|
||||
'benchmarkDetail',
|
||||
'deployment',
|
||||
'video'
|
||||
];
|
||||
|
||||
const CHECK_RESOURCE_PATH = [
|
||||
'/resources/workers',
|
||||
@@ -118,7 +133,7 @@ const mapRoutes = (routes: IRoute[], role: string) => {
|
||||
|
||||
export default (props: any) => {
|
||||
const [, contextHolder] = Modal.useModal();
|
||||
const { themeData, setUserSettings, userSettings } = useUserSettings();
|
||||
const { themeData, userSettings } = useUserSettings();
|
||||
const [userInfo] = useAtom(userAtom);
|
||||
const [routeCache] = useAtom(routeCacheAtom);
|
||||
const location = useLocation();
|
||||
@@ -223,13 +238,6 @@ 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'),
|
||||
@@ -254,16 +262,24 @@ export default (props: any) => {
|
||||
[location.pathname]
|
||||
);
|
||||
|
||||
const collapsed = useMemo(() => {
|
||||
return userSettings.collapsed || false;
|
||||
}, [userSettings.collapsed]);
|
||||
const isNoContainerPage = useMemo(() => {
|
||||
// @ts-ignore
|
||||
return NO_CONTAINER_PAGES.includes(matchedRoute?.name as string);
|
||||
}, [matchedRoute]);
|
||||
|
||||
const renderMenuHeader = (logo: React.ReactNode, title: React.ReactNode) => {
|
||||
return <>{logo}</>;
|
||||
};
|
||||
|
||||
const menuContentRender = (menuProps: any, defaultDom: React.ReactNode) => {
|
||||
return <SiderMenu {...menuProps}></SiderMenu>;
|
||||
const headerContentRender = (
|
||||
headerProps: any,
|
||||
defaultDom: React.ReactNode
|
||||
) => {
|
||||
return <HeaderMenu {...headerProps}></HeaderMenu>;
|
||||
};
|
||||
|
||||
const actionsRender = () => {
|
||||
return <ExtraContent isDarkTheme={userSettings.isDarkTheme} />;
|
||||
};
|
||||
|
||||
const onPageChange = async (route: any) => {
|
||||
@@ -317,17 +333,6 @@ 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"
|
||||
@@ -388,48 +393,27 @@ export default (props: any) => {
|
||||
<DarkMask></DarkMask>
|
||||
<ProLayout
|
||||
fixSiderbar
|
||||
fixedHeader={false}
|
||||
headerRender={false}
|
||||
fixedHeader
|
||||
breadcrumbRender={false}
|
||||
route={route}
|
||||
location={location}
|
||||
title={userConfig.title}
|
||||
navTheme={userSettings.theme}
|
||||
layout="side"
|
||||
layout="top"
|
||||
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,
|
||||
type: 'group'
|
||||
locale: true
|
||||
}}
|
||||
splitMenus={true}
|
||||
logo={userSettings.collapsed ? <SLogoIcon /> : <LogoIcon />}
|
||||
menuContentRender={menuContentRender}
|
||||
logo={<LogoIcon />}
|
||||
headerContentRender={headerContentRender}
|
||||
actionsRender={actionsRender}
|
||||
{...runtimeConfig}
|
||||
ErrorBoundary={ErrorBoundary}
|
||||
>
|
||||
@@ -437,7 +421,7 @@ export default (props: any) => {
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100vh',
|
||||
height: '100%',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
@@ -449,11 +433,15 @@ export default (props: any) => {
|
||||
unAccessible={runtimeConfig?.unAccessible}
|
||||
noAccessible={runtimeConfig?.noAccessible}
|
||||
>
|
||||
<PageContainerInner>
|
||||
<div>
|
||||
<Outlet />
|
||||
</div>
|
||||
</PageContainerInner>
|
||||
{isNoContainerPage ? (
|
||||
<Outlet />
|
||||
) : (
|
||||
<PageContainerInner>
|
||||
<div>
|
||||
<Outlet />
|
||||
</div>
|
||||
</PageContainerInner>
|
||||
)}
|
||||
</Exception>
|
||||
</div>
|
||||
{NoResourceModal}
|
||||
|
||||
@@ -82,7 +82,7 @@ export const getRightRenderContent = (opts: {
|
||||
{
|
||||
key: 'site',
|
||||
icon: <HomeOutlined />,
|
||||
label: 'GPUStack',
|
||||
label: 'MesaStack',
|
||||
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 GPUStack Enterprise to manage billing.',
|
||||
'Track spend, generate invoices, and enforce budgets across teams. Upgrade to MesaStack Enterprise to manage billing.',
|
||||
'billing.upsell.featuresTitle': 'What you get in Enterprise',
|
||||
'billing.upsell.feature.usage':
|
||||
'See cost breakdowns by organization, user, and model',
|
||||
|
||||
@@ -44,8 +44,6 @@ export default {
|
||||
'On the Worker that needs to be added, run the following command to join it to the cluster.',
|
||||
'clusters.create.addCommand.k8s.tips':
|
||||
'On the Kubernetes cluster that needs to be registered, run the following command to create the Kubernetes resources and register the cluster.',
|
||||
'clusters.create.addCommand.k8s.version.warning':
|
||||
'The minimum supported Kubernetes version is 1.23. To use the GPU Service feature, the minimum supported Kubernetes version is 1.27.',
|
||||
'clusters.create.register.tips':
|
||||
'On the Kubernetes cluster that needs to be added, run the following command to join its nodes to the cluster.',
|
||||
'cluster.create.checkEnv.tips':
|
||||
@@ -87,7 +85,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 GPUStack Server.',
|
||||
'If running in a VPC or private network, please specify the Worker external address reachable by the MesaStack 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 +113,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': 'GPUStack Server URL',
|
||||
'clusters.create.serverUrl': 'MesaStack 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': 'GPUStack Data Volume',
|
||||
'clusters.addworker.dataVolume': 'MesaStack Data Volume',
|
||||
'clusters.addworker.dataVolume.tips':
|
||||
'Specify a data storage path for GPUStack.',
|
||||
'Specify a data storage path for MesaStack.',
|
||||
'clusters.table.ip.internal': 'Internal',
|
||||
'clusters.table.ip.external': 'External',
|
||||
'clusters.form.serverUrl.tips':
|
||||
'Specify an externally accessible GPUStack service URL if the worker cannot access GPUStack Server directly.',
|
||||
'Specify an externally accessible MesaStack service URL if the worker cannot access MesaStack 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 +144,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 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.',
|
||||
'The built-in inference backends in MesaStack 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 +171,7 @@ export default {
|
||||
'clusters.volume.add': 'Add Volume Mount',
|
||||
'clusters.systemDefaultContainerRegistry.title': 'Default Container Registry',
|
||||
'clusters.systemDefaultContainerRegistry.tip':
|
||||
'Default registry used to resolve GPUStack images for this cluster. Falls back to the server default when unset.',
|
||||
'Default registry used to resolve MesaStack 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 +183,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 GPUStack Operator container image. Leave empty to use the server default.',
|
||||
'Override for the MesaStack 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 GPUStack Enterprise',
|
||||
'common.enterprise.feature': 'Available in MesaStack 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': 'GPUStack.ai',
|
||||
'settings.company': 'MesaStack',
|
||||
'common.button.help': 'Help',
|
||||
'common.button.feedback': 'Feedback',
|
||||
'common.button.docs': 'Documentation',
|
||||
@@ -267,10 +267,6 @@ export default {
|
||||
'common.select.count': '{count} selected',
|
||||
'common.login.auth': 'Authenticating...',
|
||||
'common.login.auth.failed': 'Authentication failed',
|
||||
'common.login.error.source_conflict':
|
||||
'An account with this username already exists from a different authentication source. Please contact an administrator to link or convert it.',
|
||||
'common.login.error.auth_failed':
|
||||
'Authentication with the identity provider failed. Please try again or contact your administrator.',
|
||||
'common.login.password': 'Log in with Password',
|
||||
'common.login.username.holder': 'Please enter username',
|
||||
'common.login.password.holder': 'Please enter password',
|
||||
|
||||
@@ -2,7 +2,6 @@ export default {
|
||||
'gpuservice.template': 'GPU Instance Template',
|
||||
'gpuservice.template.add': 'Add Instance Template',
|
||||
'gpuservice.template.edit': 'Edit Instance Template',
|
||||
'gpuservice.template.clone': 'Clone Instance Template',
|
||||
'gpuservice.template.filter.name': 'Filter by name',
|
||||
'gpuservice.template.filter.vendor': 'Filter by vendor',
|
||||
'gpuservice.template.image': 'Image',
|
||||
|
||||
@@ -13,7 +13,7 @@ export default {
|
||||
'menu.models.modelCatalog': 'Catalog',
|
||||
'menu.models.catalog': 'Model Catalog',
|
||||
'menu.models.deployment': 'Deployments',
|
||||
'menu.models.userModels': 'Models',
|
||||
'menu.models.userModels': 'My Models',
|
||||
'menu.models.benchmark': 'Benchmarks',
|
||||
'menu.models.benchmarkDetail': 'Benchmark Details',
|
||||
'menu.models.providers': 'Providers',
|
||||
|
||||
@@ -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, 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.partialoffload.tips': `When CPU offloading is enabled, MesaStack 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 GPUStack. 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 MesaStack. 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 GPUStack 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 MesaStack 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 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.modelfile.notfound': `The model file path you specified does not exist on the MesaStack server. It's recommended to place the model file at the same path on both the MesaStack server and MesaStack workers. This helps MesaStack make better decisions.`,
|
||||
'models.form.readyWorkers': 'workers ready',
|
||||
'models.form.maxContextLength': 'Maximum Context Length',
|
||||
'models.form.backend.helperText':
|
||||
|
||||
@@ -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 GPUStack Enterprise to manage organizations.',
|
||||
'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to MesaStack 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 GPUStack installation.',
|
||||
'Install <a href="https://docs.gpustack.ai/latest/installation/installation-requirements/" target="_blank">required drivers and libraries</a> prior to MesaStack 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 GPUStack on {os}',
|
||||
'resources.register.install.title': 'Install MesaStack 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 GPUStack Server version.',
|
||||
'Please upgrade to match the MesaStack Server version.',
|
||||
'resources.worker.version': 'Worker Version: {version}',
|
||||
'resources.server.version': 'Server Version: {version}',
|
||||
'resources.worker.currentVersion': 'Current Version: {version}',
|
||||
|
||||
@@ -12,11 +12,6 @@ export default {
|
||||
'users.form.active.description': 'Enable or disable this user account',
|
||||
'users.form.fullname': 'Full Name',
|
||||
'users.form.source': 'Source',
|
||||
'users.form.source.local': 'Local',
|
||||
'users.form.source.tip.switchToLocal':
|
||||
'Switching to Local requires a new password. The user will sign in via the standard login form.',
|
||||
'users.form.source.tip.switchToExternal':
|
||||
"Switching to an external source clears the user's local password. They will sign in via the configured identity provider.",
|
||||
'users.table.user': 'users',
|
||||
'users.form.admin': 'Admin',
|
||||
'users.form.user': 'User',
|
||||
@@ -39,12 +34,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': 'GPUStack {version} is the latest version',
|
||||
'users.version.update': 'GPUStack {version} is available',
|
||||
'users.version.islatest': 'MesaStack {version} is the latest version',
|
||||
'users.version.update': 'MesaStack {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 GPUStack Server to retrieve the initial admin password.'
|
||||
'Run the following command on your MesaStack 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 GPUStack Enterprise to manage billing.',
|
||||
'Track spend, generate invoices, and enforce budgets across teams. Upgrade to MesaStack Enterprise to manage billing.',
|
||||
'billing.upsell.featuresTitle': 'What you get in Enterprise',
|
||||
'billing.upsell.feature.usage':
|
||||
'See cost breakdowns by organization, user, and model',
|
||||
|
||||
@@ -44,8 +44,6 @@ export default {
|
||||
'On the Worker that needs to be added, run the following command to join it to the cluster.',
|
||||
'clusters.create.addCommand.k8s.tips':
|
||||
'登録する Kubernetes クラスターで以下のコマンドを実行し、Kubernetes リソースを作成してクラスターを登録します。',
|
||||
'clusters.create.addCommand.k8s.version.warning':
|
||||
'サポートされる Kubernetes の最小バージョンは 1.23 です。GPU Service 機能を使用する場合、サポートされる Kubernetes の最小バージョンは 1.27 です。',
|
||||
'cluster.create.checkEnv.tips':
|
||||
'Use the following command to check if the environment is ready.',
|
||||
'clusters.create.register.tips':
|
||||
@@ -115,20 +113,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': 'GPUStack Server URL',
|
||||
'clusters.create.serverUrl': 'MesaStack 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': 'GPUStack Data Volume',
|
||||
'clusters.addworker.dataVolume': 'MesaStack Data Volume',
|
||||
'clusters.addworker.dataVolume.tips':
|
||||
'Specify a data storage path for GPUStack.',
|
||||
'Specify a data storage path for MesaStack.',
|
||||
'clusters.table.ip.internal': 'Internal',
|
||||
'clusters.table.ip.external': 'External',
|
||||
'clusters.form.serverUrl.tips':
|
||||
'Specify an externally accessible GPUStack service URL if the worker cannot access GPUStack Server directly.',
|
||||
'Specify an externally accessible MesaStack service URL if the worker cannot access MesaStack 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 +144,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 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.',
|
||||
'The built-in inference backends in MesaStack 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 +171,7 @@ export default {
|
||||
'clusters.volume.add': 'Add Volume Mount',
|
||||
'clusters.systemDefaultContainerRegistry.title': 'Default Container Registry',
|
||||
'clusters.systemDefaultContainerRegistry.tip':
|
||||
'Default registry used to resolve GPUStack images for this cluster. Falls back to the server default when unset.',
|
||||
'Default registry used to resolve MesaStack 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 +183,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 GPUStack Operator container image. Leave empty to use the server default.',
|
||||
'Override for the MesaStack 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 +276,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': 'GPUStack Server URL',
|
||||
// 76. 'clusters.create.serverUrl': 'MesaStack 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': 'GPUStack Data Volume',
|
||||
// 78. 'clusters.addworker.dataVolume.tips': 'Specify a data storage path for GPUStack.',
|
||||
// 77. 'clusters.addworker.dataVolume': 'MesaStack Data Volume',
|
||||
// 78. 'clusters.addworker.dataVolume.tips': 'Specify a data storage path for MesaStack.',
|
||||
// 79. 'clusters.table.ip.internal': 'Internal',
|
||||
// 80. 'clusters.table.ip.external': 'External',
|
||||
// 81. 'clusters.form.serverUrl.tips': 'Specify an externally accessible GPUStack service URL if the worker cannot access GPUStack Server directly.',
|
||||
// 81. 'clusters.form.serverUrl.tips': 'Specify an externally accessible MesaStack service URL if the worker cannot access MesaStack 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 +300,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 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.'
|
||||
// 101. 'clusters.addworker.nvidiaNotes': 'The built-in inference backends in MesaStack 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 GPUStack Enterprise',
|
||||
'common.enterprise.feature': 'Available in MesaStack 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': 'GPUStack.ai',
|
||||
'settings.company': 'MesaStack',
|
||||
'common.button.help': 'ヘルプ',
|
||||
'common.button.feedback': 'フィードバック',
|
||||
'common.button.docs': 'ドキュメント',
|
||||
@@ -266,10 +266,6 @@ export default {
|
||||
'common.select.count': '{count} selected',
|
||||
'common.login.auth': 'Authenticating...',
|
||||
'common.login.auth.failed': 'Authentication failed',
|
||||
'common.login.error.source_conflict':
|
||||
'このユーザー名のアカウントは別の認証ソースで既に存在します。管理者にリンクまたは変換を依頼してください。',
|
||||
'common.login.error.auth_failed':
|
||||
'ID プロバイダーでの認証に失敗しました。再試行するか、管理者にお問い合わせください。',
|
||||
'common.login.password': 'Log in with Password',
|
||||
'common.login.username.holder': 'Please enter username',
|
||||
'common.login.password.holder': 'Please enter password',
|
||||
|
||||
@@ -2,7 +2,6 @@ export default {
|
||||
'gpuservice.template': 'GPU インスタンステンプレート',
|
||||
'gpuservice.template.add': 'インスタンステンプレートを追加',
|
||||
'gpuservice.template.edit': 'インスタンステンプレートを編集',
|
||||
'gpuservice.template.clone': 'インスタンステンプレートを複製',
|
||||
'gpuservice.template.filter.name': '名前でフィルター',
|
||||
'gpuservice.template.filter.vendor': 'ベンダーでフィルター',
|
||||
'gpuservice.template.image': 'コンテナイメージ',
|
||||
|
||||
@@ -41,7 +41,7 @@ export default {
|
||||
'menu.accessControl.organizations': 'Organizations',
|
||||
'menu.resources.clusters': 'Clusters',
|
||||
'menu.resources.credentials': 'Cloud Credentials',
|
||||
'menu.models.userModels': 'Models',
|
||||
'menu.models.userModels': 'My Models',
|
||||
'menu.resources.clusterDetail': 'Cluster Detail',
|
||||
'menu.resources.clusterCreate': 'Create Cluster',
|
||||
'menu.models.backendsList': 'Inference Backends',
|
||||
@@ -62,11 +62,11 @@ export default {
|
||||
// 6. 'menu.accessControl.apikeys': 'API Keys',
|
||||
// 7. 'menu.accessControl.users': 'Users',
|
||||
// 8. 'menu.clusterManagement': 'Cluster Management',
|
||||
// 9. 'menu.resources.clusters': 'Clusters',
|
||||
// 10. 'menu.resources.credentials': 'Cloud Credentials',
|
||||
// 11. 'menu.models.userModels': 'Models'
|
||||
// 12. 'menu.resources.clusterDetail': 'Cluster Detail',
|
||||
// 13. 'menu.resources.clusterCreate': 'Create Cluster',
|
||||
// 9. 'menu.clusterManagement.clusters': 'Clusters',
|
||||
// 10. 'menu.clusterManagement.credentials': 'Cloud Credentials',
|
||||
// 11. 'menu.models.userModels': 'My Models'
|
||||
// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail',
|
||||
// 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster',
|
||||
// 14. 'menu.models.backendsList': 'Inference Backends',
|
||||
// 15. 'menu.models.benchmark': 'Benchmarks',
|
||||
// 15. 'menu.models.provider': 'Provider',
|
||||
|
||||
@@ -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, 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.partialoffload.tips': `When CPU offloading is enabled, MesaStack 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':
|
||||
'ヒント: 以下はGPUStackで事前設定されたOllamaモデルです。希望するモデルを選択するか、右側の【{name}】入力ボックスにデプロイしたいモデルを直接入力してください。',
|
||||
'ヒント: 以下はMesaStackで事前設定された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}バージョンを使用するには、システムがオンライン環境で対応するバージョンをインストールする仮想環境を自動的に作成します。GPUStackのアップグレード後もバックエンドバージョンは固定されます。{link}',
|
||||
'希望する{backend}{version}バージョンを使用するには、システムがオンライン環境で対応するバージョンをインストールする仮想環境を自動的に作成します。MesaStackのアップグレード後もバックエンドバージョンは固定されます。{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 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.modelfile.notfound': `The model file path you specified does not exist on the MesaStack server. It's recommended to place the model file at the same path on both the MesaStack server and MesaStack workers. This helps MesaStack 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, 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.`,
|
||||
// 65. 'models.form.partialoffload.tips': `When CPU offloading is enabled, MesaStack 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 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.`,
|
||||
// 74. 'models.form.modelfile.notfound': `The model file path you specified does not exist on the MesaStack server. It's recommended to place the model file at the same path on both the MesaStack server and MesaStack workers. This helps MesaStack 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. ',
|
||||
|
||||
@@ -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 GPUStack Enterprise to manage organizations.',
|
||||
'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to MesaStack 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> をGPUStackのインストール前にインストールしてください。',
|
||||
'<a href="https://docs.gpustack.ai/latest/installation/installation-requirements/" target="_blank">必要なドライバとライブラリ</a> をMesaStackのインストール前にインストールしてください。',
|
||||
'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 GPUStack on {os}',
|
||||
'resources.register.install.title': 'Install MesaStack 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 GPUStack Server version.',
|
||||
'Please upgrade to match the MesaStack 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 GPUStack on {os}',
|
||||
// 8. 'resources.register.install.title': 'Install MesaStack 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 GPUStack Server version.'
|
||||
// 25. 'resoureces.worker.upgrade.tips': 'Please upgrade to match the MesaStack Server version.'
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||