Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17527b0fa5 |
+2
-1
@@ -3,6 +3,7 @@ node_modules
|
||||
.umi-production
|
||||
public/static/*.js
|
||||
public/static/*.css
|
||||
src/components/iconfont/
|
||||
src/components/icon-font/iconfont/iconfont.js
|
||||
src/components/icon-font/iconfont/*.css
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -3,5 +3,5 @@ module.exports = {
|
||||
rules: {
|
||||
'selector-class-pattern': null
|
||||
},
|
||||
ignoreFiles: ['public/static/*.css', 'src/components/iconfont/iconfont.css']
|
||||
ignoreFiles: ['public/static/*.css']
|
||||
};
|
||||
|
||||
+62
-85
@@ -140,6 +140,15 @@ const baseRoutes = [
|
||||
access: 'canSeeOrgAdmin',
|
||||
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',
|
||||
path: '/models/providers',
|
||||
@@ -170,26 +179,6 @@ const baseRoutes = [
|
||||
access: 'canSeeOrgAdmin',
|
||||
hideInMenu: true,
|
||||
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'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -197,7 +186,6 @@ const baseRoutes = [
|
||||
name: 'gpuService',
|
||||
path: '/gpu-service',
|
||||
key: 'gpuService',
|
||||
access: 'canSeeGpuService',
|
||||
routes: [
|
||||
{
|
||||
path: '/gpu-service',
|
||||
@@ -235,12 +223,7 @@ const baseRoutes = [
|
||||
path: '/gpu-service/storage-types',
|
||||
key: 'gpuServiceStorageTypes',
|
||||
icon: 'icon-storage-outlined',
|
||||
// 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',
|
||||
access: 'canSeeAdmin',
|
||||
selectedIcon: 'icon-storage-filled',
|
||||
defaultIcon: 'icon-storage-outlined',
|
||||
component: './gpu-service/storage-types'
|
||||
@@ -266,16 +249,6 @@ const baseRoutes = [
|
||||
path: '/resources',
|
||||
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',
|
||||
path: '/resources/workers',
|
||||
@@ -295,49 +268,67 @@ const baseRoutes = [
|
||||
component: './resources/components/gpus'
|
||||
},
|
||||
{
|
||||
name: 'credentials',
|
||||
path: '/resources/credentials',
|
||||
key: 'credentials',
|
||||
icon: 'icon-credential-outline',
|
||||
selectedIcon: 'icon-credential-filled',
|
||||
defaultIcon: 'icon-credential-outline',
|
||||
component: './cluster-management/credentials'
|
||||
name: 'backendsList',
|
||||
path: '/resources/backends',
|
||||
key: 'backendsList',
|
||||
icon: 'icon-backend',
|
||||
selectedIcon: 'icon-backend-filled',
|
||||
defaultIcon: 'icon-backend',
|
||||
access: 'canSeeOrgAdmin',
|
||||
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',
|
||||
path: '/resources/clusters/detail',
|
||||
path: '/cluster-management/clusters/detail',
|
||||
key: 'clusterDetail',
|
||||
icon: 'icon-cluster2-outline',
|
||||
selectedIcon: 'icon-cluster2-filled',
|
||||
defaultIcon: 'icon-cluster2-outline',
|
||||
hideInMenu: true,
|
||||
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: 'usage',
|
||||
path: '/usage/overview',
|
||||
key: 'usage',
|
||||
icon: 'icon-usage-outlined',
|
||||
selectedIcon: 'icon-usage-filled',
|
||||
defaultIcon: 'icon-usage-outlined',
|
||||
component: './usage/index'
|
||||
name: 'credentials',
|
||||
path: '/cluster-management/credentials',
|
||||
key: 'credentials',
|
||||
icon: 'icon-credential-outline',
|
||||
selectedIcon: 'icon-credential-filled',
|
||||
defaultIcon: 'icon-credential-outline',
|
||||
component: './cluster-management/credentials'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -350,20 +341,6 @@ const baseRoutes = [
|
||||
path: '/access-control',
|
||||
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',
|
||||
path: '/access-control/users',
|
||||
@@ -397,8 +374,8 @@ const baseRoutes = [
|
||||
},
|
||||
{
|
||||
name: 'profile',
|
||||
path: '/preferences',
|
||||
key: 'preferences',
|
||||
path: '/profile',
|
||||
key: 'profile',
|
||||
hideInMenu: true,
|
||||
component: './profile',
|
||||
icon: 'User'
|
||||
|
||||
+10
-24
@@ -1,27 +1,13 @@
|
||||
import { execSync } from 'child_process';
|
||||
const child_process = require('child_process');
|
||||
|
||||
export const getBranchInfo = () => {
|
||||
// git may be absent (source archive, bare container) or this tree may
|
||||
// not be a git checkout. Swallow the failure and fall back to the env
|
||||
// overrides below — losing build info shouldn't fail the build.
|
||||
let latestCommit = '';
|
||||
let versionTag = '';
|
||||
try {
|
||||
latestCommit = execSync('git rev-parse HEAD').toString().trim();
|
||||
versionTag = execSync(`git tag --contains ${latestCommit}`)
|
||||
.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)
|
||||
};
|
||||
const latestCommit = child_process
|
||||
.execSync('git rev-parse HEAD')
|
||||
.toString()
|
||||
.trim();
|
||||
const versionTag = child_process
|
||||
.execSync(`git tag --contains ${latestCommit}`)
|
||||
.toString()
|
||||
.trim();
|
||||
return { version: versionTag || '', commitId: latestCommit.slice(0, 7) };
|
||||
};
|
||||
|
||||
+1
-2
@@ -14,8 +14,7 @@ export default defineConfig([
|
||||
'dist',
|
||||
'src/.umi/',
|
||||
'src/.umi-production/',
|
||||
'src/.umi-test/',
|
||||
'src/components/iconfont/'
|
||||
'src/.umi-test/'
|
||||
]),
|
||||
{
|
||||
files: ['**/*.{ts,tsx,js,jsx}'],
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
"@ant-design/pro-components": "3.1.0-0",
|
||||
"@antv/g6": "^5.0.51",
|
||||
"@braintree/sanitize-url": "^7.1.1",
|
||||
"@gpustack/core-ui": "^1.0.22",
|
||||
"@gpustack/core-ui": "^1.0.16",
|
||||
"@huggingface/gguf": "^0.1.7",
|
||||
"@huggingface/hub": "^0.15.1",
|
||||
"@huggingface/tasks": "^0.11.6",
|
||||
|
||||
Generated
+7
-7
@@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^7.1.1
|
||||
version: 7.1.2
|
||||
'@gpustack/core-ui':
|
||||
specifier: ^1.0.22
|
||||
version: 1.0.22(czdvzceysqw7iv6pct2ucnb23e)
|
||||
specifier: ^1.0.16
|
||||
version: 1.0.16(czdvzceysqw7iv6pct2ucnb23e)
|
||||
'@huggingface/gguf':
|
||||
specifier: ^0.1.7
|
||||
version: 0.1.18
|
||||
@@ -1484,8 +1484,8 @@ packages:
|
||||
resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz}
|
||||
deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package
|
||||
|
||||
'@gpustack/core-ui@1.0.22':
|
||||
resolution: {integrity: sha512-uFLGalziwuojjMIeXMlCZn3tLRGA6PCa0JxLxa/+kKP+xAsy7Fcxgh4PerbTyk+P+hKnYv8cD+DPpkObDGqJgg==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.22.tgz}
|
||||
'@gpustack/core-ui@1.0.16':
|
||||
resolution: {integrity: sha512-wFKDv7X0FXRmAmZPt4WKYV0+aGHcx82/3EZSJkrut/49XQd5XdrqKaimGtlFWFCNEWqsouwzs4uqaC7JFqfOkQ==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.16.tgz}
|
||||
peerDependencies:
|
||||
'@ant-design/icons': '>=6.0.0'
|
||||
'@ant-design/pro-components': 3.1.0-0
|
||||
@@ -10808,7 +10808,7 @@ snapshots:
|
||||
|
||||
'@formatjs/intl-utils@2.3.0': {}
|
||||
|
||||
'@gpustack/core-ui@1.0.22(czdvzceysqw7iv6pct2ucnb23e)':
|
||||
'@gpustack/core-ui@1.0.16(czdvzceysqw7iv6pct2ucnb23e)':
|
||||
dependencies:
|
||||
'@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@ant-design/pro-components': 3.1.0-0(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -11829,7 +11829,7 @@ snapshots:
|
||||
|
||||
'@types/history@5.0.0':
|
||||
dependencies:
|
||||
history: 5.3.0
|
||||
history: 4.10.1
|
||||
|
||||
'@types/hoist-non-react-statics@3.3.7(@types/react@18.3.28)':
|
||||
dependencies:
|
||||
@@ -11915,7 +11915,7 @@ snapshots:
|
||||
'@types/history': 4.7.11
|
||||
'@types/react': 18.3.29
|
||||
'@types/react-router': 5.1.20
|
||||
redux: 4.2.1
|
||||
redux: 3.7.2
|
||||
|
||||
'@types/react-router@5.1.20':
|
||||
dependencies:
|
||||
|
||||
+1
-22
@@ -1,10 +1,6 @@
|
||||
import { applyAccessExtensions } from './access.extensions';
|
||||
|
||||
export default (initialState: {
|
||||
currentUser?: Global.UserInfo;
|
||||
hasKubernetesCluster?: boolean;
|
||||
hasResourceEvents?: boolean;
|
||||
}) => {
|
||||
export default (initialState: { currentUser?: Global.UserInfo }) => {
|
||||
const isPlatformAdmin = !!(
|
||||
initialState &&
|
||||
initialState.currentUser &&
|
||||
@@ -15,16 +11,6 @@ export default (initialState: {
|
||||
initialState.currentUser &&
|
||||
!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:
|
||||
// * `canSeeAdmin` — strictly platform admin (`users.is_admin`).
|
||||
@@ -32,11 +18,6 @@ export default (initialState: {
|
||||
// * `canSeeOrgAdmin` — admin-style menus that work cross-org
|
||||
// (Dashboard, Resources, Models, Cluster Management). Defaults
|
||||
// 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
|
||||
// specific org context (member / group management). Defaults to
|
||||
// `false`; extensions widen when both an org is selected AND
|
||||
@@ -46,8 +27,6 @@ export default (initialState: {
|
||||
return applyAccessExtensions({
|
||||
canSeeAdmin: isPlatformAdmin,
|
||||
canSeeOrgAdmin: isPlatformAdmin,
|
||||
canSeeGpuService:
|
||||
isPlatformAdmin || hasKubernetesCluster !== false || hasResourceEvents,
|
||||
canManageCurrentOrg: false,
|
||||
canSeeUser,
|
||||
canDelete: true,
|
||||
|
||||
+3
-123
@@ -1,12 +1,8 @@
|
||||
import { userSettingsHelperAtom } from '@/atoms/settings';
|
||||
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
|
||||
import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user';
|
||||
import { setAtomStorage } from '@/atoms/utils';
|
||||
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
|
||||
import { COLOR_PRIMARY } from '@/config/theme/constants';
|
||||
import { queryClusterList } from '@/pages/cluster-management/apis';
|
||||
import { ProviderValueMap } from '@/pages/cluster-management/config';
|
||||
import { queryResourceEvents } from '@/pages/usage/apis/resource';
|
||||
import { getGPUStackPlugin } from '@/plugins';
|
||||
import { enterprisePluginReady } from '@/plugins/enterprise-ready';
|
||||
import { GPUStackPluginManager } from '@/plugins/manager';
|
||||
import { requestConfig } from '@/request-config';
|
||||
@@ -17,7 +13,6 @@ import {
|
||||
} from '@/services/profile/apis';
|
||||
import { fetchSystemConfig } from '@/services/system/query-system-config';
|
||||
import { isOnline } from '@/utils';
|
||||
import { installTenantFetch } from '@/utils/install-fetch';
|
||||
import {
|
||||
IS_FIRST_LOGIN,
|
||||
readState,
|
||||
@@ -27,8 +22,6 @@ import '@gpustack/core-ui/style.css';
|
||||
import { RequestConfig, history, request as umiRequest } from '@umijs/max';
|
||||
import { message } from 'antd';
|
||||
|
||||
installTenantFetch();
|
||||
|
||||
// only for the first login and access from http://localhost
|
||||
|
||||
const checkDefaultPage = async (userInfo: any) => {
|
||||
@@ -41,87 +34,11 @@ const checkDefaultPage = async (userInfo: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Probes the caller's cluster list once so access predicates can gate
|
||||
// GPU Service (Kubernetes-only). Cheap (one list request) and never
|
||||
// blocks login — any failure just falls back to `undefined`, which
|
||||
// the predicate treats as "unknown / don't restrict beyond role".
|
||||
// The result is also mirrored into sessionStorage so access extensions
|
||||
// that run without the initialState argument can read it (e.g. to
|
||||
// override the admin shortcut in scopes where the menu shouldn't
|
||||
// show even for admins).
|
||||
const HAS_K8S_CLUSTER_KEY = 'hasKubernetesCluster';
|
||||
const probeHasKubernetesCluster = async (): Promise<boolean | undefined> => {
|
||||
try {
|
||||
const res = await queryClusterList(
|
||||
{ page: -1 },
|
||||
{
|
||||
skipErrorHandler: true
|
||||
}
|
||||
);
|
||||
const value = (res?.items ?? []).some(
|
||||
(c) => c?.provider === ProviderValueMap.Kubernetes
|
||||
);
|
||||
try {
|
||||
window.sessionStorage.setItem(HAS_K8S_CLUSTER_KEY, JSON.stringify(value));
|
||||
} catch {
|
||||
// sessionStorage may be unavailable (Safari private mode); the
|
||||
// access predicate already handles a missing value as "unknown".
|
||||
}
|
||||
return value;
|
||||
} catch (error) {
|
||||
console.error('probeHasKubernetesCluster error', error);
|
||||
try {
|
||||
window.sessionStorage.removeItem(HAS_K8S_CLUSTER_KEY);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// Probes whether the caller has ANY resource-usage events (GPU/CPU instance or
|
||||
// storage lifecycle). Used alongside the cluster probe so a user who has run
|
||||
// GPU instances still sees GPU Service / the full Usage page even if they
|
||||
// currently have no Kubernetes cluster. Mirrored into sessionStorage for the
|
||||
// access extensions; any failure → undefined ("unknown — don't restrict").
|
||||
const HAS_RESOURCE_EVENTS_KEY = 'hasResourceEvents';
|
||||
const probeHasResourceEvents = async (): Promise<boolean | undefined> => {
|
||||
try {
|
||||
// No date range = "ever"; scope is clamped to the caller server-side.
|
||||
const res = await queryResourceEvents(
|
||||
{ perPage: 1 },
|
||||
{
|
||||
skipErrorHandler: true
|
||||
}
|
||||
);
|
||||
const value = (res?.pagination?.total ?? 0) > 0;
|
||||
try {
|
||||
window.sessionStorage.setItem(
|
||||
HAS_RESOURCE_EVENTS_KEY,
|
||||
JSON.stringify(value)
|
||||
);
|
||||
} catch {
|
||||
// sessionStorage may be unavailable; predicate treats missing as unknown.
|
||||
}
|
||||
return value;
|
||||
} catch (error) {
|
||||
console.error('probeHasResourceEvents error', error);
|
||||
try {
|
||||
window.sessionStorage.removeItem(HAS_RESOURCE_EVENTS_KEY);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// runtime configuration
|
||||
export async function getInitialState(): Promise<{
|
||||
fetchUserInfo: () => Promise<Global.UserInfo>;
|
||||
currentUser?: Global.UserInfo;
|
||||
pluginData?: Record<string, any>;
|
||||
hasKubernetesCluster?: boolean;
|
||||
hasResourceEvents?: boolean;
|
||||
}> {
|
||||
const { location } = history;
|
||||
|
||||
@@ -166,36 +83,6 @@ export async function getInitialState(): Promise<{
|
||||
getUpdateCheck();
|
||||
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;
|
||||
} catch (error: any) {
|
||||
const data = error?.response?.data;
|
||||
@@ -235,19 +122,12 @@ export async function getInitialState(): Promise<{
|
||||
getAppVersionInfo();
|
||||
|
||||
if (![DEFAULT_ENTER_PAGE.login].includes(location.pathname)) {
|
||||
const [userInfo, hasKubernetesCluster, hasResourceEvents] =
|
||||
await Promise.all([
|
||||
fetchUserInfo(),
|
||||
probeHasKubernetesCluster(),
|
||||
probeHasResourceEvents()
|
||||
]);
|
||||
const userInfo = await fetchUserInfo();
|
||||
checkDefaultPage(userInfo);
|
||||
return {
|
||||
fetchUserInfo,
|
||||
currentUser: userInfo,
|
||||
pluginData,
|
||||
hasKubernetesCluster,
|
||||
hasResourceEvents
|
||||
pluginData
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
.ant-layout-sider-children {
|
||||
border-inline: none;
|
||||
border-radius: 0;
|
||||
padding-inline-end: 0;
|
||||
padding-block-end: 8px;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,12 +63,6 @@ export const fromClusterCreationAtom = atom(false);
|
||||
export const clusterSessionAtom = atom<{
|
||||
firstAddWorker: boolean;
|
||||
firstAddCluster: boolean;
|
||||
// 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);
|
||||
|
||||
export const clusterDetailAtom = atom<ClusterListItem | null>(null);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
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
|
||||
});
|
||||
+6
-49
@@ -3,15 +3,6 @@ import { atomWithStorage } from 'jotai/utils';
|
||||
|
||||
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<{
|
||||
version: string;
|
||||
git_commit: string;
|
||||
@@ -90,57 +81,23 @@ const getStoredCurrentOrgId = (): number | null => {
|
||||
// cluster-owner fallback.
|
||||
const ORG_CACHE_KEYS = ['organizationList', 'allOrganizations'] as const;
|
||||
|
||||
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 => {
|
||||
const lookupOrgNamespace = (id: number | null): string | 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);
|
||||
for (const key of ORG_CACHE_KEYS) {
|
||||
try {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (!raw) continue;
|
||||
const list = JSON.parse(raw) as CachedOrg[];
|
||||
const list = JSON.parse(raw) as Array<{ id: number; name?: string }>;
|
||||
if (!Array.isArray(list)) continue;
|
||||
const match = list.find((item) => String(item?.id) === target);
|
||||
if (match) return match;
|
||||
if (match?.name) return `gpustack-${match.name}`;
|
||||
} catch {
|
||||
// ignore malformed cache; continue checking other keys
|
||||
}
|
||||
}
|
||||
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());
|
||||
};
|
||||
|
||||
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.
Vendored
-6
@@ -92,12 +92,6 @@ declare namespace Global {
|
||||
interface InitialStateType {
|
||||
fetchUserInfo: () => Promise<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 };
|
||||
|
||||
@@ -54,8 +54,7 @@ export default {
|
||||
itemHoverColor: 'rgba(0,0,0,1)',
|
||||
itemColor: 'rgba(0,0,0,1)',
|
||||
itemHoverBg: 'rgb(24 25 27)',
|
||||
itemActiveBg: 'rgb(24 25 27)',
|
||||
menuItemSelectedBg: '#292929'
|
||||
itemActiveBg: 'rgb(24 25 27)'
|
||||
},
|
||||
Progress: {
|
||||
lineBorderRadius: 4
|
||||
@@ -109,7 +108,6 @@ export default {
|
||||
fontSize: 14,
|
||||
motion: true,
|
||||
colorFill: '#0A0A0A',
|
||||
colorBgBase: '#0A0A0A',
|
||||
menuItemSelectedBg: '#292929'
|
||||
colorBgBase: '#0A0A0A'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -56,8 +56,7 @@ export default {
|
||||
itemHoverColor: 'rgba(0,0,0,1)',
|
||||
itemColor: 'rgba(0,0,0,1)',
|
||||
itemHoverBg: 'rgba(0,0,0,0.04)',
|
||||
itemActiveBg: 'rgba(0,0,0,0.04)',
|
||||
menuItemSelectedBg: '#e8eaed'
|
||||
itemActiveBg: 'rgba(0,0,0,0.04)'
|
||||
},
|
||||
Progress: {
|
||||
lineBorderRadius: 3
|
||||
|
||||
+37
-17
@@ -171,6 +171,8 @@ body {
|
||||
}
|
||||
|
||||
.ant-table .ant-table-container table {
|
||||
// border-spacing: 0 20px;
|
||||
|
||||
.ant-table-thead th.ant-table-column-sort {
|
||||
background-color: transparent;
|
||||
|
||||
@@ -239,8 +241,8 @@ body {
|
||||
height: 100%;
|
||||
|
||||
.ant-pro-sider-footer {
|
||||
padding-block: 4px 0;
|
||||
padding-left: 8px;
|
||||
padding-block: 2px 0;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.ant-pro-sider-actions-list-item {
|
||||
@@ -257,27 +259,46 @@ body {
|
||||
border-block-end: none;
|
||||
}
|
||||
|
||||
.ant-pro-sider-logo {
|
||||
.collapse-btn {
|
||||
color: var(--ant-color-text-tertiary);
|
||||
:hover {
|
||||
color: var(--ant-color-text);
|
||||
.ant-pro-sider-logo-collapsed {
|
||||
padding-left: 12px;
|
||||
cursor: e-resize;
|
||||
|
||||
.collapse-wrap {
|
||||
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-logo-collapsed {
|
||||
padding-left: 6px;
|
||||
.ant-pro-sider .ant-layout-sider-children {
|
||||
// border-right: 1px solid var(--ant-color-split);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table .ant-table-tbody {
|
||||
.ant-table-row {
|
||||
border-radius: var(--table-td-radius);
|
||||
.ant-table-content table {
|
||||
.ant-table-tbody {
|
||||
.ant-table-row {
|
||||
border-radius: var(--table-td-radius);
|
||||
|
||||
> td {
|
||||
background-color: unset !important;
|
||||
border-bottom: 1px solid var(--ant-color-split);
|
||||
> td {
|
||||
background-color: unset;
|
||||
border-bottom: 1px solid var(--ant-color-split);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,6 +347,7 @@ body {
|
||||
.ant-pro-layout-container {
|
||||
overflow-x: auto;
|
||||
min-height: 100vh;
|
||||
// background-color: var(--ant-color-bg-container);
|
||||
}
|
||||
|
||||
.ant-pro-sider {
|
||||
@@ -790,8 +812,6 @@ body {
|
||||
}
|
||||
|
||||
.ant-pro-sider-logo + div {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ export default function useTableFetch<T>(
|
||||
key?: (typeof PaginationKey)[keyof typeof PaginationKey];
|
||||
fetchAPI: (params: any, options?: any) => Promise<Global.PageResponse<T>>;
|
||||
deleteAPI?: (id: number, params?: any) => Promise<any>;
|
||||
afterDelete?: (id?: number | number[]) => void;
|
||||
contentForDelete?: string;
|
||||
defaultData?: any[];
|
||||
events?: EventsType[];
|
||||
@@ -49,7 +48,6 @@ export default function useTableFetch<T>(
|
||||
const {
|
||||
fetchAPI,
|
||||
deleteAPI,
|
||||
afterDelete,
|
||||
contentForDelete,
|
||||
API,
|
||||
polling = false,
|
||||
@@ -365,7 +363,7 @@ export default function useTableFetch<T>(
|
||||
|
||||
// remove the deleted id from selected ids in row selection
|
||||
rowSelection.removeSelectedKeys([row.id]);
|
||||
afterDelete?.(row.id);
|
||||
|
||||
// ======== to avoid fetch data twice, because of debounceFetchData has been run =======
|
||||
if (!updateManually) {
|
||||
fetchData();
|
||||
@@ -392,7 +390,6 @@ export default function useTableFetch<T>(
|
||||
successIds.push(id);
|
||||
}
|
||||
);
|
||||
afterDelete?.(successIds);
|
||||
rowSelection.removeSelectedKeys(successIds);
|
||||
fetchData();
|
||||
return res;
|
||||
|
||||
@@ -14,6 +14,7 @@ type EventsType = 'CREATE' | 'UPDATE' | 'DELETE' | 'INSERT';
|
||||
export function useUpdateChunkedList(options: {
|
||||
events?: EventsType[];
|
||||
dataList?: any[];
|
||||
triggerAt?: React.MutableRefObject<number>;
|
||||
limit?: number;
|
||||
onCreate?: (args: any) => void;
|
||||
onUpdate?: (args: any) => void;
|
||||
@@ -23,9 +24,9 @@ export function useUpdateChunkedList(options: {
|
||||
filterFun?: (args: any) => boolean;
|
||||
mapFun?: (args: any) => any;
|
||||
computedID?: (d: object) => string;
|
||||
isNewItem?: (item: any) => boolean;
|
||||
}) {
|
||||
const { events = ['CREATE', 'DELETE', 'UPDATE', 'INSERT'] } = options;
|
||||
const { events = ['CREATE', 'DELETE', 'UPDATE', 'INSERT'], triggerAt } =
|
||||
options;
|
||||
const deletedIdsRef = useRef<Set<number | string>>(new Set());
|
||||
const cacheDataListRef = useRef<any[]>(options.dataList || []);
|
||||
const timerRef = useRef<any>(null);
|
||||
@@ -70,14 +71,17 @@ export function useUpdateChunkedList(options: {
|
||||
(sItem: any) => sItem.id === item.id
|
||||
);
|
||||
const updateItem = { ...item };
|
||||
if (updateIndex === -1) {
|
||||
if (updateIndex === -1 && !triggerAt?.current) {
|
||||
acc.push(updateItem);
|
||||
} else {
|
||||
} else if (!triggerAt?.current) {
|
||||
cacheDataListRef.current[updateIndex] = updateItem;
|
||||
}
|
||||
// only push items created after the watch started
|
||||
|
||||
if (options.isNewItem?.(item)) {
|
||||
// TODO: only push items created after triggerAt
|
||||
if (
|
||||
triggerAt?.current &&
|
||||
Date.parse(item.created_at) >= triggerAt.current
|
||||
) {
|
||||
latestCreateList.push(updateItem);
|
||||
}
|
||||
|
||||
@@ -99,8 +103,10 @@ export function useUpdateChunkedList(options: {
|
||||
cacheDataListRef.current = cacheDataListRef.current?.filter(
|
||||
(item: any) => {
|
||||
// collect deleted items
|
||||
if (ids?.includes(item.id) && !options.isNewItem?.(item)) {
|
||||
deletedList.push(item);
|
||||
if (triggerAt?.current) {
|
||||
if (ids?.includes(item.id)) {
|
||||
deletedList.push(item);
|
||||
}
|
||||
}
|
||||
return !ids?.includes(item.id);
|
||||
}
|
||||
@@ -127,8 +133,10 @@ export function useUpdateChunkedList(options: {
|
||||
updateItem,
|
||||
...cacheDataListRef.current.slice(0, limit - 1)
|
||||
];
|
||||
if (options.onUpdate && options.isNewItem?.(item)) {
|
||||
options.onUpdate?.([updateItem]);
|
||||
if (options.onUpdate && triggerAt?.current) {
|
||||
if (Date.parse(item.created_at) >= triggerAt.current) {
|
||||
options.onUpdate?.([updateItem]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -210,14 +210,14 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
||||
key: 'settings',
|
||||
label: (
|
||||
<span className="flex flex-center">
|
||||
<IconFont type="icon-preferences" />
|
||||
<IconFont type="icon-settings-02" />
|
||||
<span className="m-l-8" style={{ marginLeft: 8 }}>
|
||||
{intl?.formatMessage?.({ id: 'common.preferences' })}
|
||||
{intl?.formatMessage?.({ id: 'common.button.settings' })}
|
||||
</span>
|
||||
</span>
|
||||
),
|
||||
onClick: () => {
|
||||
history.push('/preferences');
|
||||
history.push('/profile');
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+37
-22
@@ -1,7 +1,6 @@
|
||||
import { routeCacheAtom, setRouteCache } from '@/atoms/route-cache';
|
||||
import { userAtom } from '@/atoms/user';
|
||||
import DarkMask from '@/components/dark-mask';
|
||||
import '@/components/iconfont/iconfont.js';
|
||||
import PluginExtraFields from '@/components/plugin-extra-fields';
|
||||
import routeCachekey from '@/config/route-cachekey';
|
||||
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
|
||||
@@ -21,7 +20,11 @@ import {
|
||||
import { useAccessMarkedRoutes } from '@@/plugin-access';
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import { ProLayout } from '@ant-design/pro-components';
|
||||
import { CoreUIProvider, IconFont } from '@gpustack/core-ui';
|
||||
import {
|
||||
CoreUIProvider,
|
||||
IconFont,
|
||||
useOverlayScroller
|
||||
} from '@gpustack/core-ui';
|
||||
import {
|
||||
Access,
|
||||
Outlet,
|
||||
@@ -68,9 +71,9 @@ const NO_CONTAINER_PAGES = [
|
||||
|
||||
const CHECK_RESOURCE_PATH = [
|
||||
'/resources/workers',
|
||||
'/resources/clusters/list',
|
||||
'/resources/credentials',
|
||||
'/resources/clusters/create'
|
||||
'/cluster-management/clusters/list',
|
||||
'/cluster-management/credentials',
|
||||
'/cluster-management/clusters/create'
|
||||
];
|
||||
|
||||
type NewRoute = IRoute & {
|
||||
@@ -131,6 +134,9 @@ const mapRoutes = (routes: IRoute[], role: string) => {
|
||||
};
|
||||
|
||||
export default (props: any) => {
|
||||
const { initialize: initialize } = useOverlayScroller({
|
||||
defer: false
|
||||
});
|
||||
const [, contextHolder] = Modal.useModal();
|
||||
const { themeData, setUserSettings, userSettings } = useUserSettings();
|
||||
const [userInfo] = useAtom(userAtom);
|
||||
@@ -234,7 +240,30 @@ export default (props: any) => {
|
||||
}, [userSettings.collapsed]);
|
||||
|
||||
const renderMenuHeader = (logo: React.ReactNode, title: React.ReactNode) => {
|
||||
return <>{logo}</>;
|
||||
return (
|
||||
<>
|
||||
{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) => {
|
||||
@@ -327,7 +356,7 @@ export default (props: any) => {
|
||||
config={{
|
||||
apiBaseUrl: GPUSTACK_API_BASE_URL,
|
||||
theme: userSettings.theme,
|
||||
iconUrl: '//at.alicdn.com/t/c/font_4613488_rwchdketjg8.js',
|
||||
iconUrl: '//at.alicdn.com/t/c/font_4613488_jmdepcs90im.js',
|
||||
isDarkTheme: userSettings.isDarkTheme,
|
||||
defaultColorPrimary: COLOR_PRIMARY
|
||||
}}
|
||||
@@ -378,23 +407,9 @@ export default (props: any) => {
|
||||
openKeys={false}
|
||||
disableMobile={true}
|
||||
siderWidth={220}
|
||||
menuFooterRender={() => (
|
||||
<Button
|
||||
style={{
|
||||
border: 'none'
|
||||
}}
|
||||
size="small"
|
||||
type={'text'}
|
||||
onClick={handleToggleCollapse}
|
||||
>
|
||||
<IconFont
|
||||
type={collapsed ? 'icon-expand-left' : 'icon-expand-right'}
|
||||
className="font-size-18"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
onCollapse={onCollapse}
|
||||
onMenuHeaderClick={onMenuHeaderClick}
|
||||
menuHeaderRender={renderMenuHeader}
|
||||
collapsed={userSettings.collapsed}
|
||||
onPageChange={onPageChange}
|
||||
formatMessage={formatMessage}
|
||||
|
||||
@@ -256,7 +256,7 @@ export const getRightRenderContent = (opts: {
|
||||
</span>
|
||||
),
|
||||
onClick: () => {
|
||||
history.push('/preferences');
|
||||
history.push('/profile');
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+139
-158
@@ -1,8 +1,8 @@
|
||||
import { CaretDownOutlined } from '@ant-design/icons';
|
||||
import { IconFont, OverlayScroller } from '@gpustack/core-ui';
|
||||
import { IconFont } from '@gpustack/core-ui';
|
||||
import { Link, useLocation } from '@umijs/max';
|
||||
import { Tooltip } from 'antd';
|
||||
import { createStyles, type FullToken } from 'antd-style';
|
||||
import { createStyles } from 'antd-style';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
interface MenuItem {
|
||||
@@ -20,125 +20,117 @@ interface SiderMenuProps {
|
||||
initialState: Global.InitialStateType;
|
||||
}
|
||||
|
||||
const useStyles = createStyles(
|
||||
({ css, token }: { css: any; token: FullToken }) => {
|
||||
console.log('useStyles', token);
|
||||
const useStyles = createStyles(({ css, token }) => {
|
||||
console.log('useStyles', token);
|
||||
|
||||
// @ts-ignore
|
||||
const { Menu } = token;
|
||||
// @ts-ignore
|
||||
const { Menu } = token;
|
||||
|
||||
return {
|
||||
siderMenu: css`
|
||||
width: 100%;
|
||||
&.sider-menu-collapsed {
|
||||
.menu-item {
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
.os-scrollbar-vertical .os-scrollbar-handle {
|
||||
min-width: 4px;
|
||||
max-width: 4px;
|
||||
}
|
||||
`,
|
||||
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);
|
||||
return {
|
||||
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 {
|
||||
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`
|
||||
}
|
||||
.anticon {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
.group-title-text {
|
||||
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;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
&:hover {
|
||||
background-color: ${Menu.itemHoverBg};
|
||||
color: ${Menu.itemHoverColor};
|
||||
}
|
||||
&.menu-item-selected {
|
||||
background-color: ${Menu.menuItemSelectedBg};
|
||||
color: ${Menu.itemSelectedColor};
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.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`
|
||||
&.menu-item-group-title-collapsed {
|
||||
position: relative;
|
||||
height: 1px;
|
||||
margin-block: 6px;
|
||||
background-color: ${token.colorSplit};
|
||||
position: absolute;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
`
|
||||
};
|
||||
}
|
||||
);
|
||||
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%;
|
||||
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 { menuData, collapsed } = props;
|
||||
@@ -227,55 +219,44 @@ const SiderMenu: React.FC<SiderMenuProps> = (props) => {
|
||||
'sider-menu-collapsed': collapsed
|
||||
})}
|
||||
>
|
||||
<OverlayScroller
|
||||
styles={{
|
||||
wrapper: {
|
||||
paddingInline: 0,
|
||||
maxHeight: '100%'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ paddingRight: 8 }}>
|
||||
{menuData.map((item: MenuItem, index: number) => (
|
||||
<div key={item.key}>
|
||||
{item.children && item.children.length > 0 ? (
|
||||
<>
|
||||
<div
|
||||
className={cx(styles.groupTitle, {
|
||||
'menu-item-group-title-collapsed': collapsed
|
||||
})}
|
||||
onClick={(e) => handleToggleGroup(e, item)}
|
||||
>
|
||||
{!collapsed ? (
|
||||
<span className="group-title-text">
|
||||
<span>{item.name}</span>
|
||||
<CaretDownOutlined
|
||||
rotate={collapseKeys.has(item.key) ? -90 : 0}
|
||||
></CaretDownOutlined>
|
||||
</span>
|
||||
) : (
|
||||
<span className={styles.line}></span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cx(styles.menuItemGroup, {
|
||||
'menu-item-group-collapsed': collapsed,
|
||||
'menu-item-group-hidden':
|
||||
!collapsed && collapseKeys.has(item.key)
|
||||
})}
|
||||
>
|
||||
{item.children?.map((child: MenuItem) =>
|
||||
menuItemRender(child, child.key)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
menuItemRender(item, item.key)
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{menuData.map((item: MenuItem, index: number) => (
|
||||
<div key={item.key}>
|
||||
{item.children && item.children.length > 0 ? (
|
||||
<>
|
||||
<div
|
||||
className={cx(styles.groupTitle, {
|
||||
'menu-item-group-title-collapsed': collapsed
|
||||
})}
|
||||
onClick={(e) => handleToggleGroup(e, item)}
|
||||
>
|
||||
{!collapsed ? (
|
||||
<span className="group-title-text">
|
||||
<span>{item.name}</span>
|
||||
<CaretDownOutlined
|
||||
rotate={collapseKeys.has(item.key) ? -90 : 0}
|
||||
></CaretDownOutlined>
|
||||
</span>
|
||||
) : (
|
||||
<span className={styles.line}></span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cx(styles.menuItemGroup, {
|
||||
'menu-item-group-collapsed': collapsed,
|
||||
'menu-item-group-hidden':
|
||||
!collapsed && collapseKeys.has(item.key)
|
||||
})}
|
||||
>
|
||||
{item.children?.map((child: MenuItem) =>
|
||||
menuItemRender(child, child.key)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
menuItemRender(item, item.key)
|
||||
)}
|
||||
</div>
|
||||
</OverlayScroller>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,7 +23,5 @@ export default {
|
||||
'apikeys.accessScope.inference': 'Inference APIs',
|
||||
'apikeys.access.permissions': 'Access Permissions',
|
||||
'apikeys.type.auto': 'Auto-generated',
|
||||
'apikeys.type.custom': 'Custom',
|
||||
'apikeys.button.ipConfig': 'IP Access Control',
|
||||
'quotaLimits.button.title': 'Quota Limit'
|
||||
'apikeys.type.custom': 'Custom'
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ export default {
|
||||
'clusters.workerpool.batchSize.desc':
|
||||
'Number of workers created simultaneously in the Worker pool',
|
||||
'clusters.create.addworker.tips':
|
||||
'Please make sure the <a href={link} target="_blank">prerequisites</a> are met before executing the following command.',
|
||||
'Please make sure the <a href={link} target="_blank">prerequisites</a> for {label} are met before executing the following command.',
|
||||
'clusters.create.addCommand.tips':
|
||||
'On the Worker that needs to be added, run the following command to join it to the cluster.',
|
||||
'clusters.create.addCommand.k8s.tips':
|
||||
@@ -70,6 +70,8 @@ export default {
|
||||
'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.multiTag': 'Multi-select',
|
||||
'clusters.addworker.selectGPU.singleOnly':
|
||||
'The selected vendor is not in this cluster’s GPU vendor overrides — single-select only. Pick a vendor configured in the overrides to enable multi-select.',
|
||||
'clusters.addworker.checkEnv': 'Check Environment',
|
||||
'clusters.addworker.specifyArgs': 'Specify Arguments',
|
||||
'clusters.addworker.runCommand': 'Run Command',
|
||||
@@ -110,10 +112,6 @@ export default {
|
||||
'{count} new workers have been added to the cluster.',
|
||||
'clusters.create.serverUrl': 'GPUStack Server URL',
|
||||
'clusters.create.workerConfig': 'Worker Configuration',
|
||||
'clusters.edit.k8sOptions.changed.tip':
|
||||
'You have changed the Kubernetes options. Re-run the registration command on the target cluster for the changes to take effect.',
|
||||
'clusters.edit.workerConfig.tip':
|
||||
'Changes to the worker configuration take effect only after restarting the affected workers.',
|
||||
'clusters.addworker.containerName': 'Worker Container Name',
|
||||
'clusters.addworker.containerName.tips':
|
||||
'Specify a name for the worker container.',
|
||||
@@ -166,10 +164,6 @@ export default {
|
||||
'clusters.volume.configMap.name': 'ConfigMap Name',
|
||||
'clusters.volume.configMap.optional': 'Optional',
|
||||
'clusters.volume.add': 'Add Volume Mount',
|
||||
'clusters.systemDefaultContainerRegistry.title': 'Default Container Registry',
|
||||
'clusters.systemDefaultContainerRegistry.tip':
|
||||
'Default registry used to resolve GPUStack images for this cluster. Falls back to the server default when unset.',
|
||||
'clusters.k8sOptions.title': 'Kubernetes Deployment Options',
|
||||
'clusters.imageCredentials.title': 'Image Credentials',
|
||||
'clusters.imageCredentials.add': 'Add Credential',
|
||||
'clusters.imageCredentials.registry': 'Registry',
|
||||
@@ -178,20 +172,16 @@ export default {
|
||||
'clusters.nodeSelector.title': 'Node Selector',
|
||||
'clusters.nodeSelector.tip':
|
||||
'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.',
|
||||
'clusters.operatorImage.title': 'Operator Image',
|
||||
'clusters.operatorImage.tip':
|
||||
'Override for the GPUStack Operator container image. Leave empty to use the server default.',
|
||||
'clusters.namespace.title': 'Namespace',
|
||||
'clusters.namespace.tip':
|
||||
'Kubernetes namespace the cluster’s manifests render into. Leave empty to use gpustack-system.',
|
||||
'clusters.clusterType.title': 'Cluster Type',
|
||||
'clusters.modelService.title': 'Model Service',
|
||||
'clusters.modelService.tip':
|
||||
'For LLM inference and API serving — e.g. exposing model APIs and token-based services.',
|
||||
'clusters.gpuInstances.title': 'GPU Service',
|
||||
'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.'
|
||||
'clusters.gpuVendorOverrides.title': 'GPU Vendor Overrides',
|
||||
'clusters.gpuVendorOverrides.validate.emptySelector':
|
||||
'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.',
|
||||
'clusters.gpuVendorOverrides.validate.duplicate':
|
||||
'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.gpuVendorOverrides.validate.keyConflict':
|
||||
'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.gpuVendorOverrides.tip':
|
||||
'Required when running multiple GPU vendors in one cluster. Each entry pins that vendor’s worker DaemonSet to nodes matching its nodeSelector, and the CPU worker is steered away from those nodes via a DoesNotExist node-affinity.',
|
||||
'clusters.gpuVendorOverrides.add': 'Add Override',
|
||||
'clusters.gpuVendorOverrides.vendor': 'GPU Vendor',
|
||||
'clusters.gpuVendorOverrides.nodeSelector': 'Node Selector'
|
||||
};
|
||||
|
||||
@@ -46,7 +46,6 @@ export default {
|
||||
'common.button.enabled': 'Enabled',
|
||||
'common.button.disabled': 'Disabled',
|
||||
'common.button.upgrade': 'Upgrade',
|
||||
'common.enterprise.feature': 'Available in GPUStack Enterprise',
|
||||
'common.input.holder': 'Please enter',
|
||||
'common.validate.value': '{name} value is required',
|
||||
'common.button.edit': 'Edit',
|
||||
@@ -56,7 +55,6 @@ export default {
|
||||
'common.button.viewevent': 'View Events',
|
||||
'common.button.recreate': 'Recreate',
|
||||
'common.table.operation': 'Operations',
|
||||
'common.table.creator': 'Creator',
|
||||
'common.table.createTime': 'Created',
|
||||
'common.table.updateTime': 'Updated',
|
||||
'common.table.description': 'Description',
|
||||
@@ -67,7 +65,6 @@ export default {
|
||||
'common.search.name.placeholder': 'filter by name',
|
||||
'common.search.id.placeholder': 'filter by ID',
|
||||
'common.filter.byId': 'filter by ID',
|
||||
'common.filter.byCreator': 'Filter by creator',
|
||||
'common.table.type': 'Type',
|
||||
'common.table.default': 'Default Value',
|
||||
'common.copy.success': 'Copied success!',
|
||||
@@ -288,7 +285,5 @@ export default {
|
||||
'common.image.limit.height': 'Image height must be {height}.',
|
||||
'common.remaining': 'Remaining {count}',
|
||||
'common.max': 'Max {count}',
|
||||
'common.max.count': '{label} Count',
|
||||
'common.validate.group': 'Please complete the {group} configuration',
|
||||
'common.preferences': 'Preferences'
|
||||
'common.validate.group': 'Please complete the {group} configuration'
|
||||
};
|
||||
|
||||
@@ -13,11 +13,7 @@ export default {
|
||||
'gpuservice.template.command.placeholder':
|
||||
'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.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.tips':
|
||||
'The size of the container system disk.',
|
||||
'gpuservice.template.memory': 'Memory (GB)',
|
||||
'gpuservice.instance.containerDisk.remaining':
|
||||
'Container Disk (Max {count} GB)',
|
||||
@@ -101,30 +97,21 @@ export default {
|
||||
'gpuservice.instance.templates': 'Instance Templates',
|
||||
'gpuservice.instance.section.storage': 'Storage',
|
||||
'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.required': 'Please enter the GPU count',
|
||||
'gpuservice.instance.gpuCount.max':
|
||||
'Please select at most {count} GPU card(s)',
|
||||
'gpuservice.instance.gpuCount.min':
|
||||
'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':
|
||||
'No available GPU resources, please choose another instance type.',
|
||||
'gpuservice.instance.gpuCount.zero':
|
||||
'CPU-only setup for environment preparation.',
|
||||
'gpuservice.instance.stock': 'Stock',
|
||||
'gpuservice.instance.sliced': 'Sliced',
|
||||
'gpuservice.instance.memory': 'VRAM',
|
||||
'gpuservice.instance.memory': 'Memory',
|
||||
'gpuservice.instance.ram': 'RAM',
|
||||
'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.template.placeholder':
|
||||
'Search by template name, image or mount path',
|
||||
@@ -155,13 +142,14 @@ export default {
|
||||
'gpuservice.storage.capacity': 'Capacity',
|
||||
'gpuservice.storage.accessMode': 'Access Mode',
|
||||
'gpuservice.storage.persistent': 'Persistent',
|
||||
'gpuservice.storage.temporary': 'Ephemeral',
|
||||
'gpuservice.storage.persistentVolume': 'Persistent',
|
||||
'gpuservice.storage.temporary': 'Temporary',
|
||||
'gpuservice.storage.persistentVolume': 'Persistent Volume',
|
||||
'gpuservice.storage.temporary.tips':
|
||||
'Data is cleared when the instance stops.',
|
||||
'gpuservice.storage.persistentVolume.tips':
|
||||
'Data persists across instance restarts. Persistent volumes remain intact after instance termination and can be shared by multiple instances.',
|
||||
'gpuservice.storage.persistentVolume.required': 'Please select a storage',
|
||||
'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.',
|
||||
'gpuservice.storage.persistentVolume.required':
|
||||
'Please select a persistent volume',
|
||||
'gpuservice.storage.persistentVolume.capacity': 'Capacity (GB)',
|
||||
'gpuservice.storage.persistentVolume.capacity.required':
|
||||
'Please enter capacity',
|
||||
@@ -171,6 +159,5 @@ export default {
|
||||
'gpuservice.storage.tempCapacity.required':
|
||||
'Please enter the temporary storage capacity',
|
||||
'gpuservice.form.rule.name':
|
||||
"Lowercase letters, numbers, and '-'. Start and end with a letter or number, no consecutive '-', max 63 characters.",
|
||||
'gpuservice.form.storage.select': 'Select Storage'
|
||||
"Lowercase letters, numbers, and '-'. Start and end with a letter or number, no consecutive '-', max 63 characters."
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ export default {
|
||||
'menu.playground.text2images': 'Image',
|
||||
'menu.playground.video': 'Video',
|
||||
'menu.compare': 'Compare',
|
||||
'menu.models': 'Model Service',
|
||||
'menu.models': 'Models',
|
||||
'menu.models.modelList': 'Deploy & Manage',
|
||||
'menu.models.modelCatalog': 'Catalog',
|
||||
'menu.models.catalog': 'Model Catalog',
|
||||
@@ -27,23 +27,20 @@ export default {
|
||||
'menu.users': 'Users',
|
||||
'menu.resources.workers': 'Workers',
|
||||
'menu.resources.gpus': 'GPUs',
|
||||
'menu.models.modelfiles': 'Model Files',
|
||||
'menu.resources.modelfiles': 'Model Files',
|
||||
'menu.accessControl': 'Access Control',
|
||||
'menu.accessControl.apikeys': 'API Keys',
|
||||
'menu.accessControl.users': 'Users',
|
||||
'menu.accessControl.organizations': 'Organizations',
|
||||
'menu.profile': 'Preferences',
|
||||
'menu.profile': 'Profile',
|
||||
'menu.login': 'Login',
|
||||
'menu.usage': 'Usage',
|
||||
'menu.usage.usage': 'Usage',
|
||||
'menu.billingAndUsage': 'Usage & Billing',
|
||||
'menu.billingAndUsage.usage': 'Usage',
|
||||
'menu.404': '404',
|
||||
'menu.resources.clusters': 'Clusters',
|
||||
'menu.resources.credentials': 'Cloud Credentials',
|
||||
'menu.resources.clusterDetail': 'Cluster Detail',
|
||||
'menu.resources.clusterCreate': 'Create Cluster',
|
||||
'menu.models.backendsList': 'Inference Backends',
|
||||
'menu.clusterManagement': 'Cluster Management',
|
||||
'menu.clusterManagement.clusters': 'Clusters',
|
||||
'menu.clusterManagement.credentials': 'Cloud Credentials',
|
||||
'menu.clusterManagement.clusterDetail': 'Cluster Detail',
|
||||
'menu.clusterManagement.clusterCreate': 'Create Cluster',
|
||||
'menu.resources.backendsList': 'Inference Backends',
|
||||
'menu.gpuService': 'GPU Service',
|
||||
'menu.gpuService.instances': 'GPU Instances',
|
||||
'menu.gpuService.templates': 'Instance Templates',
|
||||
|
||||
@@ -291,7 +291,7 @@ export default {
|
||||
'models.instance.startHistory': 'Run History',
|
||||
'models.instance.startHistory.tips':
|
||||
'Shows logs from the run before the last error-triggered restart.',
|
||||
'models.form.lora.label': 'LoRA Adapters',
|
||||
'models.form.lora.label': 'LoRA Adapter',
|
||||
'models.form.lora.add': 'Add LoRA Adapter',
|
||||
'models.form.lora.select': 'Select LoRA',
|
||||
'models.form.lora.name': 'LoRA name',
|
||||
|
||||
@@ -36,12 +36,9 @@ export default {
|
||||
'noresult.catalog.nofound': 'No matching models found.',
|
||||
'noresult.resources.cluster':
|
||||
'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':
|
||||
'No workers available. Add a worker to get started.',
|
||||
'noresult.resources.gotocluster': 'Create Your First Cluster',
|
||||
'noresult.resources.addk8scluster': 'Add a Kubernetes Cluster',
|
||||
'noresult.resources.gotoworker': 'Add Worker',
|
||||
'noresult.benchmark.title': 'No Benchmarks',
|
||||
'noresult.benchmark.subTitle': 'No benchmarks have been added yet.',
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
export default {
|
||||
'organizations.upsell.title': 'Organizations are an Enterprise feature',
|
||||
'organizations.upsell.subtitle':
|
||||
'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to GPUStack Enterprise to manage organizations.',
|
||||
'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'
|
||||
};
|
||||
@@ -17,15 +17,9 @@ export default {
|
||||
'usage.table.user.apiKeysUsed': 'API Keys Used',
|
||||
'usage.table.lastActive': 'Last Active',
|
||||
'usage.filter.granularity': 'Granularity',
|
||||
'usage.filter.granularity.hour': 'Hour',
|
||||
'usage.filter.granularity.day': 'Day',
|
||||
'usage.filter.granularity.week': 'Week',
|
||||
'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.apikeys': 'API Keys',
|
||||
'usage.tabs.users': 'User',
|
||||
@@ -38,69 +32,5 @@ export default {
|
||||
'usage.chart.cached': 'Cached',
|
||||
'usage.chart.uncached': 'Uncached',
|
||||
'usage.chart.inputTokensCached': 'Input Tokens (Cached/Uncached)',
|
||||
'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.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'
|
||||
'usage.table.inputTokensCached': 'Input Tokens Cached'
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ export default {
|
||||
'vendor.hygon': 'Hygon',
|
||||
'vendor.moorthreads': 'Moore Threads',
|
||||
'vendor.iluvatar': 'Iluvatar',
|
||||
'vendor.metax': 'MetaX',
|
||||
'vendor.metax': 'Metax',
|
||||
'vendor.cambricon': 'Cambricon',
|
||||
'vendor.thead': 'T-Head PPU'
|
||||
};
|
||||
|
||||
@@ -23,9 +23,7 @@ export default {
|
||||
'apikeys.accessScope.inference': 'Inference APIs',
|
||||
'apikeys.access.permissions': 'Access Permissions',
|
||||
'apikeys.type.auto': 'Auto-generated',
|
||||
'apikeys.type.custom': 'Custom',
|
||||
'apikeys.button.ipConfig': 'IP Access Control',
|
||||
'quotaLimits.button.title': 'Quota Limit'
|
||||
'apikeys.type.custom': 'Custom'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
|
||||
@@ -39,7 +39,7 @@ export default {
|
||||
'clusters.workerpool.batchSize.desc':
|
||||
'Number of workers created simultaneously in the Worker pool',
|
||||
'clusters.create.addworker.tips':
|
||||
'Please make sure the <a href={link} target="_blank">prerequisites</a> are met before executing the following command.',
|
||||
'Please make sure the <a href={link} target="_blank">prerequisites</a> for {label} are met before executing the following command.',
|
||||
'clusters.create.addCommand.tips':
|
||||
'On the Worker that needs to be added, run the following command to join it to the cluster.',
|
||||
'clusters.create.addCommand.k8s.tips':
|
||||
@@ -70,6 +70,8 @@ export default {
|
||||
'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.multiTag': 'Multi-select',
|
||||
'clusters.addworker.selectGPU.singleOnly':
|
||||
'The selected vendor is not in this cluster’s GPU vendor overrides — single-select only. Pick a vendor configured in the overrides to enable multi-select.',
|
||||
'clusters.addworker.checkEnv': 'Check Environment',
|
||||
'clusters.addworker.specifyArgs': 'Specify Arguments',
|
||||
'clusters.addworker.runCommand': 'Run Command',
|
||||
@@ -110,10 +112,6 @@ export default {
|
||||
'{count} new workers have been added to the cluster.',
|
||||
'clusters.create.serverUrl': 'GPUStack Server URL',
|
||||
'clusters.create.workerConfig': 'Worker Configuration',
|
||||
'clusters.edit.k8sOptions.changed.tip':
|
||||
'Kubernetes オプションを変更しました。変更を有効にするには、対象クラスターで登録コマンドを再実行してください。',
|
||||
'clusters.edit.workerConfig.tip':
|
||||
'ワーカー設定の変更は、対象のワーカーを再起動した後に有効になります。',
|
||||
'clusters.addworker.containerName': 'Worker Container Name',
|
||||
'clusters.addworker.containerName.tips':
|
||||
'Specify a name for the worker container.',
|
||||
@@ -166,10 +164,6 @@ export default {
|
||||
'clusters.volume.configMap.name': 'ConfigMap Name',
|
||||
'clusters.volume.configMap.optional': 'Optional',
|
||||
'clusters.volume.add': 'Add Volume Mount',
|
||||
'clusters.systemDefaultContainerRegistry.title': 'Default Container Registry',
|
||||
'clusters.systemDefaultContainerRegistry.tip':
|
||||
'Default registry used to resolve GPUStack images for this cluster. Falls back to the server default when unset.',
|
||||
'clusters.k8sOptions.title': 'Kubernetes Deployment Options',
|
||||
'clusters.imageCredentials.title': 'Image Credentials',
|
||||
'clusters.imageCredentials.add': 'Add Credential',
|
||||
'clusters.imageCredentials.registry': 'Registry',
|
||||
@@ -178,22 +172,18 @@ export default {
|
||||
'clusters.nodeSelector.title': 'Node Selector',
|
||||
'clusters.nodeSelector.tip':
|
||||
'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.',
|
||||
'clusters.operatorImage.title': 'Operator Image',
|
||||
'clusters.operatorImage.tip':
|
||||
'Override for the GPUStack Operator container image. Leave empty to use the server default.',
|
||||
'clusters.namespace.title': 'Namespace',
|
||||
'clusters.namespace.tip':
|
||||
'Kubernetes namespace the cluster’s manifests render into. Leave empty to use gpustack-system.',
|
||||
'clusters.clusterType.title': 'Cluster Type',
|
||||
'clusters.modelService.title': 'Model Service',
|
||||
'clusters.modelService.tip':
|
||||
'For LLM inference and API serving — e.g. exposing model APIs and token-based services.',
|
||||
'clusters.gpuInstances.title': 'GPU Service',
|
||||
'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.'
|
||||
'clusters.gpuVendorOverrides.title': 'GPU Vendor Overrides',
|
||||
'clusters.gpuVendorOverrides.validate.emptySelector':
|
||||
'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.',
|
||||
'clusters.gpuVendorOverrides.validate.duplicate':
|
||||
'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.gpuVendorOverrides.validate.keyConflict':
|
||||
'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.gpuVendorOverrides.tip':
|
||||
'Required when running multiple GPU vendors in one cluster. Each entry pins that vendor’s worker DaemonSet to nodes matching its nodeSelector, and the CPU worker is steered away from those nodes via a DoesNotExist node-affinity.',
|
||||
'clusters.gpuVendorOverrides.add': 'Add Override',
|
||||
'clusters.gpuVendorOverrides.vendor': 'GPU Vendor',
|
||||
'clusters.gpuVendorOverrides.nodeSelector': 'Node Selector'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
|
||||
@@ -46,7 +46,6 @@ export default {
|
||||
'common.button.enabled': '有効',
|
||||
'common.button.disabled': '無効',
|
||||
'common.button.upgrade': 'アップグレード',
|
||||
'common.enterprise.feature': 'Available in GPUStack Enterprise',
|
||||
'common.input.holder': '入力してください',
|
||||
'common.validate.value': '{name} の値は必須です',
|
||||
'common.button.edit': '編集',
|
||||
@@ -56,7 +55,6 @@ export default {
|
||||
'common.button.viewevent': 'イベントを表示',
|
||||
'common.button.recreate': '再作成',
|
||||
'common.table.operation': '操作',
|
||||
'common.table.creator': '作成者',
|
||||
'common.table.createTime': '作成日時',
|
||||
'common.table.updateTime': '更新日時',
|
||||
'common.table.description': '説明',
|
||||
@@ -67,7 +65,6 @@ export default {
|
||||
'common.search.name.placeholder': '名前でフィルタ',
|
||||
'common.search.id.placeholder': 'IDでフィルタ',
|
||||
'common.filter.byId': 'IDでフィルタ',
|
||||
'common.filter.byCreator': '作成者でフィルタ',
|
||||
'common.table.type': 'タイプ',
|
||||
'common.table.default': 'デフォルト値',
|
||||
'common.copy.success': 'コピー成功!',
|
||||
@@ -288,9 +285,7 @@ export default {
|
||||
'common.image.limit.height': 'Image height must be {height}.',
|
||||
'common.remaining': '残り {count}',
|
||||
'common.max': '最大 {count}',
|
||||
'common.max.count': '{label} 数',
|
||||
'common.validate.group': 'Please complete the {group} configuration',
|
||||
'common.preferences': 'Preferences'
|
||||
'common.validate.group': 'Please complete the {group} configuration'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
|
||||
@@ -13,11 +13,7 @@ export default {
|
||||
'gpuservice.template.command.placeholder':
|
||||
'引数はスペースで区切り、スペースを含む引数は引用符で囲んでください。例:/bin/bash -c "echo hello world"',
|
||||
'gpuservice.template.mountPath': 'マウントパス',
|
||||
'gpuservice.template.mountPath.tips':
|
||||
'このテンプレートからインスタンスを作成する際に、ストレージボリュームがデフォルトでマウントされるパスです。インスタンスの実行中に保持する必要があるデータの永続化に使用できます。',
|
||||
'gpuservice.template.containerDisk': 'コンテナディスク (GB)',
|
||||
'gpuservice.template.containerDisk.tips':
|
||||
'コンテナシステムディスクのサイズです。',
|
||||
'gpuservice.template.memory': 'メモリ (GB)',
|
||||
'gpuservice.instance.containerDisk.remaining':
|
||||
'コンテナディスク (最大 {count} GB)',
|
||||
@@ -100,30 +96,20 @@ export default {
|
||||
'gpuservice.instance.templates': 'インスタンステンプレート',
|
||||
'gpuservice.instance.section.storage': 'ストレージボリューム',
|
||||
'gpuservice.instance.type.required': 'インスタンスタイプを選択してください',
|
||||
'gpuservice.instance.type.noAvailable':
|
||||
'利用可能なインスタンスタイプがありません',
|
||||
'gpuservice.instance.gpuCount': 'GPU 数',
|
||||
'gpuservice.instance.gpuCount.required': 'GPU 数を入力してください',
|
||||
'gpuservice.instance.gpuCount.max':
|
||||
'最大 {count} 枚の GPU カードを選択してください',
|
||||
'gpuservice.instance.gpuCount.min':
|
||||
'少なくとも {count} 枚の GPU カードを選択してください',
|
||||
'gpuservice.instance.cpuCount.max':
|
||||
'最大 {count} 個の CPU コアを選択してください',
|
||||
'gpuservice.instance.cpuCount.min':
|
||||
'少なくとも {count} 個の CPU コアを選択してください',
|
||||
'gpuservice.instance.gpuCount.noAvailable':
|
||||
'利用可能な GPU リソースがありません。別のインスタンスタイプを選択してください。',
|
||||
'gpuservice.instance.gpuCount.zero': 'CPU のみを使用し、環境準備用です。',
|
||||
'gpuservice.instance.stock': '在庫',
|
||||
'gpuservice.instance.sliced': '分割',
|
||||
'gpuservice.instance.memory': 'VRAM',
|
||||
'gpuservice.instance.memory': 'Memory',
|
||||
'gpuservice.instance.ram': 'RAM',
|
||||
'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.template.placeholder':
|
||||
'テンプレート名、イメージまたはマウントパスで検索',
|
||||
@@ -155,9 +141,9 @@ export default {
|
||||
'gpuservice.storage.accessMode': 'アクセスモード',
|
||||
'gpuservice.storage.persistent': '永続',
|
||||
'gpuservice.storage.temporary': '一時',
|
||||
'gpuservice.storage.persistentVolume': '永続',
|
||||
'gpuservice.storage.persistentVolume': '永続ボリューム',
|
||||
'gpuservice.storage.persistentVolume.required':
|
||||
'ストレージを選択してください',
|
||||
'永続ボリュームを選択してください',
|
||||
'gpuservice.storage.persistentVolume.capacity': '容量 (GB)',
|
||||
'gpuservice.storage.persistentVolume.capacity.required':
|
||||
'容量を入力してください',
|
||||
@@ -171,6 +157,5 @@ export default {
|
||||
'gpuservice.storage.temporary.tips':
|
||||
'Data is cleared when the instance stops.',
|
||||
'gpuservice.storage.persistentVolume.tips':
|
||||
'Data persists across instance restarts. Persistent volumes remain intact after instance termination and can be shared by multiple instances.',
|
||||
'gpuservice.form.storage.select': 'ストレージを選択'
|
||||
'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.'
|
||||
};
|
||||
|
||||
+11
-14
@@ -8,7 +8,7 @@ export default {
|
||||
'menu.playground.text2images': '画像生成',
|
||||
'menu.playground.video': '動画',
|
||||
'menu.compare': '比較',
|
||||
'menu.models': 'モデルサービス',
|
||||
'menu.models': 'モデル',
|
||||
'menu.models.modelList': 'デプロイと管理',
|
||||
'menu.models.modelCatalog': 'カタログ',
|
||||
'menu.models.catalog': 'モデルカタログ',
|
||||
@@ -23,27 +23,24 @@ export default {
|
||||
'menu.resources': 'リソース',
|
||||
'menu.apikeys': 'APIキー',
|
||||
'menu.users': 'ユーザー',
|
||||
'menu.profile': 'Preferences',
|
||||
'menu.profile': 'プロフィール',
|
||||
'menu.login': 'ログイン',
|
||||
'menu.usage': '使用状況',
|
||||
'menu.usage.usage': '使用状況',
|
||||
'menu.billingAndUsage': '使用状況と請求',
|
||||
'menu.billingAndUsage.usage': '使用状況',
|
||||
'menu.404': '404',
|
||||
'menu.settings': 'Settings',
|
||||
'menu.resources.workers': 'Workers',
|
||||
'menu.resources.gpus': 'GPUs',
|
||||
'menu.models.modelfiles': 'Model Files',
|
||||
'menu.resources.modelfiles': 'Model Files',
|
||||
'menu.accessControl': 'Access Control',
|
||||
'menu.accessControl.apikeys': 'API Keys',
|
||||
'menu.accessControl.users': 'Users',
|
||||
'menu.accessControl.organizations': 'Organizations',
|
||||
'menu.resources.clusters': 'Clusters',
|
||||
'menu.resources.credentials': 'Cloud Credentials',
|
||||
'menu.clusterManagement': 'Cluster Management',
|
||||
'menu.clusterManagement.clusters': 'Clusters',
|
||||
'menu.clusterManagement.credentials': 'Cloud Credentials',
|
||||
'menu.models.userModels': 'My Models',
|
||||
'menu.resources.clusterDetail': 'Cluster Detail',
|
||||
'menu.resources.clusterCreate': 'Create Cluster',
|
||||
'menu.models.backendsList': 'Inference Backends',
|
||||
'menu.clusterManagement.clusterDetail': 'Cluster Detail',
|
||||
'menu.clusterManagement.clusterCreate': 'Create Cluster',
|
||||
'menu.resources.backendsList': 'Inference Backends',
|
||||
'menu.gpuService': 'GPU Service',
|
||||
'menu.gpuService.instances': 'GPU Instances',
|
||||
'menu.gpuService.templates': 'Instance Templates',
|
||||
@@ -56,7 +53,7 @@ export default {
|
||||
// 1. 'menu.models.deployment': 'Deployment',
|
||||
// 2. 'menu.resources.workers': 'Workers',
|
||||
// 3. 'menu.resources.gpus': 'GPUs',
|
||||
// 4. 'menu.models.modelfiles': 'Model Files',
|
||||
// 4. 'menu.resources.modelfiles': 'Model Files',
|
||||
// 5. 'menu.accessControl': 'Access Control',
|
||||
// 6. 'menu.accessControl.apikeys': 'API Keys',
|
||||
// 7. 'menu.accessControl.users': 'Users',
|
||||
@@ -66,7 +63,7 @@ export default {
|
||||
// 11. 'menu.models.userModels': 'My Models'
|
||||
// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail',
|
||||
// 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster',
|
||||
// 14. 'menu.models.backendsList': 'Inference Backends',
|
||||
// 14. 'menu.resources.backendsList': 'Inference Backends',
|
||||
// 15. 'menu.models.benchmark': 'Benchmarks',
|
||||
// 15. 'menu.models.provider': 'Provider',
|
||||
// 15. 'menu.models.providers': 'Provider',
|
||||
|
||||
@@ -291,7 +291,7 @@ export default {
|
||||
'models.instance.startHistory': 'Run History',
|
||||
'models.instance.startHistory.tips':
|
||||
'Shows logs from the run before the last error-triggered restart.',
|
||||
'models.form.lora.label': 'LoRA Adapters',
|
||||
'models.form.lora.label': 'LoRA Adapter',
|
||||
'models.form.lora.add': 'Add LoRA Adapter',
|
||||
'models.form.lora.select': 'Select LoRA',
|
||||
'models.form.lora.name': 'LoRA name',
|
||||
|
||||
@@ -36,12 +36,9 @@ export default {
|
||||
'noresult.catalog.nofound': 'No matching models found.',
|
||||
'noresult.resources.cluster':
|
||||
'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':
|
||||
'No workers available. Add a worker to get started.',
|
||||
'noresult.resources.gotocluster': 'Create Your First Cluster',
|
||||
'noresult.resources.addk8scluster': 'Add a Kubernetes Cluster',
|
||||
'noresult.resources.gotoworker': 'Add Worker',
|
||||
'noresult.benchmark.title': 'No Benchmarks',
|
||||
'noresult.benchmark.subTitle': 'No benchmarks have been added yet.',
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
export default {
|
||||
'organizations.upsell.title': 'Organizations are an Enterprise feature',
|
||||
'organizations.upsell.subtitle':
|
||||
'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to GPUStack Enterprise to manage organizations.',
|
||||
'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'
|
||||
};
|
||||
@@ -17,15 +17,9 @@ export default {
|
||||
'usage.table.user.apiKeysUsed': 'API Keys Used',
|
||||
'usage.table.lastActive': 'Last Active',
|
||||
'usage.filter.granularity': 'Granularity',
|
||||
'usage.filter.granularity.hour': '時間',
|
||||
'usage.filter.granularity.day': 'Day',
|
||||
'usage.filter.granularity.week': 'Week',
|
||||
'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.apikeys': 'API Keys',
|
||||
'usage.tabs.users': 'User',
|
||||
@@ -38,69 +32,5 @@ export default {
|
||||
'usage.chart.cached': 'Cached',
|
||||
'usage.chart.uncached': 'Uncached',
|
||||
'usage.chart.inputTokensCached': 'Input Tokens (Cached/Uncached)',
|
||||
'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.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': 'デタッチ済み'
|
||||
'usage.table.inputTokensCached': 'Input Tokens Cached'
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ export default {
|
||||
'vendor.hygon': 'Hygon',
|
||||
'vendor.moorthreads': 'Moore Threads',
|
||||
'vendor.iluvatar': 'Iluvatar',
|
||||
'vendor.metax': 'MetaX',
|
||||
'vendor.metax': 'Metax',
|
||||
'vendor.cambricon': 'Cambricon',
|
||||
'vendor.thead': 'T-Head PPU'
|
||||
};
|
||||
|
||||
@@ -23,9 +23,7 @@ export default {
|
||||
'apikeys.accessScope.inference': 'Inference APIs',
|
||||
'apikeys.access.permissions': 'Access Permissions',
|
||||
'apikeys.type.auto': 'Auto-generated',
|
||||
'apikeys.type.custom': 'Custom',
|
||||
'apikeys.button.ipConfig': 'IP Access Control',
|
||||
'quotaLimits.button.title': 'Quota Limit'
|
||||
'apikeys.type.custom': 'Custom'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
|
||||
@@ -39,7 +39,7 @@ export default {
|
||||
'clusters.workerpool.batchSize.desc':
|
||||
'Количество воркеров, создаваемых одновременно в пуле воркеров',
|
||||
'clusters.create.addworker.tips':
|
||||
'Пожалуйста, убедитесь, что выполнены <a href={link} target="_blank">предварительные условия</a> перед выполнением следующей команды.',
|
||||
'Пожалуйста, убедитесь, что выполнены <a href={link} target="_blank">предварительные условия</a> для {label} перед выполнением следующей команды.',
|
||||
'clusters.create.addCommand.tips':
|
||||
'На воркере, который необходимо добавить, выполните следующую команду, чтобы присоединить его к кластеру.',
|
||||
'clusters.create.addCommand.k8s.tips':
|
||||
@@ -70,6 +70,8 @@ export default {
|
||||
'Для <span class="bold-text">не-Docker</span> кластеров, пожалуйста, регистрируйте кластеры или управляйте пулами воркеров на странице Кластеры.',
|
||||
'clusters.addworker.selectGPU': 'Выбрать производителя GPU',
|
||||
'clusters.addworker.selectGPU.multiTag': 'Multi-select',
|
||||
'clusters.addworker.selectGPU.singleOnly':
|
||||
'The selected vendor is not in this cluster’s GPU vendor overrides — single-select only. Pick a vendor configured in the overrides to enable multi-select.',
|
||||
'clusters.addworker.checkEnv': 'Проверить окружение',
|
||||
'clusters.addworker.specifyArgs': 'Указать аргументы',
|
||||
'clusters.addworker.runCommand': 'Выполнить команду',
|
||||
@@ -110,10 +112,6 @@ export default {
|
||||
'{count} новых воркеров были добавлены в кластер.',
|
||||
'clusters.create.serverUrl': 'URL сервера GPUStack',
|
||||
'clusters.create.workerConfig': 'Конфигурация воркера',
|
||||
'clusters.edit.k8sOptions.changed.tip':
|
||||
'Вы изменили параметры Kubernetes. Чтобы изменения вступили в силу, повторно выполните команду регистрации в целевом кластере.',
|
||||
'clusters.edit.workerConfig.tip':
|
||||
'Изменения конфигурации воркера вступают в силу только после перезапуска соответствующих воркеров.',
|
||||
'clusters.addworker.containerName': 'Имя контейнера воркера',
|
||||
'clusters.addworker.containerName.tips':
|
||||
'Укажите имя для контейнера воркера.',
|
||||
@@ -167,10 +165,6 @@ export default {
|
||||
'clusters.volume.configMap.name': 'ConfigMap Name',
|
||||
'clusters.volume.configMap.optional': 'Optional',
|
||||
'clusters.volume.add': 'Add Volume Mount',
|
||||
'clusters.systemDefaultContainerRegistry.title': 'Default Container Registry',
|
||||
'clusters.systemDefaultContainerRegistry.tip':
|
||||
'Default registry used to resolve GPUStack images for this cluster. Falls back to the server default when unset.',
|
||||
'clusters.k8sOptions.title': 'Kubernetes Deployment Options',
|
||||
'clusters.imageCredentials.title': 'Image Credentials',
|
||||
'clusters.imageCredentials.add': 'Add Credential',
|
||||
'clusters.imageCredentials.registry': 'Registry',
|
||||
@@ -179,22 +173,18 @@ export default {
|
||||
'clusters.nodeSelector.title': 'Node Selector',
|
||||
'clusters.nodeSelector.tip':
|
||||
'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.',
|
||||
'clusters.operatorImage.title': 'Operator Image',
|
||||
'clusters.operatorImage.tip':
|
||||
'Override for the GPUStack Operator container image. Leave empty to use the server default.',
|
||||
'clusters.namespace.title': 'Namespace',
|
||||
'clusters.namespace.tip':
|
||||
'Kubernetes namespace the cluster’s manifests render into. Leave empty to use gpustack-system.',
|
||||
'clusters.clusterType.title': 'Cluster Type',
|
||||
'clusters.modelService.title': 'Model Service',
|
||||
'clusters.modelService.tip':
|
||||
'For LLM inference and API serving — e.g. exposing model APIs and token-based services.',
|
||||
'clusters.gpuInstances.title': 'GPU Service',
|
||||
'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.'
|
||||
'clusters.gpuVendorOverrides.title': 'GPU Vendor Overrides',
|
||||
'clusters.gpuVendorOverrides.validate.emptySelector':
|
||||
'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.',
|
||||
'clusters.gpuVendorOverrides.validate.duplicate':
|
||||
'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.gpuVendorOverrides.validate.keyConflict':
|
||||
'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.gpuVendorOverrides.tip':
|
||||
'Required when running multiple GPU vendors in one cluster. Each entry pins that vendor’s worker DaemonSet to nodes matching its nodeSelector, and the CPU worker is steered away from those nodes via a DoesNotExist node-affinity.',
|
||||
'clusters.gpuVendorOverrides.add': 'Add Override',
|
||||
'clusters.gpuVendorOverrides.vendor': 'GPU Vendor',
|
||||
'clusters.gpuVendorOverrides.nodeSelector': 'Node Selector'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
|
||||
@@ -46,7 +46,6 @@ export default {
|
||||
'common.button.enabled': 'Активно',
|
||||
'common.button.disabled': 'Отключено',
|
||||
'common.button.upgrade': 'Обновить',
|
||||
'common.enterprise.feature': 'Available in GPUStack Enterprise',
|
||||
'common.input.holder': 'Введите значение',
|
||||
'common.validate.value': 'Поле {name} обязательно',
|
||||
'common.button.edit': 'Редактировать',
|
||||
@@ -56,7 +55,6 @@ export default {
|
||||
'common.button.viewevent': 'Просмотр событий',
|
||||
'common.button.recreate': 'Пересоздать',
|
||||
'common.table.operation': 'Действия',
|
||||
'common.table.creator': 'Создатель',
|
||||
'common.table.createTime': 'Создано',
|
||||
'common.table.updateTime': 'Обновлено',
|
||||
'common.table.description': 'Описание',
|
||||
@@ -67,7 +65,6 @@ export default {
|
||||
'common.search.name.placeholder': 'Фильтр по названию',
|
||||
'common.search.id.placeholder': 'Фильтр по ID',
|
||||
'common.filter.byId': 'Фильтр по ID',
|
||||
'common.filter.byCreator': 'Фильтр по создателю',
|
||||
'common.table.type': 'Тип',
|
||||
'common.table.default': 'Значение по умолчанию',
|
||||
'common.copy.success': 'Скопировано!',
|
||||
@@ -263,7 +260,7 @@ export default {
|
||||
'common.sso.noConfig':
|
||||
'Единый вход не настроен в этой системе. Пожалуйста, обратитесь к администратору.',
|
||||
'common.button.edit.item': 'Редактировать {name}',
|
||||
'common.button.copy.item': 'Дублировать {name}',
|
||||
'common.button.copy.item': 'Duplicate {name}',
|
||||
'common.button.terminal': 'Терминал',
|
||||
'common.button.addItem': 'Добавить элемент',
|
||||
'common.help.default': 'По умолчанию: {content}',
|
||||
@@ -287,9 +284,7 @@ export default {
|
||||
'common.image.limit.height': 'Image height must be {height}.',
|
||||
'common.remaining': 'Остаток {count}',
|
||||
'common.max': 'Макс. {count}',
|
||||
'common.max.count': 'Количество {label}',
|
||||
'common.validate.group': 'Please complete the {group} configuration',
|
||||
'common.preferences': 'Preferences'
|
||||
'common.validate.group': 'Please complete the {group} configuration'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
|
||||
@@ -14,11 +14,7 @@ export default {
|
||||
'gpuservice.template.command.placeholder':
|
||||
'Разделяйте аргументы пробелами; аргументы с пробелами заключайте в кавычки, например: /bin/bash -c "echo hello world"',
|
||||
'gpuservice.template.mountPath': 'Путь монтирования',
|
||||
'gpuservice.template.mountPath.tips':
|
||||
'Путь, по которому том хранилища монтируется по умолчанию при создании экземпляра из этого шаблона. Может использоваться для сохранения данных, которые нужно сохранить во время работы экземпляра.',
|
||||
'gpuservice.template.containerDisk': 'Диск контейнера (GB)',
|
||||
'gpuservice.template.containerDisk.tips':
|
||||
'Размер системного диска контейнера.',
|
||||
'gpuservice.template.memory': 'Память (GB)',
|
||||
'gpuservice.instance.containerDisk.remaining':
|
||||
'Диск контейнера (Макс. {count} GB)',
|
||||
@@ -104,25 +100,18 @@ export default {
|
||||
'gpuservice.instance.templates': 'Шаблоны экземпляров',
|
||||
'gpuservice.instance.section.storage': 'Том хранилища',
|
||||
'gpuservice.instance.type.required': 'Выберите тип экземпляра',
|
||||
'gpuservice.instance.type.noAvailable': 'Нет доступных типов экземпляров',
|
||||
'gpuservice.instance.gpuCount': 'Количество GPU',
|
||||
'gpuservice.instance.gpuCount.required': 'Введите количество GPU',
|
||||
'gpuservice.instance.gpuCount.max': 'Выберите максимум {count} GPU-карт',
|
||||
'gpuservice.instance.gpuCount.min': 'Выберите минимум {count} GPU-карт',
|
||||
'gpuservice.instance.cpuCount.max': 'Выберите максимум {count} ядер CPU',
|
||||
'gpuservice.instance.cpuCount.min': 'Выберите минимум {count} ядер CPU',
|
||||
'gpuservice.instance.gpuCount.noAvailable':
|
||||
'Нет доступных ресурсов GPU, выберите другой тип экземпляра.',
|
||||
'gpuservice.instance.gpuCount.zero': 'Только CPU, для подготовки окружения.',
|
||||
'gpuservice.instance.stock': 'Остаток',
|
||||
'gpuservice.instance.sliced': 'Разделено',
|
||||
'gpuservice.instance.memory': 'VRAM',
|
||||
'gpuservice.instance.memory': 'Память',
|
||||
'gpuservice.instance.ram': 'RAM',
|
||||
'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.template.placeholder':
|
||||
'Поиск по имени шаблона, образу или пути монтирования',
|
||||
@@ -154,8 +143,8 @@ export default {
|
||||
'gpuservice.storage.accessMode': 'Режим доступа',
|
||||
'gpuservice.storage.persistent': 'Постоянное',
|
||||
'gpuservice.storage.temporary': 'Временное',
|
||||
'gpuservice.storage.persistentVolume': 'Постоянное',
|
||||
'gpuservice.storage.persistentVolume.required': 'Выберите хранилище',
|
||||
'gpuservice.storage.persistentVolume': 'Постоянный том',
|
||||
'gpuservice.storage.persistentVolume.required': 'Выберите постоянный том',
|
||||
'gpuservice.storage.persistentVolume.capacity': 'Ёмкость (ГБ)',
|
||||
'gpuservice.storage.persistentVolume.capacity.required': 'Введите ёмкость',
|
||||
'gpuservice.storage.persistentVolume.releaseWithInstance':
|
||||
@@ -168,6 +157,5 @@ export default {
|
||||
'gpuservice.storage.temporary.tips':
|
||||
'Data is cleared when the instance stops.',
|
||||
'gpuservice.storage.persistentVolume.tips':
|
||||
'Data persists across instance restarts. Persistent volumes remain intact after instance termination and can be shared by multiple instances.',
|
||||
'gpuservice.form.storage.select': 'Выберите хранилище'
|
||||
'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.'
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ export default {
|
||||
'menu.playground.text2images': 'Генерация изображений',
|
||||
'menu.playground.video': 'Видео',
|
||||
'menu.compare': 'Сравнение',
|
||||
'menu.models': 'Сервисы моделей',
|
||||
'menu.models': 'Модели',
|
||||
'menu.models.modelList': 'Развертывание и управление',
|
||||
'menu.models.modelCatalog': 'Каталог',
|
||||
'menu.models.catalog': 'Каталог моделей',
|
||||
@@ -23,26 +23,23 @@ export default {
|
||||
'menu.resources': 'Ресурсы',
|
||||
'menu.apikeys': 'API-ключи',
|
||||
'menu.users': 'Пользователи',
|
||||
'menu.profile': 'Preferences',
|
||||
'menu.profile': 'Профиль',
|
||||
'menu.login': 'Авторизация',
|
||||
'menu.usage': 'Использование',
|
||||
'menu.usage.usage': 'Использование',
|
||||
'menu.billingAndUsage': 'Использование и биллинг',
|
||||
'menu.billingAndUsage.usage': 'Использование',
|
||||
'menu.404': 'Ошибка 404',
|
||||
'menu.resources.workers': 'Воркеры',
|
||||
'menu.resources.gpus': 'GPUs',
|
||||
'menu.models.modelfiles': 'Файлы модлей',
|
||||
'menu.resources.modelfiles': 'Файлы модлей',
|
||||
'menu.accessControl': 'Управление доступом',
|
||||
'menu.accessControl.apikeys': 'API Ключи',
|
||||
'menu.accessControl.users': 'Пользователи',
|
||||
'menu.accessControl.organizations': 'Организации',
|
||||
'menu.resources.clusters': 'Кластеры',
|
||||
'menu.resources.credentials': 'Облачные аккаунты',
|
||||
'menu.clusterManagement': 'Управление кластерами',
|
||||
'menu.clusterManagement.clusters': 'Кластеры',
|
||||
'menu.clusterManagement.credentials': 'Облачные аккаунты',
|
||||
'menu.models.userModels': 'Мои модели',
|
||||
'menu.resources.clusterDetail': 'Детали кластера',
|
||||
'menu.resources.clusterCreate': 'Создать кластер',
|
||||
'menu.models.backendsList': 'Бэкенды запуска',
|
||||
'menu.clusterManagement.clusterDetail': 'Детали кластера',
|
||||
'menu.clusterManagement.clusterCreate': 'Создать кластер',
|
||||
'menu.resources.backendsList': 'Бэкенды запуска',
|
||||
'menu.settings': 'Settings',
|
||||
'menu.gpuService': 'GPU Service',
|
||||
'menu.gpuService.instances': 'GPU Instances',
|
||||
|
||||
@@ -295,7 +295,7 @@ export default {
|
||||
'models.instance.startHistory': 'Run History',
|
||||
'models.instance.startHistory.tips':
|
||||
'Shows logs from the run before the last error-triggered restart.',
|
||||
'models.form.lora.label': 'LoRA Adapters',
|
||||
'models.form.lora.label': 'LoRA Adapter',
|
||||
'models.form.lora.add': 'Add LoRA Adapter',
|
||||
'models.form.lora.select': 'Select LoRA',
|
||||
'models.form.lora.name': 'LoRA name',
|
||||
|
||||
@@ -37,12 +37,9 @@ export default {
|
||||
'noresult.catalog.nofound': 'Подходящие модели не найдены.',
|
||||
'noresult.resources.cluster':
|
||||
'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':
|
||||
'No workers available. Add a worker to get started.',
|
||||
'noresult.resources.gotocluster': 'Create Your First Cluster',
|
||||
'noresult.resources.addk8scluster': 'Add a Kubernetes Cluster',
|
||||
'noresult.resources.gotoworker': 'Add Worker',
|
||||
'noresult.benchmark.title': 'No Benchmarks',
|
||||
'noresult.benchmark.subTitle': 'No benchmarks have been added yet.',
|
||||
@@ -70,7 +67,8 @@ export default {
|
||||
'noresult.gpuservice.storage.subTitle': 'Хранилища ещё не добавлены.',
|
||||
'noresult.gpuservice.storage.nofound': 'Подходящие хранилища не найдены.',
|
||||
'noresult.gpuservice.storageType.title': 'Нет типов хранилищ',
|
||||
'noresult.gpuservice.storageType.subTitle': 'Типы хранилищ ещё не добавлены.',
|
||||
'noresult.gpuservice.storageType.subTitle':
|
||||
'Типы хранилищ ещё не добавлены.',
|
||||
'noresult.gpuservice.storageType.nofound':
|
||||
'Подходящие типы хранилищ не найдены.',
|
||||
'noresult.gpuservice.sshkey.title': 'Нет открытых ключей SSH',
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
export default {
|
||||
'organizations.upsell.title': 'Organizations are an Enterprise feature',
|
||||
'organizations.upsell.subtitle':
|
||||
'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to GPUStack Enterprise to manage organizations.',
|
||||
'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'
|
||||
};
|
||||
@@ -17,15 +17,9 @@ export default {
|
||||
'usage.table.user.apiKeysUsed': 'API Keys Used',
|
||||
'usage.table.lastActive': 'Last Active',
|
||||
'usage.filter.granularity': 'Granularity',
|
||||
'usage.filter.granularity.hour': 'Час',
|
||||
'usage.filter.granularity.day': 'Day',
|
||||
'usage.filter.granularity.week': 'Week',
|
||||
'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.apikeys': 'API Keys',
|
||||
'usage.tabs.users': 'User',
|
||||
@@ -38,69 +32,5 @@ export default {
|
||||
'usage.chart.cached': 'Cached',
|
||||
'usage.chart.uncached': 'Uncached',
|
||||
'usage.chart.inputTokensCached': 'Input Tokens (Cached/Uncached)',
|
||||
'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.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': 'Отключён'
|
||||
'usage.table.inputTokensCached': 'Input Tokens Cached'
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ export default {
|
||||
'vendor.hygon': 'Hygon',
|
||||
'vendor.moorthreads': 'Moore Threads',
|
||||
'vendor.iluvatar': 'Iluvatar',
|
||||
'vendor.metax': 'MetaX',
|
||||
'vendor.metax': 'Metax',
|
||||
'vendor.cambricon': 'Cambricon',
|
||||
'vendor.thead': 'T-Head PPU'
|
||||
};
|
||||
|
||||
@@ -23,7 +23,5 @@ export default {
|
||||
'apikeys.accessScope.inference': 'Inference APIs',
|
||||
'apikeys.access.permissions': 'Access Permissions',
|
||||
'apikeys.type.auto': 'Auto-generated',
|
||||
'apikeys.type.custom': 'Custom',
|
||||
'apikeys.button.ipConfig': 'IP Access Control',
|
||||
'quotaLimits.button.title': 'Quota Limit'
|
||||
'apikeys.type.custom': 'Custom'
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ export default {
|
||||
'clusters.workerpool.batchSize.desc':
|
||||
'İşçi havuzunda eşzamanlı olarak oluşturulan işçi düğüm sayısı',
|
||||
'clusters.create.addworker.tips':
|
||||
'Aşağıdaki komutu çalıştırmadan önce lütfen <a href={link} target="_blank">ön koşulların</a> karşılandığından emin olun.',
|
||||
'Aşağıdaki komutu çalıştırmadan önce lütfen {label} için <a href={link} target="_blank">ön koşulların</a> karşılandığından emin olun.',
|
||||
'clusters.create.addCommand.tips':
|
||||
'Eklenmesi gereken İşçi Düğümde, kümeye katılması için aşağıdaki komutu çalıştırın.',
|
||||
'clusters.create.addCommand.k8s.tips':
|
||||
@@ -70,6 +70,8 @@ export default {
|
||||
'<span class="bold-text">Docker dışı</span> kümeler için lütfen Kümeler sayfasından küme kaydı oluşturun veya işçi havuzlarını yönetin.',
|
||||
'clusters.addworker.selectGPU': 'GPU Üreticisi Seç',
|
||||
'clusters.addworker.selectGPU.multiTag': 'Multi-select',
|
||||
'clusters.addworker.selectGPU.singleOnly':
|
||||
'The selected vendor is not in this cluster’s GPU vendor overrides — single-select only. Pick a vendor configured in the overrides to enable multi-select.',
|
||||
'clusters.addworker.checkEnv': 'Ortamı Kontrol Et',
|
||||
'clusters.addworker.specifyArgs': 'Argümanları Belirle',
|
||||
'clusters.addworker.runCommand': 'Komutu Çalıştır',
|
||||
@@ -110,10 +112,6 @@ export default {
|
||||
'{count} yeni işçi düğüm kümeye eklendi.',
|
||||
'clusters.create.serverUrl': "GPUStack Sunucu URL'si",
|
||||
'clusters.create.workerConfig': 'İşçi Düğüm Yapılandırması',
|
||||
'clusters.edit.k8sOptions.changed.tip':
|
||||
'Kubernetes seçeneklerini değiştirdiniz. Değişikliklerin etkili olması için kayıt komutunu hedef kümede yeniden çalıştırın.',
|
||||
'clusters.edit.workerConfig.tip':
|
||||
'İşçi düğüm yapılandırmasındaki değişiklikler yalnızca ilgili işçi düğümleri yeniden başlatıldıktan sonra etkili olur.',
|
||||
'clusters.addworker.containerName': 'İşçi Düğüm Konteyner Adı',
|
||||
'clusters.addworker.containerName.tips':
|
||||
'İşçi düğüm konteyneri için bir ad belirtin.',
|
||||
@@ -167,10 +165,6 @@ export default {
|
||||
'clusters.volume.configMap.name': 'ConfigMap Name',
|
||||
'clusters.volume.configMap.optional': 'Optional',
|
||||
'clusters.volume.add': 'Add Volume Mount',
|
||||
'clusters.systemDefaultContainerRegistry.title': 'Default Container Registry',
|
||||
'clusters.systemDefaultContainerRegistry.tip':
|
||||
'Default registry used to resolve GPUStack images for this cluster. Falls back to the server default when unset.',
|
||||
'clusters.k8sOptions.title': 'Kubernetes Deployment Options',
|
||||
'clusters.imageCredentials.title': 'Image Credentials',
|
||||
'clusters.imageCredentials.add': 'Add Credential',
|
||||
'clusters.imageCredentials.registry': 'Registry',
|
||||
@@ -179,20 +173,16 @@ export default {
|
||||
'clusters.nodeSelector.title': 'Node Selector',
|
||||
'clusters.nodeSelector.tip':
|
||||
'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.',
|
||||
'clusters.operatorImage.title': 'Operator Image',
|
||||
'clusters.operatorImage.tip':
|
||||
'Override for the GPUStack Operator container image. Leave empty to use the server default.',
|
||||
'clusters.namespace.title': 'Namespace',
|
||||
'clusters.namespace.tip':
|
||||
'Kubernetes namespace the cluster’s manifests render into. Leave empty to use gpustack-system.',
|
||||
'clusters.clusterType.title': 'Cluster Type',
|
||||
'clusters.modelService.title': 'Model Service',
|
||||
'clusters.modelService.tip':
|
||||
'For LLM inference and API serving — e.g. exposing model APIs and token-based services.',
|
||||
'clusters.gpuInstances.title': 'GPU Service',
|
||||
'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.'
|
||||
'clusters.gpuVendorOverrides.title': 'GPU Vendor Overrides',
|
||||
'clusters.gpuVendorOverrides.validate.emptySelector':
|
||||
'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.',
|
||||
'clusters.gpuVendorOverrides.validate.duplicate':
|
||||
'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.gpuVendorOverrides.validate.keyConflict':
|
||||
'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.gpuVendorOverrides.tip':
|
||||
'Required when running multiple GPU vendors in one cluster. Each entry pins that vendor’s worker DaemonSet to nodes matching its nodeSelector, and the CPU worker is steered away from those nodes via a DoesNotExist node-affinity.',
|
||||
'clusters.gpuVendorOverrides.add': 'Add Override',
|
||||
'clusters.gpuVendorOverrides.vendor': 'GPU Vendor',
|
||||
'clusters.gpuVendorOverrides.nodeSelector': 'Node Selector'
|
||||
};
|
||||
|
||||
@@ -46,7 +46,6 @@ export default {
|
||||
'common.button.enabled': 'Etkin',
|
||||
'common.button.disabled': 'Devre dışı',
|
||||
'common.button.upgrade': 'Yükselt',
|
||||
'common.enterprise.feature': 'Available in GPUStack Enterprise',
|
||||
'common.input.holder': 'Lütfen girin',
|
||||
'common.validate.value': '{name} değeri gereklidir',
|
||||
'common.button.edit': 'Düzenle',
|
||||
@@ -56,7 +55,6 @@ export default {
|
||||
'common.button.viewevent': 'Olayları Görüntüle',
|
||||
'common.button.recreate': 'Yeniden Oluştur',
|
||||
'common.table.operation': 'İşlemler',
|
||||
'common.table.creator': 'Oluşturan',
|
||||
'common.table.createTime': 'Oluşturulma',
|
||||
'common.table.updateTime': 'Güncellenme',
|
||||
'common.table.description': 'Açıklama',
|
||||
@@ -67,7 +65,6 @@ export default {
|
||||
'common.search.name.placeholder': 'ada göre filtrele',
|
||||
'common.search.id.placeholder': 'kimliğe göre filtrele',
|
||||
'common.filter.byId': 'kimliğe göre filtrele',
|
||||
'common.filter.byCreator': 'Oluşturana göre filtrele',
|
||||
'common.table.type': 'Tür',
|
||||
'common.table.default': 'Varsayılan Değer',
|
||||
'common.copy.success': 'Kopyalama başarılı!',
|
||||
@@ -291,7 +288,5 @@ export default {
|
||||
'common.image.limit.height': 'Image height must be {height}.',
|
||||
'common.remaining': 'Kalan {count}',
|
||||
'common.max': 'Maks. {count}',
|
||||
'common.max.count': '{label} Sayısı',
|
||||
'common.validate.group': 'Please complete the {group} configuration',
|
||||
'common.preferences': 'Tercihler'
|
||||
'common.validate.group': 'Please complete the {group} configuration'
|
||||
};
|
||||
|
||||
@@ -13,10 +13,7 @@ export default {
|
||||
'gpuservice.template.command.placeholder':
|
||||
'Argümanları boşlukla ayırın; boşluk içeren argümanları tırnak içine alın, örn.: /bin/bash -c "echo hello world"',
|
||||
'gpuservice.template.mountPath': 'Bağlama Yolu',
|
||||
'gpuservice.template.mountPath.tips':
|
||||
'Bu şablondan bir örnek oluşturulurken depolama biriminin varsayılan olarak bağlanacağı yol. Örnek çalışırken saklanması gereken verileri kalıcı hale getirmek için kullanılabilir.',
|
||||
'gpuservice.template.containerDisk': 'Konteyner Diski (GB)',
|
||||
'gpuservice.template.containerDisk.tips': 'Konteyner sistem diskinin boyutu.',
|
||||
'gpuservice.template.memory': 'Bellek (GB)',
|
||||
'gpuservice.instance.containerDisk.remaining':
|
||||
'Konteyner Diski (Maks. {count} GB)',
|
||||
@@ -100,25 +97,18 @@ export default {
|
||||
'gpuservice.instance.templates': 'Örnek Şablonları',
|
||||
'gpuservice.instance.section.storage': 'Depolama Hacmi',
|
||||
'gpuservice.instance.type.required': 'Lütfen bir örnek türü seçin',
|
||||
'gpuservice.instance.type.noAvailable': 'Kullanılabilir örnek türü yok',
|
||||
'gpuservice.instance.gpuCount': 'GPU Sayısı',
|
||||
'gpuservice.instance.gpuCount.required': 'Lütfen GPU sayısını girin',
|
||||
'gpuservice.instance.gpuCount.max': 'En fazla {count} GPU kartı seçin',
|
||||
'gpuservice.instance.gpuCount.min': 'En az {count} GPU kartı seçin',
|
||||
'gpuservice.instance.cpuCount.max': 'En fazla {count} CPU çekirdeği seçin',
|
||||
'gpuservice.instance.cpuCount.min': 'En az {count} CPU çekirdeği seçin',
|
||||
'gpuservice.instance.gpuCount.noAvailable':
|
||||
'Kullanılabilir GPU kaynağı yok, lütfen başka bir örnek türü seçin.',
|
||||
'gpuservice.instance.gpuCount.zero': 'Yalnızca CPU, ortam hazırlığı için.',
|
||||
'gpuservice.instance.stock': 'Stok',
|
||||
'gpuservice.instance.sliced': 'Bölünmüş',
|
||||
'gpuservice.instance.memory': 'VRAM',
|
||||
'gpuservice.instance.memory': 'Bellek',
|
||||
'gpuservice.instance.ram': 'RAM',
|
||||
'gpuservice.instance.disk': 'Disk',
|
||||
'gpuservice.table.count': 'Sayı',
|
||||
'gpuservice.instance.disk.system': 'Sistem Diski',
|
||||
'gpuservice.instance.disk.ephemeral': 'Geçici Depolama',
|
||||
'gpuservice.instance.disk.persistent': 'Kalıcı Depolama',
|
||||
'gpuservice.instance.search.type.placeholder': 'Ada göre ara',
|
||||
'gpuservice.instance.search.template.placeholder':
|
||||
'Şablon adına, imaja veya bağlama yoluna göre ara',
|
||||
@@ -151,8 +141,9 @@ export default {
|
||||
'gpuservice.storage.accessMode': 'Erişim Modu',
|
||||
'gpuservice.storage.persistent': 'Kalıcı',
|
||||
'gpuservice.storage.temporary': 'Geçici',
|
||||
'gpuservice.storage.persistentVolume': 'Kalıcı',
|
||||
'gpuservice.storage.persistentVolume.required': 'Lütfen bir depolama seçin',
|
||||
'gpuservice.storage.persistentVolume': 'Kalıcı Hacim',
|
||||
'gpuservice.storage.persistentVolume.required':
|
||||
'Lütfen bir kalıcı hacim seçin',
|
||||
'gpuservice.storage.persistentVolume.capacity': 'Kapasite (GB)',
|
||||
'gpuservice.storage.persistentVolume.capacity.required':
|
||||
'Lütfen kapasiteyi girin',
|
||||
@@ -166,6 +157,5 @@ export default {
|
||||
'gpuservice.storage.temporary.tips':
|
||||
'Data is cleared when the instance stops.',
|
||||
'gpuservice.storage.persistentVolume.tips':
|
||||
'Data persists across instance restarts. Persistent volumes remain intact after instance termination and can be shared by multiple instances.',
|
||||
'gpuservice.form.storage.select': 'Depolamayı Seç'
|
||||
'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.'
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ export default {
|
||||
'menu.playground.text2images': 'Görsel',
|
||||
'menu.playground.video': 'Video',
|
||||
'menu.compare': 'Karşılaştır',
|
||||
'menu.models': 'Model Hizmetleri',
|
||||
'menu.models': 'Modeller',
|
||||
'menu.models.modelList': 'Dağıt ve Yönet',
|
||||
'menu.models.modelCatalog': 'Katalog',
|
||||
'menu.models.catalog': 'Model Kataloğu',
|
||||
@@ -25,23 +25,20 @@ export default {
|
||||
'menu.users': 'Kullanıcılar',
|
||||
'menu.resources.workers': 'İşçi Düğümler',
|
||||
'menu.resources.gpus': "GPU'lar",
|
||||
'menu.models.modelfiles': 'Model Dosyaları',
|
||||
'menu.resources.modelfiles': 'Model Dosyaları',
|
||||
'menu.accessControl': 'Erişim Kontrolü',
|
||||
'menu.accessControl.apikeys': 'API Anahtarları',
|
||||
'menu.accessControl.users': 'Kullanıcılar',
|
||||
'menu.accessControl.organizations': 'Organizasyonlar',
|
||||
'menu.profile': 'Preferences',
|
||||
'menu.profile': 'Profil',
|
||||
'menu.login': 'Giriş',
|
||||
'menu.usage': 'Kullanım',
|
||||
'menu.usage.usage': 'Kullanım',
|
||||
'menu.billingAndUsage': 'Kullanım ve Faturalandırma',
|
||||
'menu.billingAndUsage.usage': 'Kullanım',
|
||||
'menu.404': '404',
|
||||
'menu.resources.clusters': 'Kümeler',
|
||||
'menu.resources.credentials': 'Bulut Kimlik Bilgileri',
|
||||
'menu.resources.clusterDetail': 'Küme Detayı',
|
||||
'menu.resources.clusterCreate': 'Küme Oluştur',
|
||||
'menu.models.backendsList': 'Çıkarım Altyapıları',
|
||||
'menu.clusterManagement': 'Küme Yönetimi',
|
||||
'menu.clusterManagement.clusters': 'Kümeler',
|
||||
'menu.clusterManagement.credentials': 'Bulut Kimlik Bilgileri',
|
||||
'menu.clusterManagement.clusterDetail': 'Küme Detayı',
|
||||
'menu.clusterManagement.clusterCreate': 'Küme Oluştur',
|
||||
'menu.resources.backendsList': 'Çıkarım Altyapıları',
|
||||
'menu.models.instances': 'Instances',
|
||||
'menu.settings': 'Settings',
|
||||
'menu.gpuService': 'GPU Service',
|
||||
|
||||
@@ -291,7 +291,7 @@ export default {
|
||||
'models.instance.startHistory': 'Run History',
|
||||
'models.instance.startHistory.tips':
|
||||
'Shows logs from the run before the last error-triggered restart.',
|
||||
'models.form.lora.label': 'LoRA Adapters',
|
||||
'models.form.lora.label': 'LoRA Adapter',
|
||||
'models.form.lora.add': 'Add LoRA Adapter',
|
||||
'models.form.lora.select': 'Select LoRA',
|
||||
'models.form.lora.name': 'LoRA name',
|
||||
|
||||
@@ -38,12 +38,9 @@ export default {
|
||||
'noresult.catalog.nofound': 'Eşleşen model bulunamadı.',
|
||||
'noresult.resources.cluster':
|
||||
'Kullanılabilir küme yok. Başlamak için bir küme ekleyin.',
|
||||
'noresult.resources.k8sCluster':
|
||||
'Kullanılabilir küme yok. Başlamak için bir Kubernetes kümesi ekleyin.',
|
||||
'noresult.resources.worker':
|
||||
'Kullanılabilir işçi düğüm yok. Başlamak için bir işçi düğüm ekleyin.',
|
||||
'noresult.resources.gotocluster': 'İlk Kümenizi Oluşturun',
|
||||
'noresult.resources.addk8scluster': 'Kubernetes Kümesi Ekle',
|
||||
'noresult.resources.gotoworker': 'İşçi Düğüm Ekle',
|
||||
'noresult.benchmark.title': 'Kıyaslama Yok',
|
||||
'noresult.benchmark.subTitle': 'Henüz kıyaslama eklenmedi.',
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
export default {
|
||||
'organizations.upsell.title': 'Organizations are an Enterprise feature',
|
||||
'organizations.upsell.subtitle':
|
||||
'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to GPUStack Enterprise to manage organizations.',
|
||||
'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'
|
||||
};
|
||||
@@ -17,15 +17,9 @@ export default {
|
||||
'usage.table.user.apiKeysUsed': 'API Keys Used',
|
||||
'usage.table.lastActive': 'Last Active',
|
||||
'usage.filter.granularity': 'Granularity',
|
||||
'usage.filter.granularity.hour': 'Saat',
|
||||
'usage.filter.granularity.day': 'Day',
|
||||
'usage.filter.granularity.week': 'Week',
|
||||
'usage.filter.granularity.month': 'Month',
|
||||
'usage.tabs.summary': 'Özet',
|
||||
'usage.tabs.tokens': 'Token',
|
||||
'usage.tabs.gpuInstances': 'GPU Örnekleri',
|
||||
'usage.tabs.storage': 'Depolama',
|
||||
'usage.tabs.resourceEvents': 'Kaynak Olayları',
|
||||
'usage.tabs.models': 'Models',
|
||||
'usage.tabs.apikeys': 'API Keys',
|
||||
'usage.tabs.users': 'User',
|
||||
@@ -38,69 +32,5 @@ export default {
|
||||
'usage.chart.cached': 'Cached',
|
||||
'usage.chart.uncached': 'Uncached',
|
||||
'usage.chart.inputTokensCached': 'Input Tokens (Cached/Uncached)',
|
||||
'usage.table.inputTokensCached': 'Input Tokens Cached',
|
||||
|
||||
// --- Resource usage: shared metrics & units ---
|
||||
'usage.metric.tokens': 'Token',
|
||||
'usage.metric.input': 'Girdi',
|
||||
'usage.metric.output': 'Çıktı',
|
||||
'usage.metric.gpuHours': 'GPU Saati',
|
||||
'usage.metric.instanceHours': 'Örnek Saati',
|
||||
'usage.metric.gbDays': 'GB-Gün',
|
||||
'usage.metric.gbHours': 'GB-Saat',
|
||||
'usage.metric.activeUsers': 'Aktif Kullanıcılar',
|
||||
'usage.metric.activeInstances': 'Aktif Örnekler',
|
||||
'usage.metric.activeStorage': 'Aktif Depolama',
|
||||
'usage.metric.activeVolumes': 'Aktif Birimler',
|
||||
'usage.metric.storageTypes': 'Depolama Türleri',
|
||||
'usage.metric.gpuHours.tip':
|
||||
'Örnek çalışma süresinin GPU sayısına göre ağırlıklandırılmış hali: N GPU kullanan bir örnek H saat çalıştığında N × H GPU-saat olarak sayılır. Her örnek tek bir GPU kullandığında Örnek Saati ile eşittir.',
|
||||
'usage.metric.instanceHours.tip':
|
||||
'Her örneğin kullandığı GPU sayısından bağımsız olarak tüm örneklerin toplam çalışma süresi. 2 saat çalışan bir örnek = 2 örnek-saat.',
|
||||
'usage.metric.gbDays.tip':
|
||||
'Depolama kapasitesinin zamana göre integrali, GB × gün cinsinden: 5 gün boyunca tutulan 10 GB = 50 GB-gün. (= GB-Saat ÷ 24)',
|
||||
'usage.metric.gbHours.tip':
|
||||
'Depolama kapasitesinin zamana göre integrali, GB × saat cinsinden: 5 saat boyunca tutulan 10 GB = 50 GB-saat.',
|
||||
|
||||
// --- Resource usage: common table / labels ---
|
||||
'usage.common.noData': 'Veri yok',
|
||||
'usage.common.unknown': 'bilinmiyor',
|
||||
'usage.table.date': 'Tarih',
|
||||
'usage.table.name': 'Ad',
|
||||
'usage.table.user': 'Kullanıcı',
|
||||
'usage.table.users': 'Kullanıcılar',
|
||||
'usage.table.type': 'Tür',
|
||||
'usage.table.instance': 'Örnek',
|
||||
'usage.table.instanceType': 'Örnek Türü',
|
||||
'usage.table.instanceTypes': 'Örnek Türleri',
|
||||
'usage.table.instances': 'Örnekler',
|
||||
'usage.table.capacity': 'Kapasite',
|
||||
'usage.export.tableNamed': 'Tablo Verilerini Dışa Aktar — {name}',
|
||||
|
||||
// --- Summary tab ---
|
||||
'usage.summary.compute': 'Hesaplama',
|
||||
'usage.summary.tokensOverTime': 'Zaman içinde Token',
|
||||
'usage.summary.gpuHoursOverTime': 'Zaman içinde GPU Saati',
|
||||
'usage.summary.gbDaysOverTime': 'Zaman içinde GB-Gün',
|
||||
|
||||
// --- GPU Instances / Storage filters ---
|
||||
'usage.filter.instance': 'Örneğe göre filtrele',
|
||||
'usage.filter.storage': 'Depolamaya göre filtrele',
|
||||
|
||||
// --- Resource events ---
|
||||
'usage.events.resourceType': 'Kaynak türü',
|
||||
'usage.events.eventType': 'Olay türü',
|
||||
'usage.events.col.time': 'Zaman',
|
||||
'usage.events.col.resource': 'Kaynak',
|
||||
'usage.events.col.event': 'Olay',
|
||||
'usage.events.col.message': 'Mesaj',
|
||||
'usage.events.resource.gpuInstance': 'GPU Örneği',
|
||||
'usage.events.resource.cpuInstance': 'CPU Örneği',
|
||||
'usage.events.type.created': 'Oluşturuldu',
|
||||
'usage.events.type.deleted': 'Silindi',
|
||||
'usage.events.type.started': 'Başlatıldı',
|
||||
'usage.events.type.stopped': 'Durduruldu',
|
||||
'usage.events.type.updated': 'Güncellendi',
|
||||
'usage.events.type.attached': 'Eklendi',
|
||||
'usage.events.type.detached': 'Ayrıldı'
|
||||
'usage.table.inputTokensCached': 'Input Tokens Cached'
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ export default {
|
||||
'vendor.hygon': 'Hygon',
|
||||
'vendor.moorthreads': 'Moore Threads',
|
||||
'vendor.iluvatar': 'Iluvatar',
|
||||
'vendor.metax': 'MetaX',
|
||||
'vendor.metax': 'Metax',
|
||||
'vendor.cambricon': 'Cambricon',
|
||||
'vendor.thead': 'T-Head PPU'
|
||||
};
|
||||
|
||||
@@ -22,7 +22,5 @@ export default {
|
||||
'apikeys.accessScope.inference': '推理接口',
|
||||
'apikeys.access.permissions': '访问权限',
|
||||
'apikeys.type.auto': '自动生成',
|
||||
'apikeys.type.custom': '自定义',
|
||||
'apikeys.button.ipConfig': 'IP 访问控制',
|
||||
'quotaLimits.button.title': '配额限制'
|
||||
'apikeys.type.custom': '自定义'
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export default {
|
||||
'clusters.create.noRegions': '无可用的区域',
|
||||
'clusters.workerpool.batchSize.desc': '节点池中同时创建的节点数量。',
|
||||
'clusters.create.addworker.tips':
|
||||
'在执行以下命令之前,请确保已满足<a href={link} target="_blank">先决条件</a>。',
|
||||
'在执行以下命令之前,请确保已满足 {label} 的<a href={link} target="_blank">先决条件</a>。',
|
||||
'clusters.create.addCommand.tips':
|
||||
'在需要添加的节点上运行以下命令,将其加入到集群中。',
|
||||
'clusters.create.addCommand.k8s.tips':
|
||||
@@ -68,6 +68,8 @@ export default {
|
||||
'<span class="bold-text">非 Docker</span> 集群请前往集群页面注册集群或管理节点池。',
|
||||
'clusters.addworker.selectGPU': '选择 GPU 厂商',
|
||||
'clusters.addworker.selectGPU.multiTag': '可多选',
|
||||
'clusters.addworker.selectGPU.singleOnly':
|
||||
'当前所选厂商未在集群的 GPU 厂商覆盖(override node selector)中配置,仅支持单选。如需多选,请先选择 override 中已配置的厂商。',
|
||||
'clusters.addworker.checkEnv': '检查环境',
|
||||
'clusters.addworker.specifyArgs': '指定参数',
|
||||
'clusters.addworker.runCommand': '运行指令',
|
||||
@@ -108,10 +110,6 @@ export default {
|
||||
'已将 {count} 个新节点添加到集群中。',
|
||||
'clusters.create.serverUrl': 'GPUStack Server 节点地址',
|
||||
'clusters.create.workerConfig': '节点配置',
|
||||
'clusters.edit.k8sOptions.changed.tip':
|
||||
'您已修改 Kubernetes 选项,需要在目标集群上重新运行注册命令才会生效。',
|
||||
'clusters.edit.workerConfig.tip':
|
||||
'修改节点配置后,需要重启对应节点才会生效。',
|
||||
'clusters.addworker.containerName': '节点容器名称',
|
||||
'clusters.addworker.containerName.tips': '为节点容器指定一个名称。',
|
||||
'clusters.addworker.dataVolume': 'GPUStack 数据卷',
|
||||
@@ -159,10 +157,6 @@ export default {
|
||||
'clusters.volume.configMap.name': '配置名称',
|
||||
'clusters.volume.configMap.optional': '可选',
|
||||
'clusters.volume.add': '添加卷挂载',
|
||||
'clusters.systemDefaultContainerRegistry.title': '默认容器镜像仓库',
|
||||
'clusters.systemDefaultContainerRegistry.tip':
|
||||
'用于解析该集群 GPUStack 镜像的默认镜像仓库。未设置时回退到服务端默认值。',
|
||||
'clusters.k8sOptions.title': 'Kubernetes 部署选项',
|
||||
'clusters.imageCredentials.title': '镜像仓库凭证',
|
||||
'clusters.imageCredentials.add': '添加凭证',
|
||||
'clusters.imageCredentials.registry': '镜像仓库地址',
|
||||
@@ -171,20 +165,16 @@ export default {
|
||||
'clusters.nodeSelector.title': '节点选择器',
|
||||
'clusters.nodeSelector.tip':
|
||||
'应用到每个 worker DaemonSet 的 Pod nodeSelector,只有标签匹配的节点才会被调度运行 worker。',
|
||||
'clusters.operatorImage.title': 'Operator 镜像',
|
||||
'clusters.operatorImage.tip':
|
||||
'GPUStack Operator 容器镜像的覆盖值。留空则使用服务端默认值。',
|
||||
'clusters.namespace.title': '命名空间',
|
||||
'clusters.namespace.tip':
|
||||
'集群清单渲染所使用的 Kubernetes 命名空间。留空则使用 gpustack-system。',
|
||||
'clusters.clusterType.title': '集群类型',
|
||||
'clusters.modelService.title': '模型服务',
|
||||
'clusters.modelService.tip':
|
||||
'适用于大模型推理与 API 服务化场景,例如对外提供模型 API 与 Token 服务能力。',
|
||||
'clusters.gpuInstances.title': 'GPU 服务',
|
||||
'clusters.gpuInstances.tip':
|
||||
'适用于按需分配 GPU 计算资源的场景,例如交互式开发、训练任务或自定义运行环境。',
|
||||
'clusters.gpuInstances.staticAddress': 'GPU 服务静态访问地址',
|
||||
'clusters.gpuInstances.staticAddress.tip':
|
||||
'Operator 访问该集群 GPU 实例所使用的静态地址(例如 LoadBalancer VIP)。可选。'
|
||||
'clusters.gpuVendorOverrides.title': 'GPU 厂商覆盖配置',
|
||||
'clusters.gpuVendorOverrides.validate.emptySelector':
|
||||
'{vendor} 的覆盖配置至少需要一项 nodeSelector —— 空覆盖会让多 vendor manifest 无法定位该 runtime 的节点。',
|
||||
'clusters.gpuVendorOverrides.validate.duplicate':
|
||||
'{v1} 与 {v2} 的覆盖配置使用了完全相同的 nodeSelector。每个 vendor 必须对应不同的节点集,否则它们的 worker DaemonSet 会相互争抢同一批节点。',
|
||||
'clusters.gpuVendorOverrides.validate.keyConflict':
|
||||
'{vendor} 的覆盖配置重用了基础节点选择器中已有的 key({keys})—— 这会让 CPU worker 同时"要求"和"禁止"这些 key,永远无法被调度。',
|
||||
'clusters.gpuVendorOverrides.tip':
|
||||
'集群中存在多个 GPU 厂商时必填。每项的 nodeSelector 将该厂商的 worker DaemonSet 固定到匹配的节点,同时 CPU worker 通过 DoesNotExist 节点亲和性避开这些节点。',
|
||||
'clusters.gpuVendorOverrides.add': '添加覆盖配置',
|
||||
'clusters.gpuVendorOverrides.vendor': 'GPU 厂商',
|
||||
'clusters.gpuVendorOverrides.nodeSelector': '节点选择器'
|
||||
};
|
||||
|
||||
@@ -44,7 +44,6 @@ export default {
|
||||
'common.button.rollback': '回滚',
|
||||
'common.button.new': '新建{ text }',
|
||||
'common.button.upgrade': '升级',
|
||||
'common.enterprise.feature': 'GPUStack 企业版可用',
|
||||
'common.input.holder': '请输入',
|
||||
'common.holder.search': '搜索',
|
||||
'common.button.edit': '编辑',
|
||||
@@ -52,8 +51,8 @@ export default {
|
||||
'common.button.confirm': '确定',
|
||||
'common.button.viewlog': '查看日志',
|
||||
'common.button.viewevent': '查看事件',
|
||||
'common.button.recreate': '重新创建',
|
||||
'common.table.operation': '操作',
|
||||
'common.table.creator': '创建者',
|
||||
'common.table.createTime': '创建时间',
|
||||
'common.table.updateTime': '更新时间',
|
||||
'common.table.description': '描述',
|
||||
@@ -234,7 +233,6 @@ export default {
|
||||
'common.text.tips': '提示',
|
||||
'settings.system': '系统设置',
|
||||
'common.filter.byId': '按 ID 查询',
|
||||
'common.filter.byCreator': '按创建者筛选',
|
||||
'common.appearance': '外观',
|
||||
'common.appearance.dark': '深色',
|
||||
'common.appearance.light': '浅色',
|
||||
@@ -280,7 +278,5 @@ export default {
|
||||
'common.image.limit.height': '图片高度须为{height}。',
|
||||
'common.remaining': '剩余 {count}',
|
||||
'common.max': '最大 {count}',
|
||||
'common.max.count': '{label} 数量',
|
||||
'common.validate.group': '请填写完整的{group}配置',
|
||||
'common.preferences': '偏好设置'
|
||||
'common.validate.group': '请填写完整的{group}配置'
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ export default {
|
||||
'dashboard.workers': '节点',
|
||||
'dashboard.deployments': '部署',
|
||||
'dashboard.clusters': '集群',
|
||||
'dashboard.totalgpus': 'GPU',
|
||||
'dashboard.totalgpus': 'GPUs',
|
||||
'dashboard.systemload': '系统负载',
|
||||
'dashboard.memory': '内存',
|
||||
'dashboard.vram': '显存',
|
||||
|
||||
@@ -13,13 +13,10 @@ export default {
|
||||
'gpuservice.template.command.placeholder':
|
||||
'使用空格分隔参数;含空格的参数请用引号包裹,例如:/bin/bash -c "echo hello world"',
|
||||
'gpuservice.template.mountPath': '挂载路径',
|
||||
'gpuservice.template.mountPath.tips':
|
||||
'在使用该模板创建实例时,存储卷默认挂载的路径,可用于持久化实例运行过程中需要保留的数据。',
|
||||
'gpuservice.template.containerDisk': '容器磁盘(GB)',
|
||||
'gpuservice.template.containerDisk.tips': '容器系统盘大小。',
|
||||
'gpuservice.template.memory': '内存(GB)',
|
||||
'gpuservice.instance.containerDisk.remaining': '容器磁盘(最大 {count} GB)',
|
||||
'gpuservice.instance.memory.remaining': '内存(最大 {count} GB)',
|
||||
'gpuservice.template.containerDisk': '容器磁盘(GB)',
|
||||
'gpuservice.template.memory': '内存(GB)',
|
||||
'gpuservice.instance.containerDisk.remaining': '容器磁盘(最大{count}GB)',
|
||||
'gpuservice.instance.memory.remaining': '内存(最大{count}GB)',
|
||||
'gpuservice.template.displayName': '显示名称',
|
||||
'gpuservice.template.displayName.max': '显示名称不能超过 63 个字符',
|
||||
'gpuservice.template.ports': '端口',
|
||||
@@ -95,13 +92,10 @@ export default {
|
||||
'gpuservice.instance.templates': '实例模板',
|
||||
'gpuservice.instance.section.storage': '存储卷',
|
||||
'gpuservice.instance.type.required': '请选择实例类型',
|
||||
'gpuservice.instance.type.noAvailable': '无可用的实例类型',
|
||||
'gpuservice.instance.gpuCount': 'GPU 数量',
|
||||
'gpuservice.instance.gpuCount.required': '请输入 GPU 数量',
|
||||
'gpuservice.instance.gpuCount.max': '最多选择 {count} 张卡',
|
||||
'gpuservice.instance.gpuCount.min': '至少选择 {count} 张卡',
|
||||
'gpuservice.instance.cpuCount.max': '最多选择 {count} 核',
|
||||
'gpuservice.instance.cpuCount.min': '至少选择 {count} 核',
|
||||
'gpuservice.instance.gpuCount.noAvailable':
|
||||
'没有可用的 GPU 资源,请选择其他实例类型。',
|
||||
'gpuservice.instance.gpuCount.zero': '仅使用 CPU,用于环境准备。',
|
||||
@@ -110,10 +104,6 @@ export default {
|
||||
'gpuservice.instance.memory': '显存',
|
||||
'gpuservice.instance.ram': '内存',
|
||||
'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.template.placeholder':
|
||||
'搜索模板名称、镜像或挂载路径',
|
||||
@@ -144,17 +134,16 @@ export default {
|
||||
'gpuservice.storage.accessMode': '访问模式',
|
||||
'gpuservice.storage.persistent': '持久',
|
||||
'gpuservice.storage.temporary': '临时',
|
||||
'gpuservice.storage.persistentVolume': '持久',
|
||||
'gpuservice.storage.persistentVolume': '持久卷',
|
||||
'gpuservice.storage.temporary.tips': '实例停止后,数据将被清除。',
|
||||
'gpuservice.storage.persistentVolume.tips':
|
||||
'数据在实例重启后仍会保留。持久卷不会随实例终止而删除,并可被多个实例共享。',
|
||||
'gpuservice.storage.persistentVolume.required': '请选择存储',
|
||||
'数据在实例重启后仍会保留,仅在实例被终止时删除。无法被其它实例共享。',
|
||||
'gpuservice.storage.persistentVolume.required': '请选择持久卷',
|
||||
'gpuservice.storage.persistentVolume.capacity': '容量(GB)',
|
||||
'gpuservice.storage.persistentVolume.capacity.required': '请输入容量',
|
||||
'gpuservice.storage.persistentVolume.releaseWithInstance': '随实例释放',
|
||||
'gpuservice.storage.tempCapacity': '容量(GB)',
|
||||
'gpuservice.storage.tempCapacity.required': '请输入临时存储容量',
|
||||
'gpuservice.form.rule.name':
|
||||
'由小写字母、数字和 "-" 组成,以字母或数字开头和结尾,不能包含连续的 "-",最多 63 个字符。',
|
||||
'gpuservice.form.storage.select': '选择存储'
|
||||
'由小写字母、数字和 "-" 组成,以字母或数字开头和结尾,不能包含连续的 "-",最多 63 个字符。'
|
||||
};
|
||||
|
||||
+10
-13
@@ -8,7 +8,7 @@ export default {
|
||||
'menu.playground.text2images': '图像',
|
||||
'menu.playground.video': '视频',
|
||||
'menu.compare': '多模型对比',
|
||||
'menu.models': '模型服务',
|
||||
'menu.models': '模型',
|
||||
'menu.models.modelList': '部署与管理',
|
||||
'menu.models.modelCatalog': '模型库',
|
||||
'menu.models.deployment': '部署',
|
||||
@@ -25,25 +25,22 @@ export default {
|
||||
'menu.resources': '资源',
|
||||
'menu.apikeys': 'API 密钥',
|
||||
'menu.users': '用户',
|
||||
'menu.profile': '偏好',
|
||||
'menu.profile': '个人信息',
|
||||
'menu.login': '登录',
|
||||
'menu.usage': '使用量',
|
||||
'menu.usage.usage': '使用量',
|
||||
'menu.billingAndUsage': '用量与计费',
|
||||
'menu.billingAndUsage.usage': '用量统计',
|
||||
'menu.404': '404',
|
||||
'menu.resources.workers': '节点',
|
||||
'menu.resources.gpus': 'GPU',
|
||||
'menu.models.modelfiles': '模型文件',
|
||||
'menu.resources.gpus': 'GPUs',
|
||||
'menu.resources.modelfiles': '模型文件',
|
||||
'menu.accessControl': '访问控制',
|
||||
'menu.accessControl.apikeys': 'API 密钥',
|
||||
'menu.accessControl.users': '用户',
|
||||
'menu.accessControl.organizations': '组织',
|
||||
'menu.resources.clusters': '集群',
|
||||
'menu.resources.credentials': '云凭证',
|
||||
'menu.resources.clusterDetail': '集群详情',
|
||||
'menu.resources.clusterCreate': '创建集群',
|
||||
'menu.models.backendsList': '推理后端',
|
||||
'menu.clusterManagement': '集群管理',
|
||||
'menu.clusterManagement.clusters': '集群',
|
||||
'menu.clusterManagement.credentials': '云凭证',
|
||||
'menu.clusterManagement.clusterDetail': '集群详情',
|
||||
'menu.clusterManagement.clusterCreate': '创建集群',
|
||||
'menu.resources.backendsList': '推理后端',
|
||||
'menu.gpuService': 'GPU 服务',
|
||||
'menu.gpuService.instances': 'GPU 实例',
|
||||
'menu.gpuService.templates': '实例模板',
|
||||
|
||||
@@ -35,11 +35,8 @@ export default {
|
||||
'noresult.catalog.subTitle': '尚未配置任何模型。',
|
||||
'noresult.catalog.nofound': '未找到匹配的模型',
|
||||
'noresult.resources.cluster': '暂无可用集群,请添加集群以开始使用。',
|
||||
'noresult.resources.k8sCluster':
|
||||
'暂无可用集群,请添加 Kubernetes 集群以开始使用。',
|
||||
'noresult.resources.worker': '暂无可用节点,请添加节点以开始使用。',
|
||||
'noresult.resources.gotocluster': '创建您的第一个集群',
|
||||
'noresult.resources.addk8scluster': '添加 Kubernetes 集群',
|
||||
'noresult.resources.gotoworker': '添加节点',
|
||||
'noresult.benchmark.title': '暂无基准测试',
|
||||
'noresult.benchmark.subTitle': '尚未添加任何基准测试。',
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
export default {
|
||||
'organizations.upsell.title': '组织是企业版功能',
|
||||
'organizations.upsell.subtitle':
|
||||
'多租户可在团队间隔离用户、资源与配额。升级到 GPUStack 企业版即可管理组织。',
|
||||
'organizations.upsell.featuresTitle': '企业版包含的能力',
|
||||
'organizations.upsell.feature.orgs': '创建组织来分组用户并隔离工作负载',
|
||||
'organizations.upsell.feature.members': '为每个组织管理成员与角色',
|
||||
'organizations.upsell.feature.quotas': '为每个组织设置资源与 Token 配额',
|
||||
'organizations.upsell.feature.isolation':
|
||||
'按组织隔离 API 密钥、模型部署与资源',
|
||||
'organizations.upsell.cta': '了解企业版'
|
||||
};
|
||||
@@ -17,15 +17,9 @@ export default {
|
||||
'usage.table.user.apiKeysUsed': '使用的 API 密钥',
|
||||
'usage.table.lastActive': '最后活跃时间',
|
||||
'usage.filter.granularity': '粒度',
|
||||
'usage.filter.granularity.hour': '按小时',
|
||||
'usage.filter.granularity.day': '按天',
|
||||
'usage.filter.granularity.week': '按周',
|
||||
'usage.filter.granularity.month': '按月',
|
||||
'usage.tabs.summary': '总览',
|
||||
'usage.tabs.tokens': 'Token',
|
||||
'usage.tabs.gpuInstances': 'GPU 实例',
|
||||
'usage.tabs.storage': '存储',
|
||||
'usage.tabs.resourceEvents': '资源事件',
|
||||
'usage.tabs.models': '模型',
|
||||
'usage.tabs.apikeys': 'API 密钥',
|
||||
'usage.tabs.users': '用户',
|
||||
@@ -38,69 +32,5 @@ export default {
|
||||
'usage.chart.cached': '缓存',
|
||||
'usage.chart.uncached': '非缓存',
|
||||
'usage.chart.inputTokensCached': '输入 Token 数(缓存/非缓存)',
|
||||
'usage.table.inputTokensCached': '输入 Token 缓存数',
|
||||
|
||||
// --- Resource usage: shared metrics & units ---
|
||||
'usage.metric.tokens': 'Token 数',
|
||||
'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 数量无关。一个实例运行 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': 'Token 数趋势',
|
||||
'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.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': '已卸载'
|
||||
'usage.table.inputTokensCached': '输入 Token 缓存数'
|
||||
};
|
||||
|
||||
@@ -100,11 +100,7 @@ const BarChart: React.FC<BarChartProps> = (props) => {
|
||||
data: processedData,
|
||||
type: 'bar',
|
||||
barMaxWidth: 20,
|
||||
// Keep a small floor only — a large barMinWidth would force wide bars
|
||||
// when there are many categories (e.g. hourly buckets), squeezing out
|
||||
// the category gap so bars look fused. A low floor lets barCategoryGap
|
||||
// win, so even dense hourly views keep visible gaps.
|
||||
barMinWidth: 2,
|
||||
barMinWidth: 8,
|
||||
barGap: '30%',
|
||||
barCategoryGap: '50%',
|
||||
...(stack === false || stack === undefined ? {} : { stack }),
|
||||
@@ -126,7 +122,7 @@ const BarChart: React.FC<BarChartProps> = (props) => {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: title ? 30 : 10,
|
||||
bottom: 0,
|
||||
bottom: 28,
|
||||
containLabel: true
|
||||
},
|
||||
tooltip: {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { Input as CInput } from '@gpustack/core-ui';
|
||||
import { Form } from 'antd';
|
||||
import type { NamePath } from 'antd/es/form/interface';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
interface OwnerPrincipalIdFieldProps {
|
||||
name?: NamePath;
|
||||
}
|
||||
|
||||
const OwnerPrincipalIdField: React.FC<OwnerPrincipalIdFieldProps> = ({
|
||||
name = 'owner_principal_id'
|
||||
}) => {
|
||||
const currentCluster = useAtomValue(currentClusterAtom);
|
||||
const ownerPrincipalId = currentCluster?.owner_principal_id;
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
useEffect(() => {
|
||||
if (ownerPrincipalId != null) {
|
||||
form.setFieldValue(name, ownerPrincipalId);
|
||||
}
|
||||
}, [ownerPrincipalId, name, form]);
|
||||
|
||||
return (
|
||||
<Form.Item name={name} hidden>
|
||||
<CInput.Input />
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export default OwnerPrincipalIdField;
|
||||
@@ -19,11 +19,6 @@ interface PieChartProps {
|
||||
totalLabel?: string;
|
||||
}
|
||||
|
||||
// Donut sits on the left; a vertical legend hugs it just to the right and
|
||||
// scrolls when there are many entries.
|
||||
const CENTER_X = '32%';
|
||||
const round2 = (n: number) => Math.round((Number(n) || 0) * 100) / 100;
|
||||
|
||||
const PieChart: React.FC<PieChartProps> = ({
|
||||
data,
|
||||
height = 300,
|
||||
@@ -45,20 +40,8 @@ const PieChart: React.FC<PieChartProps> = ({
|
||||
const options = useMemo(
|
||||
() => ({
|
||||
color: colors,
|
||||
grid: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
containLabel: true
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
position: 'top',
|
||||
// Keep the tooltip inside the chart box — the donut sits on the left, so
|
||||
// a left-slice tooltip could otherwise spill out and get hidden behind
|
||||
// the side menu.
|
||||
confine: false,
|
||||
backgroundColor: token.colorBgElevated,
|
||||
borderColor: 'transparent',
|
||||
formatter: (params: any) => {
|
||||
@@ -68,7 +51,7 @@ const PieChart: React.FC<PieChartProps> = ({
|
||||
<span class="tooltip-item-dot" style="border-radius:50%;background-color:${params.color};"></span>
|
||||
<span class="tooltip-item-title">${params.name}</span>:
|
||||
</span>
|
||||
<span class="tooltip-value">${formatLargeNumber(round2(params.value))} (${params.percent}%)</span>
|
||||
<span class="tooltip-value">${formatLargeNumber(params.value)}</span>
|
||||
</span>
|
||||
</div>`;
|
||||
}
|
||||
@@ -76,21 +59,18 @@ const PieChart: React.FC<PieChartProps> = ({
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
orient: 'vertical',
|
||||
left: '56%',
|
||||
top: 'middle',
|
||||
right: 0,
|
||||
top: 18,
|
||||
bottom: 18,
|
||||
itemWidth: 8,
|
||||
itemHeight: 8,
|
||||
itemGap: 10,
|
||||
// Long names are truncated in the legend; hovering shows the full name
|
||||
// in a tooltip.
|
||||
tooltip: { show: true },
|
||||
itemGap: 12,
|
||||
textStyle: {
|
||||
color: token.colorTextTertiary,
|
||||
overflow: 'truncate',
|
||||
width: 120
|
||||
width: 180
|
||||
},
|
||||
pageTextStyle: { color: token.colorTextTertiary },
|
||||
pageIconSize: 10,
|
||||
pageIconColor: token.colorTextTertiary,
|
||||
pageIconInactiveColor: token.colorTextDisabled,
|
||||
data: data.map((item) => item.name),
|
||||
@@ -99,19 +79,24 @@ const PieChart: React.FC<PieChartProps> = ({
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['54%', '78%'],
|
||||
center: [CENTER_X, '50%'],
|
||||
radius: ['52%', '72%'],
|
||||
center: ['34%', '50%'],
|
||||
avoidLabelOverlap: true,
|
||||
// No on-arc label — the percentage lives in the tooltip instead, so
|
||||
// hovering shows value + percent in one place.
|
||||
label: { show: false },
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
scale: true
|
||||
show: true,
|
||||
formatter: (params: any) => `${params.percent}%`,
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
color: token.colorText
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
labelLine: { show: false },
|
||||
data
|
||||
}
|
||||
]
|
||||
@@ -152,7 +137,7 @@ const PieChart: React.FC<PieChartProps> = ({
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: CENTER_X,
|
||||
left: '34%',
|
||||
top: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
display: 'flex',
|
||||
|
||||
@@ -98,11 +98,10 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
expires_in: getExpireValue(data.expires_in)
|
||||
};
|
||||
const res = await createApisKey({ data: params });
|
||||
onOk();
|
||||
|
||||
// if custom value
|
||||
if (data.custom) {
|
||||
onCancel();
|
||||
return;
|
||||
return onOk();
|
||||
}
|
||||
|
||||
setAPIKeyValue(res.value);
|
||||
@@ -112,7 +111,6 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
const updateAPIKey = async (data: FormData) => {
|
||||
await updateApisKey(currentData?.id as number, { data });
|
||||
onOk();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const handleOnOk = async (formdata: FormData) => {
|
||||
@@ -144,7 +142,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
};
|
||||
|
||||
const handleDone = () => {
|
||||
onCancel();
|
||||
onOk();
|
||||
};
|
||||
|
||||
const handleAfterOpenChange = (isOpen: boolean) => {
|
||||
|
||||
@@ -1,27 +1,15 @@
|
||||
// columns.ts
|
||||
import { tableSorter } from '@/config/settings';
|
||||
import { DashboardOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
AutoTooltip,
|
||||
DropdownButtons,
|
||||
IconFont,
|
||||
icons
|
||||
} from '@gpustack/core-ui';
|
||||
import { AutoTooltip, DropdownButtons, icons } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { MenuProps, Tag, Tooltip } from 'antd';
|
||||
import { MenuProps, Tag } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import dayjs from 'dayjs';
|
||||
import { useMemo } from 'react';
|
||||
import { ListItem } from '../config/types';
|
||||
import type { APIKeyConfigAction } from '../plugin';
|
||||
|
||||
type APIKeyAction = Omit<Global.ActionItem<ListItem>, 'disabled' | 'label'> & {
|
||||
// Per-row callback (function) is the host's default; placeholders use a
|
||||
// plain boolean to render the menu item grayed out unconditionally.
|
||||
disabled?: boolean | ((record: ListItem) => boolean);
|
||||
// ReactNode allowed so disabled placeholders can render a Tooltip-
|
||||
// wrapped label (paired with `locale: false`).
|
||||
label: string | React.ReactNode;
|
||||
type APIKeyAction = Global.ActionItem<ListItem> & {
|
||||
onClick?: (record: ListItem) => void;
|
||||
};
|
||||
|
||||
@@ -30,10 +18,7 @@ type RankedAction = APIKeyAction & { priority: number };
|
||||
interface ColumnsHookProps {
|
||||
handleSelect: (val: string, record: ListItem, item?: APIKeyAction) => void;
|
||||
sortOrder: string[];
|
||||
// Reveal the Creator column to callers who can see other users' keys
|
||||
// (platform admin or current-Org owner). Members only see their own
|
||||
// keys, so the column would be redundant for them.
|
||||
showCreator?: boolean;
|
||||
is_admin?: boolean;
|
||||
configActions?: APIKeyConfigAction[];
|
||||
// Dispatches the click for a plugin-contributed dropdown entry to the
|
||||
// controller `useCreate()` returned for that entry.
|
||||
@@ -43,7 +28,7 @@ interface ColumnsHookProps {
|
||||
const useModelsColumns = ({
|
||||
handleSelect,
|
||||
sortOrder,
|
||||
showCreator,
|
||||
is_admin,
|
||||
configActions = [],
|
||||
onConfigAction
|
||||
}: ColumnsHookProps): ColumnsType<ListItem> => {
|
||||
@@ -80,48 +65,10 @@ const useModelsColumns = ({
|
||||
onClick: (record: ListItem) => onConfigAction?.(a.key, record)
|
||||
}));
|
||||
|
||||
// Show disabled placeholders for IP Access Control / Quota Limit in
|
||||
// the OSS build only — when the enterprise plugin contributes the
|
||||
// real entry under the same key, skip the placeholder so the live
|
||||
// action takes over. Keeps the dropdown's surface area consistent
|
||||
// between editions while making the upgrade path discoverable.
|
||||
const pluginKeys = new Set(configActions.map((a) => a.key));
|
||||
const enterpriseTooltip = intl.formatMessage({
|
||||
id: 'common.enterprise.feature'
|
||||
});
|
||||
const enterprisePlaceholder = (labelId: string): React.ReactNode => (
|
||||
<Tooltip title={enterpriseTooltip} placement="left">
|
||||
<span style={{ display: 'inline-block' }}>
|
||||
{intl.formatMessage({ id: labelId })}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
const placeholders: RankedAction[] = [];
|
||||
if (!pluginKeys.has('ipConfig')) {
|
||||
placeholders.push({
|
||||
key: 'ipConfig',
|
||||
label: enterprisePlaceholder('apikeys.button.ipConfig'),
|
||||
locale: false,
|
||||
icon: <IconFont type="icon-safe-ip" />,
|
||||
disabled: true,
|
||||
priority: 12
|
||||
});
|
||||
}
|
||||
if (!pluginKeys.has('quotaLimit')) {
|
||||
placeholders.push({
|
||||
key: 'quotaLimit',
|
||||
label: enterprisePlaceholder('quotaLimits.button.title'),
|
||||
locale: false,
|
||||
icon: <DashboardOutlined />,
|
||||
disabled: true,
|
||||
priority: 14
|
||||
});
|
||||
}
|
||||
|
||||
return [...builtIns, ...fromPlugins, ...placeholders].sort(
|
||||
return [...builtIns, ...fromPlugins].sort(
|
||||
(a, b) => a.priority - b.priority
|
||||
);
|
||||
}, [intl, configActions, onConfigAction]);
|
||||
}, [configActions, onConfigAction]);
|
||||
|
||||
return useMemo(() => {
|
||||
return [
|
||||
@@ -162,6 +109,17 @@ const useModelsColumns = ({
|
||||
</AutoTooltip>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'users.table.username' }),
|
||||
dataIndex: 'user_name',
|
||||
key: 'user_name',
|
||||
hidden: !is_admin,
|
||||
render: (text: string, record: ListItem) => (
|
||||
<AutoTooltip ghost style={{ maxWidth: 200 }}>
|
||||
{text || '-'}
|
||||
</AutoTooltip>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'apikeys.form.expiretime' }),
|
||||
dataIndex: 'expires_at',
|
||||
@@ -229,17 +187,6 @@ const useModelsColumns = ({
|
||||
<AutoTooltip ghost>{text}</AutoTooltip>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.creator' }),
|
||||
dataIndex: 'user_name',
|
||||
key: 'user_name',
|
||||
hidden: !showCreator,
|
||||
render: (text: string) => (
|
||||
<AutoTooltip ghost style={{ maxWidth: 200 }}>
|
||||
{text || '-'}
|
||||
</AutoTooltip>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
||||
dataIndex: 'created_at',
|
||||
@@ -269,7 +216,7 @@ const useModelsColumns = ({
|
||||
)
|
||||
}
|
||||
];
|
||||
}, [intl, showCreator, handleSelect, actionList]);
|
||||
}, [intl, is_admin, handleSelect, actionList]);
|
||||
};
|
||||
|
||||
export default useModelsColumns;
|
||||
|
||||
@@ -3,8 +3,9 @@ import { PaginationKey } from '@/config/settings';
|
||||
import type { PageActionType } from '@/config/types';
|
||||
import useTableFetch from '@/hooks/use-table-fetch';
|
||||
import useQueryUserList from '@/pages/users/services/use-query-user-list';
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import { DeleteModal, FilterBar, IconFont, NoResult } from '@gpustack/core-ui';
|
||||
import { useAccess, useIntl } from '@umijs/max';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
|
||||
import { ConfigProvider, Table } from 'antd';
|
||||
import _ from 'lodash';
|
||||
@@ -21,12 +22,8 @@ import {
|
||||
} from './plugin';
|
||||
|
||||
const APIKeys: React.FC = () => {
|
||||
const access = useAccess();
|
||||
// `canSeeOrgAdmin` widens to Org owners in the enterprise build —
|
||||
// mirrors the BE's "platform admin OR current-Org owner" gate on
|
||||
// listing every key in scope. Personal/member users continue to see
|
||||
// only their own keys (`user_id: undefined`).
|
||||
const canSeeAllKeys = !!access.canSeeOrgAdmin;
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const currentUser = initialState?.currentUser;
|
||||
const {
|
||||
TABLE_SORT_DIRECTIONS,
|
||||
dataSource,
|
||||
@@ -48,7 +45,7 @@ const APIKeys: React.FC = () => {
|
||||
deleteAPI: deleteApisKey,
|
||||
contentForDelete: 'apikeys.table.apikeys',
|
||||
defaultQueryParams: {
|
||||
user_id: canSeeAllKeys ? '*' : undefined
|
||||
user_id: currentUser?.is_admin ? '*' : undefined
|
||||
}
|
||||
});
|
||||
const {
|
||||
@@ -97,15 +94,8 @@ const APIKeys: React.FC = () => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// `scope=current_org` limits the creator dropdown to members of the
|
||||
// active Org. Without it, an Org owner sees every user in the
|
||||
// system — most of whom can't own a key visible in this list, so
|
||||
// selecting them produces an empty result. The BE drops the
|
||||
// param silently when the request has no Org context, so callers
|
||||
// without an Org keep the full-directory behavior.
|
||||
fetchUserData({
|
||||
page: -1,
|
||||
scope: 'current_org'
|
||||
page: -1
|
||||
});
|
||||
return () => {
|
||||
cancelUserRequest();
|
||||
@@ -133,6 +123,12 @@ const APIKeys: React.FC = () => {
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
await fetchData();
|
||||
setOpenAddModal({
|
||||
open: false,
|
||||
title: '',
|
||||
action: PageAction.CREATE,
|
||||
currentData: null
|
||||
});
|
||||
} catch (error) {
|
||||
// do nothing
|
||||
}
|
||||
@@ -202,7 +198,7 @@ const APIKeys: React.FC = () => {
|
||||
const columns = useKeysColumns({
|
||||
handleSelect: onSelect,
|
||||
sortOrder,
|
||||
showCreator: canSeeAllKeys,
|
||||
is_admin: currentUser?.is_admin,
|
||||
configActions,
|
||||
onConfigAction: handleConfigAction
|
||||
});
|
||||
@@ -213,10 +209,10 @@ const APIKeys: React.FC = () => {
|
||||
<FilterBar
|
||||
marginBottom={22}
|
||||
marginTop={30}
|
||||
showSelect={canSeeAllKeys}
|
||||
showSelect={currentUser?.is_admin}
|
||||
selectOptions={userList}
|
||||
select={{ showSearch: { optionFilterProp: 'label' } }}
|
||||
selectHolder={intl.formatMessage({ id: 'common.filter.byCreator' })}
|
||||
selectHolder={intl.formatMessage({ id: 'models.table.filterByName' })}
|
||||
buttonText={intl.formatMessage({ id: 'apikeys.button.create' })}
|
||||
handleSearch={handleSearch}
|
||||
handleDeleteByBatch={handleDeleteBatch}
|
||||
|
||||
@@ -276,9 +276,7 @@ const VersionsForm: React.FC<AddModalProps> = ({
|
||||
key={name}
|
||||
defaultOpen
|
||||
styles={{
|
||||
body: collapseKey.has(name)
|
||||
? { paddingInline: 16, paddingBlock: '16px 0' }
|
||||
: {},
|
||||
body: collapseKey.has(name) ? { padding: 16 } : {},
|
||||
content: { paddingTop: 0 },
|
||||
header: {
|
||||
backgroundColor: 'unset'
|
||||
|
||||
@@ -84,6 +84,13 @@ const Instance: React.FC = () => {
|
||||
}),
|
||||
children: renderParams(instanceData?.backend_parameters || [])
|
||||
},
|
||||
{
|
||||
key: '1-1',
|
||||
label: intl.formatMessage({
|
||||
id: 'models.instance.params.autoInjected'
|
||||
}),
|
||||
children: renderParams(instanceData?.injected_backend_parameters || [])
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: intl.formatMessage({ id: 'benchmark.detail.kvCache' }),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { modelCategoriesMap } from '@/pages/llmodels/config';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { BaseSelect, FilterForm } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
@@ -22,13 +23,17 @@ interface FilterFormContentProps {
|
||||
initialValues?: any;
|
||||
open?: boolean;
|
||||
ref?: any;
|
||||
modelList?: Global.BaseOption<number, { categories: string[] }>[];
|
||||
onClose?: () => void;
|
||||
onClear?: () => void;
|
||||
onValuesChange: (values: any) => void;
|
||||
}
|
||||
|
||||
const FilterFormContent: React.FC<FilterFormContentProps> = forwardRef(
|
||||
({ initialValues, onClose, onClear, onValuesChange, open }, ref) => {
|
||||
(
|
||||
{ initialValues, onClose, onClear, onValuesChange, open, modelList },
|
||||
ref
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const filterRef = useRef<any>(null);
|
||||
|
||||
@@ -36,6 +41,15 @@ const FilterFormContent: React.FC<FilterFormContentProps> = forwardRef(
|
||||
onValuesChange?.(allValues);
|
||||
};
|
||||
|
||||
const modelOptions = modelList
|
||||
?.filter((item) => {
|
||||
return item.categories?.includes(modelCategoriesMap.llm);
|
||||
})
|
||||
.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.label
|
||||
}));
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
reset: () => {
|
||||
filterRef.current?.reset();
|
||||
@@ -102,17 +116,16 @@ const FilterFormContent: React.FC<FilterFormContentProps> = forwardRef(
|
||||
</Form.Item>
|
||||
<Label>{intl.formatMessage({ id: 'benchmark.table.model' })}</Label>
|
||||
<Form.Item noStyle name="model_name">
|
||||
<Input
|
||||
prefix={
|
||||
<SearchOutlined
|
||||
style={{ color: 'var(--ant-color-text-placeholder)' }}
|
||||
></SearchOutlined>
|
||||
}
|
||||
{/* <PillButtonGroup
|
||||
options={modelCategories.filter((item) => item.value)}
|
||||
></PillButtonGroup> */}
|
||||
<BaseSelect
|
||||
allowClear
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'benchmark.table.filter.bymodel'
|
||||
})}
|
||||
allowClear
|
||||
></Input>
|
||||
options={modelOptions}
|
||||
></BaseSelect>
|
||||
</Form.Item>
|
||||
</Content>
|
||||
</FilterForm>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import PluginExtraFields from '@/components/plugin-extra-fields';
|
||||
import { modelNameReg, PageAction } from '@/config';
|
||||
import { ClusterStatusValueMap } from '@/pages/cluster-management/config';
|
||||
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
|
||||
import { useBenchmarkTargetInstance } from '@/pages/llmodels/hooks/use-run-benchmark';
|
||||
import {
|
||||
Input as CInput,
|
||||
@@ -10,7 +8,7 @@ import {
|
||||
} from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form } from 'antd';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useFormContext } from '../config/form-context';
|
||||
import { FormData } from '../config/types';
|
||||
import ModelInstanceForm from './model-instance';
|
||||
@@ -22,70 +20,31 @@ const BasicForm: React.FC = () => {
|
||||
const { action, open, clusterList } = useFormContext();
|
||||
const { benchmarkTargetInstance } = useBenchmarkTargetInstance();
|
||||
|
||||
// `organization_id` is owned by the create-scope picker slot (admin "All"
|
||||
// view). When a platform admin targets an org (and we're not pre-filling
|
||||
// from a launched instance), fetch *that org's* clusters directly — the
|
||||
// request header carries the chosen org — rather than filtering the
|
||||
// page-level list, which is fetched once and may not include the org's
|
||||
// clusters. The benchmark's owner is derived from the chosen cluster.
|
||||
const scopeOrgId = Form.useWatch('organization_id', form);
|
||||
const orgScoped = scopeOrgId != null && !benchmarkTargetInstance.cluster_id;
|
||||
const {
|
||||
clusterList: scopedClusterList,
|
||||
fetchClusterList: fetchScopedClusters
|
||||
} = useQueryClusterList();
|
||||
// The org-scoped fetch returns clusters *visible* to the org — its own plus
|
||||
// any granted via cluster_access (and the platform principal can see a lot).
|
||||
// A benchmark's owner is the chosen cluster's owner, so keep only clusters
|
||||
// actually owned by the selected org. The owner filter also keeps this
|
||||
// correct if the request header isn't applied (fetch falls back to all).
|
||||
const effectiveClusterList = useMemo(() => {
|
||||
if (!orgScoped) {
|
||||
return clusterList || [];
|
||||
}
|
||||
return (scopedClusterList || []).filter(
|
||||
(item: any) => item.owner_principal_id === scopeOrgId
|
||||
);
|
||||
}, [orgScoped, scopedClusterList, clusterList, scopeOrgId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (action === PageAction.CREATE && orgScoped) {
|
||||
fetchScopedClusters({ page: -1 });
|
||||
}
|
||||
}, [scopeOrgId, orgScoped, action]);
|
||||
|
||||
useEffect(() => {
|
||||
if (action !== PageAction.CREATE) {
|
||||
return;
|
||||
}
|
||||
const clusterValue = (item: any) => item?.value ?? item?.id;
|
||||
const initClusterId = (list: any[]) => {
|
||||
// Find default cluster
|
||||
const defaultCluster = list?.find((item) => item.is_default);
|
||||
if (defaultCluster) {
|
||||
return clusterValue(defaultCluster);
|
||||
return defaultCluster.id;
|
||||
}
|
||||
|
||||
const readyCluster = list?.find(
|
||||
(item) => item.state === ClusterStatusValueMap.Ready
|
||||
);
|
||||
return clusterValue(readyCluster) ?? clusterValue(list?.[0]);
|
||||
const cluster_id =
|
||||
list?.find((item) => item.state === ClusterStatusValueMap.Ready)?.id ||
|
||||
list?.[0]?.id;
|
||||
|
||||
return cluster_id;
|
||||
};
|
||||
const current = form.getFieldValue('cluster_id');
|
||||
const stillValid = effectiveClusterList.some(
|
||||
(item: any) => clusterValue(item) === current
|
||||
);
|
||||
if (stillValid) {
|
||||
return;
|
||||
if (
|
||||
clusterList &&
|
||||
clusterList?.length > 0 &&
|
||||
action === PageAction.CREATE
|
||||
) {
|
||||
form.setFieldValue(
|
||||
'cluster_id',
|
||||
benchmarkTargetInstance.cluster_id || initClusterId(clusterList)
|
||||
);
|
||||
}
|
||||
// Re-pick within the (org-scoped) list. When the chosen org owns no
|
||||
// clusters this resolves to undefined, clearing a stale cross-org cluster
|
||||
// instead of leaving it selected.
|
||||
form.setFieldValue(
|
||||
'cluster_id',
|
||||
benchmarkTargetInstance.cluster_id || initClusterId(effectiveClusterList)
|
||||
);
|
||||
}, [form, action, effectiveClusterList, benchmarkTargetInstance]);
|
||||
}, [form, action, clusterList, benchmarkTargetInstance]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -108,7 +67,6 @@ const BasicForm: React.FC = () => {
|
||||
required
|
||||
></CInput.Input>
|
||||
</Form.Item>
|
||||
<PluginExtraFields name="CreateOrgScopeField" context={{ action }} />
|
||||
<Form.Item<FormData>
|
||||
name="cluster_id"
|
||||
rules={[
|
||||
@@ -120,7 +78,7 @@ const BasicForm: React.FC = () => {
|
||||
>
|
||||
<SealSelect
|
||||
disabled={action === PageAction.EDIT}
|
||||
options={effectiveClusterList}
|
||||
options={clusterList}
|
||||
label={intl.formatMessage({ id: 'clusters.title' })}
|
||||
required
|
||||
></SealSelect>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useQueryModelList } from '@/pages/llmodels/services/use-query-model-lis
|
||||
import { Cascader as SealCascader, useAppUtils } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form, Tooltip } from 'antd';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useFormContext } from '../config/form-context';
|
||||
import { FormData } from '../config/types';
|
||||
|
||||
@@ -45,11 +45,6 @@ const ModelInstanceForm: React.FC = () => {
|
||||
const form = Form.useFormInstance();
|
||||
const { getRuleMessage } = useAppUtils();
|
||||
const { action, open } = useFormContext();
|
||||
// Owned by the create-scope picker slot (admin "All" view). The model list
|
||||
// is tenant-scoped by the request header, so refetch it when the org
|
||||
// changes so only the chosen org's models/instances are offered.
|
||||
const scopeOrgId = Form.useWatch('organization_id', form);
|
||||
const prevScopeRef = useRef<number | null | undefined>(undefined);
|
||||
const [modelList, setModelList] = React.useState<any[]>([]);
|
||||
const {
|
||||
loading: modelLoading,
|
||||
@@ -163,29 +158,14 @@ const ModelInstanceForm: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (open && action === PageAction.CREATE) {
|
||||
// On a genuine org change, clear the stale (possibly cross-org) target
|
||||
// so the refetched list re-selects within the new org.
|
||||
if (
|
||||
prevScopeRef.current !== undefined &&
|
||||
prevScopeRef.current !== scopeOrgId
|
||||
) {
|
||||
form.setFieldsValue({
|
||||
model_name: undefined,
|
||||
model_id: undefined,
|
||||
model_instance_name: undefined,
|
||||
model_instance: undefined
|
||||
});
|
||||
}
|
||||
prevScopeRef.current = scopeOrgId;
|
||||
initModelInstance();
|
||||
}
|
||||
if (!open) {
|
||||
prevScopeRef.current = undefined;
|
||||
cancelModelRequest();
|
||||
cancelInstanceRequest();
|
||||
clearBenchmarkTargetInstance();
|
||||
}
|
||||
}, [open, benchmarkTargetInstance, action, scopeOrgId]);
|
||||
}, [open, benchmarkTargetInstance, action]);
|
||||
|
||||
return (
|
||||
<Form.Item<FormData>
|
||||
|
||||
@@ -27,7 +27,7 @@ const useBenchmarkColumns = (params: {
|
||||
dataIndex: 'name',
|
||||
sorter: tableSorter(1),
|
||||
render: (text: string, record) => (
|
||||
<AutoTooltip ghost minWidth={20} title={text}>
|
||||
<AutoTooltip ghost minWidth={20}>
|
||||
<Typography.Link onClick={() => onCellClick?.(record, 'name')}>
|
||||
{text}
|
||||
</Typography.Link>
|
||||
|
||||
@@ -113,8 +113,7 @@ export async function queryClusterList(
|
||||
return request<Global.PageResponse<ClusterListItem>>(`${CLUSTERS_API}`, {
|
||||
method: 'GET',
|
||||
params,
|
||||
cancelToken: options?.token,
|
||||
skipErrorHandler: options?.skipErrorHandler
|
||||
cancelToken: options?.token
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -55,22 +55,17 @@ const MainWrapper = styled.div`
|
||||
|
||||
const ClusterCreate: React.FC<{
|
||||
action: PageActionType;
|
||||
// Preselect a provider and skip the provider-catalog step. Set by
|
||||
// empty-state CTAs that already know which kind of cluster the user
|
||||
// is heading for (e.g. GPU Service's "Add a Kubernetes Cluster").
|
||||
providerHint?: string;
|
||||
setCurrentTitle?: (title: string) => void;
|
||||
onClose?: () => void;
|
||||
}> = ({ onClose, action, providerHint, setCurrentTitle }) => {
|
||||
}> = ({ onClose, action, setCurrentTitle }) => {
|
||||
const startStep = 0;
|
||||
const stepList = useStepList();
|
||||
const [systemConfigState] = useAtom(systemConfigAtom);
|
||||
const intl = useIntl();
|
||||
const [credentialList, setCredentialList] = useState<
|
||||
Global.BaseOption<number, { provider: ProviderType }>[]
|
||||
>([]);
|
||||
// When the caller already picked a provider for us, start one step
|
||||
// in — provider catalog is step 0; configure is step 1.
|
||||
const [currentStep, setCurrentStep] = useState<number>(providerHint ? 1 : 0);
|
||||
const [currentStep, setCurrentStep] = useState<number>(startStep);
|
||||
const [registrationInfo, setRegistrationInfo] = useState<{
|
||||
token: string;
|
||||
image: string;
|
||||
@@ -84,7 +79,7 @@ const ClusterCreate: React.FC<{
|
||||
cluster_id: 0
|
||||
});
|
||||
const [extraData, setExtraData] = useState<ClusterFormData>({
|
||||
provider: (providerHint as ProviderType) ?? ProviderValueMap.Docker
|
||||
provider: ProviderValueMap.Docker
|
||||
} as ClusterFormData);
|
||||
const [formValues, setFormValues] = useState<Record<string, any>>({});
|
||||
const [submitLoading, setSubmitLoading] = useState<boolean>(false);
|
||||
@@ -331,11 +326,7 @@ const ClusterCreate: React.FC<{
|
||||
<MainWrapper>
|
||||
{!isAddWorkerStep && (
|
||||
<StepWrapper>
|
||||
<ClusterSteps
|
||||
steps={steps}
|
||||
currentStep={currentStep}
|
||||
selectedProvider={extraData.provider}
|
||||
></ClusterSteps>
|
||||
<ClusterSteps steps={steps} currentStep={currentStep}></ClusterSteps>
|
||||
</StepWrapper>
|
||||
)}
|
||||
<ColumnWrapper
|
||||
@@ -360,12 +351,7 @@ const ClusterCreate: React.FC<{
|
||||
}
|
||||
>
|
||||
<div style={{ flex: 1 }}>
|
||||
{currentStep === 0 && (
|
||||
// Catalog belongs to step 0 only. The previous gate used
|
||||
// ``startStep`` which is 1 when ``providerHint`` skips the
|
||||
// catalog — that wrongly re-rendered it on top of the
|
||||
// configure form for entry points like "Add a Kubernetes
|
||||
// Cluster".
|
||||
{currentStep === startStep && (
|
||||
<ProviderCatalog
|
||||
cols={2}
|
||||
dataList={providerList}
|
||||
|
||||
@@ -62,15 +62,13 @@ const ClusterDetailModal = () => {
|
||||
key: 'workers',
|
||||
label: `Workers`,
|
||||
icon: <IconFont type="icon-resources" />,
|
||||
children: (
|
||||
<WorkerList clusterId={Number(id)} source="clusterDetail" />
|
||||
)
|
||||
children: <WorkerList clusterId={Number(id)} />
|
||||
},
|
||||
{
|
||||
key: 'gpus',
|
||||
label: `GPUs`,
|
||||
icon: <IconFont type="icon-gpu1" />,
|
||||
children: <GPUList clusterId={Number(id)} source="clusterDetail" />
|
||||
children: <GPUList clusterId={Number(id)} />
|
||||
},
|
||||
...extraTabs
|
||||
]}
|
||||
|
||||
@@ -6,18 +6,13 @@ import ClusterCreate from './cluster-create';
|
||||
interface ClusterModalProps {
|
||||
open: boolean;
|
||||
title: string;
|
||||
// When set, ClusterCreate preselects this provider and skips the
|
||||
// provider-catalog step. Used by feature pages (e.g. GPU Service)
|
||||
// whose empty state already implies which kind of cluster is needed.
|
||||
providerHint?: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ClusterModal: React.FC<ClusterModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
title,
|
||||
providerHint
|
||||
title
|
||||
}) => {
|
||||
const [currentTitle, setCurrentTitle] = React.useState<string>(title);
|
||||
const handleCancel = () => {
|
||||
@@ -46,7 +41,6 @@ const ClusterModal: React.FC<ClusterModalProps> = ({
|
||||
<ClusterCreate
|
||||
onClose={handleCancel}
|
||||
action={PageAction.CREATE}
|
||||
providerHint={providerHint}
|
||||
setCurrentTitle={setCurrentTitle}
|
||||
></ClusterCreate>
|
||||
</GSDrawer>
|
||||
|
||||
@@ -73,14 +73,7 @@ const Clusters: React.FC = () => {
|
||||
deleteAPI: deleteCluster,
|
||||
watch: true,
|
||||
API: CLUSTERS_API,
|
||||
contentForDelete: 'menu.clusterManagement.clusters',
|
||||
defaultQueryParams: {
|
||||
// Management view: drop cross-Org cluster_access grants. Org
|
||||
// Owner only sees the clusters they own here. Pickers that
|
||||
// need "everything I can use" (GPU-instance create, etc.)
|
||||
// query without ``mine`` and still see granted clusters.
|
||||
mine: true
|
||||
}
|
||||
contentForDelete: 'menu.clusterManagement.clusters'
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
const { goToGrafana, ActionButton } = useGranfanaLink({
|
||||
@@ -271,7 +264,7 @@ const Clusters: React.FC = () => {
|
||||
const handleOnCell = useMemoizedFn((record: ClusterListItem, dataIndex) => {
|
||||
if (dataIndex === 'name') {
|
||||
navigate(
|
||||
`/resources/clusters/detail?id=${record.id}&name=${record.name}&page=clusters`
|
||||
`/cluster-management/clusters/detail?id=${record.id}&name=${record.name}&page=clusters`
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -317,28 +310,14 @@ const Clusters: React.FC = () => {
|
||||
}
|
||||
}, [clusterSession, dataSource.loadend, dataSource.dataList]);
|
||||
|
||||
// Provider hint follows the auto-open from one render to the next:
|
||||
// the session atom is cleared right after we open the modal, but
|
||||
// ClusterCreate mounts a tick later and needs the value to skip the
|
||||
// provider-catalog step. Cache it locally and clear on close.
|
||||
const [pendingProviderHint, setPendingProviderHint] = useState<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (clusterSession?.firstAddCluster && dataSource.loadend) {
|
||||
setPendingProviderHint(clusterSession.providerHint);
|
||||
openClusterModal();
|
||||
// reset session
|
||||
setClusterSession(null);
|
||||
}
|
||||
}, [clusterSession, dataSource.loadend]);
|
||||
|
||||
const handleClusterModalClose = () => {
|
||||
setPendingProviderHint(undefined);
|
||||
closeClusterModal();
|
||||
};
|
||||
|
||||
const renderChildren = (
|
||||
list: any,
|
||||
options: { parent?: any; [key: string]: any }
|
||||
@@ -461,11 +440,10 @@ const Clusters: React.FC = () => {
|
||||
<DeleteModal ref={modalRef}></DeleteModal>
|
||||
<ClusterModal
|
||||
title={intl.formatMessage({
|
||||
id: 'menu.resources.clusterCreate'
|
||||
id: 'menu.clusterManagement.clusterCreate'
|
||||
})}
|
||||
open={clusterModalStatus.open}
|
||||
providerHint={pendingProviderHint}
|
||||
onClose={handleClusterModalClose}
|
||||
onClose={closeClusterModal}
|
||||
></ClusterModal>
|
||||
{AddWorkerModal}
|
||||
</>
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
import { PageAction } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||
import { AlertBlockInfo, FormDrawer, ModalFooter } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { ProviderType, ProviderValueMap } from '../config';
|
||||
import { FormDrawer } from '@gpustack/core-ui';
|
||||
import React, { useRef } from 'react';
|
||||
import { ProviderType } from '../config';
|
||||
import {
|
||||
ClusterFormData as FormData,
|
||||
ClusterListItem as ListItem
|
||||
} from '../config/types';
|
||||
import ClusterForm from './cluster-form';
|
||||
|
||||
const ModalFooterStyle = {
|
||||
padding: '16px 24px 8px',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end'
|
||||
};
|
||||
|
||||
type AddModalProps = {
|
||||
title: string;
|
||||
action: PageActionType;
|
||||
@@ -37,12 +28,7 @@ const AddCluster: React.FC<AddModalProps> = ({
|
||||
onOk,
|
||||
onCancel
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const form = useRef<any>(null);
|
||||
// Whether the user has changed any k8s_options field. Lifted from ClusterForm
|
||||
// so the "re-run registration" notice can sit in the drawer footer, above the
|
||||
// Save/Cancel buttons (mirrors the model edit interaction).
|
||||
const [k8sOptionsChanged, setK8sOptionsChanged] = useState<boolean>(false);
|
||||
|
||||
const handleSubmit = () => {
|
||||
form.current?.submit();
|
||||
@@ -67,27 +53,6 @@ const AddCluster: React.FC<AddModalProps> = ({
|
||||
onCancel={handleCancel}
|
||||
onSubmit={handleSubmit}
|
||||
width={710}
|
||||
footer={
|
||||
<>
|
||||
{action === PageAction.EDIT &&
|
||||
provider === ProviderValueMap.Kubernetes &&
|
||||
k8sOptionsChanged && (
|
||||
<AlertBlockInfo
|
||||
type="warning"
|
||||
style={{ margin: '8px 24px 0' }}
|
||||
icon={<ExclamationCircleFilled />}
|
||||
message={intl.formatMessage({
|
||||
id: 'clusters.edit.k8sOptions.changed.tip'
|
||||
})}
|
||||
></AlertBlockInfo>
|
||||
)}
|
||||
<ModalFooter
|
||||
onOk={handleSubmit}
|
||||
onCancel={handleCancel}
|
||||
style={ModalFooterStyle}
|
||||
></ModalFooter>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ClusterForm
|
||||
ref={form}
|
||||
@@ -96,7 +61,6 @@ const AddCluster: React.FC<AddModalProps> = ({
|
||||
action={action}
|
||||
currentData={currentData}
|
||||
onFinish={handleOk}
|
||||
onK8sOptionsChange={setK8sOptionsChanged}
|
||||
/>
|
||||
</FormDrawer>
|
||||
);
|
||||
|
||||
@@ -67,7 +67,6 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
|
||||
);
|
||||
const { update, summary, register } = useSummaryStatus();
|
||||
const { addedCount, createModelsChunkRequest } = useAddWorkerMessage();
|
||||
console.log('addedCount=========', addedCount);
|
||||
|
||||
const onToggle = (open: boolean, key: string) => {
|
||||
setCollapseKey(open ? new Set([key]) : new Set());
|
||||
@@ -82,7 +81,6 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
|
||||
setCollapseKey(new Set([stepList[0]]));
|
||||
}, [stepList]);
|
||||
|
||||
console.log('actionSource=========', actionSource, registrationInfo);
|
||||
React.useEffect(() => {
|
||||
// this effect is only triggered when used in cluster create page inner
|
||||
if (actionSource === 'page' && registrationInfo?.cluster_id) {
|
||||
@@ -166,7 +164,6 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
|
||||
</>
|
||||
)}
|
||||
{actionSource === 'modal' && (
|
||||
// show in cluster create page inner
|
||||
<AddedMessage addedCount={addedCount}></AddedMessage>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
@@ -75,7 +75,6 @@ const AddWorker: React.FC<AddWorkerProps> = (props) => {
|
||||
});
|
||||
|
||||
const handleOnClusterChange = async (value: number, row?: any) => {
|
||||
console.log('handleOnClusterChange value, row=========', value, row);
|
||||
try {
|
||||
createModelsChunkRequest({ cluster_id: value });
|
||||
axiosTokenRef.current?.cancel?.();
|
||||
@@ -122,8 +121,6 @@ const AddWorker: React.FC<AddWorkerProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
console.log('addedCount 1========', addedCount);
|
||||
|
||||
return (
|
||||
<GSDrawer
|
||||
title={title}
|
||||
|
||||
@@ -3,10 +3,13 @@ import {
|
||||
GPUDriverMap,
|
||||
GPUsConfigs
|
||||
} from '@/pages/resources/config/gpu-driver';
|
||||
import { BulbOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Tag } from 'antd';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Alert, Tag } from 'antd';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { queryClusterItem } from '../../apis';
|
||||
import { ProviderValueMap } from '../../config';
|
||||
import { ClusterListItem } from '../../config/types';
|
||||
import SupportedGPUs from '../support-gpus';
|
||||
import { useAddWorkerContext } from './add-worker-context';
|
||||
import { AddWorkerStepProps, StepNamesMap } from './config';
|
||||
@@ -23,23 +26,75 @@ const buildWorkerCommand = (
|
||||
});
|
||||
|
||||
const SelectVendor: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
||||
const { stepList, registerField, updateField, provider } =
|
||||
const { stepList, registerField, updateField, provider, registrationInfo } =
|
||||
useAddWorkerContext();
|
||||
const intl = useIntl();
|
||||
|
||||
const stepIndex = stepList.indexOf(StepNamesMap.SelectGPU) + 1;
|
||||
|
||||
// K8s clusters render one worker DaemonSet per requested GPU runtime and
|
||||
// derive each DaemonSet's nodeSelector from the vendor's PCI-presence label
|
||||
// at manifest time, so multiple vendors can be registered without any
|
||||
// per-cluster override config. Multi-select is therefore always available
|
||||
// for the Kubernetes provider; other providers stay single-select.
|
||||
const multiCapable = provider === ProviderValueMap.Kubernetes;
|
||||
// Pull the cluster so we know whether gpuVendorOverrides was configured.
|
||||
// The K8s register flow gates multi-select on that being present, and
|
||||
// restricts available runtimes to its keys.
|
||||
const [overrideRuntimes, setOverrideRuntimes] = useState<string[]>([]);
|
||||
useEffect(() => {
|
||||
const id = registrationInfo?.cluster_id;
|
||||
if (!id) {
|
||||
// Reset when the cluster context goes away so we don't leak the
|
||||
// previous cluster's overrides into the next session.
|
||||
setOverrideRuntimes([]);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
queryClusterItem({ id })
|
||||
.then((c) => {
|
||||
if (cancelled) return;
|
||||
const overrides = (c as ClusterListItem)?.k8s_options
|
||||
?.gpuVendorOverrides;
|
||||
setOverrideRuntimes(overrides ? Object.keys(overrides) : []);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (cancelled) return;
|
||||
console.error('Failed to query cluster for vendor overrides:', err);
|
||||
setOverrideRuntimes([]);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [registrationInfo?.cluster_id]);
|
||||
|
||||
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
||||
// Set of GPU *driver keys* (e.g. "cuda", "cann") that match the cluster's
|
||||
// override runtimes. Used to detect when multi-add becomes available.
|
||||
const overrideKeys = useMemo(() => {
|
||||
const set = new Set<string>();
|
||||
if (!overrideRuntimes.length) return set;
|
||||
Object.values(GPUsConfigs).forEach((cfg) => {
|
||||
if (cfg.gpuVendor && overrideRuntimes.includes(cfg.gpuVendor)) {
|
||||
set.add(cfg.value);
|
||||
}
|
||||
});
|
||||
return set;
|
||||
}, [overrideRuntimes]);
|
||||
|
||||
// No vendor is gated anymore — every card stays selectable.
|
||||
const availableKeys = undefined;
|
||||
// Multi-add is only meaningful when the cluster has 2+ vendor overrides
|
||||
// AND the current selection already includes one. Until both hold, the
|
||||
// picker behaves like a single-select (so the user can freely land on
|
||||
// any vendor — including ones outside the override list).
|
||||
const multiCapable =
|
||||
provider === ProviderValueMap.Kubernetes && overrideKeys.size >= 2;
|
||||
|
||||
const [selectedKeys, setSelectedKeys] = useState<string[]>([
|
||||
GPUDriverMap.NVIDIA
|
||||
]);
|
||||
|
||||
const isMultiActive = useMemo(
|
||||
() => multiCapable && selectedKeys.some((k) => overrideKeys.has(k)),
|
||||
[multiCapable, selectedKeys, overrideKeys]
|
||||
);
|
||||
|
||||
// In multi-active mode, non-override vendors are disabled — picking one
|
||||
// would break the backend invariant that a multi-vendor manifest must
|
||||
// only target configured runtimes. Otherwise everything stays enabled.
|
||||
const availableKeys = isMultiActive ? overrideKeys : undefined;
|
||||
|
||||
// Cache vendor metadata (label/link from SupportedGPUs items) so we can
|
||||
// rebuild workerCommand on toggle without re-clicking the card.
|
||||
@@ -47,6 +102,18 @@ const SelectVendor: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
||||
{}
|
||||
);
|
||||
|
||||
// Push current selection into the shared summary so consumers
|
||||
// (K8sRunCommand, CheckEnvironment, VendorNotes) can read it.
|
||||
useEffect(() => {
|
||||
const primary = selectedKeys[0] || '';
|
||||
updateField('currentGPU', primary);
|
||||
updateField('selectedGPUs', selectedKeys);
|
||||
updateField(
|
||||
'workerCommand',
|
||||
primary ? buildWorkerCommand(primary, itemMetaRef.current[primary]) : null
|
||||
);
|
||||
}, [selectedKeys]);
|
||||
|
||||
useEffect(() => {
|
||||
const unregister1 = registerField('currentGPU');
|
||||
const unregister2 = registerField('workerCommand');
|
||||
@@ -58,54 +125,37 @@ const SelectVendor: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const buildSelectedKeys = (key: string) => {
|
||||
const prev = [...selectedKeys];
|
||||
const has = prev.includes(key);
|
||||
if (has) {
|
||||
// Clicking a selected card always toggles it off.
|
||||
return prev.filter((v) => v !== key);
|
||||
}
|
||||
// K8s clusters support multiple GPU runtimes, so accumulate picks.
|
||||
// Other providers stay single-select and replace the current pick.
|
||||
if (multiCapable) return [...prev, key];
|
||||
return [key];
|
||||
};
|
||||
|
||||
const updateFieldsOnSelect = (keys: string[]) => {
|
||||
const primary = keys[0] || '';
|
||||
updateField('currentGPU', primary);
|
||||
updateField('selectedGPUs', keys);
|
||||
updateField(
|
||||
'workerCommand',
|
||||
primary ? buildWorkerCommand(primary, itemMetaRef.current[primary]) : null
|
||||
);
|
||||
};
|
||||
|
||||
const handleSelect = (key: string, item: any) => {
|
||||
// Disabled cards are already blocked by TemplateCard; this is a
|
||||
// defensive check for the multi-active case (only override runtimes
|
||||
// can be added once multi-add is open).
|
||||
if (availableKeys && !availableKeys.has(key)) return;
|
||||
if (item) {
|
||||
itemMetaRef.current[key] = {
|
||||
label: item.label,
|
||||
link: item.link
|
||||
};
|
||||
}
|
||||
const keys = buildSelectedKeys(key);
|
||||
updateFieldsOnSelect(keys);
|
||||
setSelectedKeys(keys);
|
||||
setSelectedKeys((prev) => {
|
||||
const has = prev.includes(key);
|
||||
if (has) {
|
||||
// Clicking a selected card always toggles it off.
|
||||
return prev.filter((v) => v !== key);
|
||||
}
|
||||
// Multi-add only when the current state already includes an override
|
||||
// pick. Otherwise (still in single-select land), replace.
|
||||
if (isMultiActive) return [...prev, key];
|
||||
return [key];
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// init a default selection
|
||||
handleSelect(GPUDriverMap.NVIDIA, {
|
||||
label: 'NVIDIA',
|
||||
hiddenTitle: true,
|
||||
value: GPUDriverMap.NVIDIA,
|
||||
description: '',
|
||||
key: GPUDriverMap.NVIDIA,
|
||||
locale: false,
|
||||
notes: AddWorkerDockerNotes[GPUDriverMap.NVIDIA],
|
||||
link: 'https://docs.gpustack.ai/latest/installation/requirements/#nvidia-gpu'
|
||||
});
|
||||
}, []);
|
||||
// Warn when multi-select is possible on this cluster but the user's
|
||||
// current pick lands outside the override list — they're effectively
|
||||
// locked into single-select until they switch to an override vendor.
|
||||
const showSingleOnlyHint =
|
||||
multiCapable &&
|
||||
selectedKeys.length > 0 &&
|
||||
selectedKeys.every((k) => !overrideKeys.has(k));
|
||||
|
||||
return (
|
||||
<StepCollapse
|
||||
@@ -132,6 +182,17 @@ const SelectVendor: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
||||
</Title>
|
||||
}
|
||||
>
|
||||
{showSingleOnlyHint && (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
icon={<BulbOutlined />}
|
||||
style={{ marginBottom: 8 }}
|
||||
message={intl.formatMessage({
|
||||
id: 'clusters.addworker.selectGPU.singleOnly'
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
<SupportedGPUs
|
||||
onSelect={handleSelect}
|
||||
current={selectedKeys}
|
||||
|
||||
@@ -16,7 +16,6 @@ interface StepItemProps {
|
||||
const Box = styled.div`
|
||||
border: 1px solid var(--ant-color-border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
&.step-collapse-open {
|
||||
border-color: var(--ant-color-primary);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user