Compare commits

...
Author SHA1 Message Date
lofyerandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> a90e02a04c feat(layout): move navigation menu from sidebar to top header
Switch ProLayout to top layout with a custom horizontal HeaderMenu
(dropdowns for grouped items) and relocate the user/help/version actions
into the header right area.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-07-02 09:17:48 +08:00
lofyerandClaude Opus 4.8 9a223f9565 feat(branding): rebrand footer vendor to MesaStack, trim footer/topbar
- Footer vendor label unified to "MesaStack" across all locales.
- Remove footer Help button; keep version as plain text (no click).
- Remove top-right GitHub star widget (delete unused github-star.tsx);
  keep version as plain text (no click), retain upgrade "New" badge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 12:35:39 +08:00
lofyerandClaude Opus 4.8 4150070829 docs(agents): document rebrand step in fork workflow
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 22:16:54 +08:00
lofyerandClaude Opus 4.8 62351a781f feat(branding): rebrand user-facing GPUStack to MesaStack
Applied via scripts/rebrand across UI labels, page title, and all
locale strings (en/zh/ja/ru/tr). Functional references untouched:
npm package, URLs, paths, JS identifiers, GPUSTACK_ constants, and the
X-GPUStack-Model header. The backend-contract startsWith string in
llmodels/hooks keeps "GPUStack server" to stay matchable. Logo images
intentionally left unchanged for now.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 22:16:17 +08:00
lofyerandClaude Opus 4.8 eb2561ff1e feat(scripts): add rebrand script to swap brand identity
Replace the standalone "GPUStack" brand word with a target brand
(default MesaStack) across user-facing text. Word-boundary + case-
sensitive matching deliberately skips functional references: lowercase
gpustack (npm pkg/URLs/paths/k8s ns), ALL-CAPS GPUSTACK_ constants,
JS identifiers (getGPUStackPlugin etc.), and X-* HTTP headers. A
line-level skip list protects backend-contract strings matched at
runtime (e.g. the model-file-not-found startsWith check).

Re-run on each new v<version>-lofyer branch after syncing upstream.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 22:16:01 +08:00
lofyerandClaude Opus 4.8 c6643f2bc9 docs(agents): document downstream fork sync workflow
Describe the upstream->private mirror chain, the v<version>-lofyer branch
naming for appearance customizations, and how to run scripts/sync-github.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 20:56:50 +08:00
lofyerandClaude Opus 4.8 8901118bd2 feat(scripts): add sync-github to mirror upstream to private registry
Fetch all branches and tags from upstream GitHub and force-push them to
the 192.168.0.23 private origin (full mirror). Supports DRY_RUN and an
opt-in PRUNE_BRANCHES for true mirroring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 20:55:01 +08:00
jialinandjialin db17192e55 fix(usage): carry full filter set in breakdown tables and fix double fetch
- breakdown sub-tables now send all active filters (route/user/api_key), matching the trend chart
- summary tab filters the token trend by user and unions user options from both meta APIs (deduped by id)
- stabilize the filters reference so meta load no longer retriggers a second fetch on mount
2026-06-30 17:02:17 +08:00
gitlawr a1dd1ec861 ci: trigger backend pack on UI merge
After build-publish uploads the UI tarball to COS, dispatch a
repository_dispatch event (ui-built) to gpustack/gpustack so the
backend repack picks up the new UI.

Skipped for tag pushes — release tags drive the backend pack via
its own tag trigger. Requires a DISPATCH_PAT secret with
actions:write on gpustack/gpustack.
2026-06-30 15:57:33 +08:00
jialinandjialin a516e6ce72 feat(dashboard): split top user token usage into prompt/completion 2026-06-30 14:38:35 +08:00
micheliaandjialin 2e375c6603 fix(usage): label per-instance rows by shape, not an undefined title
The Instances-tab Instance Type column passed isCpu ? cpuOnlyLabel : undefined.
For a GPU row with vram but a missing/zero gpu_count, undefined let
renderInstanceType fall back to "CPU Only" (acceleratable=false). Pass
instanceTypeSeriesLabel(row) directly — consistent with the Instance Types
column and robust to that edge. (PR #1245 review)
2026-06-30 14:11:39 +08:00
micheliaandjialin 7d94c77c15 fix(usage): sum same-label buckets in the trend series
buildTrendSeries overwrote a (label, bucket) value on collision. Should two
backend groups ever map to the same display label, their values must add up
rather than letting the last write win. Accumulate instead.
2026-06-30 14:11:39 +08:00
micheliaandjialin 457d2f2f72 feat(usage): render instance types by actual shape (CPU spec / GPU x cards)
Instance Types are grouped by actual shape now, so each row/series is one
concrete type. Label them accordingly via a shared instanceTypeSeriesLabel,
used by both the table column and the trend legend so they read identically:
GPU shows "<product> x <cards>", CPU shows "CPU Only · N vCPU · M GB" (the
instance's real size, not the bare "CPU Only" + hover tooltip). The Instances
tab also uses the per-instance cpu/mem totals, so a 3c6g instance of a 1c2g
flavor reads 3c6g instead of 1c2g. flatten carries cpu_milli/memory_mib/
gpu_count from the breakdown dimensions.
2026-06-30 14:11:39 +08:00
gitlawrandjialin 5e7d83e5dd fix(api-keys): let platform admin create a non-tenant-pinned API key
The CreateOrgScopeField slot only renders a "Global" choice when the
host form passes ``allowGlobal: true``. Without it, an admin in the
"All Orgs" view sees only Personal + every org in the dropdown — and
the form's submit injects an ``X-Organization-Id`` header for every
choice, pinning the new key to a tenant. The matching backend change
(api_keys.owner_principal_id nullable) only kicks in when no header
is sent.

Pass ``allowGlobal: true`` (default-selects Global for admin) plus
``globalLabelId: 'scope.global'`` so the dropdown option reads as
plain "Global" — matching the existing tag / column labels for the
same concept elsewhere in the UI, instead of the longer
inference-backend "shared with all organizations" copy.

Relax the ListItem type so ``owner_principal_id`` may be ``null`` —
the OrganizationCell / OwnerScopeTag renderers already render the
"Global" placeholder for null/undefined; the type was the only thing
still claiming the column was always present.
2026-06-30 11:54:00 +08:00
jialinandjialin b1cffe047c docs: expand CLAUDE.md conventions (naming, types, config, core-ui)
- Clarify styles rule: avoid styled-components in new code; existing
  usage is legacy debt, not migrated wholesale
- Add naming conventions (module structure, file naming)
- Add config & types section (FormData/ListItem, config/index.ts)
- Expand Common components with frequently-reused core-ui inventory
2026-06-29 19:44:28 +08:00
jialinandjialin 6509f2a4ff docs: consolidate component conventions into CLAUDE.md and skill
- Add "Dynamic add-item form fields" guidance to CLAUDE.md (pick
  component by field schema: LabelSelector / ListInput / MetadataList)
- Fold StatusTag status-display recipe into create-crud-page skill
- Remove DESIGN.md (content migrated to the above)
2026-06-29 19:44:28 +08:00
jialinandjialin f719c11606 refactor(perferences): use center wrapper layout 2026-06-29 19:44:28 +08:00
jialinandjialin b8d7873b77 chore: dedupe agent instruction files into CLAUDE.md 2026-06-29 12:10:18 +08:00
jialinandjialin a9248a98a5 chore: add create-crud-page and form-patterns skills, refresh CLAUDE.md 2026-06-29 12:10:18 +08:00
micheliaandjialin 8f85e9a082 fix(usage): use frontend gpu_type token for instance-type legend
flattenItem receives the unmapped frontend dimension; the instance-type
grouping is 'gpu_type' (mapped to backend 'instance_type' via GROUP_BY_MAP),
so the previous 'instance_type' check never matched and the trend legend kept
showing the raw flavor slug. Check 'gpu_type'. (PR #1240 review)
2026-06-27 22:27:42 +08:00
micheliaandjialin 1a6d1654b2 fix(usage): label instance_type trend series by product name
The grouped-trend legend used the raw group key, which for instance_type is
the flavor slug (e.g. gpustack--generic-ln-x64-40c-160g--nvidia-a100-80g-4d).
Prefer the enriched product name so the chart legend matches the GPU Instances
list (#5700); falls back to the slug for legacy rows without dimension data.
2026-06-27 22:27:42 +08:00
micheliaandjialin abe705c034 fix(usage): fetch full trend/export series via page=-1
Trend charts and breakdown exports need the complete date series. The old
perPage=10000 workaround (and the missing page param on the token trend,
which fell back to perPage=20) dropped low-traffic — often most recent —
buckets, leaving gaps in the charts (#5690).

Send page=-1 (the backend's no-pagination sentinel) for the token trend,
the storage/instances trend charts and table exports, the resource export
dialog, and the summary-tab date trends.
2026-06-27 22:27:42 +08:00
jialinandjialin 8b34f68824 feat(cluster): resource topology entrance 2026-06-26 22:14:37 +08:00
jialinandjialin e37828a2cd fix: resource topology ux 2026-06-26 22:14:37 +08:00
jialinandjialin 4709661f93 fix(tts): reset form fields to initialvalues after model changed 2026-06-25 14:34:15 +08:00
jialinandjialin 1d7543f19c chore: adjust colors generator 2026-06-25 14:34:15 +08:00
jialinandjialin 1cf146ca32 fix(usage): dynamic bar color value 2026-06-24 20:54:16 +08:00
jialinandjialin b05c510776 fix: add warning tips when deleting ssh key 2026-06-24 20:54:16 +08:00
jialinandjialin e79d5f8962 fix(usage): selection maxTagCount 2026-06-24 20:54:16 +08:00
hibigandjialin 5503fdc21e chore: bump @gpustack/core-ui to v1.0.32 2026-06-24 19:00:22 +08:00
gitlawrandjialin 36a1038d12 feat(login): render SSO button data-driven from /auth/config
The backend now advertises the active external auth provider on
``/auth/config`` as a single ``external_auth: {type, login_url} | null``
field (replacing the per-provider ``is_oidc`` / ``is_saml`` booleans).
This is the API needed to add CAS without per-provider UI conditionals.

Wire the login UI accordingly:

- ``useSSOAuth`` exposes a single ``loginWithExternalAuth()`` action and
  ``options.external_auth`` carrying the provider info; the OIDC- and
  SAML-specific exports are gone.
- ``LoginForm`` renders one SSO button whenever ``external_auth`` is
  set, navigating to ``login_url``. New providers (CAS, future LDAP /
  Azure AD / …) need zero UI changes — only a backend route.
- ``LocalUserForm`` and ``LoginKit`` type definitions drop the
  per-provider booleans.
2026-06-24 18:59:19 +08:00
jialinandjialin ee42a9d0ed refactor(components): migrate shared components to core-ui and remove unused ones 2026-06-24 14:02:41 +08:00
jialinandjialin 28976dea4b fix(cardpage): duplicate data appears after clicking refresh 2026-06-24 14:02:41 +08:00
jialinandjialin 22e5cc3aa5 fix(usage): display usage tabs base on access permissions 2026-06-24 11:26:40 +08:00
jialinandjialin 3052c0b23e fix(app): run probe resources at two stages 2026-06-24 11:26:40 +08:00
jialinandjialin 1c9cabf7e7 fix(usage): instance type check conditions 2026-06-22 15:25:18 +08:00
jialinandjialin 03842d1741 fix: gpu selector height 2026-06-22 15:25:18 +08:00
jialinandjialin 84304eadb7 fix(playgroundimage): allow to input size 2026-06-18 16:03:35 +08:00
jialinandjialin ab3739219e chore: update vllm, sglang, preset argument options 2026-06-18 16:03:35 +08:00
jialinandjialin 7cf4932f3a fix(deploymentlog): show the checkbox only when container exsiting previous 2026-06-18 11:50:19 +08:00
jialinandjialin 032f1eefef fix(deployments): show a prefix before model name in playground 2026-06-18 11:50:19 +08:00
jialinandjialin e35be94f7c fix(provider): filter bailian do not work 2026-06-18 11:50:19 +08:00
jialinandjialin e76e2ad85a fix(worker): IP cell text wrapping 2026-06-18 11:50:19 +08:00
jialinandjialin cb19cd2476 fix: show cpu only spec info 2026-06-17 20:11:06 +08:00
jialinandjialin c532732ce9 fix: show CPU Only in usage instanc types 2026-06-17 18:29:58 +08:00
jialinandjialin 2bc10dbb2e fix: bailian logo, instance logs params container_name 2026-06-17 16:49:25 +08:00
jialinandjialin b634a03ef9 fix: use alibaba cloud icon for qwen provider 2026-06-17 15:30:43 +08:00
jialinandjialin ac64bb98ad fix: Only display name editable when editing storage and storage type 2026-06-17 12:35:24 +08:00
jialinandjialin aa49539210 fix: select gpu stype when creating cluster from gpu service 2026-06-17 12:35:24 +08:00
jialinandjialin 1f5678f781 fix: update nividia notes 2026-06-17 12:35:24 +08:00
jialinandjialin 7adc089e94 fix: save the default_width, default_height in image size options 2026-06-17 12:35:24 +08:00
jialinandjialin 6e1e9c07ee fix: template name readonly in editting 2026-06-17 11:06:39 +08:00
jialinandjialin 1d55382f3b fix: fetch all model instance after stopping 2026-06-17 11:06:39 +08:00
jialinandjialin e68bb0bc4e fix: export data should contain all tables 2026-06-16 22:28:46 +08:00
jialinandjialin c8e14dade5 fix: remove vendor logo in template list 2026-06-16 22:28:46 +08:00
jialinandjialin e4c1a3f5c2 refactor: usage instance tab, split tables 2026-06-16 22:28:46 +08:00
jialinandjialin cdd6ef9cb3 refactor: use hook for fetch event 2026-06-16 22:28:46 +08:00
jialinandjialin 737d43fd16 refactor: use hook for fetch chart 2026-06-16 22:28:46 +08:00
jialinandjialin 9f5163531c refactor: usage storage tab, split tables 2026-06-16 22:28:46 +08:00
jialinandjialin df5cf0f4e5 chore: nvidia notes in add worker 2026-06-16 18:35:19 +08:00
jialinandjialin e160b64697 fix: do not show billing menu in ent 2026-06-16 18:35:19 +08:00
jialinandjialin 7d36c6ac4a revert: show model name from api directly in apikeys 2026-06-16 15:25:46 +08:00
jialinandjialin 0820edf55f fix: refresh data in card page list 2026-06-16 15:25:46 +08:00
jialinandjialin 12e324893f fix: gpu instance state color 2026-06-16 14:17:43 +08:00
jialinandjialin 4017c4dd33 style: table hover bg, remove description in billing 2026-06-16 14:17:43 +08:00
jialinandjialin 17b65d80e1 fix(style): API info access url overflow 2026-06-16 14:17:43 +08:00
jialinandjialin 47cb3321ab fix(style): forgot password layout 2026-06-16 14:17:43 +08:00
jialinandjialin 547b9f617b fix(style): forget button in login page 2026-06-15 15:48:41 +08:00
jialinandjialin 9e7bd78693 fix: allowed models do not contain the org name 2026-06-15 14:45:27 +08:00
jialinandjialin c53d60b628 fix: remove the depude code in buildTrend 2026-06-14 19:40:19 +08:00
jialinandjialin 9566e0cdb2 fix: queryParams do not be updated timely 2026-06-14 19:21:36 +08:00
jialinandjialin ed3e779d22 fix: do not trigger setInitialized in catch when error occured 2026-06-14 19:14:18 +08:00
jialinandjialin 3e2133753d fix: barchart, piechart flash issue 2026-06-14 19:14:18 +08:00
jialinandjialin 736a853192 chore: add cancel token 2026-06-14 19:14:18 +08:00
jialinandjialin 08ef2ffa9a refactor(usagesummary): fetch data 2026-06-14 19:14:18 +08:00
jialinandjialin 6cf512cccd refactor(usage): split tabs into separate dirs 2026-06-14 19:14:18 +08:00
jialinandjialin 6f56e614f3 chore: instance creationg UE 2026-06-14 19:14:18 +08:00
jialinandjialin 8c06862705 style: login placeholder 2026-06-12 22:20:31 +08:00
jialinandjialin 48159171c2 fix: keep uniform for instance type render content 2026-06-12 22:20:31 +08:00
jialinandjialin d5d0749b72 chore(deps): update ui 1.0.27 2026-06-12 21:16:07 +08:00
jialinandjialin dc4dd81e11 refactor(login): layout style 2026-06-12 21:16:07 +08:00
jialinandjialin c736e2186d revert: display the max count cpu instance type 2026-06-12 20:49:47 +08:00
gitlawr 7a76bda484 feat: distinguish same-name GPU service resources across users
Resource names are unique per owning principal, so different users
can legally create same-name GPU instances, templates, SSH public
keys, storage volumes and storage types — but the admin's
cross-tenant lists and the instance-create template picker rendered
them indistinguishably.

- Group the template picker by owning scope: Your Templates, Global
  Templates, then — admin cross-tenant view only — one group per
  owner. A single group renders flat without headers. A plugin can
  take over grouping via hooks.useTemplateOwnerGroups when its
  principal model scopes templates beyond USER owners; a plugin
  without the hook keeps the flat list.
- Add an admin-only Creator column to the Instances, SSH Public
  Keys, Storage and Storage Types tables, sitting right before
  Created (mirroring the API Keys page convention). Resolves
  creator_id to a username via the user directory, falling back to
  owner_principal_id for legacy rows — a personal-scope row's
  creator IS its owner.
- Tag template cards with their owner scope (Global / username) for
  admins.

The Creator column and the card tag defer to a plugin that registers
the page's list columns / the OwnerScopeTag slot. Requires the
gpustack server change that records creator_id on templates, SSH
public keys and storage types.
2026-06-12 20:22:56 +08:00
yxfandjialin 778ea36a9d refactor(parameter): rename parameter_format to Flag Format and update related logic 2026-06-12 17:12:10 +08:00
jialinandjialin 04e1e1e566 chore: remove remember me 2026-06-12 16:02:22 +08:00
jialinandjialin 01f66085ab fix: only keep the max count cpu instance type 2026-06-12 16:02:22 +08:00
jialinandjialin ddf723525a fix: instance types switch between gpu and cpu 2026-06-12 12:10:14 +08:00
Yuxing Deng 7b18a92388 fix: register k8s cluster should selected nvidia vendor by default 2026-06-11 23:55:40 +08:00
jialinandjialin b985dcee63 chore(deps): update ui 1.0.26 2026-06-11 20:31:36 +08:00
jialinandjialin 77e92432ed fix: changed notice always display 2026-06-11 20:02:27 +08:00
jialinandjialin a7b33a928b fix: anti double submit in form 2026-06-11 20:02:27 +08:00
jialinandjialin 1218c6d096 fix: prevent duplicate submit and unlock on validation failure in deploy/update modals 2026-06-11 20:02:27 +08:00
jialinandjialin 6337431ed4 chore(deps): update ui 1.0.25 2026-06-11 14:13:08 +08:00
jialinandjialin 2ba5118bc2 fix: filter modelInstance by cluster in benchmark form 2026-06-11 14:13:08 +08:00
jialinandjialin 12649c7ccf chore(deps): update ui 1.0.24 2026-06-10 20:29:50 +08:00
jialinandjialin 5d96932870 fix: logs viewer scroll to bottom aumatically 2026-06-10 20:29:50 +08:00
jialinandjialin e6cfa6cf77 fix: template display after initialized 2026-06-10 20:29:50 +08:00
gitlawrandjialin 55d16c5681 feat: persist sider menu group collapse state across refresh 2026-06-10 20:24:53 +08:00
micheliaandjialin ab75685955 fix: extend usage query limit 2026-06-10 16:17:54 +08:00
micheliaandjialin bd0bf0bf63 fix(usage): render resource timestamps in their rollup tz, not browser-local 2026-06-10 16:17:54 +08:00
jialinandjialin fd050b33d9 fix: dedupe backend params when switching backend 2026-06-10 15:37:27 +08:00
jialinandjialin 698090b17a fix: watch list do not update when creating 2026-06-10 15:37:27 +08:00
jialinandjialin 6f96260993 fix(style): pie legend text overflow 2026-06-10 15:37:27 +08:00
jialinandjialin 98a6b19298 fix: flatten all filters in table list 2026-06-10 15:37:27 +08:00
Yuxing Dengandjialin ded270ff25 feat: support deploying cpu workers for k8s cluster 2026-06-10 15:26:13 +08:00
gitlawrandjialin c8860207e8 feat(apikeys): wire Organization plugin column on API Keys list
Splice the per-page extra-columns slot ('apiKeys') after the Name
column so plugin-contributed columns (e.g. Organization in the admin
All-org view) render on the API Keys list, matching the other resource
list pages. Declare owner_principal_id (and the already-consumed
masked_value / user_name / user_id) on ListItem.
2026-06-10 14:46:12 +08:00
gitlawrandjialin 8d045ffd8b feat(header): add GitHub star button to top bar
Compact pill (GitHub icon | star count) placed left of the version
display. Star count is fetched from the GitHub API once per 24h and
cached in localStorage; the button hides entirely when no count is
available (e.g. air-gapped deployments), matching the existing update-
check graceful-degradation pattern. Fetch is bounded by a 4s abort
timeout to avoid hanging requests offline.
2026-06-10 12:00:35 +08:00
jialinandjialin fd7437e74a fix: show start only on stopped 2026-06-10 10:03:43 +08:00
jialinandjialin 807ba866d3 fix: only ready allow deleting 2026-06-09 21:00:17 +08:00
jialinandjialin 5e2808d7c4 fix(style): usage model name overflow 2026-06-09 20:01:11 +08:00
jialinandjialin a26a3ff8f9 fix: safe mergeBackendParameters 2026-06-09 20:01:11 +08:00
jialinandjialin a2e9f69b96 fix: gpu instance always show delete, dedupe params 2026-06-09 19:01:32 +08:00
Yuxing Dengandjialin ac800a303a fix: remove the pined worker config changed notice 2026-06-09 17:54:23 +08:00
yxfandjialin dd542a9f29 feat(params): add initialization attributes to lock task_type field 2026-06-09 16:30:35 +08:00
gitlawrandjialin 26ce456bde feat: per-page extra-columns slot for list tables
Add `usePluginListColumns(pageKey)` so a registered plugin can splice
extra columns into a list page's columns hook keyed by page id. The
slot is wired into 11 list pages (Models, Model Routes, Clusters,
Cloud Credentials, MaaS Providers, Model Files, Model Instances,
GPU Service Instances / Public Keys / Storage / Storage Types) so any
consumer of the seam reaches every org-scoped list without per-page
plumbing.

The slot accepts either a static array or a hook function — the hook
form lets the registrant use React hooks to decide visibility without
the host having to evaluate it. Columns carry a `placement`:
`after-name` is the default (sits next to the row's identifying
column); `before-time` / `before-operation` are kept for back-compat
with the existing per-page `modelRoutes.extraColumns` slot.

SealTable-grid pages (Models / Model Routes / Clusters) absorb the
plugin column's span by shrinking the widest right-side columns so
the 24-unit grid stays balanced. Model Routes' `CREATE_TIME_MIN_SPAN`
drops from 3 to 2 because with two plugin columns active the grid
was 1 unit over and the action dropdown wrapped onto a new row; the
date string ellipsizes cleanly at the lower min.

ListItem types for Model Instances, MaaS Providers, Cloud Credentials,
and Model Files gain an optional `owner_principal_id` field — the
backend already emits it (denormalized from the parent resource), the
TS types just hadn't declared it.

The GPU Instances page splices the plugin column right before the
Cluster column (rather than after Name); the two read together since
Cluster narrows down to one Org.
2026-06-09 16:28:59 +08:00
micheliaandmichela feng 7ce1faaf6c fix(usage): format Last Active as local time on GPU & Storage tabs
The Last Active column had no renderer, so it dumped the raw ISO timestamp
(gpustack/gpustack#5523). It's a UTC instant (max bucket hour), so format it
to the user's local time with dayjs — matching the Resource Events time
column and the rest of the product. (Token Last Active is a daily date, not
an instant, so it stays a plain date with no timezone shift.)
2026-06-09 11:54:28 +08:00
micheliaandmichela feng 878bdaa5a7 fix(usage): keep the grouped-trend legend off the x-axis labels
The legend renders at the chart bottom (bottom: 0); with the default grid
(bottom: 8) it overlapped the date labels once a group-by split the trend
into multiple series. Reserve grid bottom space (matching the Tokens trend)
whenever a legend is shown.
2026-06-09 11:54:28 +08:00
micheliaandmichela feng 68a7e3f66a feat(usage): group-by on the GPU Instances & Storage trend charts
Mirror the Tokens trend: add a clearable "Group by" select to the
MetricChartCard so the GPU Instances chart can split by instance type /
instance / user, and Storage by storage / user. When grouped, the chart
fetches group_by=["date", "<dim>"] (the same list style as the token usage
API) and pivots into one stacked series per group (shared buildTrendSeries
util), with a legend; ungrouped stays a single series. group_by is now a
list across the resource breakdown client; group-by options reuse the
bottom-table dimensions (Users only when org-wide).
2026-06-09 11:54:28 +08:00
micheliaandmichela feng feccc81c5f feat(usage): fix Resource Events filters — real event types, name search, no user filter
- Event type: trim the dropdown to the four types that are actually emitted
  (Created / Started / Stopped / Deleted) — updated/attached/detached were
  never recorded — and actually send the selection (event_types) to the API,
  which previously dropped it so the filter did nothing.
- Add a resource-name text filter (debounced, case-insensitive substring).
- Remove the "filter by user" select: the events list has no User column.
  Scope still applies (members see only their own events).
2026-06-09 11:54:28 +08:00
jialinandjialin 620045d7a3 fix: show the default backend params configured in backend 2026-06-08 18:25:22 +08:00
jialinandjialin 6dc28d61b0 fix: view event in starting 2026-06-08 18:25:22 +08:00
jialinandjialin 754cc29793 chore: update iconfont files 2026-06-08 16:38:27 +08:00
jialinandjialin ed525509ea chore(deps): update 1.0.23 2026-06-08 16:26:25 +08:00
jialinandjialin 2b6f26e6c8 fix: update addworker command for dtk 2026-06-08 15:43:21 +08:00
jialinandjialin 09821aab85 feat: add a billing holder page 2026-06-08 15:43:21 +08:00
jialinandjialin 636190b8e6 chore: show the backend info in instance view 2026-06-08 15:43:21 +08:00
jialinandjialin e420c22a45 fix(style): bar chart legend gap 2026-06-08 11:42:20 +08:00
jialinandjialin 993f6edcc4 fix: error message do not show in tts 2026-06-08 11:42:20 +08:00
gitlawr bda8264788 fix: playground model id alignment across org boundaries
Two QA-reported bugs on the "Open in Playground" path. Both come
down to the model id the playground submits not matching what
``/v1/models`` / the dispatcher key off:

* Routes page (OSS UI) emitted ``default/<name>`` for routes in the
  platform Org and 404'd. ``use-open-playground`` keyed off the
  ``is_platform`` flag alone; stale caches that drop the flag
  slipped through. Also accept the well-known ``name === 'default'``
  (``PLATFORM_PRINCIPAL_NAME`` on the backend) as a fallback signal.

* My Models page (enterprise UI, non-admin) emitted bare ``<name>``
  with no Org prefix for non-platform routes. Use ``model.name``
  from ``/v2/my-models`` verbatim — the backend ("fix: principal
  prefix in my-models") now rewrites that field to the OpenAI-style
  id server-side, which also closes the cross-Org grant gap a
  client-side cache lookup can't (the granting Org isn't in the
  caller's member list). Card title now shows the prefixed id, so
  users can tell apart same-named models from different Orgs.

Routes page keeps ``useOpenPlayground`` — that surface is always
scoped to the caller's own Org, the local cache is sufficient, and
``/model-routes`` still returns the raw ``name``.

Also drops the now-unused ``onClick`` prop on ``ModelItem``: the
card had ``clickable={false}`` so the parent-passed handler was
already dead code; the Button drives the playground navigation.
2026-06-07 15:45:49 +08:00
gitlawr ab8e792276 feat(cluster): card-based cluster type selector for K8s
Replace the GPU Service switch (with the small "cannot be used for
model service" caption) with a two-card radio selector that makes the
choice between Model Service and GPU Service explicit at a glance.

- Render two cards styled to match the existing SwitchCard (same
  border, radius, 12px/14px padding, and label/description typography)
  so the selector blends in with the surrounding form fields.
- Underlying form state is unchanged: picking GPU Service still seeds
  k8s_options.gpuInstanceOptions = {}, picking Model Service clears
  it — so the static-address field, the EDIT-mode change watcher, and
  the API payload all keep working as before.
- Align the GPU Service label with the top-level menu entry
  (menu.gpuService) — drop "Instance" / "实例" from the card title
  and the static access address label.
2026-06-07 13:44:50 +08:00
jialinandjialin 0e4be63b6b chore(gaas): add a unknown phase 2026-06-06 21:09:56 +08:00
jialinandjialin d3ebea9090 fix: locales, empty link in add worker 2026-06-06 14:29:49 +08:00
jialinandjialin 15a9e23243 fix: convert cpu unit to core 2026-06-06 14:29:49 +08:00
jialinandjialin eab7d9c77f style: usage chart 2026-06-06 14:29:49 +08:00
jialinandjialin 4a86b71018 fix: ignore request errors on setup 2026-06-06 14:29:49 +08:00
jialinandjialin 6350f7f11a chore(i18n): usage locales 2026-06-05 23:59:25 +08:00
jialinandLawrence Li ce6388253c fix: cpu init value failed 2026-06-05 23:08:45 +08:00
jialinandLawrence Li d8f6064d5e fix: usage locales 2026-06-05 23:08:45 +08:00
jialinandLawrence Li 9f5d95f565 fix: align instance type fields to api 2026-06-05 23:08:45 +08:00
jialinandjialin 0cabbeb81c fix(style): cluster detail locale 2026-06-05 22:29:39 +08:00
jialinandjialin 76618aa3a1 fix(style): cluster detail 2026-06-05 22:18:26 +08:00
gitlawrandjialin b19db0f67d fix(clusters): call out that GPU instance service blocks model service
The "GPU Instance Service" toggle on the cluster create form only said
what it enables. The exclusion side — once flipped, the cluster drops
out of the model-deploy picker — was invisible until the user went
looking for the cluster elsewhere. Extend the tip to spell that out
in all five locales, using the canonical "GPU instance service" /
"model service" product terms.
2026-06-05 18:56:42 +08:00
gitlawrandjialin 76f228bfe7 fix(llmodels): hide GPU-service clusters from deploy picker
The deploy form's cluster dropdown fetched every visible cluster, so
clusters configured for GPU-service (k8s_options.gpu_instance_options
set) showed up as deployable targets even though deployments cannot
run on them.

Pass gpu_instance_enabled=false on the cluster-list query so the
backend returns only model-deployment clusters. GPU-instance creation
does not pick a cluster directly (it is derived from the instance
type), so no symmetric change is needed there.
2026-06-05 18:56:42 +08:00
gitlawrandjialin be40692773 feat(login): add onUserFetched plugin hook
The host's access function is memoized on `initialState` and runs
exactly once per commit. Plugins that maintain identity-scoped
caches the access predicate reads from (e.g. an org context cache)
had no way to seed those caches synchronously before the caller's
`setInitialState({currentUser: ...})` fired — any post-commit hydrate
couldn't widen the predicate, leaving the sidebar in a stale view
until the next identity change.

Add `LoginPlugin.onUserFetched(userInfo, ctx)` and call it inside
`fetchUserInfo` after the server confirms identity but before
returning. Also commit the identity to `userAtom` storage here so
localStorage's identity marker is in lockstep with whatever caches
the plugin seeds — the predicate's first evaluation then sees a
consistent view rather than the prior session's data.

Errors thrown from the hook are swallowed and logged; they never
block fetchUserInfo.
2026-06-05 18:55:46 +08:00
gitlawrandjialin 42ac377c17 feat: tease enterprise multi-tenancy and API key controls in OSS
Add an Organizations menu entry (admin-only) that routes to an upsell
page explaining the multi-tenancy module, and surface disabled
IP Access Control / Quota Limit items in the API Key dropdown with a
tooltip pointing at the enterprise edition. Both placeholders are
shadowed by the enterprise plugin at build time: the route merger
removes the OSS Organizations entry by name, and the dropdown skips
each placeholder when the same key is contributed via configActions.
2026-06-05 16:41:34 +08:00
jialinandjialin 46f859cc3c fix: no need to show error in login for first 2026-06-05 16:24:46 +08:00
gitlawrandjialin f2bff0d1fb fix(storage): remove duplicate error toast on create failure 2026-06-05 16:20:49 +08:00
gitlawrandjialin 0bd98b9662 fix(llmodels): scope catalog deploy cluster seed by create-scope org
initClusterId picked the platform default cluster without considering
the form's organization_id, so opening the catalog deploy form in the
admin all-scope view could seed a cluster that the (org-filtered)
dropdown then hides — submit failed with "Cluster not found".

Mirror the same scope-aware selection deploy-modal already does:
filter clusterList by owner_principal_id when organization_id is set
before picking default/ready/first.

Return number | undefined honestly (the picked org may own no
clusters, or clusterList may still be loading) and guard the
open-handler caller so undefined doesn't flow into fetchSpecData /
getGPUOptionList — the user resolves the empty state by picking an
org that owns clusters.
2026-06-05 13:11:41 +08:00
micheliaandjialin 1ac2c02197 fix: change benchmark model name query condition to input 2026-06-05 12:30:25 +08:00
jialinandjialin 907187d53a fix: show worker added message 2026-06-05 10:20:55 +08:00
Yuxing Dengandjialin faf6246fe6 fix: remove worker config bg color 2026-06-05 10:19:08 +08:00
Yuxing Dengandjialin 0c16e2be7a fix: add notification when editing cluster's k8s_options 2026-06-05 10:19:08 +08:00
micheliaandjialin 3212882895 chore(menu): move Usage below Resources in the sidebar 2026-06-04 19:52:52 +08:00
micheliaandjialin 62f34ffcaf feat(usage): resource usage metering page
Add the Usage page with Summary / Tokens / GPU Instances / Storage / Resource
Events tabs over the new metering endpoints: per-resource breakdowns with
date / scope / user / resource filters, trend charts, server-side sortable
tables (GPU-Hours, Instance-Hours, GB-Days, GB-Hours), Excel export with an
in-dialog preview, and KPI cards with help tooltips explaining each metric.

MaaS-only users (no Kubernetes cluster and no resource events) get a
tokens-only view with the tab bar dropped; GPU Service / the full page unlock
for admins, cluster owners, or anyone who has run a resource. Instance-type
rows reuse the GPU Instances list styling, and deleted users / instances /
volumes are flagged in breakdowns and filters.
2026-06-04 19:52:52 +08:00
micheliaandjialin 50601b0aff feat(gpu-service): instance-type spec popover with per-card specs
Replace the raw flavor slug in the GPU Instances list with the product name
("<product> x <count>") plus an info-icon popover that breaks the spec down by
category (GPU / CPU / Memory / Disk): per-card VRAM, whole-instance CPU/RAM,
system / ephemeral / persistent disks (persistent size resolved from the
referenced PV). Extract the cell into a shared InstanceTypeCell and centralize
memory formatting in formatMemoryDisplay so the GPU Instances list and the
Usage tab render identical sizes.
2026-06-04 19:52:52 +08:00
gitlawrandjialin 4c5d42cb13 feat(version): respect GPUSTACK_UI_* env overrides in build info
Allow a wrapping build that checks this source tree out as a
sub-package to stamp its own release tag and commit id onto the UI
(otherwise the version panel reports the host tree's git HEAD,
which the wrapper doesn't control).

GPUSTACK_UI_VERSION overrides the release tag and GPUSTACK_UI_COMMIT_ID
overrides the short commit id; both fall back to the git tag /
commit at HEAD when unset, preserving existing behavior. Names are
namespaced to avoid colliding with the many tools and CI runners
that already set a generic VERSION.
2026-06-04 18:21:13 +08:00
gitlawrandjialin 85268b8917 fix(api-keys): scope creator filter to current org members
The "Filter by creator" dropdown was listing every user in the
system. For an Org owner viewing an Org-scoped key list, picking
a user outside the Org always produced an empty result. Pass
scope=current_org to `/user-directory` so the picker matches the
list's actual scope. BE drops the param when the request has no
Org context, so callers without an Org are unchanged.
2026-06-04 18:09:32 +08:00
gitlawrandjialin 973d2c4529 feat(login): add plugin seam for default landing path
Plugins can supply `LoginPlugin.resolveDefaultPath` to override the
post-login redirect target. checkDefaultPage consults the hook and
uses the returned path when non-null; otherwise falls back to the
existing admin/non-admin defaults. The lookup runs in parallel with
the IS_FIRST_LOGIN read. Applies to both first-login and
subsequent-login flows.
2026-06-04 15:56:24 +08:00
jialinandjialin ab766b8c54 style: menu collapse button 2026-06-04 15:48:44 +08:00
jialinandjialin 4945242c87 style: login page size 2026-06-04 15:48:44 +08:00
jialinandjialin c4a72eb375 style: collapse style 2026-06-04 15:48:44 +08:00
gitlawrandjialin b12d11728d fix: update model files menu access 2026-06-04 13:16:47 +08:00
Yuxing Dengandjialin 8f0e3a7576 fix: remove k8s options section and merge into advance
And re-order the configuration inputs. The current input order is:
- namespace
- volume mounts
- registry credentials
- node selector
- system-default-registry
- operator image
- gpu instance static access address
- worker config yaml
2026-06-03 23:17:38 +08:00
Yuxing Dengandjialin 4810ff2dbe fix: enhance cluster create form 2026-06-03 23:17:38 +08:00
jialinandjialin a221b22be9 style: instance type data format 2026-06-03 21:29:31 +08:00
jialinandjialin 24b458530d fix(style): sso button width 2026-06-03 21:29:31 +08:00
jialinandjialin caea43590a chore: useMemo deps 2026-06-03 18:28:31 +08:00
jialinandjialin c3d7a0b85e chore: add locales 2026-06-03 18:28:31 +08:00
jialinandjialin 5c6f28027d chore: add locales 2026-06-03 18:28:31 +08:00
jialinandjialin dc04529f5b fix: vendor options display by locales 2026-06-03 18:28:31 +08:00
jialinandjialin 2bb04a419d fix: sshkey, storage selection displayName 2026-06-03 18:28:31 +08:00
jialinandjialin 8e31b71668 chore: merge cluster menu to resource 2026-06-03 18:28:31 +08:00
micheliaandmichela feng f055453889 fix(usage): defensive optional-chaining on breakdown items + sku 2026-06-03 17:10:51 +08:00
micheliaandmichela feng 820a6cecdd fix(gpu-service): avoid stringifying unset cpu in the instance form
`${values.spec?.resources?.cpu}` turned an unset cpu into the literal
string "undefined", which fails k8s quantity validation. Omit cpu when
it has no value instead.
2026-06-03 17:10:51 +08:00
micheliaandmichela feng c3a779e4c8 feat(usage): register the Usage route and menu group
Add the /usage menu folder with its overview child pointing at the
tabbed Usage page.
2026-06-03 17:10:51 +08:00
micheliaandmichela feng 00008abf03 feat(usage): add the tabbed Usage page
A Tabs shell hosting Tokens / GPU Instances / Storage / Summary /
Resource Events:
- token-tab: the original token view, wrapped as a tab.
- gpu-instances-tab & storage-tab: per-instance / per-volume breakdowns
  with KPI cards, a metric+granularity trend chart, and grouped tables.
- summary-tab: three-domain (Tokens / Compute / Storage) overview with
  headline stats, donut, and trend.
- resource-events: the lifecycle audit log (Started / Stopped).
- resource-filter-bar + metric-chart-card: shared filter / chart controls
  mirroring the Tokens tab.
- resource-export-data: a preview modal (filter + paginated preview, then
  download) backing the Export Chart Data / Export Table Data actions.
2026-06-03 17:10:51 +08:00
micheliaandmichela feng d9c6f2f780 feat(usage): add i18n strings for the Usage page
Menu label (Usage) and the resource-tab strings (filters, export
chart/table, table headers) across all five locales.
2026-06-03 17:10:51 +08:00
micheliaandmichela feng 9a4d77e869 style(usage): tune shared bar/pie charts for the usage views
- bar-chart: barMinWidth so hour-granularity bars stay visible with gaps.
- pie-chart: single legend-beside-pie layout, value (percent%) tooltip,
  2-decimal rounding for the Summary donut.
2026-06-03 17:10:51 +08:00
micheliaandmichela feng 2407416e33 feat(usage): add resource-usage API client, meta hook, and shared utils
The data layer the resource tabs build on:
- apis/resource.ts: adapter over the unified metered_usage read API
  (resource/gpu-instances/storage/summary/events breakdowns), flattening
  the server's generic shape into the per-tab item shape.
- hooks/use-resource-meta.ts: loads creators/instances/volumes filter
  options for the current scope.
- utils/time-buckets.ts: day/week/month/hour bucket keys + range fill.
- utils/export-breakdown.ts: derive Excel columns from antd table specs.
2026-06-03 17:10:51 +08:00
jialinandjialin 234e42ccfa chore: remove comment 2026-06-03 15:50:06 +08:00
jialinandjialin 4426b8dc30 fix: request noop 2026-06-03 15:50:06 +08:00
jialinandjialin 9541f49f39 fix(style): table cell overflow 2026-06-03 15:50:06 +08:00
jialinandjialin 9f7a99f370 fix: refresh list after creating key 2026-06-03 15:50:06 +08:00
jialinandjialin 12f0bb2f98 chore: request.extensions 2026-06-03 15:50:06 +08:00
jialinandjialin ac45a72e0e fix: api acccess info model name 2026-06-03 15:50:06 +08:00
jialinandjialin 9705818b15 fix: wrap fetch for injecting headers 2026-06-03 15:50:06 +08:00
jialinandjialin 39374eafda fix: update params after switching model 2026-06-03 15:50:06 +08:00
Yuxing Dengandjialin 110bc3205e fix: failed to enable gpu instance while creating cluster 2026-06-03 15:33:33 +08:00
gitlawrandjialin 57fec89f2d feat(api-keys): show creator column and filter to org owners
Switch the gate from `currentUser.is_admin` to `access.canSeeOrgAdmin`
so Org owners get the same all-keys view (Creator column, creator
filter, `user_id: '*'` default) that platform admins have. Mirrors
the BE's "platform admin OR current-Org owner" gate on listing every
key in scope.

The user picker now fetches `/user-directory` instead of the
admin-only `/users` endpoint, which would 403 Org owners. Rename the
filter placeholder from the misnamed `models.table.filterByName` to
`common.filter.byCreator`.
2026-06-03 15:25:38 +08:00
gitlawrandjialin 72e794c281 fix: align playground default model id with org namespace
Opening an org-scoped deployment in the Playground pre-selected the bare
model name (e.g. `qwen3-0.6b`), which never matched the org-namespaced
option the `/v1/models` dropdown actually lists (`org1/qwen3-0.6b`), so
the selection showed an unmatched value.

Resolve the owning org from the route row's `owner_principal_id` (falling
back to the org the caller is currently acting under for the admin "All"
view) and reconstruct the same `{org}/{name}` id the server reports. The
platform org carries no prefix, matching the server's behaviour.

- user.ts: add `getOrgById`/`getCurrentOrg` that scan both org caches
  (`organizationList` + admin-only `allOrganizations`) with string-
  normalised id comparison; `getOrgNameById` is now a thin wrapper.
  Helpers accept `undefined` so optional row fields type-check.
- use-open-playground: build the prefix from the resolved org record,
  keying the skip-prefix decision off `is_platform`.
- RouteItem: declare the `owner_principal_id` the list API returns.
- RouteTargetFormItem: drop the duplicate `overridden_model_name`
  declaration that TS flagged as a duplicate identifier.
2026-06-03 10:59:43 +08:00
gitlawrandjialin fadbcc3e44 feat(benchmark): scope the benchmark create form by organization
Add the organization picker to the benchmark create form (platform-admin
"all organizations" view). Fetch the chosen org's clusters and keep only
those it owns; refetch the org-scoped model / instance list on org change;
clear a stale cluster or target when switching to an org with no clusters.
2026-06-03 10:14:50 +08:00
gitlawrandjialin 9e30e964e6 feat(models): scope deployment and model-file create by organization
Add the organization picker to the model deployment and model-file
download forms (platform-admin "all organizations" view). Filter the
cluster / worker pickers to clusters the chosen org owns, so the created
resource's owner stays aligned with where it runs and a cross-org cluster
can't be selected. Carry owner ids on the cluster / worker options to
drive the filter.
2026-06-03 10:14:50 +08:00
gitlawrandjialin fbb707d014 feat(gpu-service): scope create forms by the selected organization
In the platform-admin "all organizations" view, add an organization picker
(the CreateOrgScopeField slot) to the SSH public key, storage type,
storage, GPU instance and instance-template create forms, placed below the
name / display-name fields. Drop the hidden owner field and let the owner
derive from the request context, matching the model-route form.

For GPU instances, scope the instance-type list to clusters the chosen org
owns (client-side, by cluster owner) so an instance can't be scheduled onto
another org's cluster; when the org owns none, show "no instance type
available" and clear the selection, cluster and CPU/memory fields. Instance
templates gain a Global level (NULL owner) and an owner tag on the card.
2026-06-03 10:14:50 +08:00
gitlawrandjialin d2d82ee5ee feat(request): add a response-interceptor seam
Add `extraResponseInterceptors` next to the existing request-interceptor
seam and wire it into the request config, so build-time tooling can react
to responses (e.g. clear a one-shot request-scoped hint after a write
completes). Default is an empty list — no behavior change by default.
2026-06-03 10:14:50 +08:00
jialinandjialin 9061fa08ba chore(deps): update ui 1.0.22 2026-06-02 17:51:25 +08:00
jialinandjialin f29236518e style: selector max tag count 2026-06-02 17:51:25 +08:00
jialinandjialin 6537de5224 fix(style): worker step container 2026-06-02 17:51:25 +08:00
jialinandjialin 0413d470a8 fix(style): worker upgrade tips do not show 2026-06-02 17:51:25 +08:00
jialinandjialin c01f4d3ce8 style: table split 2026-06-02 15:18:00 +08:00
jialinandjialin dd9d359bb0 chore: storage label 2026-06-02 15:18:00 +08:00
jialinandjialin b444c35643 style: side menu scroll bar 2026-06-02 15:18:00 +08:00
jialinandjialin abd0d52fad chore: perferences route 2026-06-02 15:18:00 +08:00
yxfandjialin c8771dd3b9 fix(label): update LoRA Adapter label to plural form 2026-06-02 15:13:55 +08:00
jialinandjialin 6d16f56183 chore(deps): update ui 1.0.21 2026-06-01 21:06:56 +08:00
jialinandjialin 95ffe0350e fix(style): k8s form 2026-06-01 21:06:56 +08:00
jialinandjialin e314f25a77 style: gpu instance service 2026-06-01 21:06:56 +08:00
gitlawrandjialin 65ea65e9ae feat(api-keys): rename User Name column to Creator and move before Created
Rename the api-keys list column from User Name to Creator for clarity and reorder it to sit immediately before the Created column. Add the common.table.creator locale key across all supported languages.
2026-06-01 19:13:44 +08:00
jialinandjialin 4ce975562e chore(deps): update ui 1.0.20 2026-06-01 13:36:14 +08:00
jialinandjialin 07e625f2bc chore: add locales 2026-06-01 13:36:14 +08:00
jialinandjialin d95fd8dc90 fix: instance type format in list 2026-05-31 22:23:32 +08:00
jialinandjialin 55028cf3a3 fix: parse quality data 2026-05-31 22:23:32 +08:00
jialinandLawrence Li 16a97772d3 fix: instance type api update 2026-05-30 08:28:38 +08:00
jialinandLawrence Li d33167d070 style: page segment fontsize 2026-05-30 08:28:38 +08:00
Yuxing Dengandjialin f62e0ba372 feat(cluster): add default container registry field to advanced config
Surface the top-level system_default_container_registry as a dedicated
input (both Docker and K8s), coerce blank to null, and drop its
redundant worker_config YAML hint.
2026-05-29 23:11:46 +08:00
jialinandjialin 1b6466ae78 chore: menu sorting 2026-05-29 18:04:31 +08:00
jialinandjialin 29c5987bf1 style: menu 2026-05-29 18:04:31 +08:00
Yuxing Deng fcd1d01e83 feat(cluster): update K8s cluster form for new k8s_options API
Align the Kubernetes cluster create/edit page with the backend's
promotion of operator/K8s knobs out of worker_config and the removal
of gpuVendorOverrides.

- k8s_options: drop gpuVendorOverrides; add operatorImage, namespace,
  and gpuInstanceOptions (presence = GPU instances enabled, carrying an
  optional gpuInstancesAccessStaticAddress).
- Add a top-level system_default_container_registry to the cluster type
  (promoted out of worker_config on the backend).
- Group all k8s_options fields into a new top-level "K8s Deployment
  Options" collapsible section (sibling of Advanced, rendered above it),
  with namespace first followed by volume mounts, image credentials,
  node selector, operator image, and GPU instances.
- Drive the GPU instances toggle from local state instead of
  Form.useWatch (an unregistered nested path never re-rendered, leaving
  the switch unresponsive); use antd's borderless Switch and keep the
  label/switch grouped together. Namespace gains a gpustack-system
  placeholder.
- Remove the gpuVendorOverrides validation from the form and the stale
  operator_image / namespace / gpu_instances_access_static_address hints
  from the worker_config YAML template and JSON schema.
- Add-worker GPU picker: always allow multi-select for K8s clusters
  (runtime node selectors are now auto-derived), dropping the
  override-gating, cluster fetch, and single-only hint.
- Update locales (en/zh/ja/ru/tr) for the removed and added keys.
2026-05-29 17:56:01 +08:00
gitlawrandjialin a80b889760 refactor(access): fold allowed_users into allowed_principals
Move the model-route access modal off the deprecated allowed_users
policy/field onto the unified allowed_principals + principals surface,
persisting everything through a single /access POST.

- "specific users" radio now uses allowed_principals; a returned legacy
  allowed_users value is normalized so existing routes still select it.
- derive the picker's selection and the full grant set from `principals`
  in GET /access (fall back to legacy `items` if a backend doesn't
  return principals yet).
- save as `principals`: the principal-based override sends its staged
  set; the user picker maps its selection to USER-kind grants and
  preserves any non-user grants from the snapshot (no longer sends
  `users`).
- guard saving before the GET seeds principals (would wipe grants);
  share the ALLOWED_PRINCIPALS_POLICY constant.
- AccessControlFormData: `users` optional, add `principals`.
2026-05-29 17:11:12 +08:00
jialinandjialin f71bf1b074 chore(deps): update ui 1.0.19 2026-05-29 16:19:17 +08:00
jialinandjialin 7e048ee537 fix: click twice for submitting deployment form, using iconfont static files 2026-05-29 16:19:17 +08:00
jialinandjialin 34098e8950 fix: orgname not be shown in playground 2026-05-29 13:35:22 +08:00
jialinandjialin faf0d4d605 chore(deps): update ui 1.0.18 2026-05-29 11:30:17 +08:00
jialinandjialin 85ade88d55 fix: instance name text overflow 2026-05-29 11:24:03 +08:00
jialinandjialin 7d0b94330d fix: adapter request not be triggered 2026-05-29 11:24:03 +08:00
gitlawrandjialin 5cff1298fc feat(management): show only own rows on Clusters / Storage Types lists
The two management pages now pass ``mine=true`` to their list APIs.
With multi-tenant cluster_access in place an Org Owner would
otherwise see rows that came in via cross-Org grants — usable but
read-only — and editing/deleting them would 403 from the backend.
Restricting the management lists to rows the caller's scope owns
keeps the UX honest. PV-create / GPU-instance-create pickers still
query without ``mine`` so usable cross-Org clusters and types are
selectable there.
2026-05-28 18:16:15 +08:00
gitlawrandjialin d3a3d4e96e feat(templates): fix unresponsive action dropdown for non-admin
Two changes that together restore the per-row Edit / Delete menu
for non-admin users:

- ``templateActions`` drops the blanket ``access: 'canSeeAdmin'``
  gate on every menu item. With the gate on, non-admin callers got
  an empty filtered menu, so clicking the "..." did nothing.

- The management page passes ``mine=true`` to the list API. The
  endpoint now drops Global rows for non-admin callers (rows they
  can't edit anyway), so the dropdown shows up only on rows the
  caller may manage. The GPU-instance create picker reuses the
  same API without ``mine``, keeping Global presets visible there.
2026-05-28 18:16:15 +08:00
jialinandjialin f99621ec92 fix: catalog hidden fields 2026-05-28 15:51:51 +08:00
jialinandjialin e47a9304f4 fix: cluster worker_config refill 2026-05-28 15:51:51 +08:00
jialinandjialin 9488fc1251 fix: update instance item type 2026-05-28 15:51:51 +08:00
jialinandjialin b590aa746b fix: update onceMaxRequest field 2026-05-28 15:51:51 +08:00
jialinandjialin ef61134b8b chore(deps): update ui 1.0.17 2026-05-28 10:02:32 +08:00
jialinandjialin a9daa0b586 fix: max gpu count 2026-05-27 20:18:33 +08:00
jialinandjialin ae0ad25ed6 fix: an error occured in switch language in stt 2026-05-27 20:18:33 +08:00
jialinandjialin a4c48c1981 fix: remove description json format 2026-05-27 17:10:15 +08:00
jialinandjialin 3497590d2e fix: using select handler for provider hint 2026-05-27 17:10:15 +08:00
jialinandjialin 11cf1329c8 fix: always show max label 2026-05-27 17:10:15 +08:00
jialinandjialin ba5b41cf3a chore: update locales 2026-05-27 17:10:15 +08:00
gitlawrandjialin 1d70446cc4 fix(routes): let Org owners see Storage Types menu
The route was gated on ``canSeeAdmin`` so only platform admins ever
saw Storage Types in the sidebar. But the backend route
(``gpu_instance_persistent_volume_types``) is tenant-scoped — Org
owners can create / list their own — so hiding the menu from them
left an admin-defined feature reachable only by URL.

Relax to ``canSeeOrgAdmin`` to match. OSS keeps the platform-admin-only
behaviour (predicate defaults to ``isPlatformAdmin``); the enterprise
access extension widens it to the current-org owner.
2026-05-27 16:57:49 +08:00
gitlawrandjialin f15d0ab243 fix(cluster-create): hide ProviderCatalog when entering via providerHint
The catalog was gated on ``currentStep === startStep`` so it would
re-appear at step 1 whenever ``providerHint`` skipped the catalog (e.g.
GPU Service's "Add a Kubernetes Cluster" empty-state CTA). Result: the
configure step rendered the catalog *plus* the Name/Description/Advanced
form together, with Kubernetes shown as already selected — confusing
and ugly.

Pin the catalog to step 0 explicitly. ``startStep`` was only used to
seed ``currentStep``, so it's inlined into the ``useState`` initializer
to keep the "skip the catalog when providerHint is set" intent in one
place. Pre-selecting a provider now correctly lands the user straight
on the configure form alone.
2026-05-27 15:53:39 +08:00
jialinandjialin 34be4800d7 fix: lora tag 2026-05-27 15:45:28 +08:00
jialinandjialin e40b0c2e07 fix: get back lora_module_name 2026-05-27 15:45:28 +08:00
lizw2andjialin 45e0ad9e93 fix(model-routes): use overridden model name for provider targets 2026-05-27 15:45:28 +08:00
gitlawrandjialin 96e6ddfe8a fix: collapse GPU Service menu in Personal Org; add K8s-direct CTA
Two follow-ups to the GPU Service gating:

- In Personal-Org view the access extension was stripping
  canSeeAdmin/canSeeOrgAdmin but leaving canSeeGpuService at its
  admin-shortcut value, so platform admins switched into Personal
  still saw the menu even though Personal scope can't host a K8s
  cluster. Mirror the probe result through sessionStorage so the
  extension can fall back to the strict cluster-availability signal
  in that branch.

- The empty-state CTA now reads 'Add a Kubernetes Cluster' and, on
  click, opens the cluster-create flow with Kubernetes preselected
  via clusterSession.providerHint. ClusterCreate consumes the hint
  on mount: it seeds extraData.provider and starts at the configure
  step instead of the provider catalog, so the user lands one click
  closer to the form they actually need.
2026-05-27 13:41:32 +08:00
gitlawrandjialin 6b47bacfac feat: gate GPU Service on Kubernetes cluster availability
GPU Service today only schedules on Kubernetes clusters; Docker / cloud
clusters can't host the CRDs. Without any awareness of that, Org
members whose Org has no K8s cluster (and no cluster_access grant on
one) saw a menu they couldn't use and a form that bottomed out with
backend errors.

Two changes lock the UX down:

- Boot probes the caller's cluster list once and stashes
  hasKubernetesCluster in initialState. A new canSeeGpuService
  predicate gates the menu — admins and Org owners always see it
  (they can add the cluster); everyone else only sees it when a
  reachable K8s cluster actually exists.

- The instances page filters its own cluster list to Kubernetes
  before deciding what to show. With nothing reachable we render the
  Deployments-style 'No clusters available. Add a Kubernetes cluster
  to get started.' empty state and hide the create-instance CTA;
  admins and Org owners additionally get the 'Add cluster' button
  that jumps to cluster management.
2026-05-27 13:41:32 +08:00
gitlawrandjialin 099b419e34 fix: source owner_principal_id from current organization
OwnerPrincipalIdField was reading currentClusterAtom, which no callers
ever wrote — so every form submission ended up with
owner_principal_id=null and the backend rejected non-admins with
"Only platform admin can create global ...". Pin the field to
currentOrganizationId instead (the Org the caller is acting under).
Cluster ownership is the wrong source: a cluster_access grant lets one
Org schedule on another Org's cluster, but the new resource is still
owned by the caller's Org, and the backend enforces
owner_principal_id == ctx.current_principal_id.

Also drops the dead currentClusterAtom.
2026-05-27 13:41:32 +08:00
jialinandjialin e343ec0a1e fix: instance type width 2026-05-27 12:14:15 +08:00
jialinandjialin e3d6d08916 fix: show max gpu 2026-05-27 12:14:15 +08:00
jialinandjialin d0bc206c83 fix: instance type missing when using CPU type 2026-05-27 12:14:15 +08:00
jialinandjialin 7287f8c81b style: subdrawer width 2026-05-27 12:14:15 +08:00
jialinandjialin 1878753cde fix: show cluster in table 2026-05-26 23:02:28 +08:00
423 changed files with 19701 additions and 6878 deletions
+143
View File
@@ -0,0 +1,143 @@
---
name: create-crud-page
description: Scaffold a CRUD list/table page module in the gpustack-ui monorepo. Use when creating a new page module, building a list/table page, adding a create/edit drawer, or setting up the components/config/forms/hooks/services structure for a feature.
---
# Create a CRUD Table Page
## Inputs (do this first)
- The argument passed to this skill is the **module name** (e.g. `/create-crud-page api-keys` → module `api-keys`). If no name was given, ask for it.
- **Always ask the user for the API documentation before generating any code**, even if a module name was provided:
> Where is the API documentation for this module? (OpenAPI/Swagger URL, schema file path, or an interface description)
- Wait for the answer, then read/fetch it. Derive `config/types.ts` (`FormData`, `ListItem`), the `services` request hooks, and form fields from that schema. Do not guess field names or endpoints — if the doc is missing details, ask.
- **Also ask which form layout to scaffold:**
> Should the form use tabs? (1) a plain form without tabs, or (2) a tabbed form
Choose the form structure in section 3 accordingly. Default to **no tabs** unless the user picks tabs or the schema clearly has many grouped sections.
---
Before anything: **reuse common `components`, `hooks`, and `utils` from `@gpustack/core-ui` whenever possible.**
Reference implementation for sections below: `src/pages/model-routes`.
## Module structure
Create the module under `src/pages/{module}`:
```text
{module}
├── components
├── config
├── forms
├── hooks
├── index.tsx
└── services
```
## 1. components
Module-specific components.
- The create/edit form component is named `add-xxx-modal.tsx` (repo convention — keep the `-modal` suffix even though it is built with `FormDrawer`).
- Use `FormDrawer` from `@gpustack/core-ui`.
- If a table cell's render logic/structure is complex, extract it into `xxx-cell.tsx`.
## 2. config
```text
config
├── index.ts # static configs & constants
└── types.ts # TypeScript types
```
Naming: form types → `FormData`; table list item types → `ListItem`.
## 3. forms
Main form component goes in `forms/index.tsx`.
- **Complex interactions** (Form.Item split across components): create a dedicated Form Context and wrap with `FormContext.Provider`.
- **Tab-based forms**: use `ScrollSpyTabs` from `@gpustack/core-ui`, wrapping the `Form` or `FormContext.Provider`. Do not use tabs unless necessary.
- **Required-field validation**: use `getRuleMessage` for standard `input`/`select`.
- For cascading selectors and async race protection, follow the **form-patterns** skill.
## 4. hooks
- Table columns → `use-xxx-columns.tsx`.
- Open/close hooks for `add-xxx-modal.tsx``use-create-xxx.ts`.
## 5. index.tsx (list page entry)
- **Data fetching**: `useTableFetch` from `@gpustack/core-ui`.
- **Data display**:
- Standard table → Ant Design `Table`. Ref: `src/pages/users/index.tsx`.
- Expandable/collapsible rows → `Table` from `@gpustack/core-ui`. Ref: `src/pages/model-routes/index.tsx`.
- Card-style lists → use `InfiniteScrollerProvider`. Ref: `src/pages/backends/index.tsx`.
## 6. services
`request` is injected via a provider — do **not** create a centralized `apis` directory like in `gpustack-ui`. Define request hooks directly in `services`.
- Use `useRequest` from `@gpustack/core-ui`, or `useQueryData` (same underlying method).
- Ref: `src/pages/gpu-service/storage-types/services/use-create-storage-type.ts`.
## 7. Empty data
- Page table lists → `NoResult`.
- Simple (non-page) tables → `Empty` with `image={Empty.PRESENTED_IMAGE_SIMPLE}`.
## Common UI conventions
- **Drawer/Modal open/close**: use `useBodyScroll` from `@gpustack/core-ui`. Ref: `src/pages/model-routes/hooks/use-create-route.ts`.
- **Status display** (success/failed/processing/warning): use `StatusTag`, never `Tag` from `antd` directly. See **Status display** below. Ref: `src/pages/llmodels/components/table-list.tsx`.
- **Permission-gated visibility**: use `Access` / `useAccess`. Ref: `src/pages/access/index.tsx`.
- **Styles**: avoid `styled-components` for complex/large styling. Prefer `createStyles` for component-scoped dynamic styles, CSS Modules (`xxx.module.less`) for static structured styles.
## Status display
Use `StatusTag` from `@gpustack/core-ui` for status display. Do not use `Tag` from `antd` directly.
1. Define the mapping from business status values to UI status values in the module's `config/index.ts`:
```ts
import { StatusMaps } from '@/config';
import { StatusType } from '@/config/types';
export const XxxStatusValueMap = {
Running: 'running',
Pending: 'pending',
Failed: 'failed'
};
export const XxxStatusLabelMap: Record<string, string> = {
[XxxStatusValueMap.Running]: 'Running',
[XxxStatusValueMap.Pending]: 'Pending',
[XxxStatusValueMap.Failed]: 'Failed'
};
export const status: Record<string, StatusType> = {
[XxxStatusValueMap.Running]: StatusMaps.success,
[XxxStatusValueMap.Pending]: StatusMaps.transitioning,
[XxxStatusValueMap.Failed]: StatusMaps.error
};
```
2. In table columns, pass only the UI status value and display text required by `StatusTag`:
```tsx
<StatusTag
statusValue={{
status: status[value],
text: XxxStatusLabelMap[value] || value,
message: record.state_message
}}
/>
```
`statusValue.status` must be a value mapped from `StatusMaps` (`success`, `transitioning`, `warning`, `error`, `inactive`). Do not pass business status values such as `running` or `pending` directly.
+103
View File
@@ -0,0 +1,103 @@
---
name: form-patterns
description: Patterns for forms with cascading/dependent selections in the gpustack-ui monorepo. Use when building a form where picking one field derives another (pick A → auto-pick B → write form), handling async option loading on modal open, or protecting against stale async results.
---
# Form Patterns
Theme: keep cascading selections (pick A → derive B → write form) on a single, predictable path.
## Accessing the form: `form` vs `form.current`
This is **not** absolute — it depends on the call site:
- **Inside the form component** (`forms/index.tsx`), or anywhere holding a `Form.useForm()` instance → call it directly: `form.setFieldsValue(...)`.
- **In the outer Drawer/Modal wrapper** that opens the form and holds it via `ref={form}` (`const form = useRef(null)`), driven by an `open` prop → go through the ref: `form.current?.setFieldsValue(...)`.
The reference template below is written for the **Drawer-wrapper scenario** (it reacts to `open` and owns the shared `selection` state), so it uses `form.current?` throughout. If you lift this logic into the form body with a `useForm()` instance, drop the `.current`.
## 1. No fallback for derived selection
When "pick A then auto-pick B", match by rule and return `undefined` if no match — let the form field stay empty. Do **not** silently fall back to `list[0]`; a fallback hides data issues and fakes a valid selection.
```ts
const findB = (key, list) =>
key ? list.find((x) => x.key === key) : undefined;
```
For form fields, clear with `undefined`, not `''`. In Ant Design `undefined` restores the placeholder; `''` is treated as a real value.
## 2. Async race protection
For fetches triggered by a lifecycle entry (e.g. modal open), tag each invocation with a session ref. Discard stale results if the session rotated (modal closed and re-opened) before the response arrives.
```ts
const sessionRef = useRef(0);
useEffect(() => {
if (!open) {
sessionRef.current += 1;
return;
}
const session = ++sessionRef.current;
Promise.all([fetchA(), fetchB()]).then(([as, bs]) => {
if (sessionRef.current !== session) return;
applySelection(as.items[0], findB(as.items[0].key, bs.items));
});
}, [open]);
```
## 3. Reference template
Two cascading selectors backed by a single shared state, with a single atomic write (state + form together):
```ts
type Selection = { a?: string; b?: number };
const [selection, setSelection] = useState<Selection>({});
const sessionRef = useRef(0);
const form = useRef<any>(null); // wrapper holds the form via <Form ref={form} /> — see "Accessing the form" above
const findB = (key, list) =>
key ? list.find((x) => x.key === key) : undefined;
// Single atomic write: state + form together.
const applySelection = (a, b) => {
setSelection({ a: a.name, b: b?.id });
form.current?.setFieldsValue({
field: b?.field,
spec: { ...currentSpec, ...b?.spec }
});
};
// Trigger 1: modal opened
useEffect(() => {
if (!open) {
sessionRef.current++;
setSelection({});
return;
}
const session = ++sessionRef.current;
Promise.all([fetchA(), fetchB()]).then(([as, bs]) => {
if (sessionRef.current !== session) return;
const first = as.items[0];
applySelection(first, findB(first.key, bs.items));
});
}, [open]);
// Trigger 2: user picks A
const handleAChange = (a) => {
applySelection(a, findB(a.key, listB));
};
// Trigger 3: user picks B
const handleBChange = (b) => {
setSelection((prev) => ({ ...prev, b: b.id }));
form.current?.setFieldsValue({ ...b.fields });
};
```
## Related
- Module/file structure for forms lives in the **create-crud-page** skill (section 3).
- Required-field validation: use `getRuleMessage`.
+18
View File
@@ -1,6 +1,7 @@
name: CI name: CI
on: on:
workflow_dispatch: {}
push: push:
branches: branches:
- 'main' - 'main'
@@ -117,3 +118,20 @@ jobs:
remote_path: releases/${{ steps.version.outputs.version }}.tar.gz remote_path: releases/${{ steps.version.outputs.version }}.tar.gz
accelerate: true accelerate: true
clean: false clean: false
trigger-backend:
needs: build-publish
if: (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-22.04
steps:
- name: Dispatch backend build
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.DISPATCH_PAT }}
repository: gpustack/gpustack
event-type: ui-built
client-payload: |
{
"ref": "${{ github.ref }}",
"sha": "${{ github.sha }}"
}
+1 -1
View File
@@ -13,6 +13,6 @@
.swc .swc
.DS_Store .DS_Store
.idea .idea
.claude .claude/settings.local.json
/dist.zip /dist.zip
.cache .cache
+1 -2
View File
@@ -3,7 +3,6 @@ node_modules
.umi-production .umi-production
public/static/*.js public/static/*.js
public/static/*.css public/static/*.css
src/components/icon-font/iconfont/iconfont.js src/components/iconfont/
src/components/icon-font/iconfont/*.css
+1 -1
View File
@@ -3,5 +3,5 @@ module.exports = {
rules: { rules: {
'selector-class-pattern': null 'selector-class-pattern': null
}, },
ignoreFiles: ['public/static/*.css'] ignoreFiles: ['public/static/*.css', 'src/components/iconfont/iconfont.css']
}; };
-265
View File
@@ -1,265 +0,0 @@
# React State and Request Patterns
These guidelines define preferred patterns for request handling, state updates, and side-effect management in React applications.
The primary goal is to keep data flow explicit, predictable, maintainable, and performant while avoiding unnecessary rerenders and effect-driven logic.
---
## 1. Avoid Effect-Driven Requests
Do not use request functions themselves as dependencies in `useEffect`.
Avoid patterns like:
```ts
useEffect(() => {
fetchData();
}, [fetchData]);
```
Requests should be triggered explicitly by user actions or lifecycle entry points.
---
## 2. Form Requests Should Be Action-Driven
For form-related requests (such as loading `Select` options):
- Fetch data when the form is opened for the first time.
- If later requests depend on user interactions, trigger them directly inside the interaction handler.
- Do not rely on `useEffect` dependency changes to trigger requests.
Recommended:
```ts
const handleOnChange = (value) => {
fetchData(value);
};
```
Avoid:
```ts
useEffect(() => {
fetchData(value);
}, [value]);
```
The action itself should control the request.
---
## 3. Update Related States Together
If a single action updates multiple related states:
- Do not synchronize them through `useEffect`
- Do not derive them indirectly through `useMemo`
Instead, update all related states directly inside the action handler.
Recommended:
```ts
const handleOnChange = (value) => {
setState1(...);
setState2(...);
buildState(...);
};
```
Avoid implicit state synchronization chains.
---
## 4. Group Strongly Related State
If multiple states are always updated together:
- Do not split them into multiple `useState` calls.
- Prefer a single state object.
Recommended:
```ts
const [state, setState] = useState({
state1: ...,
state2: ...,
state3: ...,
});
```
This reduces unnecessary rerenders and keeps state transitions predictable.
---
## 5. Prefer Explicit State Flow
Avoid chaining business logic through multiple `useEffect` hooks.
Keep:
- request execution
- state updates
- derived calculations
close to the triggering action whenever possible.
Prefer:
```ts
const handleAction = () => {
fetchData();
setTableData(...);
setSelectedRow(...);
};
```
Over:
```ts
useEffect(() => {
buildTable();
}, [data]);
useEffect(() => {
updateSelection();
}, [tableData]);
```
---
## 6. Avoid Premature Memoization
Do not use `useMemo` or `useCallback` unless there is a confirmed rendering or computation bottleneck.
Overusing memoization:
- increases complexity
- makes state flow harder to understand
- may introduce stale dependency issues
Prefer simple and explicit logic first.
Optimize only when necessary.
---
## 7. Keep Request Logic Predictable
A user interaction should clearly show:
- what request is triggered
- which states are updated
- how the UI changes
Avoid indirect update chains caused by dependency-driven effects.
The code should make the request and update flow easy to trace.
---
## 8. Prefer Action-Driven Architecture
Prefer:
- action-driven updates
- explicit handlers
- localized state transitions
Over:
- effect-driven synchronization
- cross-hook implicit updates
- reactive chains between states
The triggering action should remain the primary source of truth for UI updates.
---
# Form
Form-specific patterns that build on the rules above. The theme: keep cascading selections (pick A → derive B → write form) on a single, predictable path.
## 1. No Fallback for Derived Selection
When "pick A then auto-pick B", match by rule and return `undefined` if no match — let the corresponding form field stay empty.
Do not silently fall back to `list[0]` or another default. A fallback hides data issues and tricks the user into thinking they have a valid selection.
```ts
const findB = (key, list) =>
key ? list.find((x) => x.key === key) : undefined;
```
For form fields, prefer clearing with `undefined` over `''`. With Ant Design, `undefined` restores the placeholder; `''` is treated as a real value.
## 2. Async Race Protection
For fetches triggered by a lifecycle entry (e.g., modal open), tag each invocation with a session ref. Discard stale results if the session has rotated (the modal was closed and re-opened) by the time the response arrives.
```ts
const sessionRef = useRef(0);
useEffect(() => {
if (!open) {
sessionRef.current += 1;
return;
}
const session = ++sessionRef.current;
Promise.all([fetchA(), fetchB()]).then(([as, bs]) => {
if (sessionRef.current !== session) return;
applySelection(as.items[0], findB(as.items[0].key, bs.items));
});
}, [open]);
```
## 3. Reference Template
A typical form with two cascading selectors backed by a single shared state:
```ts
type Selection = { a?: string; b?: number };
const [selection, setSelection] = useState<Selection>({});
const sessionRef = useRef(0);
const findB = (key, list) =>
key ? list.find((x) => x.key === key) : undefined;
// Single atomic write: state + form together.
const applySelection = (a, b) => {
setSelection({ a: a.name, b: b?.id });
form.current?.setFieldsValue({
field: b?.field,
spec: { ...currentSpec, ...b?.spec }
});
};
// Trigger 1: modal opened
useEffect(() => {
if (!open) {
sessionRef.current++;
setSelection({});
return;
}
const session = ++sessionRef.current;
Promise.all([fetchA(), fetchB()]).then(([as, bs]) => {
if (sessionRef.current !== session) return;
const first = as.items[0];
applySelection(first, findB(first.key, bs.items));
});
}, [open]);
// Trigger 2: user picks A
const handleAChange = (a) => {
applySelection(a, findB(a.key, listB));
};
// Trigger 3: user picks B
const handleBChange = (b) => {
setSelection((prev) => ({ ...prev, b: b.id }));
form.current?.setFieldsValue({ ...b.fields });
};
```
+74
View File
@@ -0,0 +1,74 @@
# Agent Instructions
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.
+129
View File
@@ -0,0 +1,129 @@
# Repo
This is the **open source UI** (`gpustack-ui`). Common `components`, `hooks`, and `utils` are published as `@gpustack/core-ui` and consumed throughout `src`.
**Always prioritize reusing common `components`, `hooks`, and `utils` from `@gpustack/core-ui`.**
Task-specific conventions live in skills: use **create-crud-page** when building a page module, **form-patterns** when building cascading/dependent forms.
# React State and Request Patterns
Keep data flow explicit, predictable, and performant. The triggering **action** is the source of truth for UI updates — not effect-driven synchronization.
## 1. Avoid effect-driven requests
Do not use request functions as `useEffect` dependencies. Trigger requests explicitly from user actions or lifecycle entry points.
```ts
// Avoid
useEffect(() => {
fetchData();
}, [fetchData]);
```
## 2. Form requests should be action-driven
- Fetch form data (e.g. `Select` options) when the form first opens.
- If later requests depend on interactions, trigger them inside the interaction handler.
- Do not rely on `useEffect` dependency changes.
```ts
// Recommended
const handleOnChange = (value) => {
fetchData(value);
};
```
## 3. Update related states together
When one action updates multiple related states, update them all directly in the handler. Do not sync via `useEffect` or derive indirectly via `useMemo`.
```ts
const handleOnChange = (value) => {
setState1(...);
setState2(...);
buildState(...);
};
```
## 4. Group strongly related state
If multiple states always update together, use a single state object instead of multiple `useState` calls — fewer rerenders, more predictable transitions.
```ts
const [state, setState] = useState({ state1: ..., state2: ..., state3: ... });
```
## 5. Prefer explicit state flow
Keep request execution, state updates, and derived calculations close to the triggering action. Avoid chaining business logic through multiple `useEffect` hooks.
```ts
// Prefer
const handleAction = () => {
fetchData();
setTableData(...);
setSelectedRow(...);
};
```
## 6. Avoid premature memoization
Do not use `useMemo` / `useCallback` unless there is a confirmed bottleneck. Overuse adds complexity, obscures state flow, and risks stale dependencies. Optimize only when necessary.
## 7. Keep request logic predictable
A user interaction should clearly show: what request fires, which states update, how the UI changes. Avoid indirect update chains from dependency-driven effects.
## 8. Prefer action-driven architecture
Prefer action-driven updates, explicit handlers, and localized state transitions over effect-driven synchronization, cross-hook implicit updates, and reactive chains between states.
# Styles
**Future direction (apply to all new code):** avoid `styled-components`. Prefer:
1. `createStyles` for component-scoped dynamic styles
2. CSS Modules (`xxx.module.less`) for structured static styles
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.
# Naming conventions
A page module lives under `src/pages/{module}` with this sub-structure: `components/`, `config/`, `forms/`, `hooks/`, `services/`, `index.tsx`. File naming:
- **Create/edit modal**: `add-{feature}-modal.tsx` (keep the `-modal` suffix even when built with `FormDrawer`).
- **Table columns hook**: `use-{feature}-columns.tsx`.
- **Open/close & request hooks**: `use-{verb}-{noun}.ts` (e.g. `use-create-user.ts`, `use-query-user-list.ts`).
- **Complex table cell**: extract into `{feature}-cell.tsx`.
# Config & types
- `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`.
# Common components
Always check `@gpustack/core-ui` first. Frequently reused:
- **Drawer/Modal open/close**: `useBodyScroll`.
- **Form drawer / footer**: `FormDrawer`, `ModalFooter`.
- **Delete confirmation**: `DeleteModal`.
- **Search + bulk actions bar**: `FilterBar`.
- **Form fields**: `BaseSelect`, `Input` (labeled).
- **Text overflow**: `AutoTooltip`.
- **Icons**: `IconFont`.
- **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`.
# 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:
- **Plain object** (key→value map) → `LabelSelector`.
- **String array** → `ListInput`. Ref `src/pages/llmodels/forms/backend-parameters-list.tsx`.
- **Object array** → `MetadataList` with a custom item renderer per entry. Ref `src/pages/llmodels/forms/model-lora-list.tsx`.
-46
View File
@@ -1,46 +0,0 @@
## Create form table list
## Create a form
## StatusTag
Use `StatusTag` from `@gpustack/core-ui` for status display. Do not use `Tag` from `antd` directly.
1. Define the mapping from business status values to UI status values in the module's `config/index.ts`:
```ts
import { StatusMaps } from '@/config';
import { StatusType } from '@/config/types';
export const XxxStatusValueMap = {
Running: 'running',
Pending: 'pending',
Failed: 'failed'
};
export const XxxStatusLabelMap: Record<string, string> = {
[XxxStatusValueMap.Running]: 'Running',
[XxxStatusValueMap.Pending]: 'Pending',
[XxxStatusValueMap.Failed]: 'Failed'
};
export const status: Record<string, StatusType> = {
[XxxStatusValueMap.Running]: StatusMaps.success,
[XxxStatusValueMap.Pending]: StatusMaps.transitioning,
[XxxStatusValueMap.Failed]: StatusMaps.error
};
```
2. In table columns, pass only the UI status value and display text required by `StatusTag`:
```tsx
<StatusTag
statusValue={{
status: status[value],
text: XxxStatusLabelMap[value] || value,
message: record.state_message
}}
/>
```
`statusValue.status` must be a value mapped from `StatusMaps`, such as `success`, `transitioning`, `warning`, `error`, or `inactive`. Do not pass business status values such as `running` or `pending` directly.
+2 -2
View File
@@ -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 ## Installation
+1 -1
View File
@@ -77,7 +77,7 @@ export default defineConfig({
antd: { antd: {
style: 'less' style: 'less'
}, },
title: 'GPUStack', title: 'MesaStack',
hash: true, hash: true,
access: {}, access: {},
model: {}, model: {},
+1
View File
@@ -31,5 +31,6 @@ export default function createProxyTable(target?: string) {
}, },
{} {}
); );
return proxyTable; return proxyTable;
} }
+99 -62
View File
@@ -140,15 +140,6 @@ const baseRoutes = [
access: 'canSeeOrgAdmin', access: 'canSeeOrgAdmin',
component: './model-routes/index' component: './model-routes/index'
}, },
{
name: 'usage',
path: '/models/usage',
key: 'usage',
icon: 'icon-usage-outlined',
selectedIcon: 'icon-usage-filled',
defaultIcon: 'icon-usage-outlined',
component: './usage/index'
},
{ {
name: 'providers', name: 'providers',
path: '/models/providers', path: '/models/providers',
@@ -179,6 +170,26 @@ const baseRoutes = [
access: 'canSeeOrgAdmin', access: 'canSeeOrgAdmin',
hideInMenu: true, hideInMenu: true,
component: './benchmark/details' component: './benchmark/details'
},
{
name: 'backendsList',
path: '/models/backends',
key: 'backendsList',
icon: 'icon-backend',
selectedIcon: 'icon-backend-filled',
defaultIcon: 'icon-backend',
access: 'canSeeOrgAdmin',
component: './backends/index'
},
{
name: 'modelfiles',
path: '/models/modelfiles',
key: 'modelfiles',
icon: 'icon-files',
selectedIcon: 'icon-files-filled',
defaultIcon: 'icon-files',
access: 'canSeeOrgAdmin',
component: './resources/components/model-files'
} }
] ]
}, },
@@ -186,6 +197,7 @@ const baseRoutes = [
name: 'gpuService', name: 'gpuService',
path: '/gpu-service', path: '/gpu-service',
key: 'gpuService', key: 'gpuService',
access: 'canSeeGpuService',
routes: [ routes: [
{ {
path: '/gpu-service', path: '/gpu-service',
@@ -223,7 +235,12 @@ const baseRoutes = [
path: '/gpu-service/storage-types', path: '/gpu-service/storage-types',
key: 'gpuServiceStorageTypes', key: 'gpuServiceStorageTypes',
icon: 'icon-storage-outlined', icon: 'icon-storage-outlined',
access: 'canSeeAdmin', // Storage types are tenant-scoped on the backend (Org owners
// can create/list their own), so the menu shouldn't be
// platform-admin-only. ``canSeeOrgAdmin`` keeps the gate at
// "admin or current-org owner" — Org members still don't see
// it, which matches the read/write model in the route.
access: 'canSeeOrgAdmin',
selectedIcon: 'icon-storage-filled', selectedIcon: 'icon-storage-filled',
defaultIcon: 'icon-storage-outlined', defaultIcon: 'icon-storage-outlined',
component: './gpu-service/storage-types' component: './gpu-service/storage-types'
@@ -249,6 +266,16 @@ const baseRoutes = [
path: '/resources', path: '/resources',
redirect: '/resources/workers' redirect: '/resources/workers'
}, },
{
name: 'clusters',
path: '/resources/clusters/list',
key: 'clusters',
icon: 'icon-cluster2-outline',
selectedIcon: 'icon-cluster2-filled',
defaultIcon: 'icon-cluster2-outline',
component: './cluster-management/clusters',
subMenu: ['/resources/clusters/detail', '/resources/clusters/create']
},
{ {
name: 'workers', name: 'workers',
path: '/resources/workers', path: '/resources/workers',
@@ -268,67 +295,63 @@ const baseRoutes = [
component: './resources/components/gpus' component: './resources/components/gpus'
}, },
{ {
name: 'backendsList', name: 'credentials',
path: '/resources/backends', path: '/resources/credentials',
key: 'backendsList', key: 'credentials',
icon: 'icon-backend', icon: 'icon-credential-outline',
selectedIcon: 'icon-backend-filled', selectedIcon: 'icon-credential-filled',
defaultIcon: 'icon-backend', defaultIcon: 'icon-credential-outline',
access: 'canSeeOrgAdmin', component: './cluster-management/credentials'
component: './backends/index'
},
{
name: 'modelfiles',
path: '/resources/modelfiles',
key: 'modelfiles',
icon: 'icon-files',
selectedIcon: 'icon-files-filled',
defaultIcon: 'icon-files',
component: './resources/components/model-files'
}
]
},
{
name: 'clusterManagement',
path: '/cluster-management',
key: 'clusterManagement',
access: 'canSeeOrgAdmin',
routes: [
{
path: '/cluster-management',
redirect: '/cluster-management/clusters/list'
},
{
name: 'clusters',
path: '/cluster-management/clusters/list',
key: 'clusters',
icon: 'icon-cluster2-outline',
selectedIcon: 'icon-cluster2-filled',
defaultIcon: 'icon-cluster2-outline',
component: './cluster-management/clusters',
subMenu: [
'/cluster-management/clusters/detail',
'/cluster-management/clusters/create'
]
}, },
{ {
name: 'clusterDetail', name: 'clusterDetail',
path: '/cluster-management/clusters/detail', path: '/resources/clusters/detail',
key: 'clusterDetail', key: 'clusterDetail',
icon: 'icon-cluster2-outline', icon: 'icon-cluster2-outline',
selectedIcon: 'icon-cluster2-filled', selectedIcon: 'icon-cluster2-filled',
defaultIcon: 'icon-cluster2-outline', defaultIcon: 'icon-cluster2-outline',
hideInMenu: true, hideInMenu: true,
component: './cluster-management/cluster-detail' component: './cluster-management/cluster-detail'
}
]
},
{
// Cross-resource consumption (tokens + GPU/CPU instances + storage).
// A folder so it matches the other top-level groups; more usage views can
// graduate in here later.
name: 'billingAndUsage',
path: '/usage',
key: 'usageGroup',
icon: 'icon-usage-outlined',
selectedIcon: 'icon-usage-filled',
defaultIcon: 'icon-usage-outlined',
routes: [
{
path: '/usage',
redirect: '/usage/overview'
}, },
{ {
name: 'credentials', name: 'usage',
path: '/cluster-management/credentials', path: '/usage/overview',
key: 'credentials', key: 'usage',
icon: 'icon-credential-outline', icon: 'icon-usage-outlined',
selectedIcon: 'icon-credential-filled', selectedIcon: 'icon-usage-filled',
defaultIcon: 'icon-credential-outline', defaultIcon: 'icon-usage-outlined',
component: './cluster-management/credentials' component: './usage/index'
},
{
name: 'billing',
path: '/usage/billing',
key: 'billing',
icon: 'icon-billing-outlined',
selectedIcon: 'icon-billing-filled',
defaultIcon: 'icon-billing-outlined',
hideInMenu: process.env.ENABLE_ENTERPRISE === 'true',
// OSS exposes the menu as a teaser for the enterprise billing
// module. The page itself just renders an upsell notice — the real
// billing UI lives in the enterprise plugin and shadows this route
// via `routes.extensions.ts`.
component: './billing'
} }
] ]
}, },
@@ -341,6 +364,20 @@ const baseRoutes = [
path: '/access-control', path: '/access-control',
redirect: '/access-control/users' redirect: '/access-control/users'
}, },
{
name: 'organizations',
path: '/access-control/organizations',
key: 'organizations',
icon: 'icon-org-outlined',
selectedIcon: 'icon-org-filled',
defaultIcon: 'icon-org-outlined',
// OSS exposes the menu to platform admins as a teaser for the
// enterprise multi-tenancy module. The page itself just renders
// an upsell notice — the real CRUD UI lives in the enterprise
// plugin and shadows this route via `routes.extensions.ts`.
access: 'canSeeAdmin',
component: './organizations'
},
{ {
name: 'users', name: 'users',
path: '/access-control/users', path: '/access-control/users',
@@ -374,8 +411,8 @@ const baseRoutes = [
}, },
{ {
name: 'profile', name: 'profile',
path: '/profile', path: '/preferences',
key: 'profile', key: 'preferences',
hideInMenu: true, hideInMenu: true,
component: './profile', component: './profile',
icon: 'User' icon: 'User'
+24 -10
View File
@@ -1,13 +1,27 @@
const child_process = require('child_process'); import { execSync } from 'child_process';
export const getBranchInfo = () => { export const getBranchInfo = () => {
const latestCommit = child_process // git may be absent (source archive, bare container) or this tree may
.execSync('git rev-parse HEAD') // not be a git checkout. Swallow the failure and fall back to the env
.toString() // overrides below — losing build info shouldn't fail the build.
.trim(); let latestCommit = '';
const versionTag = child_process let versionTag = '';
.execSync(`git tag --contains ${latestCommit}`) try {
.toString() latestCommit = execSync('git rev-parse HEAD').toString().trim();
.trim(); versionTag = execSync(`git tag --contains ${latestCommit}`)
return { version: versionTag || '', commitId: latestCommit.slice(0, 7) }; .toString()
.trim();
} catch {
// Not a git checkout / git unavailable; rely on env overrides.
}
// Respect explicit GPUSTACK_UI_* overrides so a wrapping build that
// checks this source tree out as a sub-package can stamp its own
// release tag and commit id onto the UI (otherwise the panel reports
// the host tree's git HEAD, which the wrapper doesn't control).
const overrideVersion = process.env.GPUSTACK_UI_VERSION?.trim();
const overrideCommitId = process.env.GPUSTACK_UI_COMMIT_ID?.trim();
return {
version: overrideVersion || versionTag || '',
commitId: overrideCommitId || latestCommit.slice(0, 7)
};
}; };
+2 -1
View File
@@ -14,7 +14,8 @@ export default defineConfig([
'dist', 'dist',
'src/.umi/', 'src/.umi/',
'src/.umi-production/', 'src/.umi-production/',
'src/.umi-test/' 'src/.umi-test/',
'src/components/iconfont/'
]), ]),
{ {
files: ['**/*.{ts,tsx,js,jsx}'], files: ['**/*.{ts,tsx,js,jsx}'],
+1 -1
View File
@@ -17,7 +17,7 @@
"@ant-design/pro-components": "3.1.0-0", "@ant-design/pro-components": "3.1.0-0",
"@antv/g6": "^5.0.51", "@antv/g6": "^5.0.51",
"@braintree/sanitize-url": "^7.1.1", "@braintree/sanitize-url": "^7.1.1",
"@gpustack/core-ui": "^1.0.16", "@gpustack/core-ui": "^1.0.32",
"@huggingface/gguf": "^0.1.7", "@huggingface/gguf": "^0.1.7",
"@huggingface/hub": "^0.15.1", "@huggingface/hub": "^0.15.1",
"@huggingface/tasks": "^0.11.6", "@huggingface/tasks": "^0.11.6",
+2
View File
@@ -5,6 +5,8 @@ export default (api: IApi) => {
const info = JSON.parse(process.env.VERSION || '{}'); const info = JSON.parse(process.env.VERSION || '{}');
const env = process.env.NODE_ENV; const env = process.env.NODE_ENV;
$('html').attr('lang', 'en');
$('html').attr('data-env', env); $('html').attr('data-env', env);
$('html').attr( $('html').attr(
+119 -119
View File
@@ -24,8 +24,8 @@ importers:
specifier: ^7.1.1 specifier: ^7.1.1
version: 7.1.2 version: 7.1.2
'@gpustack/core-ui': '@gpustack/core-ui':
specifier: ^1.0.16 specifier: ^1.0.32
version: 1.0.16(czdvzceysqw7iv6pct2ucnb23e) version: 1.0.32(czdvzceysqw7iv6pct2ucnb23e)
'@huggingface/gguf': '@huggingface/gguf':
specifier: ^0.1.7 specifier: ^0.1.7
version: 0.1.18 version: 0.1.18
@@ -49,7 +49,7 @@ importers:
version: 4.17.24 version: 4.17.24
'@umijs/max': '@umijs/max':
specifier: ^4.6.15 specifier: ^4.6.15
version: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)) version: 4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@xterm/addon-fit': '@xterm/addon-fit':
specifier: ^0.10.0 specifier: ^0.10.0
version: 0.10.0(@xterm/xterm@5.5.0) version: 0.10.0(@xterm/xterm@5.5.0)
@@ -109,7 +109,7 @@ importers:
version: 3.3.0 version: 3.3.0
jotai: jotai:
specifier: ^2.8.4 specifier: ^2.8.4
version: 2.20.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@18.3.28)(react@18.3.1) version: 2.20.0(@babel/core@7.23.6)(@babel/template@7.28.6)(@types/react@18.3.28)(react@18.3.1)
js-yaml: js-yaml:
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.1 version: 4.1.1
@@ -205,7 +205,7 @@ importers:
version: 6.4.1(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 6.4.1(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
umi-presets-pro: umi-presets-pro:
specifier: ^2.0.3 specifier: ^2.0.3
version: 2.0.3(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))) version: 2.0.3(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))
wavesurfer.js: wavesurfer.js:
specifier: ^7.8.8 specifier: ^7.8.8
version: 7.12.6 version: 7.12.6
@@ -233,10 +233,10 @@ importers:
version: 1.0.1 version: 1.0.1
'@umijs/plugins': '@umijs/plugins':
specifier: ^4.4.11 specifier: ^4.4.11
version: 4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 4.6.51(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
babel-plugin-named-asset-import: babel-plugin-named-asset-import:
specifier: ^0.3.8 specifier: ^0.3.8
version: 0.3.8(@babel/core@7.29.0) version: 0.3.8(@babel/core@7.23.6)
case-sensitive-paths-webpack-plugin: case-sensitive-paths-webpack-plugin:
specifier: ^2.4.0 specifier: ^2.4.0
version: 2.4.0 version: 2.4.0
@@ -1484,24 +1484,24 @@ packages:
resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz} resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz}
deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package
'@gpustack/core-ui@1.0.16': '@gpustack/core-ui@1.0.32':
resolution: {integrity: sha512-wFKDv7X0FXRmAmZPt4WKYV0+aGHcx82/3EZSJkrut/49XQd5XdrqKaimGtlFWFCNEWqsouwzs4uqaC7JFqfOkQ==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.16.tgz} resolution: {integrity: sha512-kGTazoqbK2KyZgOP6gmQaRxTiQVfF2IKLGDXjJq6w6BbmJgALXFJA2v2ROjAbjEVyfTdBzyYeXfPo/JgISpMNw==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.32.tgz}
peerDependencies: peerDependencies:
'@ant-design/icons': '>=6.0.0' '@ant-design/icons': ^6.1.0
'@ant-design/pro-components': 3.1.0-0 '@ant-design/pro-components': 3.1.0-0
'@monaco-editor/react': ^4.6.0 '@monaco-editor/react': ^4.6.0
ahooks: '>=3.0.0' ahooks: ^3.8.5
antd: '>=6.0.0' antd: ^6.3.3
antd-style: '>=3.0.0' antd-style: ^3.6.2
axios: '>=1.8.0' axios: ^1.8.2
echarts: '>=5.0.0' echarts: ^5.5.1
file-saver: ^2.0.5 file-saver: ^2.0.5
monaco-editor: ^0.30.1 monaco-editor: ^0.30.1
monaco-yaml: ^4.0.0 monaco-yaml: ^4.0.0
overlayscrollbars-react: ^0.5.6 overlayscrollbars-react: ^0.5.6
react: '>=18.0.0' react: ^18.2.0
react-dom: '>=18.0.0' react-dom: ^18.2.0
styled-components: '>=6.0.0' styled-components: ^6.1.15
'@hono/node-server@1.19.14': '@hono/node-server@1.19.14':
resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==, tarball: https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz} resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==, tarball: https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz}
@@ -9257,14 +9257,14 @@ snapshots:
'@radix-ui/popper': 0.0.10 '@radix-ui/popper': 0.0.10
react: 18.3.1 react: 18.3.1
'@alita/plugins@3.5.5(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@alita/plugins@3.5.5(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@alita/babel-transform-jsx-class': 0.0.2 '@alita/babel-transform-jsx-class': 0.0.2
'@alita/inspx': 0.0.2(react@18.3.1) '@alita/inspx': 0.0.2(react@18.3.1)
'@alita/request': 3.1.2 '@alita/request': 3.1.2
'@alita/types': 3.1.2 '@alita/types': 3.1.2
'@umijs/bundler-utils': 4.4.11 '@umijs/bundler-utils': 4.4.11
'@umijs/plugins': 4.4.11(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@umijs/plugins': 4.4.11(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/utils': 4.4.11 '@umijs/utils': 4.4.11
ahooks: 3.9.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ahooks: 3.9.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
antd-mobile-alita: 2.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) antd-mobile-alita: 2.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -10134,90 +10134,90 @@ snapshots:
dependencies: dependencies:
'@babel/types': 7.29.0 '@babel/types': 7.29.0
'@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.6)':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6 '@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.6)':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6 '@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.6)':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6 '@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.23.6)':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6 '@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.23.6)':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6 '@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.6)':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6 '@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.6)':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6 '@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.23.6)':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6 '@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.6)':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6 '@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.6)':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6 '@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.6)':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6 '@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.6)':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6 '@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.6)':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6 '@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.6)':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6 '@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.23.6)':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6 '@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.6)':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6 '@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.29.0)': '@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.23.6)':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) '@babel/helper-module-transforms': 7.28.6(@babel/core@7.23.6)
'@babel/helper-plugin-utils': 7.28.6 '@babel/helper-plugin-utils': 7.28.6
'@babel/helper-simple-access': 7.27.1 '@babel/helper-simple-access': 7.27.1
transitivePeerDependencies: transitivePeerDependencies:
@@ -10808,7 +10808,7 @@ snapshots:
'@formatjs/intl-utils@2.3.0': {} '@formatjs/intl-utils@2.3.0': {}
'@gpustack/core-ui@1.0.16(czdvzceysqw7iv6pct2ucnb23e)': '@gpustack/core-ui@1.0.32(czdvzceysqw7iv6pct2ucnb23e)':
dependencies: dependencies:
'@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@ant-design/pro-components': 3.1.0-0(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@ant-design/pro-components': 3.1.0-0(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -11829,7 +11829,7 @@ snapshots:
'@types/history@5.0.0': '@types/history@5.0.0':
dependencies: dependencies:
history: 4.10.1 history: 5.3.0
'@types/hoist-non-react-statics@3.3.7(@types/react@18.3.28)': '@types/hoist-non-react-statics@3.3.7(@types/react@18.3.28)':
dependencies: dependencies:
@@ -11915,7 +11915,7 @@ snapshots:
'@types/history': 4.7.11 '@types/history': 4.7.11
'@types/react': 18.3.29 '@types/react': 18.3.29
'@types/react-router': 5.1.20 '@types/react-router': 5.1.20
redux: 3.7.2 redux: 4.2.1
'@types/react-router@5.1.20': '@types/react-router@5.1.20':
dependencies: dependencies:
@@ -12281,11 +12281,11 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@umijs/bundler-utoopack@4.6.51(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))': '@umijs/bundler-utoopack@4.6.51(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
dependencies: dependencies:
'@umijs/bundler-utils': 4.6.51 '@umijs/bundler-utils': 4.6.51
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)) '@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@utoo/pack': 1.4.3(less-loader@11.1.0(less@4.1.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(less@4.1.3)(postcss@8.5.14)(resolve-url-loader@5.0.0)(sass-loader@13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1)) '@utoo/pack': 1.4.3(less-loader@11.1.0(less@4.1.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(less@4.1.3)(postcss@8.5.14)(resolve-url-loader@5.0.0)(sass-loader@13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))
compression: 1.8.1 compression: 1.8.1
connect-history-api-fallback: 2.0.0 connect-history-api-fallback: 2.0.0
cors: 2.8.6 cors: 2.8.6
@@ -12553,14 +12553,14 @@ snapshots:
- supports-color - supports-color
- typescript - typescript
'@umijs/max@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))': '@umijs/max@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
dependencies: dependencies:
'@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3) '@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3)
'@umijs/plugins': 4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@umijs/plugins': 4.6.51(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
antd: 4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) antd: 4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
eslint: 8.35.0 eslint: 8.35.0
stylelint: 14.8.2 stylelint: 14.8.2
umi: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)) umi: 4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
- '@rspack/core' - '@rspack/core'
@@ -12642,7 +12642,7 @@ snapshots:
dependencies: dependencies:
tsx: 3.12.2 tsx: 3.12.2
'@umijs/plugins@4.4.11(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@umijs/plugins@4.4.11(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@ahooksjs/use-request': 2.8.15(react@18.3.1) '@ahooksjs/use-request': 2.8.15(react@18.3.1)
'@ant-design/antd-theme-variable': 1.0.0 '@ant-design/antd-theme-variable': 1.0.0
@@ -12657,7 +12657,7 @@ snapshots:
antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20) antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20)
axios: 0.27.2 axios: 0.27.2
babel-plugin-import: 1.13.8 babel-plugin-import: 1.13.8
babel-plugin-styled-components: 2.1.4(@babel/core@7.29.0)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) babel-plugin-styled-components: 2.1.4(@babel/core@7.23.6)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
dayjs: 1.11.20 dayjs: 1.11.20
dva-core: 2.0.4(redux@4.2.1) dva-core: 2.0.4(redux@4.2.1)
dva-immer: 1.0.2(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) dva-immer: 1.0.2(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
@@ -12687,7 +12687,7 @@ snapshots:
- react-native - react-native
- supports-color - supports-color
'@umijs/plugins@4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@umijs/plugins@4.6.51(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@ahooksjs/use-request': 2.8.15(react@18.3.1) '@ahooksjs/use-request': 2.8.15(react@18.3.1)
'@ant-design/antd-theme-variable': 1.0.0 '@ant-design/antd-theme-variable': 1.0.0
@@ -12702,7 +12702,7 @@ snapshots:
antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20) antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20)
axios: 0.27.2 axios: 0.27.2
babel-plugin-import: 1.13.8 babel-plugin-import: 1.13.8
babel-plugin-styled-components: 2.1.4(@babel/core@7.29.0)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) babel-plugin-styled-components: 2.1.4(@babel/core@7.23.6)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
dayjs: 1.11.20 dayjs: 1.11.20
dva-core: 2.0.4(redux@4.2.1) dva-core: 2.0.4(redux@4.2.1)
dva-immer: 1.0.2(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) dva-immer: 1.0.2(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
@@ -12732,7 +12732,7 @@ snapshots:
- react-native - react-native
- supports-color - supports-color
'@umijs/plugins@4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@umijs/plugins@4.6.51(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@ahooksjs/use-request': 2.8.15(react@18.3.1) '@ahooksjs/use-request': 2.8.15(react@18.3.1)
'@ant-design/antd-theme-variable': 1.0.0 '@ant-design/antd-theme-variable': 1.0.0
@@ -12747,7 +12747,7 @@ snapshots:
antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20) antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20)
axios: 0.27.2 axios: 0.27.2
babel-plugin-import: 1.13.8 babel-plugin-import: 1.13.8
babel-plugin-styled-components: 2.1.4(@babel/core@7.29.0)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) babel-plugin-styled-components: 2.1.4(@babel/core@7.23.6)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
dayjs: 1.11.20 dayjs: 1.11.20
dva-core: 2.0.4(redux@4.2.1) dva-core: 2.0.4(redux@4.2.1)
dva-immer: 1.0.2(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) dva-immer: 1.0.2(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
@@ -12777,7 +12777,7 @@ snapshots:
- react-native - react-native
- supports-color - supports-color
'@umijs/preset-umi@4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))': '@umijs/preset-umi@4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
dependencies: dependencies:
'@iconify/utils': 2.1.1 '@iconify/utils': 2.1.1
'@stagewise/toolbar': 0.6.2 '@stagewise/toolbar': 0.6.2
@@ -12787,7 +12787,7 @@ snapshots:
'@umijs/bundler-esbuild': 4.6.51 '@umijs/bundler-esbuild': 4.6.51
'@umijs/bundler-mako': 0.11.10(postcss@8.5.14)(sass@1.54.0)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)) '@umijs/bundler-mako': 0.11.10(postcss@8.5.14)(sass@1.54.0)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/bundler-utils': 4.6.51 '@umijs/bundler-utils': 4.6.51
'@umijs/bundler-utoopack': 4.6.51(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)) '@umijs/bundler-utoopack': 4.6.51(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/bundler-vite': 4.6.51(@types/node@25.6.2)(lightningcss@1.22.1)(postcss@8.5.14)(rollup@3.30.0)(sass@1.54.0)(terser@5.48.0) '@umijs/bundler-vite': 4.6.51(@types/node@25.6.2)(lightningcss@1.22.1)(postcss@8.5.14)(rollup@3.30.0)(sass@1.54.0)(terser@5.48.0)
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)) '@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/core': 4.6.51 '@umijs/core': 4.6.51
@@ -12870,13 +12870,13 @@ snapshots:
react-helmet-async: 1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-helmet-async: 1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router-dom: 6.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-router-dom: 6.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/request-record@1.1.4(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))': '@umijs/request-record@1.1.4(umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))':
dependencies: dependencies:
chokidar: 3.6.0 chokidar: 3.6.0
express: 4.22.1 express: 4.22.1
lodash: 4.18.1 lodash: 4.18.1
prettier: 2.8.8 prettier: 2.8.8
umi: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)) umi: 4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -12892,13 +12892,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@umijs/test@4.6.51(@babel/core@7.29.0)': '@umijs/test@4.6.51(@babel/core@7.23.6)':
dependencies: dependencies:
'@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.29.0) '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.6)
'@jest/types': 27.5.1 '@jest/types': 27.5.1
'@umijs/bundler-utils': 4.6.51 '@umijs/bundler-utils': 4.6.51
'@umijs/utils': 4.6.51 '@umijs/utils': 4.6.51
babel-jest: 29.7.0(@babel/core@7.29.0) babel-jest: 29.7.0(@babel/core@7.23.6)
esbuild: 0.21.4 esbuild: 0.21.4
identity-obj-proxy: 3.0.0 identity-obj-proxy: 3.0.0
isomorphic-unfetch: 4.0.2 isomorphic-unfetch: 4.0.2
@@ -13018,7 +13018,7 @@ snapshots:
'@utoo/pack-win32-x64-msvc@1.4.3': '@utoo/pack-win32-x64-msvc@1.4.3':
optional: true optional: true
'@utoo/pack@1.4.3(less-loader@11.1.0(less@4.1.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(less@4.1.3)(postcss@8.5.14)(resolve-url-loader@5.0.0)(sass-loader@13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))': '@utoo/pack@1.4.3(less-loader@11.1.0(less@4.1.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(less@4.1.3)(postcss@8.5.14)(resolve-url-loader@5.0.0)(sass-loader@13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))':
dependencies: dependencies:
'@babel/code-frame': 7.22.5 '@babel/code-frame': 7.22.5
'@hono/node-server': 1.19.14(hono@4.12.18) '@hono/node-server': 1.19.14(hono@4.12.18)
@@ -13039,7 +13039,7 @@ snapshots:
sass-loader: 13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)) sass-loader: 13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
semver: 7.8.0 semver: 7.8.0
send: 0.17.1 send: 0.17.1
styled-jsx: 5.1.7(@babel/core@7.29.0)(react@18.3.1) styled-jsx: 5.1.7(@babel/core@7.23.6)(react@18.3.1)
ws: 8.20.0 ws: 8.20.0
optionalDependencies: optionalDependencies:
'@utoo/pack-darwin-arm64': 1.4.3 '@utoo/pack-darwin-arm64': 1.4.3
@@ -13555,13 +13555,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- debug - debug
babel-jest@29.7.0(@babel/core@7.29.0): babel-jest@29.7.0(@babel/core@7.23.6):
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
'@jest/transform': 29.7.0 '@jest/transform': 29.7.0
'@types/babel__core': 7.20.5 '@types/babel__core': 7.20.5
babel-plugin-istanbul: 6.1.1 babel-plugin-istanbul: 6.1.1
babel-preset-jest: 29.6.3(@babel/core@7.29.0) babel-preset-jest: 29.6.3(@babel/core@7.23.6)
chalk: 4.1.2 chalk: 4.1.2
graceful-fs: 4.2.11 graceful-fs: 4.2.11
slash: 3.0.0 slash: 3.0.0
@@ -13601,9 +13601,9 @@ snapshots:
cosmiconfig: 7.1.0 cosmiconfig: 7.1.0
resolve: 1.22.12 resolve: 1.22.12
babel-plugin-named-asset-import@0.3.8(@babel/core@7.29.0): babel-plugin-named-asset-import@0.3.8(@babel/core@7.23.6):
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
babel-plugin-react-compiler@0.0.0-experimental-c23de8d-20240515: babel-plugin-react-compiler@0.0.0-experimental-c23de8d-20240515:
dependencies: dependencies:
@@ -13615,11 +13615,11 @@ snapshots:
zod: 3.25.76 zod: 3.25.76
zod-validation-error: 2.1.0(zod@3.25.76) zod-validation-error: 2.1.0(zod@3.25.76)
babel-plugin-styled-components@2.1.4(@babel/core@7.29.0)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): babel-plugin-styled-components@2.1.4(@babel/core@7.23.6)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
dependencies: dependencies:
'@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-annotate-as-pure': 7.27.3
'@babel/helper-module-imports': 7.28.6 '@babel/helper-module-imports': 7.28.6
'@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.23.6)
lodash: 4.18.1 lodash: 4.18.1
picomatch: 2.3.2 picomatch: 2.3.2
styled-components: 6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) styled-components: 6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -13627,30 +13627,30 @@ snapshots:
- '@babel/core' - '@babel/core'
- supports-color - supports-color
babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): babel-preset-current-node-syntax@1.2.0(@babel/core@7.23.6):
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
'@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.6)
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.23.6)
'@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.6)
'@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.6)
'@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.23.6)
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.6)
'@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.6)
'@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.6)
'@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.6)
'@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.6)
'@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.6)
'@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.6)
'@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.6)
'@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.6)
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.6)
babel-preset-jest@29.6.3(@babel/core@7.29.0): babel-preset-jest@29.6.3(@babel/core@7.23.6):
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
babel-plugin-jest-hoist: 29.6.3 babel-plugin-jest-hoist: 29.6.3
babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) babel-preset-current-node-syntax: 1.2.0(@babel/core@7.23.6)
babel-runtime-jsx-plus@0.1.5: {} babel-runtime-jsx-plus@0.1.5: {}
@@ -16410,9 +16410,9 @@ snapshots:
jiti@2.7.0: {} jiti@2.7.0: {}
jotai@2.20.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@18.3.28)(react@18.3.1): jotai@2.20.0(@babel/core@7.23.6)(@babel/template@7.28.6)(@types/react@18.3.28)(react@18.3.1):
optionalDependencies: optionalDependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
'@babel/template': 7.28.6 '@babel/template': 7.28.6
'@types/react': 18.3.28 '@types/react': 18.3.28
react: 18.3.1 react: 18.3.1
@@ -19805,12 +19805,12 @@ snapshots:
css-to-react-native: 3.2.0 css-to-react-native: 3.2.0
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1): styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1):
dependencies: dependencies:
client-only: 0.0.1 client-only: 0.0.1
react: 18.3.1 react: 18.3.1
optionalDependencies: optionalDependencies:
'@babel/core': 7.29.0 '@babel/core': 7.23.6
stylelint-config-recommended@7.0.0(stylelint@14.8.2): stylelint-config-recommended@7.0.0(stylelint@14.8.2):
dependencies: dependencies:
@@ -20164,12 +20164,12 @@ snapshots:
ua-parser-js@0.7.41: {} ua-parser-js@0.7.41: {}
umi-presets-pro@2.0.3(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))): umi-presets-pro@2.0.3(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))):
dependencies: dependencies:
'@alita/plugins': 3.5.5(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@alita/plugins': 3.5.5(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/max-plugin-openapi': 2.0.3(chokidar@3.6.0)(encoding@0.1.13)(typescript@5.9.3) '@umijs/max-plugin-openapi': 2.0.3(chokidar@3.6.0)(encoding@0.1.13)(typescript@5.9.3)
'@umijs/plugins': 4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@umijs/plugins': 4.6.51(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/request-record': 1.1.4(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))) '@umijs/request-record': 1.1.4(umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))
swagger-ui-dist: 4.19.1 swagger-ui-dist: 4.19.1
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
@@ -20193,17 +20193,17 @@ snapshots:
isomorphic-fetch: 2.2.1 isomorphic-fetch: 2.2.1
qs: 6.15.1 qs: 6.15.1
umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)): umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)):
dependencies: dependencies:
'@babel/runtime': 7.23.6 '@babel/runtime': 7.23.6
'@umijs/bundler-utils': 4.6.51 '@umijs/bundler-utils': 4.6.51
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)) '@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/core': 4.6.51 '@umijs/core': 4.6.51
'@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3) '@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3)
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)) '@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/renderer-react': 4.6.51(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@umijs/renderer-react': 4.6.51(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/server': 4.6.51 '@umijs/server': 4.6.51
'@umijs/test': 4.6.51(@babel/core@7.29.0) '@umijs/test': 4.6.51(@babel/core@7.23.6)
'@umijs/utils': 4.6.51 '@umijs/utils': 4.6.51
prettier-plugin-organize-imports: 3.2.4(prettier@3.8.3)(typescript@5.9.3) prettier-plugin-organize-imports: 3.2.4(prettier@3.8.3)(typescript@5.9.3)
prettier-plugin-packagejson: 2.4.3(prettier@3.8.3) prettier-plugin-packagejson: 2.4.3(prettier@3.8.3)
@@ -20247,17 +20247,17 @@ snapshots:
- webpack-hot-middleware - webpack-hot-middleware
- webpack-plugin-serve - webpack-plugin-serve
umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)): umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)):
dependencies: dependencies:
'@babel/runtime': 7.23.6 '@babel/runtime': 7.23.6
'@umijs/bundler-utils': 4.6.51 '@umijs/bundler-utils': 4.6.51
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)) '@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/core': 4.6.51 '@umijs/core': 4.6.51
'@umijs/lint': 4.6.51(eslint@9.39.4(jiti@2.7.0))(stylelint@14.8.2)(typescript@5.9.3) '@umijs/lint': 4.6.51(eslint@9.39.4(jiti@2.7.0))(stylelint@14.8.2)(typescript@5.9.3)
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)) '@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/renderer-react': 4.6.51(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@umijs/renderer-react': 4.6.51(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/server': 4.6.51 '@umijs/server': 4.6.51
'@umijs/test': 4.6.51(@babel/core@7.29.0) '@umijs/test': 4.6.51(@babel/core@7.23.6)
'@umijs/utils': 4.6.51 '@umijs/utils': 4.6.51
prettier-plugin-organize-imports: 3.2.4(prettier@3.8.3)(typescript@5.9.3) prettier-plugin-organize-imports: 3.2.4(prettier@3.8.3)(typescript@5.9.3)
prettier-plugin-packagejson: 2.4.3(prettier@3.8.3) prettier-plugin-packagejson: 2.4.3(prettier@3.8.3)
+102
View File
@@ -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' 复核改动。"
+83
View File
@@ -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 "同步完成。"
+22 -1
View File
@@ -1,6 +1,10 @@
import { applyAccessExtensions } from './access.extensions'; import { applyAccessExtensions } from './access.extensions';
export default (initialState: { currentUser?: Global.UserInfo }) => { export default (initialState: {
currentUser?: Global.UserInfo;
hasKubernetesCluster?: boolean;
hasResourceEvents?: boolean;
}) => {
const isPlatformAdmin = !!( const isPlatformAdmin = !!(
initialState && initialState &&
initialState.currentUser && initialState.currentUser &&
@@ -11,6 +15,16 @@ export default (initialState: { currentUser?: Global.UserInfo }) => {
initialState.currentUser && initialState.currentUser &&
!initialState.currentUser.is_admin !initialState.currentUser.is_admin
); );
// GPU Service is Kubernetes-only. We only gate visibility down when
// the probe in `getInitialState` came back with a definitive answer;
// `undefined` (probe failed / not yet ready) collapses to the
// role-based default so a transient network blip can't lock anyone
// out of the menu.
const hasKubernetesCluster = initialState?.hasKubernetesCluster;
// Having run GPU/CPU instances or storage (any resource_events) also unlocks
// GPU Service / the full Usage page — a user who used it keeps seeing it even
// without a current cluster. MaaS-only users (no cluster, no events) don't.
const hasResourceEvents = !!initialState?.hasResourceEvents;
// Predicate roles, top-down by strictness: // Predicate roles, top-down by strictness:
// * `canSeeAdmin` — strictly platform admin (`users.is_admin`). // * `canSeeAdmin` — strictly platform admin (`users.is_admin`).
@@ -18,6 +32,11 @@ export default (initialState: { currentUser?: Global.UserInfo }) => {
// * `canSeeOrgAdmin` — admin-style menus that work cross-org // * `canSeeOrgAdmin` — admin-style menus that work cross-org
// (Dashboard, Resources, Models, Cluster Management). Defaults // (Dashboard, Resources, Models, Cluster Management). Defaults
// to platform admin; extensions widen to include org admins. // to platform admin; extensions widen to include org admins.
// * `canSeeGpuService` — GPU Service menu. Anyone allowed to
// manage clusters (admins, Org owners) sees it; non-admins fall
// through to "show only if a Kubernetes cluster is actually
// reachable" so Org members without scheduling access don't see
// a dead-end menu item.
// * `canManageCurrentOrg` — pages that only make sense inside a // * `canManageCurrentOrg` — pages that only make sense inside a
// specific org context (member / group management). Defaults to // specific org context (member / group management). Defaults to
// `false`; extensions widen when both an org is selected AND // `false`; extensions widen when both an org is selected AND
@@ -27,6 +46,8 @@ export default (initialState: { currentUser?: Global.UserInfo }) => {
return applyAccessExtensions({ return applyAccessExtensions({
canSeeAdmin: isPlatformAdmin, canSeeAdmin: isPlatformAdmin,
canSeeOrgAdmin: isPlatformAdmin, canSeeOrgAdmin: isPlatformAdmin,
canSeeGpuService:
isPlatformAdmin || hasKubernetesCluster !== false || hasResourceEvents,
canManageCurrentOrg: false, canManageCurrentOrg: false,
canSeeUser, canSeeUser,
canDelete: true, canDelete: true,
+55 -3
View File
@@ -1,8 +1,9 @@
import { userSettingsHelperAtom } from '@/atoms/settings'; import { userSettingsHelperAtom } from '@/atoms/settings';
import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user'; import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
import { setAtomStorage } from '@/atoms/utils'; import { setAtomStorage } from '@/atoms/utils';
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings'; import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
import { COLOR_PRIMARY } from '@/config/theme/constants'; import { COLOR_PRIMARY } from '@/config/theme/constants';
import { getGPUStackPlugin } from '@/plugins';
import { enterprisePluginReady } from '@/plugins/enterprise-ready'; import { enterprisePluginReady } from '@/plugins/enterprise-ready';
import { GPUStackPluginManager } from '@/plugins/manager'; import { GPUStackPluginManager } from '@/plugins/manager';
import { requestConfig } from '@/request-config'; import { requestConfig } from '@/request-config';
@@ -13,6 +14,11 @@ import {
} from '@/services/profile/apis'; } from '@/services/profile/apis';
import { fetchSystemConfig } from '@/services/system/query-system-config'; import { fetchSystemConfig } from '@/services/system/query-system-config';
import { isOnline } from '@/utils'; import { isOnline } from '@/utils';
import {
markInitialStateProbed,
probeAccessFlags
} from '@/utils/access-probes';
import { installTenantFetch } from '@/utils/install-fetch';
import { import {
IS_FIRST_LOGIN, IS_FIRST_LOGIN,
readState, readState,
@@ -22,6 +28,8 @@ import '@gpustack/core-ui/style.css';
import { RequestConfig, history, request as umiRequest } from '@umijs/max'; import { RequestConfig, history, request as umiRequest } from '@umijs/max';
import { message } from 'antd'; import { message } from 'antd';
installTenantFetch();
// only for the first login and access from http://localhost // only for the first login and access from http://localhost
const checkDefaultPage = async (userInfo: any) => { const checkDefaultPage = async (userInfo: any) => {
@@ -39,6 +47,8 @@ export async function getInitialState(): Promise<{
fetchUserInfo: () => Promise<Global.UserInfo>; fetchUserInfo: () => Promise<Global.UserInfo>;
currentUser?: Global.UserInfo; currentUser?: Global.UserInfo;
pluginData?: Record<string, any>; pluginData?: Record<string, any>;
hasKubernetesCluster?: boolean;
hasResourceEvents?: boolean;
}> { }> {
const { location } = history; const { location } = history;
@@ -83,6 +93,36 @@ export async function getInitialState(): Promise<{
getUpdateCheck(); getUpdateCheck();
fetchSystemConfig(); fetchSystemConfig();
} }
// Only commit a substantive user object. A truthy-but-empty
// `data` (e.g. server responded 200 with an empty body) would
// otherwise look like "logged in" to every `currentUser`
// reader and the access seam — break out instead and let the
// caller treat the request as failed.
if (data && typeof data === 'object' && Object.keys(data).length > 0) {
// Commit the identity to atom storage (and so to localStorage)
// before returning. The access function — memoized on
// `initialState` and run once per commit — reads identity from
// localStorage; without this preemptive write the predicate
// sees the prior session's identity on its first evaluation
// after login, and stays stale until the next identity change
// (which usually doesn't come without a manual refresh).
try {
setAtomStorage(userAtom, data);
} catch (err) {
console.error('userAtom commit error:', err);
}
// Fire `onUserFetched` so plugins maintaining identity-scoped
// caches can seed them under the new identity before any
// caller commits this user to `initialState`. Errors here are
// swallowed and logged — fetchUserInfo must still return.
try {
await getGPUStackPlugin()?.login?.onUserFetched?.(data, {
request: umiRequest
});
} catch (err) {
console.error('onUserFetched plugin hook error:', err);
}
}
return data; return data;
} catch (error: any) { } catch (error: any) {
const data = error?.response?.data; const data = error?.response?.data;
@@ -122,12 +162,24 @@ export async function getInitialState(): Promise<{
getAppVersionInfo(); getAppVersionInfo();
if (![DEFAULT_ENTER_PAGE.login].includes(location.pathname)) { if (![DEFAULT_ENTER_PAGE.login].includes(location.pathname)) {
const userInfo = await fetchUserInfo(); const [userInfo, accessFlags] = await Promise.all([
fetchUserInfo(),
probeAccessFlags()
]);
// Record that the probes ran for an authenticated user this page load
// (the refresh path) so the layout doesn't re-probe. A failed
// fetch (empty user — e.g. unauthenticated deep link that bounces to
// login) is NOT marked: the user will log in via SPA afterwards and
// the layout becomes responsible for probing.
if (userInfo?.username) {
markInitialStateProbed();
}
checkDefaultPage(userInfo); checkDefaultPage(userInfo);
return { return {
fetchUserInfo, fetchUserInfo,
currentUser: userInfo, currentUser: userInfo,
pluginData pluginData,
...accessFlags
}; };
} }
return { return {
@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>AlibabaCloud</title><path d="M14.752 4.64h5.274C22.242 4.64 24 6.475 24 8.691V15.8a3.947 3.947 0 01-3.974 3.975h-5.274l1.299-1.835 3.822-1.222c.688-.23 1.146-.918 1.146-1.605v-5.81c0-.687-.458-1.375-1.146-1.605L16.05 6.475l-1.3-1.835zM2.98 15.111c0 .688.46 1.376 1.147 1.606l3.822 1.146 1.3 1.835H3.974A3.947 3.947 0 010 15.723V8.69c0-2.216 1.758-4.05 3.975-4.05h5.273L7.95 6.474 4.127 7.697c-.688.23-1.146.918-1.146 1.606v5.808z" fill="#FF6A00"></path><path d="M16.051 11.213H8.025v1.835h8.026v-1.835z" fill="#FF6A00"></path></svg>

After

Width:  |  Height:  |  Size: 656 B

+1
View File
@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>BaiLian</title><path d="M6.336 8.919v6.162l5.335-3.083L6.337 8.92z" fill="#1C54E3"></path><path d="M21.394 5.288s-.006-.006-.01-.006L17.01 2.754 6.336 8.92l5.335 3.082 9.701-5.6.016-.01a.635.635 0 00.006-1.1v-.003z" fill="#AA9AFF"></path><path d="M21.71 12.465a.62.62 0 00-.316.085s-.006 0-.009.003l-4.375 2.528 5.05 2.915h.006a2.06 2.06 0 00.28-1.04v-3.855a.637.637 0 00-.636-.636z" fill="#00EAD1"></path><path d="M22.06 17.996l-5.05-2.915L6.34 21.242l4.27 2.465s.016.006.022.012a2.102 2.102 0 002.093 0c.006-.003.016-.006.022-.012l8.538-4.93c.003 0 .006-.003.01-.006.321-.183.589-.45.775-.772h-.006l-.004-.003z" fill="#00CEC9"></path><path d="M11.672 11.998l-5.336 3.083-1.444.832-3.605 2.083H1.28c.173.303.416.555.709.738l.078.044.016.01.02.012 4.232 2.442 10.671-6.161-5.335-3.082z" fill="#00EAD1"></path><path d="M12.74.29c-.1-.06-.208-.107-.315-.148-.02-.006-.038-.016-.057-.022a2.121 2.121 0 00-.7-.12c-.233 0-.457.038-.668.11l-.031.01a2.196 2.196 0 00-.372.17L2.068 5.222s-.003 0-.006.003c-.324.183-.592.451-.781.773h.006l5.049 2.918L17.01 2.758 12.74.29z" fill="#7347FF"></path><path d="M1.287 6.001H1.28A2.06 2.06 0 001 7.041v9.915c0 .378.1.735.28 1.043h.007l5.049-2.918V8.919l-5.05-2.918z" fill="#0423DA"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -3,6 +3,7 @@
.ant-layout-sider-children { .ant-layout-sider-children {
border-inline: none; border-inline: none;
border-radius: 0; border-radius: 0;
padding-inline-end: 0;
padding-block-end: 8px; padding-block-end: 8px;
} }
+7
View File
@@ -63,6 +63,13 @@ export const fromClusterCreationAtom = atom(false);
export const clusterSessionAtom = atom<{ export const clusterSessionAtom = atom<{
firstAddWorker: boolean; firstAddWorker: boolean;
firstAddCluster: boolean; firstAddCluster: boolean;
presetClusterType?: 'model' | 'gpu';
// Provider to preselect when the create flow opens — set by the
// empty-state CTA on feature pages that need a specific provider
// (e.g. GPU Service can only schedule on Kubernetes, so its
// "Add Cluster" button skips provider catalog and lands on the
// K8s configure step). Consumed once by ClusterCreate on mount.
providerHint?: string;
} | null>(null); } | null>(null);
export const clusterDetailAtom = atom<ClusterListItem | null>(null); export const clusterDetailAtom = atom<ClusterListItem | null>(null);
-10
View File
@@ -1,10 +0,0 @@
import { ClusterListItem } from '@/pages/cluster-management/config/types';
import { atom } from 'jotai';
export const currentClusterAtom = atom<
(Partial<ClusterListItem> & { label?: string; value?: number }) | null
>(null);
export const addSSHKeyPageAtom = atom<{ create: boolean }>({
create: false
});
+7
View File
@@ -55,3 +55,10 @@ export const userSettingsHelperAtom = atom(
} }
); );
export const hideModalTemporarilyAtom = atom<boolean>(false); export const hideModalTemporarilyAtom = atom<boolean>(false);
export const collapsedMenuGroupsAtom = atomWithStorage<string[]>(
'collapsedMenuGroups',
[],
undefined,
{ getOnInit: true }
);
+50 -10
View File
@@ -3,6 +3,15 @@ import { atomWithStorage } from 'jotai/utils';
export const userAtom = atomWithStorage<any>('userInfo', null); export const userAtom = atomWithStorage<any>('userInfo', null);
// Backs the `currentOrganizationId` localStorage key. Stays null in
// builds with no Org context (single-tenant), and is shared with any
// extension that persists the same key so both sides stay in sync
// without one side having to import from the other.
export const currentOrganizationIdAtom = atomWithStorage<number | null>(
'currentOrganizationId',
null
);
export const GPUStackVersionAtom = atom<{ export const GPUStackVersionAtom = atom<{
version: string; version: string;
git_commit: string; git_commit: string;
@@ -23,10 +32,7 @@ export const UpdateCheckAtom = atom<{
latest_version: '' latest_version: ''
}); });
export const initialPasswordAtom = atomWithStorage<string>( export const initialPasswordAtom = atom<string>('');
'initialPassword',
''
);
// Namespace the server creates for an Org's resources on each Kubernetes // Namespace the server creates for an Org's resources on each Kubernetes
// cluster. The format must match the backend's ``get_namespace_name`` // cluster. The format must match the backend's ``get_namespace_name``
@@ -81,23 +87,57 @@ const getStoredCurrentOrgId = (): number | null => {
// cluster-owner fallback. // cluster-owner fallback.
const ORG_CACHE_KEYS = ['organizationList', 'allOrganizations'] as const; const ORG_CACHE_KEYS = ['organizationList', 'allOrganizations'] as const;
const lookupOrgNamespace = (id: number | null): string | null => { export interface CachedOrg {
id: number;
name?: string;
// The platform Org (single global tenant). Its models are NOT
// namespaced in ``/v1/models`` — they appear under their bare name.
is_platform?: boolean;
}
// Resolve the cached Org record for an owner/principal id by scanning both
// org caches. ``organizationList`` (the caller's member orgs) is checked
// alongside the admin-only ``allOrganizations`` so member sessions resolve
// too. Id types vary between localStorage payloads (some writers stringify,
// others persist as a JSON number), so compare as strings — strict equality
// would silently miss those cases.
export const getOrgById = (
id: number | string | null | undefined
): CachedOrg | null => {
if (id == null) return null; if (id == null) return null;
// Normalise both sides to strings — the stored id type varies between
// localStorage payloads (some writers stringify, others persist as a
// JSON number); strict equality would silently miss those cases.
const target = String(id); const target = String(id);
for (const key of ORG_CACHE_KEYS) { for (const key of ORG_CACHE_KEYS) {
try { try {
const raw = localStorage.getItem(key); const raw = localStorage.getItem(key);
if (!raw) continue; if (!raw) continue;
const list = JSON.parse(raw) as Array<{ id: number; name?: string }>; const list = JSON.parse(raw) as CachedOrg[];
if (!Array.isArray(list)) continue; if (!Array.isArray(list)) continue;
const match = list.find((item) => String(item?.id) === target); const match = list.find((item) => String(item?.id) === target);
if (match?.name) return `gpustack-${match.name}`; if (match) return match;
} catch { } catch {
// ignore malformed cache; continue checking other keys // ignore malformed cache; continue checking other keys
} }
} }
return null; return null;
}; };
// Bare Org *name* (e.g. ``org1``) for an owner/principal id, or null.
export const getOrgNameById = (
id: number | string | null | undefined
): string | null => {
return getOrgById(id)?.name ?? null;
};
const lookupOrgNamespace = (id: number | null): string | null => {
const name = getOrgNameById(id);
return name ? `gpustack-${name}` : null;
};
// The Org the caller is currently acting under, or null in the admin-"All"
// context. The org-switcher reloads the page on switch, so the list pages
// always show this org's resources — which is how ``/v1/models`` namespaces
// their model ids (``{org}/{name}``). Callers reconstructing that id use this
// as the fallback owner when a row carries no explicit ``owner_principal_id``.
export const getCurrentOrg = (): CachedOrg | null => {
return getOrgById(getStoredCurrentOrgId());
};
+2 -25
View File
@@ -1,9 +1,7 @@
import { GPUStackVersionAtom } from '@/atoms/user'; import { GPUStackVersionAtom } from '@/atoms/user';
import { getAtomStorage } from '@/atoms/utils'; import { getAtomStorage } from '@/atoms/utils';
import VersionInfo, { modalConfig } from '@/components/version-info';
import externalLinks from '@/constants/external-links';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Divider, Modal, Typography } from 'antd'; import { Divider, Typography } from 'antd';
import { createStyles } from 'antd-style'; import { createStyles } from 'antd-style';
import styled from 'styled-components'; import styled from 'styled-components';
@@ -33,20 +31,10 @@ const useStyles = createStyles(({ token, css }) => ({
const Footer: React.FC = () => { const Footer: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const [modal, contextHolder] = Modal.useModal();
const { styles } = useStyles(); const { styles } = useStyles();
const showVersion = () => {
modal.info({
...modalConfig,
width: 460,
content: <VersionInfo intl={intl} />
});
};
return ( return (
<> <>
{contextHolder}
<div className={styles.footer}> <div className={styles.footer}>
<div className="footer-content"> <div className="footer-content">
<div className="footer-content-left"> <div className="footer-content-left">
@@ -63,18 +51,7 @@ const Footer: React.FC = () => {
</Typography.Link> </Typography.Link>
</CompanyWrapper> </CompanyWrapper>
<Divider orientation="vertical" /> <Divider orientation="vertical" />
<Button <span>{getAtomStorage(GPUStackVersionAtom)?.version}</span>
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}>
{getAtomStorage(GPUStackVersionAtom)?.version}
</Button>
</div> </div>
</div> </div>
</div> </div>
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
+6
View File
@@ -92,6 +92,12 @@ declare namespace Global {
interface InitialStateType { interface InitialStateType {
fetchUserInfo: () => Promise<UserInfo>; fetchUserInfo: () => Promise<UserInfo>;
currentUser?: UserInfo; currentUser?: UserInfo;
// Captured at app boot so access predicates can gate GPU Service —
// the feature is Kubernetes-only, and Org members without a K8s
// cluster they can schedule on shouldn't see the menu. Refreshed
// by full page reload (e.g. OrgSwitcher) which re-runs
// getInitialState.
hasKubernetesCluster?: boolean;
} }
type SearchParams = Pagination & { search?: string; [key: string]: any }; type SearchParams = Pagination & { search?: string; [key: string]: any };
+4 -2
View File
@@ -54,7 +54,8 @@ export default {
itemHoverColor: 'rgba(0,0,0,1)', itemHoverColor: 'rgba(0,0,0,1)',
itemColor: 'rgba(0,0,0,1)', itemColor: 'rgba(0,0,0,1)',
itemHoverBg: 'rgb(24 25 27)', itemHoverBg: 'rgb(24 25 27)',
itemActiveBg: 'rgb(24 25 27)' itemActiveBg: 'rgb(24 25 27)',
menuItemSelectedBg: '#292929'
}, },
Progress: { Progress: {
lineBorderRadius: 4 lineBorderRadius: 4
@@ -108,6 +109,7 @@ export default {
fontSize: 14, fontSize: 14,
motion: true, motion: true,
colorFill: '#0A0A0A', colorFill: '#0A0A0A',
colorBgBase: '#0A0A0A' colorBgBase: '#0A0A0A',
menuItemSelectedBg: '#292929'
} }
}; };
+2 -1
View File
@@ -56,7 +56,8 @@ export default {
itemHoverColor: 'rgba(0,0,0,1)', itemHoverColor: 'rgba(0,0,0,1)',
itemColor: 'rgba(0,0,0,1)', itemColor: 'rgba(0,0,0,1)',
itemHoverBg: 'rgba(0,0,0,0.04)', itemHoverBg: 'rgba(0,0,0,0.04)',
itemActiveBg: 'rgba(0,0,0,0.04)' itemActiveBg: 'rgba(0,0,0,0.04)',
menuItemSelectedBg: '#e8eaed'
}, },
Progress: { Progress: {
lineBorderRadius: 3 lineBorderRadius: 3
+26 -39
View File
@@ -171,8 +171,6 @@ body {
} }
.ant-table .ant-table-container table { .ant-table .ant-table-container table {
// border-spacing: 0 20px;
.ant-table-thead th.ant-table-column-sort { .ant-table-thead th.ant-table-column-sort {
background-color: transparent; background-color: transparent;
@@ -241,8 +239,8 @@ body {
height: 100%; height: 100%;
.ant-pro-sider-footer { .ant-pro-sider-footer {
padding-block: 2px 0; padding-block: 4px 0;
padding-left: 6px; padding-left: 8px;
} }
.ant-pro-sider-actions-list-item { .ant-pro-sider-actions-list-item {
@@ -259,46 +257,26 @@ body {
border-block-end: none; border-block-end: none;
} }
.ant-pro-sider-logo-collapsed { .ant-pro-sider-logo {
padding-left: 12px; .collapse-btn {
cursor: e-resize; color: var(--ant-color-text-tertiary);
:hover {
.collapse-wrap { color: var(--ant-color-text);
display: none;
position: absolute;
top: 12px;
&::after {
content: '';
position: absolute;
height: 48px;
width: 64px;
top: -16px;
left: -16px;
}
}
&:hover {
.collapse-wrap {
display: block;
} }
} }
} }
.ant-pro-sider .ant-layout-sider-children { .ant-pro-sider-logo-collapsed {
// border-right: 1px solid var(--ant-color-split); padding-left: 6px;
} }
} }
.ant-table-content table { .ant-table .ant-table-tbody {
.ant-table-tbody { .ant-table-row {
.ant-table-row { border-radius: var(--table-td-radius);
border-radius: var(--table-td-radius);
> td { > td {
background-color: unset; border-bottom: 1px solid var(--ant-color-split);
border-bottom: 1px solid var(--ant-color-split);
}
} }
} }
} }
@@ -347,7 +325,6 @@ body {
.ant-pro-layout-container { .ant-pro-layout-container {
overflow-x: auto; overflow-x: auto;
min-height: 100vh; min-height: 100vh;
// background-color: var(--ant-color-bg-container);
} }
.ant-pro-sider { .ant-pro-sider {
@@ -661,10 +638,15 @@ body {
inset: 53px auto auto 0 !important; inset: 53px auto auto 0 !important;
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base); border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
border-top: none; border-top: none;
max-height: 240px; .ant-cascader-menus {
min-height: 100px; align-items: stretch;
}
&.gpu-selector { &.gpu-selector {
.ant-cascader-menu {
max-height: 400px;
height: unset;
}
.ant-cascader-menu:last-child { .ant-cascader-menu:last-child {
flex: auto; flex: auto;
@@ -812,6 +794,8 @@ body {
} }
.ant-pro-sider-logo + div { .ant-pro-sider-logo + div {
display: flex;
overflow: hidden;
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 0; width: 0;
} }
@@ -867,3 +851,6 @@ body {
padding-right: 8px; padding-right: 8px;
padding-block: 8px; padding-block: 8px;
} }
.ant-select-multiple .ant-select-content {
height: 100%;
}
+113 -35
View File
@@ -1,54 +1,132 @@
import useMemoizedFn from 'ahooks/lib/useMemoizedFn'; import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
import { converter, modeHsl, modeRgb, useMode } from 'culori/fn'; import { clampChroma, formatHex, modeOklch, modeRgb, useMode } from 'culori/fn';
import useUserSettings from './use-user-settings';
const toHsl = converter('hsl'); // Linear-style minimal/neutral palette: a TIGHT blue -> violet band in OKLCH
// (perceptually uniform), kept moderate-chroma so fills read as clean and gentle
const DEFAULT_BRAND_HUE = 211; // brand color // rather than candy-colored. We deliberately stay narrow and DON'T fan out to
const COOL_HUE_START = 180; // green/magenta — in this aesthetic series are separated by LIGHTNESS, not by
const COOL_HUE_END = 280; // spreading across the hue wheel. Every series (including the first) is generated
// from this one ramp, so the whole palette stays in a single cohesive color
// family — no special high-saturation brand color that clashes with the rest.
const COOL_HUE_START = 250; // blue
const COOL_HUE_END = 315; // violet (stops short of magenta/pink, stays neutral-cool)
const COOL_HUE_RANGE = COOL_HUE_END - COOL_HUE_START; const COOL_HUE_RANGE = COOL_HUE_END - COOL_HUE_START;
const GOLDEN_RATIO_CONJUGATE = 0.618033988749895;
function colorStringToHue(input?: string): number | undefined { /**
if (!input) return undefined; * Vivid, distinct cool accents — for places that need a handful of "primary"
const color = toHsl(input); * colors, one per card/section (e.g. the summary trend cards), NOT a stacked
if (!color || typeof color.h !== 'number') return undefined; * multi-series palette. Every color is anchor-quality (bright + saturated) and
return color.h; * spread evenly across the blue→violet band, so the set reads as several equally
} * strong primaries rather than one bold + several washed-out fills.
*/
export default function useCoolColors() { export function useCoolAccents() {
// const brandHue = useMemo(
// () => colorStringToHue(userSettings.colorPrimary) ?? DEFAULT_BRAND_HUE,
// [userSettings.colorPrimary]
// );
useMode(modeRgb); useMode(modeRgb);
useMode(modeHsl); useMode(modeOklch);
const brandHue = DEFAULT_BRAND_HUE; const { isDarkTheme } = useUserSettings();
return useMemoizedFn((count: number): string[] => { return useMemoizedFn((count: number): string[] => {
if (count <= 0) return []; if (count <= 0) return [];
const colors: string[] = []; const l = isDarkTheme ? 0.62 : 0.64;
const c = isDarkTheme ? 0.16 : 0.2;
const out: string[] = [];
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
let hue: number; const t = count <= 1 ? 0 : i / (count - 1);
const hue = COOL_HUE_START + t * COOL_HUE_RANGE;
out.push(
formatHex(clampChroma({ mode: 'oklch', l, c, h: hue }, 'oklch'))
);
}
return out;
});
}
if (i === 0) { export default function useCoolColors() {
hue = brandHue - 5; useMode(modeRgb);
} else { useMode(modeOklch);
const offset = (i * GOLDEN_RATIO_CONJUGATE) % 1;
hue = COOL_HUE_START + offset * COOL_HUE_RANGE;
if (Math.abs(hue - brandHue) < 5) { const { isDarkTheme } = useUserSettings();
hue = (hue + 10) % COOL_HUE_END;
}
}
const s = i === 0 ? 100 : 75 + (i % 3) * 5; return useMemoizedFn((count: number): string[] => {
const l = i === 0 ? 50 : 55 + (i % 2) * 5; if (count <= 0) return [];
colors.push(`hsl(${Math.round(hue)}, ${s}%, ${l}%)`); // Moderate chroma: clean and crisp, but gentle (not neon). Too low reads as
// muddy/dirty; too high reads as harsh. Dark mode a touch lower so fills
// stay calm against the dark canvas.
const baseChroma = isDarkTheme ? 0.085 : 0.12;
// First series is the "primary" anchor: SAME blue family (the bluest end of
// the ramp, nearest the brand hue) but clearly brighter and more saturated —
// a vivid, clean brand-blue that reads as the base color. The rest of the
// palette stays low-chroma, so the anchor pops as the primary while the
// family still feels cohesive.
const colors: string[] = [
formatHex(
clampChroma(
{
mode: 'oklch',
l: isDarkTheme ? 0.62 : 0.66,
c: isDarkTheme ? 0.16 : 0.2,
h: COOL_HUE_START
},
'oklch'
)
)
];
const rest = count - 1;
if (rest <= 0) return colors;
// Separation is driven by LIGHTNESS, not hue. The hue band is narrow, so as
// the count grows we add lightness TIERS — each tier reuses the same narrow
// hue ramp at a distinct lightness level, multiplying how many separable
// colors fit while keeping the whole palette in one cohesive family.
const tiers = rest <= 6 ? 1 : rest <= 12 ? 2 : 3;
const steps = Math.ceil(rest / tiers);
// Distinct lightness levels per tier count (index 0 → 2 tiers, 1 → 3 tiers).
// Lighter, airier levels for a fresh/crisp feel; kept in the upper-mid range
// so fills stay clean and legible.
const lightTiers = isDarkTheme
? [
[0.68, 0.5],
[0.72, 0.6, 0.48]
]
: [
[0.82, 0.64],
[0.84, 0.72, 0.6]
];
// Single-tier light/dark zig-zag for the common small-count case.
const [lightLo, lightHi] = isDarkTheme ? [0.5, 0.68] : [0.64, 0.82];
for (let i = 0; i < rest; i++) {
// Cycle the tier on every step so consecutive series always differ in
// lightness — exactly where stacked bars are hardest to tell apart.
const tier = i % tiers;
const step = Math.floor(i / tiers);
// Even hue ramp across the narrow band. Each tier is offset by a fraction
// of a step so same-step colors in different tiers don't share a hue.
const t = steps <= 1 ? 0.5 : (step + tier / tiers) / steps;
const hue = COOL_HUE_START + t * COOL_HUE_RANGE;
// 1 tier (≤6 series): light/dark zig-zag. 2-3 tiers: the tier's level.
const lightness =
tiers === 1 ? (i % 2 ? lightLo : lightHi) : lightTiers[tiers - 2][tier];
// Clamp chroma into the sRGB gamut so values don't get distorted by a raw
// channel clip when serialized to hex.
colors.push(
formatHex(
clampChroma(
{ mode: 'oklch', l: lightness, c: baseChroma, h: hue },
'oklch'
)
)
);
} }
return colors; return colors;
+7 -3
View File
@@ -18,6 +18,7 @@ export function useQueryDataList<
Response = Array<ListItem> Response = Array<ListItem>
>(option: { >(option: {
key: string; key: string;
manual?: boolean;
responseType?: 'array' | 'object'; responseType?: 'array' | 'object';
fetchList: ( fetchList: (
params: Params, params: Params,
@@ -38,6 +39,7 @@ export function useQueryDataList<
fetchList, fetchList,
getLabel, getLabel,
getValue, getValue,
manual = true,
responseType = 'array', responseType = 'array',
errorMsg errorMsg
} = option; } = option;
@@ -70,7 +72,7 @@ export function useQueryDataList<
return responseType === 'array' ? res.items || [] : res; return responseType === 'array' ? res.items || [] : res;
}, },
{ {
manual: true, manual: manual,
debounceWait: option.debounceWait || 300, debounceWait: option.debounceWait || 300,
onSuccess: () => {}, onSuccess: () => {},
onError: (error) => { onError: (error) => {
@@ -105,16 +107,18 @@ export function useQueryDataList<
export function useQueryData<Detail, Params = any>(option: { export function useQueryData<Detail, Params = any>(option: {
key: string; key: string;
delay?: number; delay?: number;
manual?: boolean;
fetchDetail: (params: Params, options?: any) => Promise<Detail>; fetchDetail: (params: Params, options?: any) => Promise<Detail>;
getData?: (response: Detail, params?: any) => any; getData?: (response: Detail, params?: any) => any;
errorMsg?: string; errorMsg?: string;
}): { }): {
loading: boolean; loading: boolean;
detailData: Detail; detailData: Detail;
manual?: boolean;
cancelRequest: () => void; cancelRequest: () => void;
fetchData: (params: Params, extra?: any) => Promise<Detail>; fetchData: (params: Params, extra?: any) => Promise<Detail>;
} { } {
const { key, fetchDetail, getData, errorMsg, delay } = option; const { key, fetchDetail, getData, errorMsg, delay, manual = true } = option;
const axiosTokenRef = useRef<CancelTokenSource | null>(null); const axiosTokenRef = useRef<CancelTokenSource | null>(null);
const [detailData, setDetailData] = useState<Detail>({} as Detail); const [detailData, setDetailData] = useState<Detail>({} as Detail);
@@ -142,7 +146,7 @@ export function useQueryData<Detail, Params = any>(option: {
return res; return res;
}, },
{ {
manual: true, manual: manual,
onSuccess: () => {}, onSuccess: () => {},
onError: (error) => { onError: (error) => {
message.error( message.error(
+46
View File
@@ -0,0 +1,46 @@
import { useMemoizedFn } from 'ahooks';
import { useRef, useState } from 'react';
/**
* Centralizes the anti-double-submit + anti-deadlock logic shared by the
* "add/edit" modals.
*
* The lock has to straddle antd's async field validation, whose result comes
* back through two separate callbacks (`onFinish` / `onFinishFailed`). So the
* three wiring points below are intentional and map 1:1 to those anchors:
*
* - `guard` wrap the submit trigger (button / ModalFooter onOk). Blocks
* re-entry while a submit is already in flight, then fires submit.
* - `run` wrap the `onFinish` handler. Holds the lock + loading until the
* `onOk` request settles (success or error).
* - `release` pass as the form's `onFinishFailed`. Releases the lock when
* validation fails, otherwise the button would dead-lock.
*/
export default function useSubmitLock() {
const [loading, setLoading] = useState<boolean>(false);
const lockRef = useRef<boolean>(false);
const release = useMemoizedFn(() => {
setLoading(false);
lockRef.current = false;
});
const guard = useMemoizedFn((submit: () => void) => {
if (lockRef.current) {
return;
}
lockRef.current = true;
submit();
});
const run = useMemoizedFn(async (task: () => void | Promise<void>) => {
setLoading(true);
try {
await task();
} finally {
release();
}
});
return { loading, guard, run, release };
}
+4 -1
View File
@@ -37,6 +37,7 @@ export default function useTableFetch<T>(
key?: (typeof PaginationKey)[keyof typeof PaginationKey]; key?: (typeof PaginationKey)[keyof typeof PaginationKey];
fetchAPI: (params: any, options?: any) => Promise<Global.PageResponse<T>>; fetchAPI: (params: any, options?: any) => Promise<Global.PageResponse<T>>;
deleteAPI?: (id: number, params?: any) => Promise<any>; deleteAPI?: (id: number, params?: any) => Promise<any>;
afterDelete?: (id?: number | number[]) => void;
contentForDelete?: string; contentForDelete?: string;
defaultData?: any[]; defaultData?: any[];
events?: EventsType[]; events?: EventsType[];
@@ -48,6 +49,7 @@ export default function useTableFetch<T>(
const { const {
fetchAPI, fetchAPI,
deleteAPI, deleteAPI,
afterDelete,
contentForDelete, contentForDelete,
API, API,
polling = false, polling = false,
@@ -363,7 +365,7 @@ export default function useTableFetch<T>(
// remove the deleted id from selected ids in row selection // remove the deleted id from selected ids in row selection
rowSelection.removeSelectedKeys([row.id]); rowSelection.removeSelectedKeys([row.id]);
afterDelete?.(row.id);
// ======== to avoid fetch data twice, because of debounceFetchData has been run ======= // ======== to avoid fetch data twice, because of debounceFetchData has been run =======
if (!updateManually) { if (!updateManually) {
fetchData(); fetchData();
@@ -390,6 +392,7 @@ export default function useTableFetch<T>(
successIds.push(id); successIds.push(id);
} }
); );
afterDelete?.(successIds);
rowSelection.removeSelectedKeys(successIds); rowSelection.removeSelectedKeys(successIds);
fetchData(); fetchData();
return res; return res;
+14 -18
View File
@@ -14,7 +14,6 @@ type EventsType = 'CREATE' | 'UPDATE' | 'DELETE' | 'INSERT';
export function useUpdateChunkedList(options: { export function useUpdateChunkedList(options: {
events?: EventsType[]; events?: EventsType[];
dataList?: any[]; dataList?: any[];
triggerAt?: React.MutableRefObject<number>;
limit?: number; limit?: number;
onCreate?: (args: any) => void; onCreate?: (args: any) => void;
onUpdate?: (args: any) => void; onUpdate?: (args: any) => void;
@@ -24,9 +23,9 @@ export function useUpdateChunkedList(options: {
filterFun?: (args: any) => boolean; filterFun?: (args: any) => boolean;
mapFun?: (args: any) => any; mapFun?: (args: any) => any;
computedID?: (d: object) => string; computedID?: (d: object) => string;
isNewItem?: (item: any) => boolean;
}) { }) {
const { events = ['CREATE', 'DELETE', 'UPDATE', 'INSERT'], triggerAt } = const { events = ['CREATE', 'DELETE', 'UPDATE', 'INSERT'] } = options;
options;
const deletedIdsRef = useRef<Set<number | string>>(new Set()); const deletedIdsRef = useRef<Set<number | string>>(new Set());
const cacheDataListRef = useRef<any[]>(options.dataList || []); const cacheDataListRef = useRef<any[]>(options.dataList || []);
const timerRef = useRef<any>(null); const timerRef = useRef<any>(null);
@@ -71,17 +70,14 @@ export function useUpdateChunkedList(options: {
(sItem: any) => sItem.id === item.id (sItem: any) => sItem.id === item.id
); );
const updateItem = { ...item }; const updateItem = { ...item };
if (updateIndex === -1 && !triggerAt?.current) { if (updateIndex === -1) {
acc.push(updateItem); acc.push(updateItem);
} else if (!triggerAt?.current) { } else {
cacheDataListRef.current[updateIndex] = updateItem; cacheDataListRef.current[updateIndex] = updateItem;
} }
// only push items created after the watch started
// TODO only push items created after triggerAt if (options.isNewItem?.(item)) {
if (
triggerAt?.current &&
Date.parse(item.created_at) >= triggerAt.current
) {
latestCreateList.push(updateItem); latestCreateList.push(updateItem);
} }
@@ -93,6 +89,10 @@ export function useUpdateChunkedList(options: {
...cacheDataListRef.current ...cacheDataListRef.current
].slice(0, limit); ].slice(0, limit);
options.setDataList?.([...cacheDataListRef.current], {
createdIds: newDataList.map((item) => item.id)
});
options.onCreate?.(latestCreateList); options.onCreate?.(latestCreateList);
} }
@@ -103,10 +103,8 @@ export function useUpdateChunkedList(options: {
cacheDataListRef.current = cacheDataListRef.current?.filter( cacheDataListRef.current = cacheDataListRef.current?.filter(
(item: any) => { (item: any) => {
// collect deleted items // collect deleted items
if (triggerAt?.current) { if (ids?.includes(item.id) && !options.isNewItem?.(item)) {
if (ids?.includes(item.id)) { deletedList.push(item);
deletedList.push(item);
}
} }
return !ids?.includes(item.id); return !ids?.includes(item.id);
} }
@@ -133,10 +131,8 @@ export function useUpdateChunkedList(options: {
updateItem, updateItem,
...cacheDataListRef.current.slice(0, limit - 1) ...cacheDataListRef.current.slice(0, limit - 1)
]; ];
if (options.onUpdate && triggerAt?.current) { if (options.onUpdate && options.isNewItem?.(item)) {
if (Date.parse(item.created_at) >= triggerAt.current) { options.onUpdate?.([updateItem]);
options.onUpdate?.([updateItem]);
}
} }
} }
}); });
+2 -2
View File
@@ -63,8 +63,7 @@ export default function useWatchList<T = Record<string, any>>(API: string) {
listRequestTokenRef.current?.cancel?.(); listRequestTokenRef.current?.cancel?.();
listRequestTokenRef.current = createAxiosToken(); listRequestTokenRef.current = createAxiosToken();
const params = { const params = {
page: 1, page: -1
perPage: 100
}; };
const res: any = await queryAllDataList(params, { const res: any = await queryAllDataList(params, {
token: listRequestTokenRef.current.token token: listRequestTokenRef.current.token
@@ -86,6 +85,7 @@ export default function useWatchList<T = Record<string, any>>(API: string) {
return { return {
watchDataList, watchDataList,
setWatchDataList,
deleteItemFromCache: handleDeleteItemFromCache deleteItemFromCache: handleDeleteItemFromCache
}; };
} }
+8 -26
View File
@@ -1,8 +1,6 @@
import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user'; import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user';
import PluginExtraField from '@/components/plugin-extra-fields'; import PluginExtraField from '@/components/plugin-extra-fields';
import VersionInfo, { modalConfig } from '@/components/version-info';
import externalLinks from '@/constants/external-links'; import externalLinks from '@/constants/external-links';
import useBodyScroll from '@/hooks/use-body-scroll';
import { logout } from '@/pages/login/apis'; import { logout } from '@/pages/login/apis';
import { useModel } from '@@/plugin-model'; import { useModel } from '@@/plugin-model';
import { import {
@@ -13,7 +11,7 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { DropdownActions, IconFont } from '@gpustack/core-ui'; import { DropdownActions, IconFont } from '@gpustack/core-ui';
import { history, useIntl, useNavigate } from '@umijs/max'; import { history, useIntl, useNavigate } from '@umijs/max';
import { Avatar, Button, Divider, Modal } from 'antd'; import { Avatar, Divider } from 'antd';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { useMemo } from 'react'; import { useMemo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
@@ -97,11 +95,9 @@ const CustomItem = styled.div`
export const ExtraContent = (props: { isDarkTheme?: boolean }) => { export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
const { isDarkTheme } = props; const { isDarkTheme } = props;
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll(); const intl = useIntl();
const [modal, contextHolder] = Modal.useModal();
const [version] = useAtom(GPUStackVersionAtom); const [version] = useAtom(GPUStackVersionAtom);
const [updateCheck] = useAtom(UpdateCheckAtom); const [updateCheck] = useAtom(UpdateCheckAtom);
const intl = useIntl();
const initialInfo = useModel('@@initialState') || { const initialInfo = useModel('@@initialState') || {
initialState: undefined, initialState: undefined,
loading: false, loading: false,
@@ -139,16 +135,6 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
return {}; return {};
}, [isDarkTheme]); }, [isDarkTheme]);
const showVersion = () => {
saveScrollHeight();
modal.info({
...modalConfig,
width: 460,
content: <VersionInfo intl={intl} />,
onCancel: restoreScrollHeight
});
};
const handleLogout = async () => { const handleLogout = async () => {
await logout(); await logout();
navigate(loginPath); navigate(loginPath);
@@ -158,7 +144,7 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
{ {
key: 'site', key: 'site',
icon: <HomeOutlined />, icon: <HomeOutlined />,
label: 'GPUStack', label: 'MesaStack',
url: externalLinks.site url: externalLinks.site
}, },
{ {
@@ -210,14 +196,14 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
key: 'settings', key: 'settings',
label: ( label: (
<span className="flex flex-center"> <span className="flex flex-center">
<IconFont type="icon-settings-02" /> <IconFont type="icon-preferences" />
<span className="m-l-8" style={{ marginLeft: 8 }}> <span className="m-l-8" style={{ marginLeft: 8 }}>
{intl?.formatMessage?.({ id: 'common.button.settings' })} {intl?.formatMessage?.({ id: 'common.preferences' })}
</span> </span>
</span> </span>
), ),
onClick: () => { onClick: () => {
history.push('/profile'); history.push('/preferences');
} }
} }
] ]
@@ -258,7 +244,6 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
return ( return (
<Wrapper> <Wrapper>
{contextHolder}
<PluginExtraField name="OrgSwitcher" isDarkTheme={isDarkTheme} /> <PluginExtraField name="OrgSwitcher" isDarkTheme={isDarkTheme} />
<div <div
style={{ style={{
@@ -266,16 +251,13 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
alignItems: 'center' alignItems: 'center'
}} }}
> >
<Button <span
type="text"
size="small"
onClick={showVersion}
style={{ style={{
color: 'var(--ant-color-text-tertiary)' color: 'var(--ant-color-text-tertiary)'
}} }}
> >
{version.version} {version.version}
</Button> </span>
{showUpgrade && ( {showUpgrade && (
<NewLabel> <NewLabel>
<span className="text"> <span className="text">
+128
View File
@@ -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;
+73 -79
View File
@@ -1,6 +1,7 @@
import { routeCacheAtom, setRouteCache } from '@/atoms/route-cache'; import { routeCacheAtom, setRouteCache } from '@/atoms/route-cache';
import { userAtom } from '@/atoms/user'; import { userAtom } from '@/atoms/user';
import DarkMask from '@/components/dark-mask'; import DarkMask from '@/components/dark-mask';
import '@/components/iconfont/iconfont.js';
import PluginExtraFields from '@/components/plugin-extra-fields'; import PluginExtraFields from '@/components/plugin-extra-fields';
import routeCachekey from '@/config/route-cachekey'; import routeCachekey from '@/config/route-cachekey';
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings'; import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
@@ -11,6 +12,7 @@ import useUserSettings from '@/hooks/use-user-settings';
import useUserSettingsStorage from '@/hooks/use-user-settings-storage'; import useUserSettingsStorage from '@/hooks/use-user-settings-storage';
import useAddResource from '@/pages/dashboard/hooks/use-add-resource'; import useAddResource from '@/pages/dashboard/hooks/use-add-resource';
import { logout } from '@/pages/login/apis'; import { logout } from '@/pages/login/apis';
import { didInitialStateProbe, probeAccessFlags } from '@/utils/access-probes';
import { import {
readColumnSettings, readColumnSettings,
readState, readState,
@@ -20,11 +22,7 @@ import {
import { useAccessMarkedRoutes } from '@@/plugin-access'; import { useAccessMarkedRoutes } from '@@/plugin-access';
import { useModel } from '@@/plugin-model'; import { useModel } from '@@/plugin-model';
import { ProLayout } from '@ant-design/pro-components'; import { ProLayout } from '@ant-design/pro-components';
import { import { CoreUIProvider } from '@gpustack/core-ui';
CoreUIProvider,
IconFont,
useOverlayScroller
} from '@gpustack/core-ui';
import { import {
Access, Access,
Outlet, Outlet,
@@ -41,18 +39,18 @@ import {
useNavigate, useNavigate,
type IRoute type IRoute
} from '@umijs/max'; } from '@umijs/max';
import { Button, ConfigProvider, Modal, theme } from 'antd'; import { ConfigProvider, Modal, theme } from 'antd';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import 'overlayscrollbars/overlayscrollbars.css'; import 'overlayscrollbars/overlayscrollbars.css';
import { useMemo, useRef } from 'react'; import { useEffect, useMemo, useRef } from 'react';
import { PageContainerInner } from '../pages/_components/page-box'; import { PageContainerInner } from '../pages/_components/page-box';
import Exception from './Exception'; import Exception from './Exception';
import './Layout.css'; import './Layout.css';
import { LogoIcon, SLogoIcon } from './Logo'; import { LogoIcon } from './Logo';
import ErrorBoundary from './error-boundary'; import ErrorBoundary from './error-boundary';
import { ExtraContent } from './extraRender'; import { ExtraContent } from './extraRender';
import HeaderMenu from './header-menu';
import { patchRoutes } from './runtime'; import { patchRoutes } from './runtime';
import SiderMenu from './sider-menu';
// Pages that use the page container in the page // Pages that use the page container in the page
const NO_CONTAINER_PAGES = [ const NO_CONTAINER_PAGES = [
@@ -71,9 +69,9 @@ const NO_CONTAINER_PAGES = [
const CHECK_RESOURCE_PATH = [ const CHECK_RESOURCE_PATH = [
'/resources/workers', '/resources/workers',
'/cluster-management/clusters/list', '/resources/clusters/list',
'/cluster-management/credentials', '/resources/credentials',
'/cluster-management/clusters/create' '/resources/clusters/create'
]; ];
type NewRoute = IRoute & { type NewRoute = IRoute & {
@@ -134,11 +132,8 @@ const mapRoutes = (routes: IRoute[], role: string) => {
}; };
export default (props: any) => { export default (props: any) => {
const { initialize: initialize } = useOverlayScroller({
defer: false
});
const [, contextHolder] = Modal.useModal(); const [, contextHolder] = Modal.useModal();
const { themeData, setUserSettings, userSettings } = useUserSettings(); const { themeData, userSettings } = useUserSettings();
const [userInfo] = useAtom(userAtom); const [userInfo] = useAtom(userAtom);
const [routeCache] = useAtom(routeCacheAtom); const [routeCache] = useAtom(routeCacheAtom);
const location = useLocation(); const location = useLocation();
@@ -161,6 +156,50 @@ export default (props: any) => {
const { initialState, loading, setInitialState } = initialInfo; const { initialState, loading, setInitialState } = initialInfo;
const access = useAccess(); const access = useAccess();
const probedForUserRef = useRef<string | null>(null);
// Backfill the access probes (cluster / resource-events) once we're in
// the authenticated shell. `getInitialState` runs only once at app
// boot and can't probe on the login page (no session => 401), so after
// a first login the flags arrive here as `undefined` — which the
// access predicate treats as "don't restrict", flashing GPU Service /
// the full Usage page on until a manual refresh. This layout mounts
// only post-auth (login is `layout:false`) and on every entry, so it's
// the reliable place to resolve them.
//
// The effect is keyed on the user IDENTITY, not on the flag values —
// gating on the flags is what made this fragile (on a refresh
// `getInitialState` commits `currentUser` and the flags in the same
// update, so a flag-gated effect sees them already-known and never
// fires). Whether to actually hit the network is decided by the
// module-scoped `didInitialStateProbe()` marker, which is true only
// when `getInitialState` already probed for an authenticated user this
// page load (the refresh path) — so we skip the duplicate request there
// but still probe on the SPA-login path. Keying on identity also
// re-probes correctly if the signed-in user changes.
const currentUser = initialState?.currentUser;
const username = currentUser?.username;
useEffect(() => {
if (!username || !setInitialState) {
return;
}
if (probedForUserRef.current === username) {
return;
}
probedForUserRef.current = username;
// The refresh path already resolved the flags inside `getInitialState`
// for this user — don't issue a duplicate probe.
if (didInitialStateProbe()) {
return;
}
probeAccessFlags().then((accessFlags) => {
setInitialState((prev: any) => ({
...prev,
...accessFlags
}));
});
}, [username, setInitialState]);
const userConfig = { const userConfig = {
title: '', title: '',
@@ -199,13 +238,6 @@ export default (props: any) => {
const coreUISlots = useMemo(() => ({ ExtraContent }), []); const coreUISlots = useMemo(() => ({ ExtraContent }), []);
const handleToggleCollapse = (e: any) => {
e.stopPropagation();
setUserSettings({
...userSettings,
collapsed: !userSettings.collapsed
});
};
const newRoutes = filterRoutes( const newRoutes = filterRoutes(
// @ts-ignore // @ts-ignore
clientRoutes.filter((route) => route.id === 'max-tabs'), clientRoutes.filter((route) => route.id === 'max-tabs'),
@@ -235,39 +267,19 @@ export default (props: any) => {
return NO_CONTAINER_PAGES.includes(matchedRoute?.name as string); return NO_CONTAINER_PAGES.includes(matchedRoute?.name as string);
}, [matchedRoute]); }, [matchedRoute]);
const collapsed = useMemo(() => {
return userSettings.collapsed || false;
}, [userSettings.collapsed]);
const renderMenuHeader = (logo: React.ReactNode, title: React.ReactNode) => { const renderMenuHeader = (logo: React.ReactNode, title: React.ReactNode) => {
return ( return <>{logo}</>;
<>
{logo}
<div className="collapse-wrap" onClick={handleToggleCollapse}>
<Button
style={{
marginRight: collapsed ? 0 : -14,
border: 'none',
cursor: 'w-resize'
}}
size="small"
type={collapsed ? 'default' : 'text'}
>
<IconFont
type={collapsed ? 'icon-expand-left' : 'icon-expand-right'}
className="font-size-18 text-secondary"
style={{
display: 'block'
}}
/>
</Button>
</div>
</>
);
}; };
const menuContentRender = (menuProps: any, defaultDom: React.ReactNode) => { const headerContentRender = (
return <SiderMenu {...menuProps}></SiderMenu>; headerProps: any,
defaultDom: React.ReactNode
) => {
return <HeaderMenu {...headerProps}></HeaderMenu>;
};
const actionsRender = () => {
return <ExtraContent isDarkTheme={userSettings.isDarkTheme} />;
}; };
const onPageChange = async (route: any) => { const onPageChange = async (route: any) => {
@@ -321,17 +333,6 @@ export default (props: any) => {
navigate(pagepath); navigate(pagepath);
}; };
const onCollapse = (value: boolean) => {
// only trigger by window resize
if (!value) {
return;
}
setUserSettings({
...userSettings,
collapsed: value
});
};
return ( return (
<ConfigProvider <ConfigProvider
componentSize="large" componentSize="large"
@@ -356,7 +357,7 @@ export default (props: any) => {
config={{ config={{
apiBaseUrl: GPUSTACK_API_BASE_URL, apiBaseUrl: GPUSTACK_API_BASE_URL,
theme: userSettings.theme, theme: userSettings.theme,
iconUrl: '//at.alicdn.com/t/c/font_4613488_jmdepcs90im.js', iconUrl: '',
isDarkTheme: userSettings.isDarkTheme, isDarkTheme: userSettings.isDarkTheme,
defaultColorPrimary: COLOR_PRIMARY defaultColorPrimary: COLOR_PRIMARY
}} }}
@@ -392,34 +393,27 @@ export default (props: any) => {
<DarkMask></DarkMask> <DarkMask></DarkMask>
<ProLayout <ProLayout
fixSiderbar fixSiderbar
fixedHeader={false} fixedHeader
headerRender={false}
breadcrumbRender={false} breadcrumbRender={false}
route={route} route={route}
location={location} location={location}
title={userConfig.title} title={userConfig.title}
navTheme={userSettings.theme} navTheme={userSettings.theme}
layout="side" layout="top"
contentStyle={{ contentStyle={{
paddingBlock: 0, paddingBlock: 0,
paddingInline: 0 paddingInline: 0
}} }}
openKeys={false}
disableMobile={true} disableMobile={true}
siderWidth={220}
onCollapse={onCollapse}
onMenuHeaderClick={onMenuHeaderClick} onMenuHeaderClick={onMenuHeaderClick}
menuHeaderRender={renderMenuHeader}
collapsed={userSettings.collapsed}
onPageChange={onPageChange} onPageChange={onPageChange}
formatMessage={formatMessage} formatMessage={formatMessage}
menu={{ menu={{
locale: true, locale: true
type: 'group'
}} }}
splitMenus={true} logo={<LogoIcon />}
logo={userSettings.collapsed ? <SLogoIcon /> : <LogoIcon />} headerContentRender={headerContentRender}
menuContentRender={menuContentRender} actionsRender={actionsRender}
{...runtimeConfig} {...runtimeConfig}
ErrorBoundary={ErrorBoundary} ErrorBoundary={ErrorBoundary}
> >
@@ -427,7 +421,7 @@ export default (props: any) => {
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
height: '100vh', height: '100%',
overflow: 'hidden' overflow: 'hidden'
}} }}
> >
+2 -2
View File
@@ -82,7 +82,7 @@ export const getRightRenderContent = (opts: {
{ {
key: 'site', key: 'site',
icon: <HomeOutlined />, icon: <HomeOutlined />,
label: 'GPUStack', label: 'MesaStack',
url: externalLinks.site url: externalLinks.site
}, },
{ {
@@ -256,7 +256,7 @@ export const getRightRenderContent = (opts: {
</span> </span>
), ),
onClick: () => { onClick: () => {
history.push('/profile'); history.push('/preferences');
} }
}, },
{ {
+187 -158
View File
@@ -1,9 +1,11 @@
import { collapsedMenuGroupsAtom } from '@/atoms/settings';
import { CaretDownOutlined } from '@ant-design/icons'; import { CaretDownOutlined } from '@ant-design/icons';
import { IconFont } from '@gpustack/core-ui'; import { IconFont, OverlayScroller } from '@gpustack/core-ui';
import { Link, useLocation } from '@umijs/max'; import { Link, useLocation } from '@umijs/max';
import { Tooltip } from 'antd'; import { Tooltip } from 'antd';
import { createStyles } from 'antd-style'; import { createStyles, type FullToken } from 'antd-style';
import React, { useMemo, useState } from 'react'; import { useAtom } from 'jotai';
import React, { useMemo } from 'react';
interface MenuItem { interface MenuItem {
icon?: string; icon?: string;
@@ -20,124 +22,143 @@ interface SiderMenuProps {
initialState: Global.InitialStateType; initialState: Global.InitialStateType;
} }
const useStyles = createStyles(({ css, token }) => { const useStyles = createStyles(
console.log('useStyles', token); ({ css, token }: { css: any; token: FullToken }) => {
console.log('useStyles', token);
// @ts-ignore // @ts-ignore
const { Menu } = token; const { Menu } = token;
return { return {
siderMenu: css` siderMenu: css`
&.sider-menu-collapsed {
.menu-item {
justify-content: center;
padding: 0;
}
}
`,
groupTitle: css`
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
white-space: nowrap;
padding: var(--ant-padding-xs) var(--ant-padding);
font-size: 12px;
padding-bottom: 4px;
overflow: hidden;
height: 30px;
&:hover {
.group-title-text {
color: var(--ant-color-text);
}
}
.anticon {
transform: scale(0.8);
}
.group-title-text {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--ant-color-text-tertiary);
font-weight: 400;
}
&.menu-item-group-title-collapsed {
position: relative;
height: 1px;
padding-block: 0;
padding-inline: 0;
justify-content: center;
}
`,
menuItemContent: css`
margin: 2px 0;
border-radius: 4px;
overflow: hidden;
`,
menuItemWrapper: css`
display: flex;
align-items: center;
justify-content: flex-start;
gap: 12px;
cursor: pointer;
position: relative;
padding-inline: calc(var(--ant-font-size) * 2) var(--ant-padding);
padding-left: 16px;
overflow: hidden;
white-space: nowrap;
height: ${Menu.itemHeight}px;
line-height: ${Menu.itemHeight}px;
color: var(--ant-color-text-tertiary);
&:hover {
background-color: ${Menu.itemHoverBg};
color: ${Menu.itemHoverColor};
}
&.menu-item-selected {
background-color: ${Menu.itemSelectedBg};
color: ${Menu.itemSelectedColor};
.anticon {
color: ${Menu.itemSelectedColor};
}
}
&:active {
background-color: ${Menu.itemActiveBg};
color: ${Menu.itemActiveColor};
}
.anticon {
font-size: 16px;
}
.icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 100%; width: 100%;
height: 100%; &.sider-menu-collapsed {
} .menu-item {
`, justify-content: center;
menuItemGroup: css` padding: 0;
&.menu-item-group-hidden { }
display: none; }
} .os-scrollbar-vertical .os-scrollbar-handle {
`, min-width: 4px;
line: css` max-width: 4px;
height: 1px; }
margin-block: 6px; `,
background-color: ${token.colorSplit}; groupTitle: css`
position: absolute; display: flex;
left: -2px; align-items: center;
right: -2px; gap: 8px;
` cursor: pointer;
}; white-space: nowrap;
}); padding: var(--ant-padding-xs) var(--ant-padding);
font-size: 12px;
padding-bottom: 4px;
overflow: hidden;
height: 30px;
&:hover {
.group-title-text {
color: var(--ant-color-text);
}
}
.anticon {
transform: scale(0.8);
}
.group-title-text {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--ant-color-text);
font-weight: 400;
}
&.menu-item-group-title-collapsed {
position: relative;
height: 1px;
padding-block: 0;
padding-inline: 0;
justify-content: center;
}
`,
menuItemContent: css`
margin: 2px 0;
border-radius: 4px;
overflow: hidden;
`,
menuItemWrapper: css`
display: flex;
align-items: center;
justify-content: flex-start;
gap: 12px;
cursor: pointer;
position: relative;
padding-inline: calc(var(--ant-font-size) * 2) var(--ant-padding);
padding-left: 16px;
overflow: hidden;
white-space: nowrap;
height: ${Menu.itemHeight}px;
line-height: ${Menu.itemHeight}px;
color: var(--ant-color-text-tertiary);
&:hover {
background-color: ${Menu.itemHoverBg};
color: ${Menu.itemHoverColor};
}
&.menu-item-selected {
background-color: ${Menu.menuItemSelectedBg};
color: ${Menu.itemSelectedColor};
.anticon {
color: ${Menu.itemSelectedColor};
}
}
&:active {
background-color: ${Menu.itemActiveBg};
color: ${Menu.itemActiveColor};
}
.anticon {
font-size: 16px;
}
.icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
`,
menuItemGroup: css`
&.menu-item-group-hidden {
display: none;
}
`,
line: css`
height: 1px;
margin-block: 6px;
background-color: ${token.colorSplit};
position: absolute;
left: -2px;
right: -2px;
`
};
}
);
const SiderMenu: React.FC<SiderMenuProps> = (props) => { const SiderMenu: React.FC<SiderMenuProps> = (props) => {
const { menuData, collapsed } = props; const { menuData, collapsed } = props;
const { styles, cx } = useStyles(); const { styles, cx } = useStyles();
const location = useLocation(); const location = useLocation();
const [collapseKeys, setCollapseKeys] = useState<Set<string>>(new Set()); const [storedCollapsedGroups, setCollapsedGroups] = useAtom(
console.log('SiderMenu', location.pathname); collapsedMenuGroupsAtom
);
// atomWithStorage falls back to the initial value on JSON parse
// errors, but not when the stored value is valid JSON of another
// shape — normalize so array methods below can't throw.
const collapsedGroups = Array.isArray(storedCollapsedGroups)
? storedCollapsedGroups
: [];
const collapseKeys = useMemo(
() => new Set(collapsedGroups),
[collapsedGroups]
);
const dividerStyles = useMemo(() => { const dividerStyles = useMemo(() => {
if (collapsed) { if (collapsed) {
@@ -156,14 +177,11 @@ const SiderMenu: React.FC<SiderMenuProps> = (props) => {
const handleToggleGroup = (e: any, menuGroup: any) => { const handleToggleGroup = (e: any, menuGroup: any) => {
e.stopPropagation(); e.stopPropagation();
console.log('handleToggleGroup', menuGroup.key); setCollapsedGroups(
collapsedGroups.includes(menuGroup.key)
if (collapseKeys.has(menuGroup.key)) { ? collapsedGroups.filter((key) => key !== menuGroup.key)
collapseKeys.delete(menuGroup.key); : [...collapsedGroups, menuGroup.key]
} else { );
collapseKeys.add(menuGroup.key);
}
setCollapseKeys(new Set(collapseKeys));
}; };
const menuItemRender = (menuItem: MenuItem, key: string) => { const menuItemRender = (menuItem: MenuItem, key: string) => {
@@ -219,44 +237,55 @@ const SiderMenu: React.FC<SiderMenuProps> = (props) => {
'sider-menu-collapsed': collapsed 'sider-menu-collapsed': collapsed
})} })}
> >
{menuData.map((item: MenuItem, index: number) => ( <OverlayScroller
<div key={item.key}> styles={{
{item.children && item.children.length > 0 ? ( wrapper: {
<> paddingInline: 0,
<div maxHeight: '100%'
className={cx(styles.groupTitle, { }
'menu-item-group-title-collapsed': collapsed }}
})} >
onClick={(e) => handleToggleGroup(e, item)} <div style={{ paddingRight: 8 }}>
> {menuData.map((item: MenuItem, index: number) => (
{!collapsed ? ( <div key={item.key}>
<span className="group-title-text"> {item.children && item.children.length > 0 ? (
<span>{item.name}</span> <>
<CaretDownOutlined <div
rotate={collapseKeys.has(item.key) ? -90 : 0} className={cx(styles.groupTitle, {
></CaretDownOutlined> 'menu-item-group-title-collapsed': collapsed
</span> })}
) : ( onClick={(e) => handleToggleGroup(e, item)}
<span className={styles.line}></span> >
)} {!collapsed ? (
</div> <span className="group-title-text">
<div <span>{item.name}</span>
className={cx(styles.menuItemGroup, { <CaretDownOutlined
'menu-item-group-collapsed': collapsed, rotate={collapseKeys.has(item.key) ? -90 : 0}
'menu-item-group-hidden': ></CaretDownOutlined>
!collapsed && collapseKeys.has(item.key) </span>
})} ) : (
> <span className={styles.line}></span>
{item.children?.map((child: MenuItem) => )}
menuItemRender(child, child.key) </div>
)} <div
</div> className={cx(styles.menuItemGroup, {
</> 'menu-item-group-collapsed': collapsed,
) : ( 'menu-item-group-hidden':
menuItemRender(item, item.key) !collapsed && collapseKeys.has(item.key)
)} })}
>
{item.children?.map((child: MenuItem) =>
menuItemRender(child, child.key)
)}
</div>
</>
) : (
menuItemRender(item, item.key)
)}
</div>
))}
</div> </div>
))} </OverlayScroller>
</div> </div>
); );
}; };
+1 -1
View File
@@ -25,7 +25,7 @@ export default {
'ai.provider.ollama': 'Ollama', 'ai.provider.ollama': 'Ollama',
'ai.provider.openai': 'OpenAI', 'ai.provider.openai': 'OpenAI',
'ai.provider.openrouter': 'OpenRouter', 'ai.provider.openrouter': 'OpenRouter',
'ai.provider.qwen': 'Qwen', 'ai.provider.qwen': 'Alibaba Cloud Model Studio',
'ai.provider.spark': 'Spark', 'ai.provider.spark': 'Spark',
'ai.provider.stepfun': 'StepFun', 'ai.provider.stepfun': 'StepFun',
'ai.provider.together-ai': 'TogetherAI', 'ai.provider.together-ai': 'TogetherAI',
+3 -1
View File
@@ -23,5 +23,7 @@ export default {
'apikeys.accessScope.inference': 'Inference APIs', 'apikeys.accessScope.inference': 'Inference APIs',
'apikeys.access.permissions': 'Access Permissions', 'apikeys.access.permissions': 'Access Permissions',
'apikeys.type.auto': 'Auto-generated', 'apikeys.type.auto': 'Auto-generated',
'apikeys.type.custom': 'Custom' 'apikeys.type.custom': 'Custom',
'apikeys.button.ipConfig': 'IP Access Control',
'quotaLimits.button.title': 'Quota Limit'
}; };
+6 -5
View File
@@ -23,11 +23,12 @@ export default {
'backend.form.defaultExecuteCommand': 'Default Execution Command', 'backend.form.defaultExecuteCommand': 'Default Execution Command',
'backend.form.defaultExecuteCommand.tips': `'{{'model_path'}}', '{{'port'}}', '{{'worker_ip'}}' and '{{'model_name'}}' are placeholders that will be substituted with the actual values during deployment.`, 'backend.form.defaultExecuteCommand.tips': `'{{'model_path'}}', '{{'port'}}', '{{'worker_ip'}}' and '{{'model_name'}}' are placeholders that will be substituted with the actual values during deployment.`,
'backend.form.defaultBackendParameters': 'Default Backend Parameters', 'backend.form.defaultBackendParameters': 'Default Backend Parameters',
'backend.form.parameterFormat': 'Parameter Format', 'backend.form.flagFormat': 'Flag Format',
'backend.form.parameterFormat.default': 'Backend Default', 'backend.form.flagFormat.tips':
'backend.form.parameterFormat.space': 'Space (--key value)', 'The format applied between an option and its value. Leave empty to keep each parameter as entered, without normalizing it.',
'backend.form.parameterFormat.equal': 'Equal (--key=value)', 'backend.form.flagFormat.space': 'Space Separated (--key value)',
'backend.form.commonParameters': 'Common Parameters', 'backend.form.flagFormat.equal': 'Equal Sign (--key=value)',
'backend.form.commonParameters': 'Common Backend Parameters',
'backend.form.commonParameters.tips': 'backend.form.commonParameters.tips':
'Shown as suggestions in the backend parameters input during deployment.', 'Shown as suggestions in the backend parameters input during deployment.',
'backend.form.versionConfig': 'Versions Config', 'backend.form.versionConfig': 'Versions Config',
+3 -3
View File
@@ -37,10 +37,10 @@ export default {
'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.', 'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.',
'benchmark.form.profile.heavy.tips': 'benchmark.form.profile.heavy.tips':
'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.', 'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.',
'benchmark.table.filter.bygpu': 'Filter by GPU', 'benchmark.table.filter.bygpu': 'Search by GPU',
'benchmark.table.filter.bymodel': 'Filter by Model', 'benchmark.table.filter.bymodel': 'Search by model',
'benchmark.table.filter.bydataset': 'Filter by Dataset', 'benchmark.table.filter.bydataset': 'Filter by Dataset',
'benchmark.table.filter.byProfile': 'Filter by Profile', 'benchmark.table.filter.byProfile': 'Filter by profile',
'benchmark.table.avg': 'Avg', 'benchmark.table.avg': 'Avg',
'benchmark.table.columnSettings': 'Column Settings', 'benchmark.table.columnSettings': 'Column Settings',
'benchmark.detail.summary.results': 'Test Results', 'benchmark.detail.summary.results': 'Test Results',
+15
View File
@@ -0,0 +1,15 @@
export default {
'billing.upsell.title': 'Billing is an Enterprise feature',
'billing.upsell.subtitle':
'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',
'billing.upsell.feature.invoices':
'Generate invoices and export billing reports',
'billing.upsell.feature.budgets':
'Set budgets and spending limits with alerts',
'billing.upsell.feature.chargeback':
'Attribute and charge back usage to teams and projects',
'billing.upsell.cta': 'Learn about Enterprise'
};
+34 -21
View File
@@ -39,7 +39,7 @@ export default {
'clusters.workerpool.batchSize.desc': 'clusters.workerpool.batchSize.desc':
'Number of workers created simultaneously in the Worker pool', 'Number of workers created simultaneously in the Worker pool',
'clusters.create.addworker.tips': 'clusters.create.addworker.tips':
'Please make sure the <a href={link} target="_blank">prerequisites</a> for {label} are met before executing the following command.', 'Please make sure the <a href={link} target="_blank">prerequisites</a> are met before executing the following command.',
'clusters.create.addCommand.tips': 'clusters.create.addCommand.tips':
'On the Worker that needs to be added, run the following command to join it to the cluster.', 'On the Worker that needs to be added, run the following command to join it to the cluster.',
'clusters.create.addCommand.k8s.tips': 'clusters.create.addCommand.k8s.tips':
@@ -70,10 +70,13 @@ export default {
'For <span class="bold-text">non-Docker</span> clusters, please register clusters or manage worker pools from the Clusters page.', 'For <span class="bold-text">non-Docker</span> clusters, please register clusters or manage worker pools from the Clusters page.',
'clusters.addworker.selectGPU': 'Select GPU Vendor', 'clusters.addworker.selectGPU': 'Select GPU Vendor',
'clusters.addworker.selectGPU.multiTag': 'Multi-select', 'clusters.addworker.selectGPU.multiTag': 'Multi-select',
'clusters.addworker.selectGPU.singleOnly': 'clusters.addworker.selectGPU.subtitle':
'The selected vendor is not in this clusters GPU vendor overrides — single-select only. Pick a vendor configured in the overrides to enable multi-select.', 'You can select multiple GPU Vendors or none for CPU-only clusters',
'clusters.addworker.checkEnv': 'Check Environment', 'clusters.addworker.checkEnv': 'Check Environment',
'clusters.addworker.checkEnv.cpuOnlyTips':
'Use the following command to verify that the Kubernetes cluster has at least one ready node. You are registering a CPU-only cluster.',
'clusters.addworker.specifyArgs': 'Specify Arguments', 'clusters.addworker.specifyArgs': 'Specify Arguments',
'clusters.addworker.dtkVersion': 'DTK Version',
'clusters.addworker.runCommand': 'Run Command', 'clusters.addworker.runCommand': 'Run Command',
'clusters.addworker.specifyWorkerIP': 'Worker IP', 'clusters.addworker.specifyWorkerIP': 'Worker IP',
'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP', 'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP',
@@ -82,7 +85,7 @@ export default {
'clusters.addworker.detectWorkerAddress.tips': 'clusters.addworker.detectWorkerAddress.tips':
'Defaults to Worker IP if not specified.', 'Defaults to Worker IP if not specified.',
'clusters.addworker.externalIP.tips': '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': 'Enter worker IP',
'clusters.addworker.enterWorkerIP.error': 'Please enter the worker IP.', 'clusters.addworker.enterWorkerIP.error': 'Please enter the worker IP.',
'clusters.addworker.enterWorkerAddress': 'Enter worker external address', 'clusters.addworker.enterWorkerAddress': 'Enter worker external address',
@@ -110,18 +113,20 @@ export default {
'{count} new worker has been added to the cluster.', '{count} new worker has been added to the cluster.',
'clusters.addworker.message.success_multiple': 'clusters.addworker.message.success_multiple':
'{count} new workers have been added to the cluster.', '{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.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': 'Worker Container Name',
'clusters.addworker.containerName.tips': 'clusters.addworker.containerName.tips':
'Specify a name for the worker container.', 'Specify a name for the worker container.',
'clusters.addworker.dataVolume': 'GPUStack Data Volume', 'clusters.addworker.dataVolume': 'MesaStack Data Volume',
'clusters.addworker.dataVolume.tips': '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.internal': 'Internal',
'clusters.table.ip.external': 'External', 'clusters.table.ip.external': 'External',
'clusters.form.serverUrl.tips': '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': 'Set as Default',
'clusters.form.setDefault.tips': 'Default for deployment.', 'clusters.form.setDefault.tips': 'Default for deployment.',
'clusters.addworker.noClusters': 'No available Docker clusters found', 'clusters.addworker.noClusters': 'No available Docker clusters found',
@@ -139,7 +144,7 @@ export default {
'clusters.addworker.theadNotes-02': '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.', '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': '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.', '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.title': 'Volume Mounts',
'clusters.volume.name': 'Volume Name', 'clusters.volume.name': 'Volume Name',
'clusters.volume.mountPath': 'Container Path', 'clusters.volume.mountPath': 'Container Path',
@@ -164,6 +169,10 @@ export default {
'clusters.volume.configMap.name': 'ConfigMap Name', 'clusters.volume.configMap.name': 'ConfigMap Name',
'clusters.volume.configMap.optional': 'Optional', 'clusters.volume.configMap.optional': 'Optional',
'clusters.volume.add': 'Add Volume Mount', 'clusters.volume.add': 'Add Volume Mount',
'clusters.systemDefaultContainerRegistry.title': 'Default Container Registry',
'clusters.systemDefaultContainerRegistry.tip':
'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.title': 'Image Credentials',
'clusters.imageCredentials.add': 'Add Credential', 'clusters.imageCredentials.add': 'Add Credential',
'clusters.imageCredentials.registry': 'Registry', 'clusters.imageCredentials.registry': 'Registry',
@@ -172,16 +181,20 @@ export default {
'clusters.nodeSelector.title': 'Node Selector', 'clusters.nodeSelector.title': 'Node Selector',
'clusters.nodeSelector.tip': 'clusters.nodeSelector.tip':
'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.', 'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.',
'clusters.gpuVendorOverrides.title': 'GPU Vendor Overrides', 'clusters.operatorImage.title': 'Operator Image',
'clusters.gpuVendorOverrides.validate.emptySelector': 'clusters.operatorImage.tip':
'The override for {vendor} needs at least one nodeSelector entry — an empty override would leave the multi-vendor manifest unable to target nodes for this runtime.', 'Override for the MesaStack Operator container image. Leave empty to use the server default.',
'clusters.gpuVendorOverrides.validate.duplicate': 'clusters.namespace.title': 'Namespace',
'Overrides for {v1} and {v2} use the same nodeSelector. Each vendor must scope to a distinct node set, otherwise their worker DaemonSets would compete for the same nodes.', 'clusters.namespace.tip':
'clusters.gpuVendorOverrides.validate.keyConflict': 'Kubernetes namespace the clusters manifests render into. Leave empty to use gpustack-system.',
'Override for {vendor} reuses key(s) {keys} from the base Node Selector — the CPU worker would simultaneously require and forbid the key and never schedule.', 'clusters.clusterType.title': 'Cluster Type',
'clusters.gpuVendorOverrides.tip': 'clusters.modelService.title': 'Model Service',
'Required when running multiple GPU vendors in one cluster. Each entry pins that vendors worker DaemonSet to nodes matching its nodeSelector, and the CPU worker is steered away from those nodes via a DoesNotExist node-affinity.', 'clusters.modelService.tip':
'clusters.gpuVendorOverrides.add': 'Add Override', 'For LLM inference and API serving — e.g. exposing model APIs and token-based services.',
'clusters.gpuVendorOverrides.vendor': 'GPU Vendor', 'clusters.gpuInstances.title': 'GPU Service',
'clusters.gpuVendorOverrides.nodeSelector': 'Node Selector' 'clusters.gpuInstances.tip':
'For on-demand GPU compute — e.g. interactive development, training jobs, or custom environments.',
'clusters.gpuInstances.staticAddress': 'GPU Service Static Access Address',
'clusters.gpuInstances.staticAddress.tip':
'Static address the operator uses to access GPU instances in this cluster (e.g. a LoadBalancer VIP). Optional.'
}; };
+19 -2
View File
@@ -46,6 +46,7 @@ export default {
'common.button.enabled': 'Enabled', 'common.button.enabled': 'Enabled',
'common.button.disabled': 'Disabled', 'common.button.disabled': 'Disabled',
'common.button.upgrade': 'Upgrade', 'common.button.upgrade': 'Upgrade',
'common.enterprise.feature': 'Available in MesaStack Enterprise',
'common.input.holder': 'Please enter', 'common.input.holder': 'Please enter',
'common.validate.value': '{name} value is required', 'common.validate.value': '{name} value is required',
'common.button.edit': 'Edit', 'common.button.edit': 'Edit',
@@ -55,6 +56,7 @@ export default {
'common.button.viewevent': 'View Events', 'common.button.viewevent': 'View Events',
'common.button.recreate': 'Recreate', 'common.button.recreate': 'Recreate',
'common.table.operation': 'Operations', 'common.table.operation': 'Operations',
'common.table.creator': 'Creator',
'common.table.createTime': 'Created', 'common.table.createTime': 'Created',
'common.table.updateTime': 'Updated', 'common.table.updateTime': 'Updated',
'common.table.description': 'Description', 'common.table.description': 'Description',
@@ -65,6 +67,7 @@ export default {
'common.search.name.placeholder': 'filter by name', 'common.search.name.placeholder': 'filter by name',
'common.search.id.placeholder': 'filter by ID', 'common.search.id.placeholder': 'filter by ID',
'common.filter.byId': 'filter by ID', 'common.filter.byId': 'filter by ID',
'common.filter.byCreator': 'Filter by creator',
'common.table.type': 'Type', 'common.table.type': 'Type',
'common.table.default': 'Default Value', 'common.table.default': 'Default Value',
'common.copy.success': 'Copied success!', 'common.copy.success': 'Copied success!',
@@ -166,6 +169,7 @@ export default {
'common.time.hour': 'hour', 'common.time.hour': 'hour',
'common.time.minute': 'minutes', 'common.time.minute': 'minutes',
'common.issue.report': 'Report an issue', 'common.issue.report': 'Report an issue',
'common.github.star.tooltip': 'Star us on GitHub',
'common.social.discord': 'Join Our Discord', 'common.social.discord': 'Join Our Discord',
'common.table.mark': 'Comment', 'common.table.mark': 'Comment',
'common.table.rollback.mark': 'Rollback Comment', 'common.table.rollback.mark': 'Rollback Comment',
@@ -194,6 +198,8 @@ export default {
'common.table.user': 'User', 'common.table.user': 'User',
'common.settings.instructions': 'Instructions', 'common.settings.instructions': 'Instructions',
'common.settings.language': 'Language', 'common.settings.language': 'Language',
'common.settings.language.tips':
'Set the display language for the interface.',
'common.delete.confirm': 'common.delete.confirm':
'Are you sure you want to delete the selected {type}?', 'Are you sure you want to delete the selected {type}?',
'common.delete.single.confirm': 'common.delete.single.confirm':
@@ -208,7 +214,7 @@ export default {
'common.form.password': 'Password', 'common.form.password': 'Password',
'common.form.username': 'Username', 'common.form.username': 'Username',
'common.login.rember': 'Remember me', 'common.login.rember': 'Remember me',
'settings.company': 'GPUStack.ai', 'settings.company': 'MesaStack',
'common.button.help': 'Help', 'common.button.help': 'Help',
'common.button.feedback': 'Feedback', 'common.button.feedback': 'Feedback',
'common.button.docs': 'Documentation', 'common.button.docs': 'Documentation',
@@ -247,6 +253,11 @@ export default {
'common.appearance.tips': 'Default follows system preference.', 'common.appearance.tips': 'Default follows system preference.',
'common.button.forgotpassword': 'Forgot password?', 'common.button.forgotpassword': 'Forgot password?',
'common.appearance.theme': 'Theme', 'common.appearance.theme': 'Theme',
'common.appearance.description':
'Customize how the interface looks on your device.',
'common.security': 'Security',
'common.security.description':
'Manage the password used to sign in to your account.',
'common.page.wentwrong': 'Something went wrong.', 'common.page.wentwrong': 'Something went wrong.',
'common.page.refresh.tips': 'common.page.refresh.tips':
'The page may need to be updated. Try refreshing it!', 'The page may need to be updated. Try refreshing it!',
@@ -257,6 +268,10 @@ export default {
'common.login.auth': 'Authenticating...', 'common.login.auth': 'Authenticating...',
'common.login.auth.failed': 'Authentication failed', 'common.login.auth.failed': 'Authentication failed',
'common.login.password': 'Log in with Password', 'common.login.password': 'Log in with Password',
'common.login.username.holder': 'Please enter username',
'common.login.password.holder': 'Please enter password',
'common.login.newpassword.holder': 'Please enter new password',
'common.login.confirm.holder': 'Please enter password again',
'common.external.login': 'Log in with {type}', 'common.external.login': 'Log in with {type}',
'common.sso.noConfig': 'common.sso.noConfig':
'Single sign-on is not enabled on this system. Please contact your administrator.', 'Single sign-on is not enabled on this system. Please contact your administrator.',
@@ -285,5 +300,7 @@ export default {
'common.image.limit.height': 'Image height must be {height}.', 'common.image.limit.height': 'Image height must be {height}.',
'common.remaining': 'Remaining {count}', 'common.remaining': 'Remaining {count}',
'common.max': 'Max {count}', 'common.max': 'Max {count}',
'common.validate.group': 'Please complete the {group} configuration' 'common.max.count': '{label} Count',
'common.validate.group': 'Please complete the {group} configuration',
'common.preferences': 'Preferences'
}; };
+28 -7
View File
@@ -13,7 +13,11 @@ export default {
'gpuservice.template.command.placeholder': 'gpuservice.template.command.placeholder':
'Separate arguments with spaces; wrap arguments containing spaces in quotes, e.g.: /bin/bash -c "echo hello world"', 'Separate arguments with spaces; wrap arguments containing spaces in quotes, e.g.: /bin/bash -c "echo hello world"',
'gpuservice.template.mountPath': 'Mount Path', 'gpuservice.template.mountPath': 'Mount Path',
'gpuservice.template.mountPath.tips':
'The default mount path for the storage volume when creating an instance from this template. Useful for persisting data that needs to be retained while the instance is running.',
'gpuservice.template.containerDisk': 'Container Disk (GB)', 'gpuservice.template.containerDisk': 'Container Disk (GB)',
'gpuservice.template.containerDisk.tips':
'The size of the container system disk.',
'gpuservice.template.memory': 'Memory (GB)', 'gpuservice.template.memory': 'Memory (GB)',
'gpuservice.instance.containerDisk.remaining': 'gpuservice.instance.containerDisk.remaining':
'Container Disk (Max {count} GB)', 'Container Disk (Max {count} GB)',
@@ -77,6 +81,8 @@ export default {
'gpuservice.publicKey': 'SSH Public Key', 'gpuservice.publicKey': 'SSH Public Key',
'gpuservice.publicKey.add': 'Add SSH Public Key', 'gpuservice.publicKey.add': 'Add SSH Public Key',
'gpuservice.publicKey.edit': 'Edit SSH Public Key', 'gpuservice.publicKey.edit': 'Edit SSH Public Key',
'gpuservice.publicKey.delete.tips':
'Deleting an SSH Public Key will not revoke access for existing attached Instances. To remove access, edit those Instances separately.',
'gpuservice.publicKey.filter.name': 'Search by name', 'gpuservice.publicKey.filter.name': 'Search by name',
'gpuservice.publicKey.label': 'SSH Public Key', 'gpuservice.publicKey.label': 'SSH Public Key',
'gpuservice.instance.ssh.enable': 'Enable SSH Access', 'gpuservice.instance.ssh.enable': 'Enable SSH Access',
@@ -97,21 +103,32 @@ export default {
'gpuservice.instance.templates': 'Instance Templates', 'gpuservice.instance.templates': 'Instance Templates',
'gpuservice.instance.section.storage': 'Storage', 'gpuservice.instance.section.storage': 'Storage',
'gpuservice.instance.type.required': 'Please select an instance type', 'gpuservice.instance.type.required': 'Please select an instance type',
'gpuservice.instance.type.noAvailable': 'No instance type available',
'gpuservice.instance.gpuCount': 'GPU Count', 'gpuservice.instance.gpuCount': 'GPU Count',
'gpuservice.instance.gpuCount.required': 'Please enter the GPU count', 'gpuservice.instance.gpuCount.required': 'Please enter the GPU count',
'gpuservice.instance.gpuCount.max': 'gpuservice.instance.gpuCount.max':
'Please select at most {count} GPU card(s)', 'Please select at most {count} GPU card(s)',
'gpuservice.instance.gpuCount.min': 'gpuservice.instance.gpuCount.min':
'Please select at least {count} GPU card(s)', 'Please select at least {count} GPU card(s)',
'gpuservice.instance.cpuCount.max':
'Please select at most {count} CPU core(s)',
'gpuservice.instance.cpuCount.min':
'Please select at least {count} CPU core(s)',
'gpuservice.instance.gpuCount.noAvailable': 'gpuservice.instance.gpuCount.noAvailable':
'No available GPU resources, please choose another instance type.', 'No available GPU resources, please choose another instance type.',
'gpuservice.instance.gpuCount.zero': 'gpuservice.instance.gpuCount.zero':
'CPU-only setup for environment preparation.', 'CPU-only setup for environment preparation.',
'gpuservice.instance.stock': 'Stock', 'gpuservice.instance.stock': 'Stock',
'gpuservice.instance.sliced': 'Sliced', 'gpuservice.instance.sliced': 'Sliced',
'gpuservice.instance.memory': 'Memory', 'gpuservice.instance.memory': 'VRAM',
'gpuservice.instance.ram': 'RAM', 'gpuservice.instance.ram': 'RAM',
'gpuservice.instance.os': 'OS',
'gpuservice.instance.arch': 'Arch',
'gpuservice.instance.disk': 'Disk', 'gpuservice.instance.disk': 'Disk',
'gpuservice.table.count': 'Count',
'gpuservice.instance.disk.system': 'System Disk',
'gpuservice.instance.disk.ephemeral': 'Ephemeral Storage',
'gpuservice.instance.disk.persistent': 'Persistent Storage',
'gpuservice.instance.search.type.placeholder': 'Search by name', 'gpuservice.instance.search.type.placeholder': 'Search by name',
'gpuservice.instance.search.template.placeholder': 'gpuservice.instance.search.template.placeholder':
'Search by template name, image or mount path', 'Search by template name, image or mount path',
@@ -142,14 +159,13 @@ export default {
'gpuservice.storage.capacity': 'Capacity', 'gpuservice.storage.capacity': 'Capacity',
'gpuservice.storage.accessMode': 'Access Mode', 'gpuservice.storage.accessMode': 'Access Mode',
'gpuservice.storage.persistent': 'Persistent', 'gpuservice.storage.persistent': 'Persistent',
'gpuservice.storage.temporary': 'Temporary', 'gpuservice.storage.temporary': 'Ephemeral',
'gpuservice.storage.persistentVolume': 'Persistent Volume', 'gpuservice.storage.persistentVolume': 'Persistent',
'gpuservice.storage.temporary.tips': 'gpuservice.storage.temporary.tips':
'Data is cleared when the instance stops.', 'Data is cleared when the instance stops.',
'gpuservice.storage.persistentVolume.tips': 'gpuservice.storage.persistentVolume.tips':
'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.', 'Data persists across instance restarts. Persistent volumes remain intact after instance termination and can be shared by multiple instances.',
'gpuservice.storage.persistentVolume.required': 'gpuservice.storage.persistentVolume.required': 'Please select a storage',
'Please select a persistent volume',
'gpuservice.storage.persistentVolume.capacity': 'Capacity (GB)', 'gpuservice.storage.persistentVolume.capacity': 'Capacity (GB)',
'gpuservice.storage.persistentVolume.capacity.required': 'gpuservice.storage.persistentVolume.capacity.required':
'Please enter capacity', 'Please enter capacity',
@@ -159,5 +175,10 @@ export default {
'gpuservice.storage.tempCapacity.required': 'gpuservice.storage.tempCapacity.required':
'Please enter the temporary storage capacity', 'Please enter the temporary storage capacity',
'gpuservice.form.rule.name': 'gpuservice.form.rule.name':
"Lowercase letters, numbers, and '-'. Start and end with a letter or number, no consecutive '-', max 63 characters." "Lowercase letters, numbers, and '-'. Start and end with a letter or number, no consecutive '-', max 63 characters.",
'gpuservice.form.storage.select': 'Select Storage',
'gpuservice.creator': 'Creator',
'gpuservice.owner.global': 'Global',
'gpuservice.template.group.yours': 'Your Templates',
'gpuservice.template.group.global': 'Global Templates'
}; };
+13 -9
View File
@@ -8,7 +8,7 @@ export default {
'menu.playground.text2images': 'Image', 'menu.playground.text2images': 'Image',
'menu.playground.video': 'Video', 'menu.playground.video': 'Video',
'menu.compare': 'Compare', 'menu.compare': 'Compare',
'menu.models': 'Models', 'menu.models': 'Model Service',
'menu.models.modelList': 'Deploy & Manage', 'menu.models.modelList': 'Deploy & Manage',
'menu.models.modelCatalog': 'Catalog', 'menu.models.modelCatalog': 'Catalog',
'menu.models.catalog': 'Model Catalog', 'menu.models.catalog': 'Model Catalog',
@@ -27,20 +27,24 @@ export default {
'menu.users': 'Users', 'menu.users': 'Users',
'menu.resources.workers': 'Workers', 'menu.resources.workers': 'Workers',
'menu.resources.gpus': 'GPUs', 'menu.resources.gpus': 'GPUs',
'menu.resources.modelfiles': 'Model Files', 'menu.models.modelfiles': 'Model Files',
'menu.accessControl': 'Access Control', 'menu.accessControl': 'Access Control',
'menu.accessControl.apikeys': 'API Keys', 'menu.accessControl.apikeys': 'API Keys',
'menu.accessControl.users': 'Users', 'menu.accessControl.users': 'Users',
'menu.profile': 'Profile', 'menu.accessControl.organizations': 'Organizations',
'menu.profile': 'Preferences',
'menu.login': 'Login', 'menu.login': 'Login',
'menu.usage': 'Usage', 'menu.usage': 'Usage',
'menu.usage.usage': 'Usage',
'menu.billingAndUsage': 'Usage & Billing',
'menu.billingAndUsage.usage': 'Usage',
'menu.billingAndUsage.billing': 'Billing',
'menu.404': '404', 'menu.404': '404',
'menu.clusterManagement': 'Cluster Management', 'menu.resources.clusters': 'Clusters',
'menu.clusterManagement.clusters': 'Clusters', 'menu.resources.credentials': 'Cloud Credentials',
'menu.clusterManagement.credentials': 'Cloud Credentials', 'menu.resources.clusterDetail': 'Cluster Detail',
'menu.clusterManagement.clusterDetail': 'Cluster Detail', 'menu.resources.clusterCreate': 'Create Cluster',
'menu.clusterManagement.clusterCreate': 'Create Cluster', 'menu.models.backendsList': 'Inference Backends',
'menu.resources.backendsList': 'Inference Backends',
'menu.gpuService': 'GPU Service', 'menu.gpuService': 'GPU Service',
'menu.gpuService.instances': 'GPU Instances', 'menu.gpuService.instances': 'GPU Instances',
'menu.gpuService.templates': 'Instance Templates', 'menu.gpuService.templates': 'Instance Templates',
+5 -5
View File
@@ -14,7 +14,7 @@ export default {
'models.form.env': 'Environment Variables', 'models.form.env': 'Environment Variables',
'models.form.configurations': 'Configurations', 'models.form.configurations': 'Configurations',
'models.form.s3address': 'S3 Address', '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.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.openinplayground': 'Open in Playground',
'models.instances': 'instances', 'models.instances': 'instances',
@@ -24,7 +24,7 @@ export default {
'model.deploy.sort': 'Sort', 'model.deploy.sort': 'Sort',
'model.deploy.search.placeholder': 'Type <kbd>/</kbd> to search models', 'model.deploy.search.placeholder': 'Type <kbd>/</kbd> to search models',
'model.form.ollamatips': '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.name': 'Name',
'models.sort.size': 'Size', 'models.sort.size': 'Size',
'models.sort.likes': 'Likes', 'models.sort.likes': 'Likes',
@@ -87,7 +87,7 @@ export default {
'models.form.filePath': 'Model Path', 'models.form.filePath': 'Model Path',
'models.form.backendVersion': 'Backend Version', 'models.form.backendVersion': 'Backend Version',
'models.form.backendVersion.tips': '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.gpuselector': 'GPU Selector',
'models.form.backend.llamabox': 'models.form.backend.llamabox':
'For GGUF format models, supports Linux, macOS, and Windows.', '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.form.backendVersions.tips': `To use more versions, go to the {link} page and edit the backend to add versions.`,
'models.catalog.nogpus.tips': 'models.catalog.nogpus.tips':
'No compatible GPUs are available in the selected cluster for this model.', '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.readyWorkers': 'workers ready',
'models.form.maxContextLength': 'Maximum Context Length', 'models.form.maxContextLength': 'Maximum Context Length',
'models.form.backend.helperText': 'models.form.backend.helperText':
@@ -291,7 +291,7 @@ export default {
'models.instance.startHistory': 'Run History', 'models.instance.startHistory': 'Run History',
'models.instance.startHistory.tips': 'models.instance.startHistory.tips':
'Shows logs from the run before the last error-triggered restart.', 'Shows logs from the run before the last error-triggered restart.',
'models.form.lora.label': 'LoRA Adapter', 'models.form.lora.label': 'LoRA Adapters',
'models.form.lora.add': 'Add LoRA Adapter', 'models.form.lora.add': 'Add LoRA Adapter',
'models.form.lora.select': 'Select LoRA', 'models.form.lora.select': 'Select LoRA',
'models.form.lora.name': 'LoRA name', 'models.form.lora.name': 'LoRA name',
+3
View File
@@ -36,9 +36,12 @@ export default {
'noresult.catalog.nofound': 'No matching models found.', 'noresult.catalog.nofound': 'No matching models found.',
'noresult.resources.cluster': 'noresult.resources.cluster':
'No clusters available. Add a cluster to get started.', 'No clusters available. Add a cluster to get started.',
'noresult.resources.k8sCluster':
'No clusters available. Add a Kubernetes cluster to get started.',
'noresult.resources.worker': 'noresult.resources.worker':
'No workers available. Add a worker to get started.', 'No workers available. Add a worker to get started.',
'noresult.resources.gotocluster': 'Create Your First Cluster', 'noresult.resources.gotocluster': 'Create Your First Cluster',
'noresult.resources.addk8scluster': 'Add a Kubernetes Cluster',
'noresult.resources.gotoworker': 'Add Worker', 'noresult.resources.gotoworker': 'Add Worker',
'noresult.benchmark.title': 'No Benchmarks', 'noresult.benchmark.title': 'No Benchmarks',
'noresult.benchmark.subTitle': 'No benchmarks have been added yet.', 'noresult.benchmark.subTitle': 'No benchmarks have been added yet.',
+15
View File
@@ -0,0 +1,15 @@
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 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',
'organizations.upsell.feature.members':
'Manage members and roles per organization',
'organizations.upsell.feature.quotas':
'Set resource and token quotas per organization',
'organizations.upsell.feature.isolation':
'Scope API keys, model deployments, and resources per organization',
'organizations.upsell.cta': 'Learn about Enterprise'
};
+3 -3
View File
@@ -52,7 +52,7 @@ export default {
'resources.worker.container.supported': 'Do not support macOS or Windows.', 'resources.worker.container.supported': 'Do not support macOS or Windows.',
'resources.worker.current.version': 'Current version is {version}.', 'resources.worker.current.version': 'Current version is {version}.',
'resources.worker.driver.install': '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': 'resources.worker.select.command':
'Select a label to generate the command and copy it using the copy button.', 'Select a label to generate the command and copy it using the copy button.',
'resources.worker.script.install': 'Script Installation', 'resources.worker.script.install': 'Script Installation',
@@ -89,7 +89,7 @@ export default {
'Paste the <span class="bold-text">Token</span>.', 'Paste the <span class="bold-text">Token</span>.',
'resources.register.worker.step7': 'resources.register.worker.step7':
'Click <span class="bold-text">Restart</span> to apply the settings.', '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': 'resources.register.download':
'Download and install the <a href={url} target="_blank">installer</a>. Only supported: {versions}.', 'Download and install the <a href={url} target="_blank">installer</a>. Only supported: {versions}.',
'resource.register.maos.support': 'Apple Silicon (M series), macOS 14+', '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.', 'No available clusters. Please create a cluster before adding a node.',
'resources.metrics.details': 'Monitoring', 'resources.metrics.details': 'Monitoring',
'resoureces.worker.upgrade.tips': '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.worker.version': 'Worker Version: {version}',
'resources.server.version': 'Server Version: {version}', 'resources.server.version': 'Server Version: {version}',
'resources.worker.currentVersion': 'Current Version: {version}', 'resources.worker.currentVersion': 'Current Version: {version}',
+72 -1
View File
@@ -17,9 +17,15 @@ export default {
'usage.table.user.apiKeysUsed': 'API Keys Used', 'usage.table.user.apiKeysUsed': 'API Keys Used',
'usage.table.lastActive': 'Last Active', 'usage.table.lastActive': 'Last Active',
'usage.filter.granularity': 'Granularity', 'usage.filter.granularity': 'Granularity',
'usage.filter.granularity.hour': 'Hour',
'usage.filter.granularity.day': 'Day', 'usage.filter.granularity.day': 'Day',
'usage.filter.granularity.week': 'Week', 'usage.filter.granularity.week': 'Week',
'usage.filter.granularity.month': 'Month', 'usage.filter.granularity.month': 'Month',
'usage.tabs.summary': 'Summary',
'usage.tabs.tokens': 'Tokens',
'usage.tabs.gpuInstances': 'GPU Instances',
'usage.tabs.storage': 'Storage',
'usage.tabs.resourceEvents': 'Resource Events',
'usage.tabs.models': 'Models', 'usage.tabs.models': 'Models',
'usage.tabs.apikeys': 'API Keys', 'usage.tabs.apikeys': 'API Keys',
'usage.tabs.users': 'User', 'usage.tabs.users': 'User',
@@ -32,5 +38,70 @@ export default {
'usage.chart.cached': 'Cached', 'usage.chart.cached': 'Cached',
'usage.chart.uncached': 'Uncached', 'usage.chart.uncached': 'Uncached',
'usage.chart.inputTokensCached': 'Input Tokens (Cached/Uncached)', 'usage.chart.inputTokensCached': 'Input Tokens (Cached/Uncached)',
'usage.table.inputTokensCached': 'Input Tokens Cached' 'usage.table.inputTokensCached': 'Input Tokens Cached',
// --- Resource usage: shared metrics & units ---
'usage.metric.tokens': 'Tokens',
'usage.metric.input': 'Input',
'usage.metric.output': 'Output',
'usage.metric.gpuHours': 'GPU Hours',
'usage.metric.instanceHours': 'Instance Hours',
'usage.metric.gbDays': 'GB-Days',
'usage.metric.gbHours': 'GB-Hours',
'usage.metric.activeUsers': 'Active Users',
'usage.metric.activeInstances': 'Active Instances',
'usage.metric.activeStorage': 'Active Storage',
'usage.metric.activeVolumes': 'Active Volumes',
'usage.metric.storageTypes': 'Storage Types',
'usage.metric.gpuHours.tip':
'Instance running time weighted by GPU count: an instance with N GPUs running for H hours counts as N × H GPU-hours. Equal to Instance Hours when every instance uses a single GPU.',
'usage.metric.instanceHours.tip':
'Total running time summed across all instances, regardless of how many GPUs each uses. One instance running for 2 hours = 2 instance-hours.',
'usage.metric.gbDays.tip':
'Storage capacity integrated over time, in GB × days: 10 GB kept for 5 days = 50 GB-days. (= GB-Hours ÷ 24)',
'usage.metric.gbHours.tip':
'Storage capacity integrated over time, in GB × hours: 10 GB kept for 5 hours = 50 GB-hours.',
// --- Resource usage: common table / labels ---
'usage.common.noData': 'No data',
'usage.common.unknown': 'unknown',
'usage.table.date': 'Date',
'usage.table.name': 'Name',
'usage.table.user': 'User',
'usage.table.users': 'Users',
'usage.table.type': 'Type',
'usage.table.instance': 'Instance',
'usage.table.instanceType': 'Instance Type',
'usage.table.instanceTypes': 'Instance Types',
'usage.table.instances': 'Instances',
'usage.table.capacity': 'Capacity',
'usage.export.tableNamed': 'Export Table Data — {name}',
// --- Summary tab ---
'usage.summary.compute': 'Compute',
'usage.summary.tokensOverTime': 'Tokens over time',
'usage.summary.gpuHoursOverTime': 'GPU Hours over time',
'usage.summary.gbDaysOverTime': 'GB-Days over time',
// --- GPU Instances / Storage filters ---
'usage.filter.instance': 'Filter by instance',
'usage.filter.storage': 'Filter by storage',
// --- Resource events ---
'usage.events.resourceType': 'Resource type',
'usage.events.eventType': 'Event type',
'usage.events.resourceName': 'Filter by name',
'usage.events.col.time': 'Time',
'usage.events.col.resource': 'Resource',
'usage.events.col.event': 'Event',
'usage.events.col.message': 'Message',
'usage.events.resource.gpuInstance': 'GPU Instance',
'usage.events.resource.cpuInstance': 'CPU Instance',
'usage.events.type.created': 'Created',
'usage.events.type.deleted': 'Deleted',
'usage.events.type.started': 'Started',
'usage.events.type.stopped': 'Stopped',
'usage.events.type.updated': 'Updated',
'usage.events.type.attached': 'Attached',
'usage.events.type.detached': 'Detached'
}; };
+5 -3
View File
@@ -28,16 +28,18 @@ export default {
'users.password.modify.title': 'Modify Password', 'users.password.modify.title': 'Modify Password',
'users.password.modify.description': 'users.password.modify.description':
"For your account's security, please change your initial password.", "For your account's security, please change your initial password.",
'users.password.modify.tips':
'Regularly updating your password helps keep your account secure.',
'users.password.confirm': 'Confirm New Password', 'users.password.confirm': 'Confirm New Password',
'users.password.confirm.empty': 'Please confirm the new password.', 'users.password.confirm.empty': 'Please confirm the new password.',
'users.password.confirm.error': 'The two passwords entered do not match.', 'users.password.confirm.error': 'The two passwords entered do not match.',
'users.login.title': 'Log in to', 'users.login.title': 'Log in to',
'users.version.islatest': 'GPUStack {version} is the latest version', 'users.version.islatest': 'MesaStack {version} is the latest version',
'users.version.update': 'GPUStack {version} is available', 'users.version.update': 'MesaStack {version} is available',
'users.settings.title': 'User Settings', 'users.settings.title': 'User Settings',
'users.status.activate': 'Activate Account', 'users.status.activate': 'Activate Account',
'users.status.deactivate': 'Deactivate Account', 'users.status.deactivate': 'Deactivate Account',
'users.status.inactiveAccount': 'Inactive Account', 'users.status.inactiveAccount': 'Inactive Account',
'users.login.getInitialPassword': '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 -1
View File
@@ -4,7 +4,7 @@ export default {
'vendor.hygon': 'Hygon', 'vendor.hygon': 'Hygon',
'vendor.moorthreads': 'Moore Threads', 'vendor.moorthreads': 'Moore Threads',
'vendor.iluvatar': 'Iluvatar', 'vendor.iluvatar': 'Iluvatar',
'vendor.metax': 'Metax', 'vendor.metax': 'MetaX',
'vendor.cambricon': 'Cambricon', 'vendor.cambricon': 'Cambricon',
'vendor.thead': 'T-Head PPU' 'vendor.thead': 'T-Head PPU'
}; };
+1 -1
View File
@@ -25,7 +25,7 @@ export default {
'ai.provider.ollama': 'Ollama', 'ai.provider.ollama': 'Ollama',
'ai.provider.openai': 'OpenAI', 'ai.provider.openai': 'OpenAI',
'ai.provider.openrouter': 'OpenRouter', 'ai.provider.openrouter': 'OpenRouter',
'ai.provider.qwen': 'Qwen', 'ai.provider.qwen': 'Alibaba Cloud Model Studio',
'ai.provider.spark': 'Spark', 'ai.provider.spark': 'Spark',
'ai.provider.stepfun': 'StepFun', 'ai.provider.stepfun': 'StepFun',
'ai.provider.together-ai': 'TogetherAI', 'ai.provider.together-ai': 'TogetherAI',
+3 -1
View File
@@ -23,7 +23,9 @@ export default {
'apikeys.accessScope.inference': 'Inference APIs', 'apikeys.accessScope.inference': 'Inference APIs',
'apikeys.access.permissions': 'Access Permissions', 'apikeys.access.permissions': 'Access Permissions',
'apikeys.type.auto': 'Auto-generated', 'apikeys.type.auto': 'Auto-generated',
'apikeys.type.custom': 'Custom' 'apikeys.type.custom': 'Custom',
'apikeys.button.ipConfig': 'IP Access Control',
'quotaLimits.button.title': 'Quota Limit'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
+6 -5
View File
@@ -23,11 +23,12 @@ export default {
'backend.form.defaultExecuteCommand': 'Default Execution Command', 'backend.form.defaultExecuteCommand': 'Default Execution Command',
'backend.form.defaultExecuteCommand.tips': `'{{'model_path'}}', '{{'port'}}', '{{'worker_ip'}}' and '{{'model_name'}}' are placeholders that will be substituted with the actual values during deployment.`, 'backend.form.defaultExecuteCommand.tips': `'{{'model_path'}}', '{{'port'}}', '{{'worker_ip'}}' and '{{'model_name'}}' are placeholders that will be substituted with the actual values during deployment.`,
'backend.form.defaultBackendParameters': 'Default Backend Parameters', 'backend.form.defaultBackendParameters': 'Default Backend Parameters',
'backend.form.parameterFormat': 'パラメータ形式', 'backend.form.flagFormat': 'フラグ形式',
'backend.form.parameterFormat.default': 'バックエンドのデフォルト', 'backend.form.flagFormat.tips':
'backend.form.parameterFormat.space': 'スペース区切り (--key value)', 'オプションとその値を連結する形式。空欄の場合は各パラメータを入力されたまま保持し、形式を統一しません。',
'backend.form.parameterFormat.equal': 'イコール連結 (--key=value)', 'backend.form.flagFormat.space': 'スペース区切り (--key value)',
'backend.form.commonParameters': 'よく使うパラメータ', 'backend.form.flagFormat.equal': 'イコール記号 (--key=value)',
'backend.form.commonParameters': 'よく使うバックエンドパラメータ',
'backend.form.commonParameters.tips': 'backend.form.commonParameters.tips':
'モデルのデプロイ時にバックエンドパラメータ入力欄の候補として表示されます。', 'モデルのデプロイ時にバックエンドパラメータ入力欄の候補として表示されます。',
'backend.form.versionConfig': 'Versions Config', 'backend.form.versionConfig': 'Versions Config',
+2 -2
View File
@@ -37,8 +37,8 @@ export default {
'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.', 'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.',
'benchmark.form.profile.heavy.tips': 'benchmark.form.profile.heavy.tips':
'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.', 'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.',
'benchmark.table.filter.bygpu': 'Filter by GPU', 'benchmark.table.filter.bygpu': 'GPU 検索',
'benchmark.table.filter.bymodel': 'Filter by Model', 'benchmark.table.filter.bymodel': 'モデル検索',
'benchmark.table.filter.bydataset': 'Filter by Dataset', 'benchmark.table.filter.bydataset': 'Filter by Dataset',
'benchmark.table.filter.byProfile': 'Filter by Profile', 'benchmark.table.filter.byProfile': 'Filter by Profile',
'benchmark.table.avg': 'Avg', 'benchmark.table.avg': 'Avg',
+15
View File
@@ -0,0 +1,15 @@
export default {
'billing.upsell.title': 'Billing is an Enterprise feature',
'billing.upsell.subtitle':
'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',
'billing.upsell.feature.invoices':
'Generate invoices and export billing reports',
'billing.upsell.feature.budgets':
'Set budgets and spending limits with alerts',
'billing.upsell.feature.chargeback':
'Attribute and charge back usage to teams and projects',
'billing.upsell.cta': 'Learn about Enterprise'
};
+38 -25
View File
@@ -39,7 +39,7 @@ export default {
'clusters.workerpool.batchSize.desc': 'clusters.workerpool.batchSize.desc':
'Number of workers created simultaneously in the Worker pool', 'Number of workers created simultaneously in the Worker pool',
'clusters.create.addworker.tips': 'clusters.create.addworker.tips':
'Please make sure the <a href={link} target="_blank">prerequisites</a> for {label} are met before executing the following command.', 'Please make sure the <a href={link} target="_blank">prerequisites</a> are met before executing the following command.',
'clusters.create.addCommand.tips': 'clusters.create.addCommand.tips':
'On the Worker that needs to be added, run the following command to join it to the cluster.', 'On the Worker that needs to be added, run the following command to join it to the cluster.',
'clusters.create.addCommand.k8s.tips': 'clusters.create.addCommand.k8s.tips':
@@ -70,10 +70,13 @@ export default {
'For <span class="bold-text">non-Docker</span> clusters, please register clusters or manage worker pools from the Clusters page.', 'For <span class="bold-text">non-Docker</span> clusters, please register clusters or manage worker pools from the Clusters page.',
'clusters.addworker.selectGPU': 'Select GPU Vendor', 'clusters.addworker.selectGPU': 'Select GPU Vendor',
'clusters.addworker.selectGPU.multiTag': 'Multi-select', 'clusters.addworker.selectGPU.multiTag': 'Multi-select',
'clusters.addworker.selectGPU.singleOnly': 'clusters.addworker.selectGPU.subtitle':
'The selected vendor is not in this clusters GPU vendor overrides — single-select only. Pick a vendor configured in the overrides to enable multi-select.', '複数の GPU ベンダーを選択するか、CPU クラスター専用の場合は選択不要です',
'clusters.addworker.checkEnv': 'Check Environment', 'clusters.addworker.checkEnv': 'Check Environment',
'clusters.addworker.checkEnv.cpuOnlyTips':
'以下のコマンドを使用して、Kubernetes クラスターに少なくとも 1 つのレディーノードがあることを確認してください。CPU クラスターを登録しています。',
'clusters.addworker.specifyArgs': 'Specify Arguments', 'clusters.addworker.specifyArgs': 'Specify Arguments',
'clusters.addworker.dtkVersion': 'DTK バージョン',
'clusters.addworker.runCommand': 'Run Command', 'clusters.addworker.runCommand': 'Run Command',
'clusters.addworker.specifyWorkerIP': 'Worker IP', 'clusters.addworker.specifyWorkerIP': 'Worker IP',
'clusters.addworker.detectWorkerIP': 'Worker IP を自動検出', 'clusters.addworker.detectWorkerIP': 'Worker IP を自動検出',
@@ -110,18 +113,20 @@ export default {
'{count} new worker has been added to the cluster.', '{count} new worker has been added to the cluster.',
'clusters.addworker.message.success_multiple': 'clusters.addworker.message.success_multiple':
'{count} new workers have been added to the cluster.', '{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.create.workerConfig': 'Worker Configuration',
'clusters.edit.k8sOptions.changed.tip':
'Kubernetes オプションを変更しました。変更を有効にするには、対象クラスターで登録コマンドを再実行してください。',
'clusters.addworker.containerName': 'Worker Container Name', 'clusters.addworker.containerName': 'Worker Container Name',
'clusters.addworker.containerName.tips': 'clusters.addworker.containerName.tips':
'Specify a name for the worker container.', 'Specify a name for the worker container.',
'clusters.addworker.dataVolume': 'GPUStack Data Volume', 'clusters.addworker.dataVolume': 'MesaStack Data Volume',
'clusters.addworker.dataVolume.tips': '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.internal': 'Internal',
'clusters.table.ip.external': 'External', 'clusters.table.ip.external': 'External',
'clusters.form.serverUrl.tips': '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': 'Set as Default',
'clusters.form.setDefault.tips': 'Default for deployment.', 'clusters.form.setDefault.tips': 'Default for deployment.',
'clusters.addworker.noClusters': 'No available Docker clusters found', 'clusters.addworker.noClusters': 'No available Docker clusters found',
@@ -139,7 +144,7 @@ export default {
'clusters.addworker.theadNotes-02': '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.', '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': '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.', '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.title': 'Volume Mounts',
'clusters.volume.name': 'Volume Name', 'clusters.volume.name': 'Volume Name',
'clusters.volume.mountPath': 'Container Path', 'clusters.volume.mountPath': 'Container Path',
@@ -164,6 +169,10 @@ export default {
'clusters.volume.configMap.name': 'ConfigMap Name', 'clusters.volume.configMap.name': 'ConfigMap Name',
'clusters.volume.configMap.optional': 'Optional', 'clusters.volume.configMap.optional': 'Optional',
'clusters.volume.add': 'Add Volume Mount', 'clusters.volume.add': 'Add Volume Mount',
'clusters.systemDefaultContainerRegistry.title': 'Default Container Registry',
'clusters.systemDefaultContainerRegistry.tip':
'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.title': 'Image Credentials',
'clusters.imageCredentials.add': 'Add Credential', 'clusters.imageCredentials.add': 'Add Credential',
'clusters.imageCredentials.registry': 'Registry', 'clusters.imageCredentials.registry': 'Registry',
@@ -172,18 +181,22 @@ export default {
'clusters.nodeSelector.title': 'Node Selector', 'clusters.nodeSelector.title': 'Node Selector',
'clusters.nodeSelector.tip': 'clusters.nodeSelector.tip':
'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.', 'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.',
'clusters.gpuVendorOverrides.title': 'GPU Vendor Overrides', 'clusters.operatorImage.title': 'Operator Image',
'clusters.gpuVendorOverrides.validate.emptySelector': 'clusters.operatorImage.tip':
'The override for {vendor} needs at least one nodeSelector entry — an empty override would leave the multi-vendor manifest unable to target nodes for this runtime.', 'Override for the MesaStack Operator container image. Leave empty to use the server default.',
'clusters.gpuVendorOverrides.validate.duplicate': 'clusters.namespace.title': 'Namespace',
'Overrides for {v1} and {v2} use the same nodeSelector. Each vendor must scope to a distinct node set, otherwise their worker DaemonSets would compete for the same nodes.', 'clusters.namespace.tip':
'clusters.gpuVendorOverrides.validate.keyConflict': 'Kubernetes namespace the clusters manifests render into. Leave empty to use gpustack-system.',
'Override for {vendor} reuses key(s) {keys} from the base Node Selector — the CPU worker would simultaneously require and forbid the key and never schedule.', 'clusters.clusterType.title': 'Cluster Type',
'clusters.gpuVendorOverrides.tip': 'clusters.modelService.title': 'Model Service',
'Required when running multiple GPU vendors in one cluster. Each entry pins that vendors worker DaemonSet to nodes matching its nodeSelector, and the CPU worker is steered away from those nodes via a DoesNotExist node-affinity.', 'clusters.modelService.tip':
'clusters.gpuVendorOverrides.add': 'Add Override', 'For LLM inference and API serving — e.g. exposing model APIs and token-based services.',
'clusters.gpuVendorOverrides.vendor': 'GPU Vendor', 'clusters.gpuInstances.title': 'GPU Service',
'clusters.gpuVendorOverrides.nodeSelector': 'Node Selector' 'clusters.gpuInstances.tip':
'For on-demand GPU compute — e.g. interactive development, training jobs, or custom environments.',
'clusters.gpuInstances.staticAddress': 'GPU Service Static Access Address',
'clusters.gpuInstances.staticAddress.tip':
'Static address the operator uses to access GPU instances in this cluster (e.g. a LoadBalancer VIP). Optional.'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -263,15 +276,15 @@ export default {
// 73. 'clusters.addworker.cacheVolume.holder': 'e.g. /data/cache (path must start with /)', // 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.', // 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.', // 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' // 77. 'clusters.create.workerConfig': 'Worker Configuration'
// 78. 'clusters.addworker.containerName': 'Worker Container Name', // 78. 'clusters.addworker.containerName': 'Worker Container Name',
// 79. 'clusters.addworker.containerName.tips':'Specify a name for the worker container.', // 79. 'clusters.addworker.containerName.tips':'Specify a name for the worker container.',
// 77. 'clusters.addworker.dataVolume': 'GPUStack Data Volume', // 77. 'clusters.addworker.dataVolume': 'MesaStack Data Volume',
// 78. 'clusters.addworker.dataVolume.tips': 'Specify a data storage path for GPUStack.', // 78. 'clusters.addworker.dataVolume.tips': 'Specify a data storage path for MesaStack.',
// 79. 'clusters.table.ip.internal': 'Internal', // 79. 'clusters.table.ip.internal': 'Internal',
// 80. 'clusters.table.ip.external': 'External', // 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.', // 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', // 83. 'clusters.form.setDefault': 'Set as Default',
// 84. 'clusters.form.setDefault.tips': 'Default for deployment', // 84. 'clusters.form.setDefault.tips': 'Default for deployment',
@@ -287,5 +300,5 @@ export default {
// 94. 'clusters.create.steps.configure': 'Configure', // 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>', // 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.', // 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 ========== // ========== End of To-Do List ==========
+18 -2
View File
@@ -46,6 +46,7 @@ export default {
'common.button.enabled': '有効', 'common.button.enabled': '有効',
'common.button.disabled': '無効', 'common.button.disabled': '無効',
'common.button.upgrade': 'アップグレード', 'common.button.upgrade': 'アップグレード',
'common.enterprise.feature': 'Available in MesaStack Enterprise',
'common.input.holder': '入力してください', 'common.input.holder': '入力してください',
'common.validate.value': '{name} の値は必須です', 'common.validate.value': '{name} の値は必須です',
'common.button.edit': '編集', 'common.button.edit': '編集',
@@ -55,6 +56,7 @@ export default {
'common.button.viewevent': 'イベントを表示', 'common.button.viewevent': 'イベントを表示',
'common.button.recreate': '再作成', 'common.button.recreate': '再作成',
'common.table.operation': '操作', 'common.table.operation': '操作',
'common.table.creator': '作成者',
'common.table.createTime': '作成日時', 'common.table.createTime': '作成日時',
'common.table.updateTime': '更新日時', 'common.table.updateTime': '更新日時',
'common.table.description': '説明', 'common.table.description': '説明',
@@ -65,6 +67,7 @@ export default {
'common.search.name.placeholder': '名前でフィルタ', 'common.search.name.placeholder': '名前でフィルタ',
'common.search.id.placeholder': 'IDでフィルタ', 'common.search.id.placeholder': 'IDでフィルタ',
'common.filter.byId': 'IDでフィルタ', 'common.filter.byId': 'IDでフィルタ',
'common.filter.byCreator': '作成者でフィルタ',
'common.table.type': 'タイプ', 'common.table.type': 'タイプ',
'common.table.default': 'デフォルト値', 'common.table.default': 'デフォルト値',
'common.copy.success': 'コピー成功!', 'common.copy.success': 'コピー成功!',
@@ -167,6 +170,7 @@ export default {
'common.time.hour': '時間', 'common.time.hour': '時間',
'common.time.minute': '分', 'common.time.minute': '分',
'common.issue.report': '問題を報告', 'common.issue.report': '問題を報告',
'common.github.star.tooltip': 'GitHub でスターをつける',
'common.social.discord': 'Discordに参加', 'common.social.discord': 'Discordに参加',
'common.table.mark': 'コメント', 'common.table.mark': 'コメント',
'common.table.rollback.mark': 'ロールバックコメント', 'common.table.rollback.mark': 'ロールバックコメント',
@@ -195,6 +199,7 @@ export default {
'common.table.user': 'ユーザー', 'common.table.user': 'ユーザー',
'common.settings.instructions': '手順', 'common.settings.instructions': '手順',
'common.settings.language': '言語', 'common.settings.language': '言語',
'common.settings.language.tips': 'インターフェースの表示言語を設定します。',
'common.delete.confirm': '選択した {type} を削除してもよろしいですか?', 'common.delete.confirm': '選択した {type} を削除してもよろしいですか?',
'common.delete.single.confirm': 'common.delete.single.confirm':
'<span style="font-size: 13px;font-weight: 700">{name}</span> を削除してもよろしいですか?', '<span style="font-size: 13px;font-weight: 700">{name}</span> を削除してもよろしいですか?',
@@ -208,7 +213,7 @@ export default {
'common.form.password': 'パスワード', 'common.form.password': 'パスワード',
'common.form.username': 'ユーザー名', 'common.form.username': 'ユーザー名',
'common.login.rember': 'ログイン状態を保持', 'common.login.rember': 'ログイン状態を保持',
'settings.company': 'GPUStack.ai', 'settings.company': 'MesaStack',
'common.button.help': 'ヘルプ', 'common.button.help': 'ヘルプ',
'common.button.feedback': 'フィードバック', 'common.button.feedback': 'フィードバック',
'common.button.docs': 'ドキュメント', 'common.button.docs': 'ドキュメント',
@@ -247,6 +252,11 @@ export default {
'common.appearance.tips': 'Default follows system preference.', 'common.appearance.tips': 'Default follows system preference.',
'common.button.forgotpassword': 'Forgot password?', 'common.button.forgotpassword': 'Forgot password?',
'common.appearance.theme': 'Theme', 'common.appearance.theme': 'Theme',
'common.appearance.description':
'デバイス上でのインターフェースの表示をカスタマイズします。',
'common.security': 'セキュリティ',
'common.security.description':
'アカウントへのログインに使用するパスワードを管理します。',
'common.page.wentwrong': 'Something went wrong.', 'common.page.wentwrong': 'Something went wrong.',
'common.page.refresh.tips': 'common.page.refresh.tips':
'The page may need to be updated. Try refreshing it!', 'The page may need to be updated. Try refreshing it!',
@@ -257,6 +267,10 @@ export default {
'common.login.auth': 'Authenticating...', 'common.login.auth': 'Authenticating...',
'common.login.auth.failed': 'Authentication failed', 'common.login.auth.failed': 'Authentication failed',
'common.login.password': 'Log in with Password', 'common.login.password': 'Log in with Password',
'common.login.username.holder': 'Please enter username',
'common.login.password.holder': 'Please enter password',
'common.login.newpassword.holder': 'Please enter new password',
'common.login.confirm.holder': 'Please enter password again',
'common.external.login': 'Log in with {type}', 'common.external.login': 'Log in with {type}',
'common.sso.noConfig': 'common.sso.noConfig':
'Single sign-on is not enabled on this system. Please contact your administrator.', 'Single sign-on is not enabled on this system. Please contact your administrator.',
@@ -285,7 +299,9 @@ export default {
'common.image.limit.height': 'Image height must be {height}.', 'common.image.limit.height': 'Image height must be {height}.',
'common.remaining': '残り {count}', 'common.remaining': '残り {count}',
'common.max': '最大 {count}', 'common.max': '最大 {count}',
'common.validate.group': 'Please complete the {group} configuration' 'common.max.count': '{label} 数',
'common.validate.group': 'Please complete the {group} configuration',
'common.preferences': 'Preferences'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
+27 -4
View File
@@ -13,7 +13,11 @@ export default {
'gpuservice.template.command.placeholder': 'gpuservice.template.command.placeholder':
'引数はスペースで区切り、スペースを含む引数は引用符で囲んでください。例:/bin/bash -c "echo hello world"', '引数はスペースで区切り、スペースを含む引数は引用符で囲んでください。例:/bin/bash -c "echo hello world"',
'gpuservice.template.mountPath': 'マウントパス', 'gpuservice.template.mountPath': 'マウントパス',
'gpuservice.template.mountPath.tips':
'このテンプレートからインスタンスを作成する際に、ストレージボリュームがデフォルトでマウントされるパスです。インスタンスの実行中に保持する必要があるデータの永続化に使用できます。',
'gpuservice.template.containerDisk': 'コンテナディスク (GB)', 'gpuservice.template.containerDisk': 'コンテナディスク (GB)',
'gpuservice.template.containerDisk.tips':
'コンテナシステムディスクのサイズです。',
'gpuservice.template.memory': 'メモリ (GB)', 'gpuservice.template.memory': 'メモリ (GB)',
'gpuservice.instance.containerDisk.remaining': 'gpuservice.instance.containerDisk.remaining':
'コンテナディスク (最大 {count} GB)', 'コンテナディスク (最大 {count} GB)',
@@ -76,6 +80,8 @@ export default {
'gpuservice.publicKey': 'SSH 公開鍵', 'gpuservice.publicKey': 'SSH 公開鍵',
'gpuservice.publicKey.add': 'SSH 公開鍵を追加', 'gpuservice.publicKey.add': 'SSH 公開鍵を追加',
'gpuservice.publicKey.edit': 'SSH 公開鍵を編集', 'gpuservice.publicKey.edit': 'SSH 公開鍵を編集',
'gpuservice.publicKey.delete.tips':
'SSH 公開鍵を削除しても、既にアタッチされているインスタンスのアクセス権は取り消されません。アクセス権を削除するには、対象のインスタンスを個別に編集してください。',
'gpuservice.publicKey.filter.name': '名前で検索', 'gpuservice.publicKey.filter.name': '名前で検索',
'gpuservice.publicKey.label': 'SSH 公開鍵', 'gpuservice.publicKey.label': 'SSH 公開鍵',
'gpuservice.instance.ssh.enable': 'SSH アクセスを有効化', 'gpuservice.instance.ssh.enable': 'SSH アクセスを有効化',
@@ -96,20 +102,32 @@ export default {
'gpuservice.instance.templates': 'インスタンステンプレート', 'gpuservice.instance.templates': 'インスタンステンプレート',
'gpuservice.instance.section.storage': 'ストレージボリューム', 'gpuservice.instance.section.storage': 'ストレージボリューム',
'gpuservice.instance.type.required': 'インスタンスタイプを選択してください', 'gpuservice.instance.type.required': 'インスタンスタイプを選択してください',
'gpuservice.instance.type.noAvailable':
'利用可能なインスタンスタイプがありません',
'gpuservice.instance.gpuCount': 'GPU 数', 'gpuservice.instance.gpuCount': 'GPU 数',
'gpuservice.instance.gpuCount.required': 'GPU 数を入力してください', 'gpuservice.instance.gpuCount.required': 'GPU 数を入力してください',
'gpuservice.instance.gpuCount.max': 'gpuservice.instance.gpuCount.max':
'最大 {count} 枚の GPU カードを選択してください', '最大 {count} 枚の GPU カードを選択してください',
'gpuservice.instance.gpuCount.min': 'gpuservice.instance.gpuCount.min':
'少なくとも {count} 枚の GPU カードを選択してください', '少なくとも {count} 枚の GPU カードを選択してください',
'gpuservice.instance.cpuCount.max':
'最大 {count} 個の CPU コアを選択してください',
'gpuservice.instance.cpuCount.min':
'少なくとも {count} 個の CPU コアを選択してください',
'gpuservice.instance.gpuCount.noAvailable': 'gpuservice.instance.gpuCount.noAvailable':
'利用可能な GPU リソースがありません。別のインスタンスタイプを選択してください。', '利用可能な GPU リソースがありません。別のインスタンスタイプを選択してください。',
'gpuservice.instance.gpuCount.zero': 'CPU のみを使用し、環境準備用です。', 'gpuservice.instance.gpuCount.zero': 'CPU のみを使用し、環境準備用です。',
'gpuservice.instance.stock': '在庫', 'gpuservice.instance.stock': '在庫',
'gpuservice.instance.sliced': '分割', 'gpuservice.instance.sliced': '分割',
'gpuservice.instance.memory': 'Memory', 'gpuservice.instance.memory': 'VRAM',
'gpuservice.instance.ram': 'RAM', 'gpuservice.instance.ram': 'RAM',
'gpuservice.instance.os': 'OS',
'gpuservice.instance.arch': 'アーキテクチャ',
'gpuservice.instance.disk': 'ディスク', 'gpuservice.instance.disk': 'ディスク',
'gpuservice.table.count': '数量',
'gpuservice.instance.disk.system': 'システムディスク',
'gpuservice.instance.disk.ephemeral': '一時ストレージ',
'gpuservice.instance.disk.persistent': '永続ストレージ',
'gpuservice.instance.search.type.placeholder': '名前で検索', 'gpuservice.instance.search.type.placeholder': '名前で検索',
'gpuservice.instance.search.template.placeholder': 'gpuservice.instance.search.template.placeholder':
'テンプレート名、イメージまたはマウントパスで検索', 'テンプレート名、イメージまたはマウントパスで検索',
@@ -141,9 +159,9 @@ export default {
'gpuservice.storage.accessMode': 'アクセスモード', 'gpuservice.storage.accessMode': 'アクセスモード',
'gpuservice.storage.persistent': '永続', 'gpuservice.storage.persistent': '永続',
'gpuservice.storage.temporary': '一時', 'gpuservice.storage.temporary': '一時',
'gpuservice.storage.persistentVolume': '永続ボリューム', 'gpuservice.storage.persistentVolume': '永続',
'gpuservice.storage.persistentVolume.required': 'gpuservice.storage.persistentVolume.required':
'永続ボリュームを選択してください', 'ストレージを選択してください',
'gpuservice.storage.persistentVolume.capacity': '容量 (GB)', 'gpuservice.storage.persistentVolume.capacity': '容量 (GB)',
'gpuservice.storage.persistentVolume.capacity.required': 'gpuservice.storage.persistentVolume.capacity.required':
'容量を入力してください', '容量を入力してください',
@@ -157,5 +175,10 @@ export default {
'gpuservice.storage.temporary.tips': 'gpuservice.storage.temporary.tips':
'Data is cleared when the instance stops.', 'Data is cleared when the instance stops.',
'gpuservice.storage.persistentVolume.tips': 'gpuservice.storage.persistentVolume.tips':
'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.' 'Data persists across instance restarts. Persistent volumes remain intact after instance termination and can be shared by multiple instances.',
'gpuservice.form.storage.select': 'ストレージを選択',
'gpuservice.creator': '作成者',
'gpuservice.owner.global': 'グローバル',
'gpuservice.template.group.yours': '自分のテンプレート',
'gpuservice.template.group.global': 'グローバルテンプレート'
}; };
+15 -11
View File
@@ -8,7 +8,7 @@ export default {
'menu.playground.text2images': '画像生成', 'menu.playground.text2images': '画像生成',
'menu.playground.video': '動画', 'menu.playground.video': '動画',
'menu.compare': '比較', 'menu.compare': '比較',
'menu.models': 'モデル', 'menu.models': 'モデルサービス',
'menu.models.modelList': 'デプロイと管理', 'menu.models.modelList': 'デプロイと管理',
'menu.models.modelCatalog': 'カタログ', 'menu.models.modelCatalog': 'カタログ',
'menu.models.catalog': 'モデルカタログ', 'menu.models.catalog': 'モデルカタログ',
@@ -23,24 +23,28 @@ export default {
'menu.resources': 'リソース', 'menu.resources': 'リソース',
'menu.apikeys': 'APIキー', 'menu.apikeys': 'APIキー',
'menu.users': 'ユーザー', 'menu.users': 'ユーザー',
'menu.profile': 'プロフィール', 'menu.profile': 'Preferences',
'menu.login': 'ログイン', 'menu.login': 'ログイン',
'menu.usage': '使用状況', 'menu.usage': '使用状況',
'menu.usage.usage': '使用状況',
'menu.billingAndUsage': '使用状況と請求',
'menu.billingAndUsage.usage': '使用状況',
'menu.billingAndUsage.billing': '請求',
'menu.404': '404', 'menu.404': '404',
'menu.settings': 'Settings', 'menu.settings': 'Settings',
'menu.resources.workers': 'Workers', 'menu.resources.workers': 'Workers',
'menu.resources.gpus': 'GPUs', 'menu.resources.gpus': 'GPUs',
'menu.resources.modelfiles': 'Model Files', 'menu.models.modelfiles': 'Model Files',
'menu.accessControl': 'Access Control', 'menu.accessControl': 'Access Control',
'menu.accessControl.apikeys': 'API Keys', 'menu.accessControl.apikeys': 'API Keys',
'menu.accessControl.users': 'Users', 'menu.accessControl.users': 'Users',
'menu.clusterManagement': 'Cluster Management', 'menu.accessControl.organizations': 'Organizations',
'menu.clusterManagement.clusters': 'Clusters', 'menu.resources.clusters': 'Clusters',
'menu.clusterManagement.credentials': 'Cloud Credentials', 'menu.resources.credentials': 'Cloud Credentials',
'menu.models.userModels': 'My Models', 'menu.models.userModels': 'My Models',
'menu.clusterManagement.clusterDetail': 'Cluster Detail', 'menu.resources.clusterDetail': 'Cluster Detail',
'menu.clusterManagement.clusterCreate': 'Create Cluster', 'menu.resources.clusterCreate': 'Create Cluster',
'menu.resources.backendsList': 'Inference Backends', 'menu.models.backendsList': 'Inference Backends',
'menu.gpuService': 'GPU Service', 'menu.gpuService': 'GPU Service',
'menu.gpuService.instances': 'GPU Instances', 'menu.gpuService.instances': 'GPU Instances',
'menu.gpuService.templates': 'Instance Templates', 'menu.gpuService.templates': 'Instance Templates',
@@ -53,7 +57,7 @@ export default {
// 1. 'menu.models.deployment': 'Deployment', // 1. 'menu.models.deployment': 'Deployment',
// 2. 'menu.resources.workers': 'Workers', // 2. 'menu.resources.workers': 'Workers',
// 3. 'menu.resources.gpus': 'GPUs', // 3. 'menu.resources.gpus': 'GPUs',
// 4. 'menu.resources.modelfiles': 'Model Files', // 4. 'menu.models.modelfiles': 'Model Files',
// 5. 'menu.accessControl': 'Access Control', // 5. 'menu.accessControl': 'Access Control',
// 6. 'menu.accessControl.apikeys': 'API Keys', // 6. 'menu.accessControl.apikeys': 'API Keys',
// 7. 'menu.accessControl.users': 'Users', // 7. 'menu.accessControl.users': 'Users',
@@ -63,7 +67,7 @@ export default {
// 11. 'menu.models.userModels': 'My Models' // 11. 'menu.models.userModels': 'My Models'
// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail', // 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail',
// 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster', // 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster',
// 14. 'menu.resources.backendsList': 'Inference Backends', // 14. 'menu.models.backendsList': 'Inference Backends',
// 15. 'menu.models.benchmark': 'Benchmarks', // 15. 'menu.models.benchmark': 'Benchmarks',
// 15. 'menu.models.provider': 'Provider', // 15. 'menu.models.provider': 'Provider',
// 15. 'menu.models.providers': 'Provider', // 15. 'menu.models.providers': 'Provider',
+7 -7
View File
@@ -14,7 +14,7 @@ export default {
'models.form.env': '環境変数', 'models.form.env': '環境変数',
'models.form.configurations': '設定', 'models.form.configurations': '設定',
'models.form.s3address': 'S3アドレス', '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.form.distribution.tips':
'ワーカーのリソースが不足している場合、モデルの一部のレイヤーを単一または複数のリモートワーカーにオフロードすることができます。', 'ワーカーのリソースが不足している場合、モデルの一部のレイヤーを単一または複数のリモートワーカーにオフロードすることができます。',
'models.openinplayground': 'プレイグラウンドで開く', 'models.openinplayground': 'プレイグラウンドで開く',
@@ -25,7 +25,7 @@ export default {
'model.deploy.sort': '並び替え', 'model.deploy.sort': '並び替え',
'model.deploy.search.placeholder': '<kbd>/</kbd>を入力してモデルを検索', 'model.deploy.search.placeholder': '<kbd>/</kbd>を入力してモデルを検索',
'model.form.ollamatips': 'model.form.ollamatips':
'ヒント: 以下はGPUStackで事前設定されたOllamaモデルです。希望するモデルを選択するか、右側の【{name}】入力ボックスにデプロイしたいモデルを直接入力してください。', 'ヒント: 以下はMesaStackで事前設定されたOllamaモデルです。希望するモデルを選択するか、右側の【{name}】入力ボックスにデプロイしたいモデルを直接入力してください。',
'models.sort.name': '名前', 'models.sort.name': '名前',
'models.sort.size': 'サイズ', 'models.sort.size': 'サイズ',
'models.sort.likes': 'いいね', 'models.sort.likes': 'いいね',
@@ -88,7 +88,7 @@ export default {
'models.form.filePath': 'モデルパス', 'models.form.filePath': 'モデルパス',
'models.form.backendVersion': 'バックエンドバージョン', 'models.form.backendVersion': 'バックエンドバージョン',
'models.form.backendVersion.tips': 'models.form.backendVersion.tips':
'希望する{backend}{version}バージョンを使用するには、システムがオンライン環境で対応するバージョンをインストールする仮想環境を自動的に作成します。GPUStackのアップグレード後もバックエンドバージョンは固定されます。{link}', '希望する{backend}{version}バージョンを使用するには、システムがオンライン環境で対応するバージョンをインストールする仮想環境を自動的に作成します。MesaStackのアップグレード後もバックエンドバージョンは固定されます。{link}',
'models.form.gpuselector': 'GPUセレクター', 'models.form.gpuselector': 'GPUセレクター',
'models.form.backend.llamabox': 'models.form.backend.llamabox':
'GGUF形式のモデル用(Linux、macOS、Windowsをサポート)。', '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.form.backendVersions.tips': `To use more versions, go to the {link} page and edit the backend to add versions.`,
'models.catalog.nogpus.tips': 'models.catalog.nogpus.tips':
'No compatible GPUs are available in the selected cluster for this model.', '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.readyWorkers': 'workers ready',
'models.form.maxContextLength': 'Maximum Context Length', 'models.form.maxContextLength': 'Maximum Context Length',
'models.form.backend.helperText': 'models.form.backend.helperText':
@@ -291,7 +291,7 @@ export default {
'models.instance.startHistory': 'Run History', 'models.instance.startHistory': 'Run History',
'models.instance.startHistory.tips': 'models.instance.startHistory.tips':
'Shows logs from the run before the last error-triggered restart.', 'Shows logs from the run before the last error-triggered restart.',
'models.form.lora.label': 'LoRA Adapter', 'models.form.lora.label': 'LoRA Adapters',
'models.form.lora.add': 'Add LoRA Adapter', 'models.form.lora.add': 'Add LoRA Adapter',
'models.form.lora.select': 'Select LoRA', 'models.form.lora.select': 'Select LoRA',
'models.form.lora.name': 'LoRA name', 'models.form.lora.name': 'LoRA name',
@@ -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>.', // 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.', // 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.', // 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.', // 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.',, // 67. 'models.form.backend.warning.gguf': 'Please ensure that the selected custom backend supports GGUF models.',,
// 68. 'models.form.backendVersion.deprecated': 'Deprecated', // 68. 'models.form.backendVersion.deprecated': 'Deprecated',
@@ -390,7 +390,7 @@ export default {
// 71.'models.accessSettings.allowedUsers.tips': 'Only designated users can access the model.', // 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.`, // 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.', // 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', // 75. 'models.form.readyWorkers': 'workers ready',
// 76. 'models.form.maxContextLength': 'Maximum Context Length', // 76. 'models.form.maxContextLength': 'Maximum Context Length',
// 77. 'models.form.backend.helperText': 'Not enabled yet. Will be enabled after deployment. ', // 77. 'models.form.backend.helperText': 'Not enabled yet. Will be enabled after deployment. ',
+3
View File
@@ -36,9 +36,12 @@ export default {
'noresult.catalog.nofound': 'No matching models found.', 'noresult.catalog.nofound': 'No matching models found.',
'noresult.resources.cluster': 'noresult.resources.cluster':
'No clusters available. Add a cluster to get started.', 'No clusters available. Add a cluster to get started.',
'noresult.resources.k8sCluster':
'No clusters available. Add a Kubernetes cluster to get started.',
'noresult.resources.worker': 'noresult.resources.worker':
'No workers available. Add a worker to get started.', 'No workers available. Add a worker to get started.',
'noresult.resources.gotocluster': 'Create Your First Cluster', 'noresult.resources.gotocluster': 'Create Your First Cluster',
'noresult.resources.addk8scluster': 'Add a Kubernetes Cluster',
'noresult.resources.gotoworker': 'Add Worker', 'noresult.resources.gotoworker': 'Add Worker',
'noresult.benchmark.title': 'No Benchmarks', 'noresult.benchmark.title': 'No Benchmarks',
'noresult.benchmark.subTitle': 'No benchmarks have been added yet.', 'noresult.benchmark.subTitle': 'No benchmarks have been added yet.',
+15
View File
@@ -0,0 +1,15 @@
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 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',
'organizations.upsell.feature.members':
'Manage members and roles per organization',
'organizations.upsell.feature.quotas':
'Set resource and token quotas per organization',
'organizations.upsell.feature.isolation':
'Scope API keys, model deployments, and resources per organization',
'organizations.upsell.cta': 'Learn about Enterprise'
};
+5 -5
View File
@@ -52,7 +52,7 @@ export default {
'MacOSまたはWindowsはサポートされていません。', 'MacOSまたはWindowsはサポートされていません。',
'resources.worker.current.version': '現在のバージョンは {version} です。', 'resources.worker.current.version': '現在のバージョンは {version} です。',
'resources.worker.driver.install': '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.select.command':
'ラベルを選択してコマンドを生成し、コピーを使用してコマンドをコピーします。', 'ラベルを選択してコマンドを生成し、コピーを使用してコマンドをコピーします。',
'resources.worker.script.install': 'スクリプトインストール', 'resources.worker.script.install': 'スクリプトインストール',
@@ -89,7 +89,7 @@ export default {
'Paste the <span class="bold-text">Token</span>.', 'Paste the <span class="bold-text">Token</span>.',
'resources.register.worker.step7': 'resources.register.worker.step7':
'Click <span class="bold-text">Restart</span> to apply the settings.', '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': 'resources.register.download':
'Download and install the <a href={url} target="_blank">installer</a>. Only supported: {versions}.', 'Download and install the <a href={url} target="_blank">installer</a>. Only supported: {versions}.',
'resource.register.maos.support': 'Apple Silicon (M series), macOS 14+', '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.', 'No available clusters. Please create a cluster before adding a node.',
'resources.metrics.details': 'Monitoring', 'resources.metrics.details': 'Monitoring',
'resoureces.worker.upgrade.tips': '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.worker.version': 'Worker Version: {version}',
'resources.server.version': 'Server Version: {version}', 'resources.server.version': 'Server Version: {version}',
'resources.worker.currentVersion': 'Current 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}.', // 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>.', // 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.', // 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}.', // 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+', // 10. 'resource.register.maos.support': 'Apple Silicon (M series), macOS 14+',
// 11. 'resource.register.windows.support': 'win 10, win 11', // 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', // 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.', // 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.' // 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 ========== // ========== End of To-Do List ==========
+72 -1
View File
@@ -17,9 +17,15 @@ export default {
'usage.table.user.apiKeysUsed': 'API Keys Used', 'usage.table.user.apiKeysUsed': 'API Keys Used',
'usage.table.lastActive': 'Last Active', 'usage.table.lastActive': 'Last Active',
'usage.filter.granularity': 'Granularity', 'usage.filter.granularity': 'Granularity',
'usage.filter.granularity.hour': '時間',
'usage.filter.granularity.day': 'Day', 'usage.filter.granularity.day': 'Day',
'usage.filter.granularity.week': 'Week', 'usage.filter.granularity.week': 'Week',
'usage.filter.granularity.month': 'Month', 'usage.filter.granularity.month': 'Month',
'usage.tabs.summary': '概要',
'usage.tabs.tokens': 'トークン',
'usage.tabs.gpuInstances': 'GPU インスタンス',
'usage.tabs.storage': 'ストレージ',
'usage.tabs.resourceEvents': 'リソースイベント',
'usage.tabs.models': 'Models', 'usage.tabs.models': 'Models',
'usage.tabs.apikeys': 'API Keys', 'usage.tabs.apikeys': 'API Keys',
'usage.tabs.users': 'User', 'usage.tabs.users': 'User',
@@ -32,5 +38,70 @@ export default {
'usage.chart.cached': 'Cached', 'usage.chart.cached': 'Cached',
'usage.chart.uncached': 'Uncached', 'usage.chart.uncached': 'Uncached',
'usage.chart.inputTokensCached': 'Input Tokens (Cached/Uncached)', 'usage.chart.inputTokensCached': 'Input Tokens (Cached/Uncached)',
'usage.table.inputTokensCached': 'Input Tokens Cached' 'usage.table.inputTokensCached': 'Input Tokens Cached',
// --- Resource usage: shared metrics & units ---
'usage.metric.tokens': 'トークン数',
'usage.metric.input': '入力',
'usage.metric.output': '出力',
'usage.metric.gpuHours': 'GPU 時間',
'usage.metric.instanceHours': 'インスタンス時間',
'usage.metric.gbDays': 'GB·日',
'usage.metric.gbHours': 'GB·時間',
'usage.metric.activeUsers': 'アクティブユーザー',
'usage.metric.activeInstances': 'アクティブインスタンス',
'usage.metric.activeStorage': 'アクティブストレージ',
'usage.metric.activeVolumes': 'アクティブボリューム',
'usage.metric.storageTypes': 'ストレージタイプ',
'usage.metric.gpuHours.tip':
'インスタンスの稼働時間を GPU 数で重み付けした値:N 個の GPU を使用するインスタンスが H 時間稼働すると N × H GPU 時間としてカウントされます。すべてのインスタンスが単一の GPU を使用する場合はインスタンス時間と等しくなります。',
'usage.metric.instanceHours.tip':
'GPU の数に関係なく、すべてのインスタンスの稼働時間を合計した値。1 つのインスタンスが 2 時間稼働 = 2 インスタンス時間。',
'usage.metric.gbDays.tip':
'ストレージ容量を時間で積分した値(GB × 日):10 GB を 5 日間保持 = 50 GB·日。(= GB·時間 ÷ 24',
'usage.metric.gbHours.tip':
'ストレージ容量を時間で積分した値(GB × 時間):10 GB を 5 時間保持 = 50 GB·時間。',
// --- Resource usage: common table / labels ---
'usage.common.noData': 'データがありません',
'usage.common.unknown': '不明',
'usage.table.date': '日付',
'usage.table.name': '名前',
'usage.table.user': 'ユーザー',
'usage.table.users': 'ユーザー',
'usage.table.type': 'タイプ',
'usage.table.instance': 'インスタンス',
'usage.table.instanceType': 'インスタンスタイプ',
'usage.table.instanceTypes': 'インスタンスタイプ',
'usage.table.instances': 'インスタンス',
'usage.table.capacity': '容量',
'usage.export.tableNamed': 'テーブルデータをエクスポート — {name}',
// --- Summary tab ---
'usage.summary.compute': 'コンピュート',
'usage.summary.tokensOverTime': 'トークン数の推移',
'usage.summary.gpuHoursOverTime': 'GPU 時間の推移',
'usage.summary.gbDaysOverTime': 'GB·日の推移',
// --- GPU Instances / Storage filters ---
'usage.filter.instance': 'インスタンスで絞り込み',
'usage.filter.storage': 'ストレージで絞り込み',
// --- Resource events ---
'usage.events.resourceType': 'リソースタイプ',
'usage.events.eventType': 'イベントタイプ',
'usage.events.resourceName': '名前で絞り込み',
'usage.events.col.time': '時刻',
'usage.events.col.resource': 'リソース',
'usage.events.col.event': 'イベント',
'usage.events.col.message': 'メッセージ',
'usage.events.resource.gpuInstance': 'GPU インスタンス',
'usage.events.resource.cpuInstance': 'CPU インスタンス',
'usage.events.type.created': '作成済み',
'usage.events.type.deleted': '削除済み',
'usage.events.type.started': '開始',
'usage.events.type.stopped': '停止',
'usage.events.type.updated': '更新済み',
'usage.events.type.attached': 'アタッチ済み',
'usage.events.type.detached': 'デタッチ済み'
}; };
+6 -4
View File
@@ -29,18 +29,20 @@ export default {
'users.password.modify.title': 'パスワードを変更', 'users.password.modify.title': 'パスワードを変更',
'users.password.modify.description': 'users.password.modify.description':
'アカウントのセキュリティのため、初期パスワードを変更してください。', 'アカウントのセキュリティのため、初期パスワードを変更してください。',
'users.password.modify.tips':
'パスワードを定期的に更新すると、アカウントの安全を保てます。',
'users.password.confirm': '新しいパスワードを確認', 'users.password.confirm': '新しいパスワードを確認',
'users.password.confirm.empty': '新しいパスワードを確認してください。', 'users.password.confirm.empty': '新しいパスワードを確認してください。',
'users.password.confirm.error': '入力された2つのパスワードが一致しません。', 'users.password.confirm.error': '入力された2つのパスワードが一致しません。',
'users.login.title': 'ログイン', 'users.login.title': 'ログイン',
'users.version.islatest': 'GPUStack {version} は最新バージョンです', 'users.version.islatest': 'MesaStack {version} は最新バージョンです',
'users.version.update': 'GPUStack {version} が利用可能です', 'users.version.update': 'MesaStack {version} が利用可能です',
'users.settings.title': 'User Settings', 'users.settings.title': 'User Settings',
'users.status.activate': 'Activate Account', 'users.status.activate': 'Activate Account',
'users.status.deactivate': 'Deactivate Account', 'users.status.deactivate': 'Deactivate Account',
'users.status.inactiveAccount': 'Inactive Account', 'users.status.inactiveAccount': 'Inactive Account',
'users.login.getInitialPassword': '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.'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -48,5 +50,5 @@ export default {
// 2. 'users.status.activate': 'Activate Account', // 2. 'users.status.activate': 'Activate Account',
// 3. 'users.status.deactivate': 'Deactivate Account', // 3. 'users.status.deactivate': 'Deactivate Account',
// 4. 'users.status.inactiveAccount': 'Inactive Account', // 4. 'users.status.inactiveAccount': 'Inactive Account',
// 5. 'users.login.getInitialPassword': 'Run the following command on your GPUStack Server to retrieve the initial admin password.' // 5. 'users.login.getInitialPassword': 'Run the following command on your MesaStack Server to retrieve the initial admin password.'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+1 -1
View File
@@ -4,7 +4,7 @@ export default {
'vendor.hygon': 'Hygon', 'vendor.hygon': 'Hygon',
'vendor.moorthreads': 'Moore Threads', 'vendor.moorthreads': 'Moore Threads',
'vendor.iluvatar': 'Iluvatar', 'vendor.iluvatar': 'Iluvatar',
'vendor.metax': 'Metax', 'vendor.metax': 'MetaX',
'vendor.cambricon': 'Cambricon', 'vendor.cambricon': 'Cambricon',
'vendor.thead': 'T-Head PPU' 'vendor.thead': 'T-Head PPU'
}; };
+1 -1
View File
@@ -25,7 +25,7 @@ export default {
'ai.provider.ollama': 'Ollama', 'ai.provider.ollama': 'Ollama',
'ai.provider.openai': 'OpenAI', 'ai.provider.openai': 'OpenAI',
'ai.provider.openrouter': 'OpenRouter', 'ai.provider.openrouter': 'OpenRouter',
'ai.provider.qwen': 'Qwen', 'ai.provider.qwen': 'Alibaba Cloud Model Studio',
'ai.provider.spark': 'Spark', 'ai.provider.spark': 'Spark',
'ai.provider.stepfun': 'StepFun', 'ai.provider.stepfun': 'StepFun',
'ai.provider.together-ai': 'TogetherAI', 'ai.provider.together-ai': 'TogetherAI',
+3 -1
View File
@@ -23,7 +23,9 @@ export default {
'apikeys.accessScope.inference': 'Inference APIs', 'apikeys.accessScope.inference': 'Inference APIs',
'apikeys.access.permissions': 'Access Permissions', 'apikeys.access.permissions': 'Access Permissions',
'apikeys.type.auto': 'Auto-generated', 'apikeys.type.auto': 'Auto-generated',
'apikeys.type.custom': 'Custom' 'apikeys.type.custom': 'Custom',
'apikeys.button.ipConfig': 'IP Access Control',
'quotaLimits.button.title': 'Quota Limit'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
+6 -5
View File
@@ -23,11 +23,12 @@ export default {
'backend.form.defaultExecuteCommand': 'Команда выполнения по умолчанию', 'backend.form.defaultExecuteCommand': 'Команда выполнения по умолчанию',
'backend.form.defaultExecuteCommand.tips': `'{{'model_path'}}', '{{'port'}}', '{{'worker_ip'}}' и '{{'model_name'}}' заполняются реальными значениями во время запуска`, 'backend.form.defaultExecuteCommand.tips': `'{{'model_path'}}', '{{'port'}}', '{{'worker_ip'}}' и '{{'model_name'}}' заполняются реальными значениями во время запуска`,
'backend.form.defaultBackendParameters': 'Параметры бэкенда по умолчанию', 'backend.form.defaultBackendParameters': 'Параметры бэкенда по умолчанию',
'backend.form.parameterFormat': 'Формат параметров', 'backend.form.flagFormat': 'Формат флага',
'backend.form.parameterFormat.default': 'По умолчанию бэкенда', 'backend.form.flagFormat.tips':
'backend.form.parameterFormat.space': 'Пробел (--key value)', 'Формат соединения опции и её значения. Оставьте пустым, чтобы сохранить каждый параметр в исходном виде, без приведения к единому формату.',
'backend.form.parameterFormat.equal': 'Знак равенства (--key=value)', 'backend.form.flagFormat.space': 'Разделение пробелом (--key value)',
'backend.form.commonParameters': 'Часто используемые параметры', 'backend.form.flagFormat.equal': 'Знак равенства (--key=value)',
'backend.form.commonParameters': 'Общие параметры бэкенда',
'backend.form.commonParameters.tips': 'backend.form.commonParameters.tips':
'Отображаются как подсказки в поле параметров бэкенда при развёртывании.', 'Отображаются как подсказки в поле параметров бэкенда при развёртывании.',
'backend.form.versionConfig': 'Конфигурация версий', 'backend.form.versionConfig': 'Конфигурация версий',
+2 -2
View File
@@ -37,8 +37,8 @@ export default {
'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.', 'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.',
'benchmark.form.profile.heavy.tips': 'benchmark.form.profile.heavy.tips':
'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.', 'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.',
'benchmark.table.filter.bygpu': 'Filter by GPU', 'benchmark.table.filter.bygpu': 'Поиск GPU',
'benchmark.table.filter.bymodel': 'Filter by Model', 'benchmark.table.filter.bymodel': 'Поиск модели',
'benchmark.table.filter.bydataset': 'Filter by Dataset', 'benchmark.table.filter.bydataset': 'Filter by Dataset',
'benchmark.table.filter.byProfile': 'Filter by Profile', 'benchmark.table.filter.byProfile': 'Filter by Profile',
'benchmark.table.avg': 'Avg', 'benchmark.table.avg': 'Avg',
+15
View File
@@ -0,0 +1,15 @@
export default {
'billing.upsell.title': 'Billing is an Enterprise feature',
'billing.upsell.subtitle':
'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',
'billing.upsell.feature.invoices':
'Generate invoices and export billing reports',
'billing.upsell.feature.budgets':
'Set budgets and spending limits with alerts',
'billing.upsell.feature.chargeback':
'Attribute and charge back usage to teams and projects',
'billing.upsell.cta': 'Learn about Enterprise'
};
+34 -21
View File
@@ -39,7 +39,7 @@ export default {
'clusters.workerpool.batchSize.desc': 'clusters.workerpool.batchSize.desc':
'Количество воркеров, создаваемых одновременно в пуле воркеров', 'Количество воркеров, создаваемых одновременно в пуле воркеров',
'clusters.create.addworker.tips': 'clusters.create.addworker.tips':
'Пожалуйста, убедитесь, что выполнены <a href={link} target="_blank">предварительные условия</a> для {label} перед выполнением следующей команды.', 'Пожалуйста, убедитесь, что выполнены <a href={link} target="_blank">предварительные условия</a> перед выполнением следующей команды.',
'clusters.create.addCommand.tips': 'clusters.create.addCommand.tips':
'На воркере, который необходимо добавить, выполните следующую команду, чтобы присоединить его к кластеру.', 'На воркере, который необходимо добавить, выполните следующую команду, чтобы присоединить его к кластеру.',
'clusters.create.addCommand.k8s.tips': 'clusters.create.addCommand.k8s.tips':
@@ -70,10 +70,13 @@ export default {
'Для <span class="bold-text">не-Docker</span> кластеров, пожалуйста, регистрируйте кластеры или управляйте пулами воркеров на странице Кластеры.', 'Для <span class="bold-text">не-Docker</span> кластеров, пожалуйста, регистрируйте кластеры или управляйте пулами воркеров на странице Кластеры.',
'clusters.addworker.selectGPU': 'Выбрать производителя GPU', 'clusters.addworker.selectGPU': 'Выбрать производителя GPU',
'clusters.addworker.selectGPU.multiTag': 'Multi-select', 'clusters.addworker.selectGPU.multiTag': 'Multi-select',
'clusters.addworker.selectGPU.singleOnly': 'clusters.addworker.selectGPU.subtitle':
'The selected vendor is not in this clusters GPU vendor overrides — single-select only. Pick a vendor configured in the overrides to enable multi-select.', 'Вы можете выбрать несколько производителей GPU или не выбирать для кластера только с CPU',
'clusters.addworker.checkEnv': 'Проверить окружение', 'clusters.addworker.checkEnv': 'Проверить окружение',
'clusters.addworker.checkEnv.cpuOnlyTips':
'Используйте следующую команду, чтобы убедиться, что в кластере Kubernetes есть хотя бы один готовый узел. Вы регистрируете кластер только с CPU.',
'clusters.addworker.specifyArgs': 'Указать аргументы', 'clusters.addworker.specifyArgs': 'Указать аргументы',
'clusters.addworker.dtkVersion': 'Версия DTK',
'clusters.addworker.runCommand': 'Выполнить команду', 'clusters.addworker.runCommand': 'Выполнить команду',
'clusters.addworker.specifyWorkerIP': 'Указать IP воркера', 'clusters.addworker.specifyWorkerIP': 'Указать IP воркера',
'clusters.addworker.detectWorkerIP': 'Автоматически определить IP воркера', 'clusters.addworker.detectWorkerIP': 'Автоматически определить IP воркера',
@@ -110,18 +113,20 @@ export default {
'{count} новый воркер был добавлен в кластер.', '{count} новый воркер был добавлен в кластер.',
'clusters.addworker.message.success_multiple': 'clusters.addworker.message.success_multiple':
'{count} новых воркеров были добавлены в кластер.', '{count} новых воркеров были добавлены в кластер.',
'clusters.create.serverUrl': 'URL сервера GPUStack', 'clusters.create.serverUrl': 'URL сервера MesaStack',
'clusters.create.workerConfig': 'Конфигурация воркера', 'clusters.create.workerConfig': 'Конфигурация воркера',
'clusters.edit.k8sOptions.changed.tip':
'Вы изменили параметры Kubernetes. Чтобы изменения вступили в силу, повторно выполните команду регистрации в целевом кластере.',
'clusters.addworker.containerName': 'Имя контейнера воркера', 'clusters.addworker.containerName': 'Имя контейнера воркера',
'clusters.addworker.containerName.tips': 'clusters.addworker.containerName.tips':
'Укажите имя для контейнера воркера.', 'Укажите имя для контейнера воркера.',
'clusters.addworker.dataVolume': 'Том данных GPUStack', 'clusters.addworker.dataVolume': 'Том данных MesaStack',
'clusters.addworker.dataVolume.tips': 'clusters.addworker.dataVolume.tips':
'Укажите путь для хранения данных GPUStack.', 'Укажите путь для хранения данных MesaStack.',
'clusters.table.ip.internal': 'Внутренний', 'clusters.table.ip.internal': 'Внутренний',
'clusters.table.ip.external': 'Внешний', 'clusters.table.ip.external': 'Внешний',
'clusters.form.serverUrl.tips': 'clusters.form.serverUrl.tips':
'Если рабочий узел не может напрямую получить доступ к GPUStack Server, укажите внешний URL службы GPUStack Server.', 'Если рабочий узел не может напрямую получить доступ к MesaStack Server, укажите внешний URL службы MesaStack Server.',
'clusters.form.setDefault': 'Установить по умолчанию', 'clusters.form.setDefault': 'Установить по умолчанию',
'clusters.form.setDefault.tips': 'clusters.form.setDefault.tips':
'Использовать по умолчанию для развертывания.', 'Использовать по умолчанию для развертывания.',
@@ -140,7 +145,7 @@ export default {
'clusters.addworker.theadNotes-02': '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.', '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': '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.', '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.title': 'Volume Mounts',
'clusters.volume.name': 'Volume Name', 'clusters.volume.name': 'Volume Name',
'clusters.volume.mountPath': 'Container Path', 'clusters.volume.mountPath': 'Container Path',
@@ -165,6 +170,10 @@ export default {
'clusters.volume.configMap.name': 'ConfigMap Name', 'clusters.volume.configMap.name': 'ConfigMap Name',
'clusters.volume.configMap.optional': 'Optional', 'clusters.volume.configMap.optional': 'Optional',
'clusters.volume.add': 'Add Volume Mount', 'clusters.volume.add': 'Add Volume Mount',
'clusters.systemDefaultContainerRegistry.title': 'Default Container Registry',
'clusters.systemDefaultContainerRegistry.tip':
'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.title': 'Image Credentials',
'clusters.imageCredentials.add': 'Add Credential', 'clusters.imageCredentials.add': 'Add Credential',
'clusters.imageCredentials.registry': 'Registry', 'clusters.imageCredentials.registry': 'Registry',
@@ -173,18 +182,22 @@ export default {
'clusters.nodeSelector.title': 'Node Selector', 'clusters.nodeSelector.title': 'Node Selector',
'clusters.nodeSelector.tip': 'clusters.nodeSelector.tip':
'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.', 'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.',
'clusters.gpuVendorOverrides.title': 'GPU Vendor Overrides', 'clusters.operatorImage.title': 'Operator Image',
'clusters.gpuVendorOverrides.validate.emptySelector': 'clusters.operatorImage.tip':
'The override for {vendor} needs at least one nodeSelector entry — an empty override would leave the multi-vendor manifest unable to target nodes for this runtime.', 'Override for the MesaStack Operator container image. Leave empty to use the server default.',
'clusters.gpuVendorOverrides.validate.duplicate': 'clusters.namespace.title': 'Namespace',
'Overrides for {v1} and {v2} use the same nodeSelector. Each vendor must scope to a distinct node set, otherwise their worker DaemonSets would compete for the same nodes.', 'clusters.namespace.tip':
'clusters.gpuVendorOverrides.validate.keyConflict': 'Kubernetes namespace the clusters manifests render into. Leave empty to use gpustack-system.',
'Override for {vendor} reuses key(s) {keys} from the base Node Selector — the CPU worker would simultaneously require and forbid the key and never schedule.', 'clusters.clusterType.title': 'Cluster Type',
'clusters.gpuVendorOverrides.tip': 'clusters.modelService.title': 'Model Service',
'Required when running multiple GPU vendors in one cluster. Each entry pins that vendors worker DaemonSet to nodes matching its nodeSelector, and the CPU worker is steered away from those nodes via a DoesNotExist node-affinity.', 'clusters.modelService.tip':
'clusters.gpuVendorOverrides.add': 'Add Override', 'For LLM inference and API serving — e.g. exposing model APIs and token-based services.',
'clusters.gpuVendorOverrides.vendor': 'GPU Vendor', 'clusters.gpuInstances.title': 'GPU Service',
'clusters.gpuVendorOverrides.nodeSelector': 'Node Selector' 'clusters.gpuInstances.tip':
'For on-demand GPU compute — e.g. interactive development, training jobs, or custom environments.',
'clusters.gpuInstances.staticAddress': 'GPU Service Static Access Address',
'clusters.gpuInstances.staticAddress.tip':
'Static address the operator uses to access GPU instances in this cluster (e.g. a LoadBalancer VIP). Optional.'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -200,5 +213,5 @@ export default {
// 10. 'clusters.addworker.theadNotes': 'If the <span class="bold-text>/usr/local/PPU_SDK</span> directory does not exist, please create a symbolic link pointing to the T-Head PPU SDK installed path: <span class="bold-text>ln -s /path/to/PPU_SDK /usr/local/PPU_SDK</span>', // 10. 'clusters.addworker.theadNotes': 'If the <span class="bold-text>/usr/local/PPU_SDK</span> directory does not exist, please create a symbolic link pointing to the T-Head PPU SDK installed path: <span class="bold-text>ln -s /path/to/PPU_SDK /usr/local/PPU_SDK</span>',
// 11. 'clusters.addworker.theadNotes-02': 'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.' // 11. 'clusters.addworker.theadNotes-02': 'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.'
// 12. 'clusters.addworker.metaxNotes': `If the <span class="bold-text">/opt/mxdriver</span> or <span class="bold-text">/opt/maca</span> directory does not exist, create a symbolic link to the MetaX driver and SDK installation path: <span class="desc-fill">ln -s /path/to/mxdriver /opt/mxdriver</span><span class="desc-fill">ln -s /path/to/maca /opt/maca</span>.`, // 12. 'clusters.addworker.metaxNotes': `If the <span class="bold-text">/opt/mxdriver</span> or <span class="bold-text">/opt/maca</span> directory does not exist, create a symbolic link to the MetaX driver and SDK installation path: <span class="desc-fill">ln -s /path/to/mxdriver /opt/mxdriver</span><span class="desc-fill">ln -s /path/to/maca /opt/maca</span>.`,
// 13. 'clusters.addworker.nvidiaNotes': 'The built-in inference backends in 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.' // 13. '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.'
// ================================================================ // ================================================================
+19 -3
View File
@@ -46,6 +46,7 @@ export default {
'common.button.enabled': 'Активно', 'common.button.enabled': 'Активно',
'common.button.disabled': 'Отключено', 'common.button.disabled': 'Отключено',
'common.button.upgrade': 'Обновить', 'common.button.upgrade': 'Обновить',
'common.enterprise.feature': 'Available in MesaStack Enterprise',
'common.input.holder': 'Введите значение', 'common.input.holder': 'Введите значение',
'common.validate.value': 'Поле {name} обязательно', 'common.validate.value': 'Поле {name} обязательно',
'common.button.edit': 'Редактировать', 'common.button.edit': 'Редактировать',
@@ -55,6 +56,7 @@ export default {
'common.button.viewevent': 'Просмотр событий', 'common.button.viewevent': 'Просмотр событий',
'common.button.recreate': 'Пересоздать', 'common.button.recreate': 'Пересоздать',
'common.table.operation': 'Действия', 'common.table.operation': 'Действия',
'common.table.creator': 'Создатель',
'common.table.createTime': 'Создано', 'common.table.createTime': 'Создано',
'common.table.updateTime': 'Обновлено', 'common.table.updateTime': 'Обновлено',
'common.table.description': 'Описание', 'common.table.description': 'Описание',
@@ -65,6 +67,7 @@ export default {
'common.search.name.placeholder': 'Фильтр по названию', 'common.search.name.placeholder': 'Фильтр по названию',
'common.search.id.placeholder': 'Фильтр по ID', 'common.search.id.placeholder': 'Фильтр по ID',
'common.filter.byId': 'Фильтр по ID', 'common.filter.byId': 'Фильтр по ID',
'common.filter.byCreator': 'Фильтр по создателю',
'common.table.type': 'Тип', 'common.table.type': 'Тип',
'common.table.default': 'Значение по умолчанию', 'common.table.default': 'Значение по умолчанию',
'common.copy.success': 'Скопировано!', 'common.copy.success': 'Скопировано!',
@@ -164,6 +167,7 @@ export default {
'common.time.hour': 'Час', 'common.time.hour': 'Час',
'common.time.minute': 'Минута', 'common.time.minute': 'Минута',
'common.issue.report': 'Сообщить о проблеме', 'common.issue.report': 'Сообщить о проблеме',
'common.github.star.tooltip': 'Поставьте нам звезду на GitHub',
'common.social.discord': 'Присоединиться к Discord', 'common.social.discord': 'Присоединиться к Discord',
'common.table.mark': 'Комментарий', 'common.table.mark': 'Комментарий',
'common.table.rollback.mark': 'Комментарий к откату', 'common.table.rollback.mark': 'Комментарий к откату',
@@ -193,6 +197,7 @@ export default {
'common.table.user': 'Пользователь', 'common.table.user': 'Пользователь',
'common.settings.instructions': 'Инструкции', 'common.settings.instructions': 'Инструкции',
'common.settings.language': 'Язык', 'common.settings.language': 'Язык',
'common.settings.language.tips': 'Задайте язык отображения интерфейса.',
'common.delete.confirm': 'Вы уверены, что хотите удалить выбранный {type}?', 'common.delete.confirm': 'Вы уверены, что хотите удалить выбранный {type}?',
'common.delete.single.confirm': 'common.delete.single.confirm':
'Вы уверены, что хотите удалить <span style="font-size: 13px;font-weight: 700">{name}</span>?', 'Вы уверены, что хотите удалить <span style="font-size: 13px;font-weight: 700">{name}</span>?',
@@ -206,7 +211,7 @@ export default {
'common.form.password': 'Пароль', 'common.form.password': 'Пароль',
'common.form.username': 'Имя пользователя', 'common.form.username': 'Имя пользователя',
'common.login.rember': 'Запомнить меня', 'common.login.rember': 'Запомнить меня',
'settings.company': 'GPUStack.ai', 'settings.company': 'MesaStack',
'common.button.help': 'Помощь', 'common.button.help': 'Помощь',
'common.button.feedback': 'Обратная связь', 'common.button.feedback': 'Обратная связь',
'common.button.docs': 'Документация', 'common.button.docs': 'Документация',
@@ -246,6 +251,11 @@ export default {
'common.appearance.tips': 'По умолчанию соответствует системным настройкам.', 'common.appearance.tips': 'По умолчанию соответствует системным настройкам.',
'common.button.forgotpassword': 'Забыли пароль?', 'common.button.forgotpassword': 'Забыли пароль?',
'common.appearance.theme': 'Тема', 'common.appearance.theme': 'Тема',
'common.appearance.description':
'Настройте внешний вид интерфейса на вашем устройстве.',
'common.security': 'Безопасность',
'common.security.description':
'Управляйте паролем для входа в учётную запись.',
'common.page.wentwrong': 'Что-то пошло не так.', 'common.page.wentwrong': 'Что-то пошло не так.',
'common.page.refresh.tips': 'common.page.refresh.tips':
'Страница может нуждаться в обновлении. Попробуйте обновить её!', 'Страница может нуждаться в обновлении. Попробуйте обновить её!',
@@ -256,11 +266,15 @@ export default {
'common.login.auth': 'Аутентификация...', 'common.login.auth': 'Аутентификация...',
'common.login.auth.failed': 'Ошибка аутентификации', 'common.login.auth.failed': 'Ошибка аутентификации',
'common.login.password': 'Войти с паролем', 'common.login.password': 'Войти с паролем',
'common.login.username.holder': 'Введите имя пользователя',
'common.login.password.holder': 'Введите пароль',
'common.login.newpassword.holder': 'Введите новый пароль',
'common.login.confirm.holder': 'Введите пароль ещё раз',
'common.external.login': 'Войти через {type}', 'common.external.login': 'Войти через {type}',
'common.sso.noConfig': 'common.sso.noConfig':
'Единый вход не настроен в этой системе. Пожалуйста, обратитесь к администратору.', 'Единый вход не настроен в этой системе. Пожалуйста, обратитесь к администратору.',
'common.button.edit.item': 'Редактировать {name}', 'common.button.edit.item': 'Редактировать {name}',
'common.button.copy.item': 'Duplicate {name}', 'common.button.copy.item': 'Дублировать {name}',
'common.button.terminal': 'Терминал', 'common.button.terminal': 'Терминал',
'common.button.addItem': 'Добавить элемент', 'common.button.addItem': 'Добавить элемент',
'common.help.default': 'По умолчанию: {content}', 'common.help.default': 'По умолчанию: {content}',
@@ -284,7 +298,9 @@ export default {
'common.image.limit.height': 'Image height must be {height}.', 'common.image.limit.height': 'Image height must be {height}.',
'common.remaining': 'Остаток {count}', 'common.remaining': 'Остаток {count}',
'common.max': 'Макс. {count}', 'common.max': 'Макс. {count}',
'common.validate.group': 'Please complete the {group} configuration' 'common.max.count': 'Количество {label}',
'common.validate.group': 'Please complete the {group} configuration',
'common.preferences': 'Preferences'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
+24 -4
View File
@@ -14,7 +14,11 @@ export default {
'gpuservice.template.command.placeholder': 'gpuservice.template.command.placeholder':
'Разделяйте аргументы пробелами; аргументы с пробелами заключайте в кавычки, например: /bin/bash -c "echo hello world"', 'Разделяйте аргументы пробелами; аргументы с пробелами заключайте в кавычки, например: /bin/bash -c "echo hello world"',
'gpuservice.template.mountPath': 'Путь монтирования', 'gpuservice.template.mountPath': 'Путь монтирования',
'gpuservice.template.mountPath.tips':
'Путь, по которому том хранилища монтируется по умолчанию при создании экземпляра из этого шаблона. Может использоваться для сохранения данных, которые нужно сохранить во время работы экземпляра.',
'gpuservice.template.containerDisk': 'Диск контейнера (GB)', 'gpuservice.template.containerDisk': 'Диск контейнера (GB)',
'gpuservice.template.containerDisk.tips':
'Размер системного диска контейнера.',
'gpuservice.template.memory': 'Память (GB)', 'gpuservice.template.memory': 'Память (GB)',
'gpuservice.instance.containerDisk.remaining': 'gpuservice.instance.containerDisk.remaining':
'Диск контейнера (Макс. {count} GB)', 'Диск контейнера (Макс. {count} GB)',
@@ -80,6 +84,8 @@ export default {
'gpuservice.publicKey': 'Открытый ключ SSH', 'gpuservice.publicKey': 'Открытый ключ SSH',
'gpuservice.publicKey.add': 'Добавить открытый ключ SSH', 'gpuservice.publicKey.add': 'Добавить открытый ключ SSH',
'gpuservice.publicKey.edit': 'Изменить открытый ключ SSH', 'gpuservice.publicKey.edit': 'Изменить открытый ключ SSH',
'gpuservice.publicKey.delete.tips':
'Удаление открытого ключа SSH не отзывает доступ для уже подключённых экземпляров. Чтобы удалить доступ, отредактируйте эти экземпляры отдельно.',
'gpuservice.publicKey.filter.name': 'Поиск по имени', 'gpuservice.publicKey.filter.name': 'Поиск по имени',
'gpuservice.publicKey.label': 'Открытый ключ SSH', 'gpuservice.publicKey.label': 'Открытый ключ SSH',
'gpuservice.instance.ssh.enable': 'Включить SSH-доступ', 'gpuservice.instance.ssh.enable': 'Включить SSH-доступ',
@@ -100,18 +106,27 @@ export default {
'gpuservice.instance.templates': 'Шаблоны экземпляров', 'gpuservice.instance.templates': 'Шаблоны экземпляров',
'gpuservice.instance.section.storage': 'Том хранилища', 'gpuservice.instance.section.storage': 'Том хранилища',
'gpuservice.instance.type.required': 'Выберите тип экземпляра', 'gpuservice.instance.type.required': 'Выберите тип экземпляра',
'gpuservice.instance.type.noAvailable': 'Нет доступных типов экземпляров',
'gpuservice.instance.gpuCount': 'Количество GPU', 'gpuservice.instance.gpuCount': 'Количество GPU',
'gpuservice.instance.gpuCount.required': 'Введите количество GPU', 'gpuservice.instance.gpuCount.required': 'Введите количество GPU',
'gpuservice.instance.gpuCount.max': 'Выберите максимум {count} GPU-карт', 'gpuservice.instance.gpuCount.max': 'Выберите максимум {count} GPU-карт',
'gpuservice.instance.gpuCount.min': 'Выберите минимум {count} GPU-карт', 'gpuservice.instance.gpuCount.min': 'Выберите минимум {count} GPU-карт',
'gpuservice.instance.cpuCount.max': 'Выберите максимум {count} ядер CPU',
'gpuservice.instance.cpuCount.min': 'Выберите минимум {count} ядер CPU',
'gpuservice.instance.gpuCount.noAvailable': 'gpuservice.instance.gpuCount.noAvailable':
'Нет доступных ресурсов GPU, выберите другой тип экземпляра.', 'Нет доступных ресурсов GPU, выберите другой тип экземпляра.',
'gpuservice.instance.gpuCount.zero': 'Только CPU, для подготовки окружения.', 'gpuservice.instance.gpuCount.zero': 'Только CPU, для подготовки окружения.',
'gpuservice.instance.stock': 'Остаток', 'gpuservice.instance.stock': 'Остаток',
'gpuservice.instance.sliced': 'Разделено', 'gpuservice.instance.sliced': 'Разделено',
'gpuservice.instance.memory': 'Память', 'gpuservice.instance.memory': 'VRAM',
'gpuservice.instance.ram': 'RAM', 'gpuservice.instance.ram': 'RAM',
'gpuservice.instance.os': 'ОС',
'gpuservice.instance.arch': 'Архитектура',
'gpuservice.instance.disk': 'Диск', 'gpuservice.instance.disk': 'Диск',
'gpuservice.table.count': 'Количество',
'gpuservice.instance.disk.system': 'Системный диск',
'gpuservice.instance.disk.ephemeral': 'Временное хранилище',
'gpuservice.instance.disk.persistent': 'Постоянное хранилище',
'gpuservice.instance.search.type.placeholder': 'Поиск по имени', 'gpuservice.instance.search.type.placeholder': 'Поиск по имени',
'gpuservice.instance.search.template.placeholder': 'gpuservice.instance.search.template.placeholder':
'Поиск по имени шаблона, образу или пути монтирования', 'Поиск по имени шаблона, образу или пути монтирования',
@@ -143,8 +158,8 @@ export default {
'gpuservice.storage.accessMode': 'Режим доступа', 'gpuservice.storage.accessMode': 'Режим доступа',
'gpuservice.storage.persistent': 'Постоянное', 'gpuservice.storage.persistent': 'Постоянное',
'gpuservice.storage.temporary': 'Временное', 'gpuservice.storage.temporary': 'Временное',
'gpuservice.storage.persistentVolume': 'Постоянный том', 'gpuservice.storage.persistentVolume': 'Постоянное',
'gpuservice.storage.persistentVolume.required': 'Выберите постоянный том', 'gpuservice.storage.persistentVolume.required': 'Выберите хранилище',
'gpuservice.storage.persistentVolume.capacity': 'Ёмкость (ГБ)', 'gpuservice.storage.persistentVolume.capacity': 'Ёмкость (ГБ)',
'gpuservice.storage.persistentVolume.capacity.required': 'Введите ёмкость', 'gpuservice.storage.persistentVolume.capacity.required': 'Введите ёмкость',
'gpuservice.storage.persistentVolume.releaseWithInstance': 'gpuservice.storage.persistentVolume.releaseWithInstance':
@@ -157,5 +172,10 @@ export default {
'gpuservice.storage.temporary.tips': 'gpuservice.storage.temporary.tips':
'Data is cleared when the instance stops.', 'Data is cleared when the instance stops.',
'gpuservice.storage.persistentVolume.tips': 'gpuservice.storage.persistentVolume.tips':
'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.' 'Data persists across instance restarts. Persistent volumes remain intact after instance termination and can be shared by multiple instances.',
'gpuservice.form.storage.select': 'Выберите хранилище',
'gpuservice.creator': 'Создатель',
'gpuservice.owner.global': 'Глобальный',
'gpuservice.template.group.yours': 'Ваши шаблоны',
'gpuservice.template.group.global': 'Глобальные шаблоны'
}; };
+13 -9
View File
@@ -8,7 +8,7 @@ export default {
'menu.playground.text2images': 'Генерация изображений', 'menu.playground.text2images': 'Генерация изображений',
'menu.playground.video': 'Видео', 'menu.playground.video': 'Видео',
'menu.compare': 'Сравнение', 'menu.compare': 'Сравнение',
'menu.models': 'Модели', 'menu.models': 'Сервисы моделей',
'menu.models.modelList': 'Развертывание и управление', 'menu.models.modelList': 'Развертывание и управление',
'menu.models.modelCatalog': 'Каталог', 'menu.models.modelCatalog': 'Каталог',
'menu.models.catalog': 'Каталог моделей', 'menu.models.catalog': 'Каталог моделей',
@@ -23,23 +23,27 @@ export default {
'menu.resources': 'Ресурсы', 'menu.resources': 'Ресурсы',
'menu.apikeys': 'API-ключи', 'menu.apikeys': 'API-ключи',
'menu.users': 'Пользователи', 'menu.users': 'Пользователи',
'menu.profile': 'Профиль', 'menu.profile': 'Preferences',
'menu.login': 'Авторизация', 'menu.login': 'Авторизация',
'menu.usage': 'Использование', 'menu.usage': 'Использование',
'menu.usage.usage': 'Использование',
'menu.billingAndUsage': 'Использование и биллинг',
'menu.billingAndUsage.usage': 'Использование',
'menu.billingAndUsage.billing': 'Биллинг',
'menu.404': 'Ошибка 404', 'menu.404': 'Ошибка 404',
'menu.resources.workers': 'Воркеры', 'menu.resources.workers': 'Воркеры',
'menu.resources.gpus': 'GPUs', 'menu.resources.gpus': 'GPUs',
'menu.resources.modelfiles': 'Файлы модлей', 'menu.models.modelfiles': 'Файлы модлей',
'menu.accessControl': 'Управление доступом', 'menu.accessControl': 'Управление доступом',
'menu.accessControl.apikeys': 'API Ключи', 'menu.accessControl.apikeys': 'API Ключи',
'menu.accessControl.users': 'Пользователи', 'menu.accessControl.users': 'Пользователи',
'menu.clusterManagement': 'Управление кластерами', 'menu.accessControl.organizations': 'Организации',
'menu.clusterManagement.clusters': 'Кластеры', 'menu.resources.clusters': 'Кластеры',
'menu.clusterManagement.credentials': 'Облачные аккаунты', 'menu.resources.credentials': 'Облачные аккаунты',
'menu.models.userModels': 'Мои модели', 'menu.models.userModels': 'Мои модели',
'menu.clusterManagement.clusterDetail': 'Детали кластера', 'menu.resources.clusterDetail': 'Детали кластера',
'menu.clusterManagement.clusterCreate': 'Создать кластер', 'menu.resources.clusterCreate': 'Создать кластер',
'menu.resources.backendsList': 'Бэкенды запуска', 'menu.models.backendsList': 'Бэкенды запуска',
'menu.settings': 'Settings', 'menu.settings': 'Settings',
'menu.gpuService': 'GPU Service', 'menu.gpuService': 'GPU Service',
'menu.gpuService.instances': 'GPU Instances', 'menu.gpuService.instances': 'GPU Instances',
+5 -5
View File
@@ -15,7 +15,7 @@ export default {
'models.form.configurations': 'Конфигурации', 'models.form.configurations': 'Конфигурации',
'models.form.s3address': 'S3-адрес', 'models.form.s3address': 'S3-адрес',
'models.form.partialoffload.tips': 'models.form.partialoffload.tips':
'При включении CPU оффлоудинга GPUStack будет выделять оперативную память, если ресурсов GPU недостаточно. Вы должны правильно настроить бэкенд вывода для использования гибридного CPU+GPU или полного CPU вывода.', 'При включении CPU оффлоудинга MesaStack будет выделять оперативную память, если ресурсов GPU недостаточно. Вы должны правильно настроить бэкенд вывода для использования гибридного CPU+GPU или полного CPU вывода.',
'models.form.distribution.tips': 'models.form.distribution.tips':
'Позволяет переносить часть слоёв модели на один или несколько удалённых воркеров, когда ресурсов текущего воркера недостаточно.', 'Позволяет переносить часть слоёв модели на один или несколько удалённых воркеров, когда ресурсов текущего воркера недостаточно.',
'models.openinplayground': 'Открыть в Песочнице', 'models.openinplayground': 'Открыть в Песочнице',
@@ -26,7 +26,7 @@ export default {
'model.deploy.sort': 'Сортировка', 'model.deploy.sort': 'Сортировка',
'model.deploy.search.placeholder': 'Введите <kbd>/</kbd> для поиска моделей', 'model.deploy.search.placeholder': 'Введите <kbd>/</kbd> для поиска моделей',
'model.form.ollamatips': 'model.form.ollamatips':
'Подсказка: ниже представлены предустановленные модели Ollama в GPUStack. Выберите нужную или введите модель для развертывания в поле 【{name}】 справа.', 'Подсказка: ниже представлены предустановленные модели Ollama в MesaStack. Выберите нужную или введите модель для развертывания в поле 【{name}】 справа.',
'models.sort.name': 'По имени', 'models.sort.name': 'По имени',
'models.sort.size': 'По размеру', 'models.sort.size': 'По размеру',
'models.sort.likes': 'По лайкам', 'models.sort.likes': 'По лайкам',
@@ -89,7 +89,7 @@ export default {
'models.form.filePath': 'Путь к модели', 'models.form.filePath': 'Путь к модели',
'models.form.backendVersion': 'Версия бэкенда', 'models.form.backendVersion': 'Версия бэкенда',
'models.form.backendVersion.tips': 'models.form.backendVersion.tips':
'Чтобы использовать желаемую версию {backend} {version}, система автоматически создаст виртуальную среду в онлайн-окружении для установки соответствующей версии. После обновления GPUStack версия бэкенда останется зафиксированной. {link}', 'Чтобы использовать желаемую версию {backend} {version}, система автоматически создаст виртуальную среду в онлайн-окружении для установки соответствующей версии. После обновления MesaStack версия бэкенда останется зафиксированной. {link}',
'models.form.gpuselector': 'Селектор GPU', 'models.form.gpuselector': 'Селектор GPU',
'models.form.backend.llamabox': 'models.form.backend.llamabox':
'Для моделей формата GGUF. Поддержка Linux, macOS и Windows.', 'Для моделей формата GGUF. Поддержка Linux, macOS и Windows.',
@@ -281,7 +281,7 @@ export default {
'models.form.backendVersions.tips': `Чтобы использовать больше версий, перейдите на страницу {link} и отредактируйте бэкенд для добавления версий.`, 'models.form.backendVersions.tips': `Чтобы использовать больше версий, перейдите на страницу {link} и отредактируйте бэкенд для добавления версий.`,
'models.catalog.nogpus.tips': 'models.catalog.nogpus.tips':
'В выбранном кластере нет доступных GPU, совместимых с этой моделью.', 'В выбранном кластере нет доступных GPU, совместимых с этой моделью.',
'models.form.modelfile.notfound': `Указанный путь к файлу модели не существует на сервере GPUStack. Рекомендуется размещать файл модели по одному и тому же пути как на сервере GPUStack, так и на воркерах GPUStack. Это поможет системе принимать лучшие решения по распределению ресурсов.`, 'models.form.modelfile.notfound': `Указанный путь к файлу модели не существует на сервере MesaStack. Рекомендуется размещать файл модели по одному и тому же пути как на сервере MesaStack, так и на воркерах MesaStack. Это поможет системе принимать лучшие решения по распределению ресурсов.`,
'models.form.readyWorkers': 'воркеров готово', 'models.form.readyWorkers': 'воркеров готово',
'models.form.maxContextLength': 'Maximum Context Length', 'models.form.maxContextLength': 'Maximum Context Length',
'models.form.backend.helperText': 'models.form.backend.helperText':
@@ -295,7 +295,7 @@ export default {
'models.instance.startHistory': 'Run History', 'models.instance.startHistory': 'Run History',
'models.instance.startHistory.tips': 'models.instance.startHistory.tips':
'Shows logs from the run before the last error-triggered restart.', 'Shows logs from the run before the last error-triggered restart.',
'models.form.lora.label': 'LoRA Adapter', 'models.form.lora.label': 'LoRA Adapters',
'models.form.lora.add': 'Add LoRA Adapter', 'models.form.lora.add': 'Add LoRA Adapter',
'models.form.lora.select': 'Select LoRA', 'models.form.lora.select': 'Select LoRA',
'models.form.lora.name': 'LoRA name', 'models.form.lora.name': 'LoRA name',
+4 -2
View File
@@ -37,9 +37,12 @@ export default {
'noresult.catalog.nofound': 'Подходящие модели не найдены.', 'noresult.catalog.nofound': 'Подходящие модели не найдены.',
'noresult.resources.cluster': 'noresult.resources.cluster':
'No clusters available. Add a cluster to get started.', 'No clusters available. Add a cluster to get started.',
'noresult.resources.k8sCluster':
'No clusters available. Add a Kubernetes cluster to get started.',
'noresult.resources.worker': 'noresult.resources.worker':
'No workers available. Add a worker to get started.', 'No workers available. Add a worker to get started.',
'noresult.resources.gotocluster': 'Create Your First Cluster', 'noresult.resources.gotocluster': 'Create Your First Cluster',
'noresult.resources.addk8scluster': 'Add a Kubernetes Cluster',
'noresult.resources.gotoworker': 'Add Worker', 'noresult.resources.gotoworker': 'Add Worker',
'noresult.benchmark.title': 'No Benchmarks', 'noresult.benchmark.title': 'No Benchmarks',
'noresult.benchmark.subTitle': 'No benchmarks have been added yet.', 'noresult.benchmark.subTitle': 'No benchmarks have been added yet.',
@@ -67,8 +70,7 @@ export default {
'noresult.gpuservice.storage.subTitle': 'Хранилища ещё не добавлены.', 'noresult.gpuservice.storage.subTitle': 'Хранилища ещё не добавлены.',
'noresult.gpuservice.storage.nofound': 'Подходящие хранилища не найдены.', 'noresult.gpuservice.storage.nofound': 'Подходящие хранилища не найдены.',
'noresult.gpuservice.storageType.title': 'Нет типов хранилищ', 'noresult.gpuservice.storageType.title': 'Нет типов хранилищ',
'noresult.gpuservice.storageType.subTitle': 'noresult.gpuservice.storageType.subTitle': 'Типы хранилищ ещё не добавлены.',
'Типы хранилищ ещё не добавлены.',
'noresult.gpuservice.storageType.nofound': 'noresult.gpuservice.storageType.nofound':
'Подходящие типы хранилищ не найдены.', 'Подходящие типы хранилищ не найдены.',
'noresult.gpuservice.sshkey.title': 'Нет открытых ключей SSH', 'noresult.gpuservice.sshkey.title': 'Нет открытых ключей SSH',
+15
View File
@@ -0,0 +1,15 @@
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 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',
'organizations.upsell.feature.members':
'Manage members and roles per organization',
'organizations.upsell.feature.quotas':
'Set resource and token quotas per organization',
'organizations.upsell.feature.isolation':
'Scope API keys, model deployments, and resources per organization',
'organizations.upsell.cta': 'Learn about Enterprise'
};
+4 -4
View File
@@ -51,7 +51,7 @@ export default {
'resources.worker.container.supported': 'Только для Linux.', 'resources.worker.container.supported': 'Только для Linux.',
'resources.worker.current.version': 'Текущая версия: {version}', 'resources.worker.current.version': 'Текущая версия: {version}',
'resources.worker.driver.install': 'resources.worker.driver.install':
'Установите <a href="https://docs.gpustack.ai/latest/installation/installation-requirements/" target="_blank">необходимые драйверы и библиотеки</a> перед установкой GPUStack.', // Translated 'Установите <a href="https://docs.gpustack.ai/latest/installation/installation-requirements/" target="_blank">необходимые драйверы и библиотеки</a> перед установкой MesaStack.', // Translated
'resources.worker.select.command': 'resources.worker.select.command':
'Выберите метку для генерации команды и скопируйте её.', 'Выберите метку для генерации команды и скопируйте её.',
'resources.worker.script.install': 'Установка скриптом', 'resources.worker.script.install': 'Установка скриптом',
@@ -87,7 +87,7 @@ export default {
'Вставьте <span class="bold-text">Токен</span>.', 'Вставьте <span class="bold-text">Токен</span>.',
'resources.register.worker.step7': 'resources.register.worker.step7':
'Нажмите <span class="bold-text">Перезапуск</span> для применения настроек.', 'Нажмите <span class="bold-text">Перезапуск</span> для применения настроек.',
'resources.register.install.title': 'Установка GPUStack на {os}', 'resources.register.install.title': 'Установка MesaStack на {os}',
'resources.register.download': 'resources.register.download':
'Скачайте и установите <a href={url} target="_blank">инсталлятор</a>. Поддерживаемые версии: {versions}.', 'Скачайте и установите <a href={url} target="_blank">инсталлятор</a>. Поддерживаемые версии: {versions}.',
'resource.register.maos.support': 'Apple Silicon (серия M), macOS 14+', 'resource.register.maos.support': 'Apple Silicon (серия M), macOS 14+',
@@ -110,7 +110,7 @@ export default {
'No available clusters. Please create a cluster before adding a node.', 'No available clusters. Please create a cluster before adding a node.',
'resources.metrics.details': 'Monitoring', 'resources.metrics.details': 'Monitoring',
'resoureces.worker.upgrade.tips': '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.worker.version': 'Worker Version: {version}',
'resources.server.version': 'Server Version: {version}', 'resources.server.version': 'Server Version: {version}',
'resources.worker.currentVersion': 'Current Version: {version}', 'resources.worker.currentVersion': 'Current Version: {version}',
@@ -127,5 +127,5 @@ export default {
// 7. 'resources.worker.maintenance.tips': 'When maintenance mode is enabled, the node will stop scheduling new model deployment tasks. Running instances will not be affected.', // 7. 'resources.worker.maintenance.tips': 'When maintenance mode is enabled, the node will stop scheduling new model deployment tasks. Running instances will not be affected.',
// 8. 'resources.worker.noCluster.tips': 'No available clusters. Please create a cluster before adding a node.', // 8. 'resources.worker.noCluster.tips': 'No available clusters. Please create a cluster before adding a node.',
// 9. 'resources.metrics.details': 'Monitoring', // 9. 'resources.metrics.details': 'Monitoring',
// 10. 'resoureces.worker.upgrade.tips': 'Please upgrade to match the GPUStack Server version.' // 10. 'resoureces.worker.upgrade.tips': 'Please upgrade to match the MesaStack Server version.'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+72 -1
View File
@@ -17,9 +17,15 @@ export default {
'usage.table.user.apiKeysUsed': 'API Keys Used', 'usage.table.user.apiKeysUsed': 'API Keys Used',
'usage.table.lastActive': 'Last Active', 'usage.table.lastActive': 'Last Active',
'usage.filter.granularity': 'Granularity', 'usage.filter.granularity': 'Granularity',
'usage.filter.granularity.hour': 'Час',
'usage.filter.granularity.day': 'Day', 'usage.filter.granularity.day': 'Day',
'usage.filter.granularity.week': 'Week', 'usage.filter.granularity.week': 'Week',
'usage.filter.granularity.month': 'Month', 'usage.filter.granularity.month': 'Month',
'usage.tabs.summary': 'Обзор',
'usage.tabs.tokens': 'Токены',
'usage.tabs.gpuInstances': 'GPU-инстансы',
'usage.tabs.storage': 'Хранилище',
'usage.tabs.resourceEvents': 'События ресурсов',
'usage.tabs.models': 'Models', 'usage.tabs.models': 'Models',
'usage.tabs.apikeys': 'API Keys', 'usage.tabs.apikeys': 'API Keys',
'usage.tabs.users': 'User', 'usage.tabs.users': 'User',
@@ -32,5 +38,70 @@ export default {
'usage.chart.cached': 'Cached', 'usage.chart.cached': 'Cached',
'usage.chart.uncached': 'Uncached', 'usage.chart.uncached': 'Uncached',
'usage.chart.inputTokensCached': 'Input Tokens (Cached/Uncached)', 'usage.chart.inputTokensCached': 'Input Tokens (Cached/Uncached)',
'usage.table.inputTokensCached': 'Input Tokens Cached' 'usage.table.inputTokensCached': 'Input Tokens Cached',
// --- Resource usage: shared metrics & units ---
'usage.metric.tokens': 'Токены',
'usage.metric.input': 'Вход',
'usage.metric.output': 'Выход',
'usage.metric.gpuHours': 'GPU-часы',
'usage.metric.instanceHours': 'Часы инстансов',
'usage.metric.gbDays': 'ГБ-дни',
'usage.metric.gbHours': 'ГБ-часы',
'usage.metric.activeUsers': 'Активные пользователи',
'usage.metric.activeInstances': 'Активные инстансы',
'usage.metric.activeStorage': 'Активное хранилище',
'usage.metric.activeVolumes': 'Активные тома',
'usage.metric.storageTypes': 'Типы хранилищ',
'usage.metric.gpuHours.tip':
'Время работы инстанса, взвешенное по количеству GPU: инстанс с N GPU, работающий H часов, считается как N × H GPU-часов. Равно часам инстансов, когда каждый инстанс использует один GPU.',
'usage.metric.instanceHours.tip':
'Суммарное время работы всех инстансов независимо от количества используемых GPU. Один инстанс, работающий 2 часа = 2 часа инстанса.',
'usage.metric.gbDays.tip':
'Ёмкость хранилища, проинтегрированная по времени, в ГБ × дни: 10 ГБ в течение 5 дней = 50 ГБ-дней. (= ГБ-часы ÷ 24)',
'usage.metric.gbHours.tip':
'Ёмкость хранилища, проинтегрированная по времени, в ГБ × часы: 10 ГБ в течение 5 часов = 50 ГБ-часов.',
// --- Resource usage: common table / labels ---
'usage.common.noData': 'Нет данных',
'usage.common.unknown': 'неизвестно',
'usage.table.date': 'Дата',
'usage.table.name': 'Имя',
'usage.table.user': 'Пользователь',
'usage.table.users': 'Пользователи',
'usage.table.type': 'Тип',
'usage.table.instance': 'Инстанс',
'usage.table.instanceType': 'Тип инстанса',
'usage.table.instanceTypes': 'Типы инстансов',
'usage.table.instances': 'Инстансы',
'usage.table.capacity': 'Ёмкость',
'usage.export.tableNamed': 'Экспорт данных таблицы — {name}',
// --- Summary tab ---
'usage.summary.compute': 'Вычисления',
'usage.summary.tokensOverTime': 'Токены по времени',
'usage.summary.gpuHoursOverTime': 'GPU-часы по времени',
'usage.summary.gbDaysOverTime': 'ГБ-дни по времени',
// --- GPU Instances / Storage filters ---
'usage.filter.instance': 'Фильтр по инстансу',
'usage.filter.storage': 'Фильтр по хранилищу',
// --- Resource events ---
'usage.events.resourceType': 'Тип ресурса',
'usage.events.eventType': 'Тип события',
'usage.events.resourceName': 'Фильтр по имени',
'usage.events.col.time': 'Время',
'usage.events.col.resource': 'Ресурс',
'usage.events.col.event': 'Событие',
'usage.events.col.message': 'Сообщение',
'usage.events.resource.gpuInstance': 'GPU-инстанс',
'usage.events.resource.cpuInstance': 'CPU-инстанс',
'usage.events.type.created': 'Создан',
'usage.events.type.deleted': 'Удалён',
'usage.events.type.started': 'Запущен',
'usage.events.type.stopped': 'Остановлен',
'usage.events.type.updated': 'Обновлён',
'usage.events.type.attached': 'Подключён',
'usage.events.type.detached': 'Отключён'
}; };
+4 -2
View File
@@ -29,12 +29,14 @@ export default {
'users.password.modify.title': 'Смена пароля', 'users.password.modify.title': 'Смена пароля',
'users.password.modify.description': 'users.password.modify.description':
'В целях безопасности измените первоначальный пароль.', 'В целях безопасности измените первоначальный пароль.',
'users.password.modify.tips':
'Регулярное обновление пароля помогает защитить вашу учётную запись.',
'users.password.confirm': 'Подтвердите новый пароль', 'users.password.confirm': 'Подтвердите новый пароль',
'users.password.confirm.empty': 'Подтвердите новый пароль', 'users.password.confirm.empty': 'Подтвердите новый пароль',
'users.password.confirm.error': 'Пароли не совпадают', 'users.password.confirm.error': 'Пароли не совпадают',
'users.login.title': 'Вход в', 'users.login.title': 'Вход в',
'users.version.islatest': 'GPUStack {version} — последняя версия', 'users.version.islatest': 'MesaStack {version} — последняя версия',
'users.version.update': 'Доступно обновление GPUStack {version}', 'users.version.update': 'Доступно обновление MesaStack {version}',
'users.settings.title': 'Настройки пользователя', 'users.settings.title': 'Настройки пользователя',
'users.status.activate': 'Активировать аккаунт', 'users.status.activate': 'Активировать аккаунт',
'users.status.deactivate': 'Деактивировать аккаунт', 'users.status.deactivate': 'Деактивировать аккаунт',
+1 -1
View File
@@ -4,7 +4,7 @@ export default {
'vendor.hygon': 'Hygon', 'vendor.hygon': 'Hygon',
'vendor.moorthreads': 'Moore Threads', 'vendor.moorthreads': 'Moore Threads',
'vendor.iluvatar': 'Iluvatar', 'vendor.iluvatar': 'Iluvatar',
'vendor.metax': 'Metax', 'vendor.metax': 'MetaX',
'vendor.cambricon': 'Cambricon', 'vendor.cambricon': 'Cambricon',
'vendor.thead': 'T-Head PPU' 'vendor.thead': 'T-Head PPU'
}; };

Some files were not shown because too many files have changed in this diff Show More