Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a19fea0c6c |
@@ -1,7 +1,6 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch: {}
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'main'
|
- 'main'
|
||||||
@@ -118,20 +117,3 @@ jobs:
|
|||||||
remote_path: releases/${{ steps.version.outputs.version }}.tar.gz
|
remote_path: releases/${{ steps.version.outputs.version }}.tar.gz
|
||||||
accelerate: true
|
accelerate: true
|
||||||
clean: false
|
clean: false
|
||||||
|
|
||||||
trigger-backend:
|
|
||||||
needs: build-publish
|
|
||||||
if: (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) || github.event_name == 'workflow_dispatch'
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
steps:
|
|
||||||
- name: Dispatch backend build
|
|
||||||
uses: peter-evans/repository-dispatch@v3
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.DISPATCH_PAT }}
|
|
||||||
repository: gpustack/gpustack
|
|
||||||
event-type: ui-built
|
|
||||||
client-payload: |
|
|
||||||
{
|
|
||||||
"ref": "${{ github.ref }}",
|
|
||||||
"sha": "${{ github.sha }}"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -88,16 +88,6 @@ Prefer action-driven updates, explicit handlers, and localized state transitions
|
|||||||
|
|
||||||
Existing `styled-components` usage is legacy tech debt — do not migrate it wholesale, but do not add new `styled-components` either. Theme tokens (`var(--ant-color-*)`) work in all three approaches.
|
Existing `styled-components` usage is legacy tech debt — do not migrate it wholesale, but do not add new `styled-components` either. Theme tokens (`var(--ant-color-*)`) work in all three approaches.
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
Compose layout with Ant components, not hand-written `display: flex`.
|
|
||||||
|
|
||||||
- **1D flex** (row/column with `gap`, `align`, `justify`) → `Flex`. Do not write raw `display: flex` in new code.
|
|
||||||
- **Inline sequence** of a few elements with uniform spacing → `Space`.
|
|
||||||
- **Page/grid columns** → `Row` / `Col`.
|
|
||||||
|
|
||||||
Drive spacing with the theme scale (`Flex`/`Space` `gap`, or `var(--ant-*)` spacing tokens), not scattered `px` literals.
|
|
||||||
|
|
||||||
# Naming conventions
|
# Naming conventions
|
||||||
|
|
||||||
A page module lives under `src/pages/{module}` with this sub-structure: `components/`, `config/`, `forms/`, `hooks/`, `services/`, `index.tsx`. File naming:
|
A page module lives under `src/pages/{module}` with this sub-structure: `components/`, `config/`, `forms/`, `hooks/`, `services/`, `index.tsx`. File naming:
|
||||||
@@ -111,7 +101,6 @@ A page module lives under `src/pages/{module}` with this sub-structure: `compone
|
|||||||
|
|
||||||
- `config/types.ts` — TypeScript types. Form shape → `FormData`; table/list row → `ListItem`.
|
- `config/types.ts` — TypeScript types. Form shape → `FormData`; table/list row → `ListItem`.
|
||||||
- `config/index.ts` — static constants, enums, and value/label maps (e.g. `XxxStatusValueMap`, `XxxStatusLabelMap`). Keep constants out of `types.ts`.
|
- `config/index.ts` — static constants, enums, and value/label maps (e.g. `XxxStatusValueMap`, `XxxStatusLabelMap`). Keep constants out of `types.ts`.
|
||||||
- **`Select` options that need i18n**: set `label` to the message key and add `locale: true` on the option — the field translates it at render. Omit `locale` for options whose label is already final text. Ref `src/pages/benchmark/config/index.ts`.
|
|
||||||
|
|
||||||
# Common components
|
# Common components
|
||||||
|
|
||||||
|
|||||||
@@ -100,15 +100,6 @@ const baseRoutes = [
|
|||||||
path: '/models',
|
path: '/models',
|
||||||
redirect: '/models/deployments'
|
redirect: '/models/deployments'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'userModels',
|
|
||||||
path: '/models/user-models',
|
|
||||||
key: 'userModels',
|
|
||||||
icon: 'icon-models',
|
|
||||||
selectedIcon: 'icon-models-filled',
|
|
||||||
defaultIcon: 'icon-models',
|
|
||||||
component: './llmodels/user-models'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'modelCatalog',
|
name: 'modelCatalog',
|
||||||
path: '/models/catalog',
|
path: '/models/catalog',
|
||||||
@@ -119,6 +110,16 @@ const baseRoutes = [
|
|||||||
access: 'canSeeOrgAdmin',
|
access: 'canSeeOrgAdmin',
|
||||||
component: './llmodels/catalog'
|
component: './llmodels/catalog'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'userModels',
|
||||||
|
path: '/models/user-models',
|
||||||
|
key: 'userModels',
|
||||||
|
icon: 'icon-models',
|
||||||
|
selectedIcon: 'icon-models-filled',
|
||||||
|
defaultIcon: 'icon-models',
|
||||||
|
access: 'canSeeUser',
|
||||||
|
component: './llmodels/user-models'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'deployment',
|
name: 'deployment',
|
||||||
path: '/models/deployments',
|
path: '/models/deployments',
|
||||||
@@ -273,7 +274,7 @@ const baseRoutes = [
|
|||||||
selectedIcon: 'icon-cluster2-filled',
|
selectedIcon: 'icon-cluster2-filled',
|
||||||
defaultIcon: 'icon-cluster2-outline',
|
defaultIcon: 'icon-cluster2-outline',
|
||||||
component: './cluster-management/clusters',
|
component: './cluster-management/clusters',
|
||||||
subMenu: ['/resources/clusters/create']
|
subMenu: ['/resources/clusters/detail', '/resources/clusters/create']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'workers',
|
name: 'workers',
|
||||||
@@ -301,6 +302,16 @@ const baseRoutes = [
|
|||||||
selectedIcon: 'icon-credential-filled',
|
selectedIcon: 'icon-credential-filled',
|
||||||
defaultIcon: 'icon-credential-outline',
|
defaultIcon: 'icon-credential-outline',
|
||||||
component: './cluster-management/credentials'
|
component: './cluster-management/credentials'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'clusterDetail',
|
||||||
|
path: '/resources/clusters/detail',
|
||||||
|
key: 'clusterDetail',
|
||||||
|
icon: 'icon-cluster2-outline',
|
||||||
|
selectedIcon: 'icon-cluster2-filled',
|
||||||
|
defaultIcon: 'icon-cluster2-outline',
|
||||||
|
hideInMenu: true,
|
||||||
|
component: './cluster-management/cluster-detail'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"@ant-design/pro-components": "3.1.0-0",
|
"@ant-design/pro-components": "3.1.0-0",
|
||||||
"@antv/g6": "^5.0.51",
|
"@antv/g6": "^5.0.51",
|
||||||
"@braintree/sanitize-url": "^7.1.1",
|
"@braintree/sanitize-url": "^7.1.1",
|
||||||
"@gpustack/core-ui": "^1.0.36",
|
"@gpustack/core-ui": "^1.0.32",
|
||||||
"@huggingface/gguf": "^0.1.7",
|
"@huggingface/gguf": "^0.1.7",
|
||||||
"@huggingface/hub": "^0.15.1",
|
"@huggingface/hub": "^0.15.1",
|
||||||
"@huggingface/tasks": "^0.11.6",
|
"@huggingface/tasks": "^0.11.6",
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
"culori": "^4.0.2",
|
"culori": "^4.0.2",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
|
"driver.js": "^1.3.1",
|
||||||
"echarts": "^5.5.1",
|
"echarts": "^5.5.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"has-ansi": "^5.0.1",
|
"has-ansi": "^5.0.1",
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ importers:
|
|||||||
specifier: ^7.1.1
|
specifier: ^7.1.1
|
||||||
version: 7.1.2
|
version: 7.1.2
|
||||||
'@gpustack/core-ui':
|
'@gpustack/core-ui':
|
||||||
specifier: ^1.0.36
|
specifier: ^1.0.32
|
||||||
version: 1.0.36(czdvzceysqw7iv6pct2ucnb23e)
|
version: 1.0.32(czdvzceysqw7iv6pct2ucnb23e)
|
||||||
'@huggingface/gguf':
|
'@huggingface/gguf':
|
||||||
specifier: ^0.1.7
|
specifier: ^0.1.7
|
||||||
version: 0.1.18
|
version: 0.1.18
|
||||||
@@ -89,6 +89,9 @@ importers:
|
|||||||
dompurify:
|
dompurify:
|
||||||
specifier: ^3.2.6
|
specifier: ^3.2.6
|
||||||
version: 3.4.2
|
version: 3.4.2
|
||||||
|
driver.js:
|
||||||
|
specifier: ^1.3.1
|
||||||
|
version: 1.4.0
|
||||||
echarts:
|
echarts:
|
||||||
specifier: ^5.5.1
|
specifier: ^5.5.1
|
||||||
version: 5.6.0
|
version: 5.6.0
|
||||||
@@ -1481,8 +1484,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz}
|
resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz}
|
||||||
deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package
|
deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package
|
||||||
|
|
||||||
'@gpustack/core-ui@1.0.36':
|
'@gpustack/core-ui@1.0.32':
|
||||||
resolution: {integrity: sha512-gF8ShMZ2SKYo3+skfsI3zo+aBSsXqKGMX+jWaKv81e38bDwJw71C33TR64d+6MyV9iUoFOoSK+ht1GefyvR17g==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.36.tgz}
|
resolution: {integrity: sha512-kGTazoqbK2KyZgOP6gmQaRxTiQVfF2IKLGDXjJq6w6BbmJgALXFJA2v2ROjAbjEVyfTdBzyYeXfPo/JgISpMNw==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.32.tgz}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@ant-design/icons': ^6.1.0
|
'@ant-design/icons': ^6.1.0
|
||||||
'@ant-design/pro-components': 3.1.0-0
|
'@ant-design/pro-components': 3.1.0-0
|
||||||
@@ -4317,6 +4320,9 @@ packages:
|
|||||||
dot-case@3.0.4:
|
dot-case@3.0.4:
|
||||||
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==, tarball: https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz}
|
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==, tarball: https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz}
|
||||||
|
|
||||||
|
driver.js@1.4.0:
|
||||||
|
resolution: {integrity: sha512-Gm64jm6PmcU+si21sQhBrTAM1JvUrR0QhNmjkprNLxohOBzul9+pNHXgQaT9lW84gwg9GMLB3NZGuGolsz5uew==, tarball: https://registry.npmjs.org/driver.js/-/driver.js-1.4.0.tgz}
|
||||||
|
|
||||||
duck@0.1.12:
|
duck@0.1.12:
|
||||||
resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==, tarball: https://registry.npmjs.org/duck/-/duck-0.1.12.tgz}
|
resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==, tarball: https://registry.npmjs.org/duck/-/duck-0.1.12.tgz}
|
||||||
|
|
||||||
@@ -10802,7 +10808,7 @@ snapshots:
|
|||||||
|
|
||||||
'@formatjs/intl-utils@2.3.0': {}
|
'@formatjs/intl-utils@2.3.0': {}
|
||||||
|
|
||||||
'@gpustack/core-ui@1.0.36(czdvzceysqw7iv6pct2ucnb23e)':
|
'@gpustack/core-ui@1.0.32(czdvzceysqw7iv6pct2ucnb23e)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@ant-design/pro-components': 3.1.0-0(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@ant-design/pro-components': 3.1.0-0(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
@@ -14548,6 +14554,8 @@ snapshots:
|
|||||||
no-case: 3.0.4
|
no-case: 3.0.4
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
driver.js@1.4.0: {}
|
||||||
|
|
||||||
duck@0.1.12:
|
duck@0.1.12:
|
||||||
dependencies:
|
dependencies:
|
||||||
underscore: 1.13.8
|
underscore: 1.13.8
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 640 B |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 801 B |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -1,4 +1,3 @@
|
|||||||
import { nsLocalJSONStorage } from '@gpustack/core-ui/utils';
|
|
||||||
import { getDefaultStore } from 'jotai';
|
import { getDefaultStore } from 'jotai';
|
||||||
import { atomWithStorage } from 'jotai/utils';
|
import { atomWithStorage } from 'jotai/utils';
|
||||||
|
|
||||||
@@ -9,7 +8,7 @@ export interface PaginationState {
|
|||||||
export const paginationAtom = atomWithStorage<Record<string, any>>(
|
export const paginationAtom = atomWithStorage<Record<string, any>>(
|
||||||
'paginationStatus',
|
'paginationStatus',
|
||||||
{},
|
{},
|
||||||
nsLocalJSONStorage,
|
undefined,
|
||||||
{ getOnInit: true }
|
{ getOnInit: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { COLOR_PRIMARY } from '@/config/theme';
|
import { COLOR_PRIMARY } from '@/config/theme';
|
||||||
import { nsLocal, nsLocalJSONStorage } from '@gpustack/core-ui/utils';
|
|
||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
import { atomWithStorage } from 'jotai/utils';
|
import { atomWithStorage } from 'jotai/utils';
|
||||||
|
|
||||||
@@ -24,7 +23,9 @@ export const defaultSettings: UserSettings = {
|
|||||||
export const getStorageUserSettings = () => {
|
export const getStorageUserSettings = () => {
|
||||||
if (typeof window === 'undefined') return defaultSettings;
|
if (typeof window === 'undefined') return defaultSettings;
|
||||||
try {
|
try {
|
||||||
const savedSettings = JSON.parse(nsLocal.get('userSettings') || '{}');
|
const savedSettings = JSON.parse(
|
||||||
|
localStorage.getItem('userSettings') || '{}'
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
...defaultSettings,
|
...defaultSettings,
|
||||||
...savedSettings
|
...savedSettings
|
||||||
@@ -34,13 +35,9 @@ export const getStorageUserSettings = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userSettingsAtom = atomWithStorage<UserSettings>(
|
export const userSettingsAtom = atomWithStorage<UserSettings>('userSettings', {
|
||||||
'userSettings',
|
|
||||||
{
|
|
||||||
...getStorageUserSettings()
|
...getStorageUserSettings()
|
||||||
},
|
});
|
||||||
nsLocalJSONStorage
|
|
||||||
);
|
|
||||||
|
|
||||||
export const userSettingsHelperAtom = atom(
|
export const userSettingsHelperAtom = atom(
|
||||||
(get) => get(userSettingsAtom),
|
(get) => get(userSettingsAtom),
|
||||||
@@ -62,6 +59,6 @@ export const hideModalTemporarilyAtom = atom<boolean>(false);
|
|||||||
export const collapsedMenuGroupsAtom = atomWithStorage<string[]>(
|
export const collapsedMenuGroupsAtom = atomWithStorage<string[]>(
|
||||||
'collapsedMenuGroups',
|
'collapsedMenuGroups',
|
||||||
[],
|
[],
|
||||||
nsLocalJSONStorage,
|
undefined,
|
||||||
{ getOnInit: true }
|
{ getOnInit: true }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { nsLocalJSONStorage } from '@gpustack/core-ui/utils';
|
|
||||||
import { getDefaultStore } from 'jotai';
|
import { getDefaultStore } from 'jotai';
|
||||||
import { atomWithStorage } from 'jotai/utils';
|
import { atomWithStorage } from 'jotai/utils';
|
||||||
|
|
||||||
export const tabActiveAtom = atomWithStorage<Map<string, any>>(
|
export const tabActiveAtom = atomWithStorage<Map<string, any>>(
|
||||||
'tabActiveStatus',
|
'tabActiveStatus',
|
||||||
new Map(),
|
new Map()
|
||||||
nsLocalJSONStorage
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const setActiveStatus = (key: string, value: any) => {
|
export const setActiveStatus = (key: string, value: any) => {
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import { nsLocal, nsLocalJSONStorage } from '@gpustack/core-ui/utils';
|
|
||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
import { atomWithStorage } from 'jotai/utils';
|
import { atomWithStorage } from 'jotai/utils';
|
||||||
|
|
||||||
export const userAtom = atomWithStorage<any>(
|
export const userAtom = atomWithStorage<any>('userInfo', null);
|
||||||
'userInfo',
|
|
||||||
null,
|
|
||||||
nsLocalJSONStorage
|
|
||||||
);
|
|
||||||
|
|
||||||
// Backs the `currentOrganizationId` localStorage key. Stays null in
|
// Backs the `currentOrganizationId` localStorage key. Stays null in
|
||||||
// builds with no Org context (single-tenant), and is shared with any
|
// builds with no Org context (single-tenant), and is shared with any
|
||||||
@@ -14,8 +9,7 @@ export const userAtom = atomWithStorage<any>(
|
|||||||
// without one side having to import from the other.
|
// without one side having to import from the other.
|
||||||
export const currentOrganizationIdAtom = atomWithStorage<number | null>(
|
export const currentOrganizationIdAtom = atomWithStorage<number | null>(
|
||||||
'currentOrganizationId',
|
'currentOrganizationId',
|
||||||
null,
|
null
|
||||||
nsLocalJSONStorage
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const GPUStackVersionAtom = atom<{
|
export const GPUStackVersionAtom = atom<{
|
||||||
@@ -76,7 +70,7 @@ export const getCurrentOrgNamespace = (
|
|||||||
|
|
||||||
const getStoredCurrentOrgId = (): number | null => {
|
const getStoredCurrentOrgId = (): number | null => {
|
||||||
try {
|
try {
|
||||||
const raw = nsLocal.get('currentOrganizationId');
|
const raw = localStorage.getItem('currentOrganizationId');
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
const value = JSON.parse(raw);
|
const value = JSON.parse(raw);
|
||||||
return typeof value === 'number' ? value : null;
|
return typeof value === 'number' ? value : null;
|
||||||
@@ -114,7 +108,7 @@ export const getOrgById = (
|
|||||||
const target = String(id);
|
const target = String(id);
|
||||||
for (const key of ORG_CACHE_KEYS) {
|
for (const key of ORG_CACHE_KEYS) {
|
||||||
try {
|
try {
|
||||||
const raw = nsLocal.get(key);
|
const raw = localStorage.getItem(key);
|
||||||
if (!raw) continue;
|
if (!raw) continue;
|
||||||
const list = JSON.parse(raw) as CachedOrg[];
|
const list = JSON.parse(raw) as CachedOrg[];
|
||||||
if (!Array.isArray(list)) continue;
|
if (!Array.isArray(list)) continue;
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import { defaultSettings } from '@/atoms/settings';
|
import { defaultSettings } from '@/atoms/settings';
|
||||||
import { nsLocal } from '@gpustack/core-ui/utils';
|
|
||||||
import { getDefaultStore } from 'jotai';
|
import { getDefaultStore } from 'jotai';
|
||||||
|
|
||||||
export const clearStorageUserSettings = () => {
|
export const clearStorageUserSettings = () => {
|
||||||
try {
|
try {
|
||||||
const savedSettings = JSON.parse(nsLocal.get('userSettings') || '{}');
|
const savedSettings = JSON.parse(
|
||||||
|
localStorage.getItem('userSettings') || '{}'
|
||||||
|
);
|
||||||
// colorPrimary is an enterprise-wide branding setting (set by admins
|
// colorPrimary is an enterprise-wide branding setting (set by admins
|
||||||
// and applied by `onAppInit` from /enterprise/settings), not a per-user
|
// and applied by `onAppInit` from /enterprise/settings), not a per-user
|
||||||
// preference. Preserve it across login — otherwise the next layout
|
// preference. Preserve it across login — otherwise the next layout
|
||||||
// mount triggers `atomWithStorage.onMount`, re-reads localStorage,
|
// mount triggers `atomWithStorage.onMount`, re-reads localStorage,
|
||||||
// and falls back to the default color until a full page refresh
|
// and falls back to the default color until a full page refresh
|
||||||
// re-runs `applyEnterpriseSettings`.
|
// re-runs `applyEnterpriseSettings`.
|
||||||
nsLocal.set(
|
localStorage.setItem(
|
||||||
'userSettings',
|
'userSettings',
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
...savedSettings,
|
...savedSettings,
|
||||||
@@ -25,7 +26,7 @@ export const clearStorageUserSettings = () => {
|
|||||||
|
|
||||||
export const resetStorageUserSettings = () => {
|
export const resetStorageUserSettings = () => {
|
||||||
try {
|
try {
|
||||||
nsLocal.set(
|
localStorage.setItem(
|
||||||
'userSettings',
|
'userSettings',
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
...defaultSettings,
|
...defaultSettings,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { GPUStackVersionAtom } from '@/atoms/user';
|
import { GPUStackVersionAtom } from '@/atoms/user';
|
||||||
|
import { getAtomStorage } from '@/atoms/utils';
|
||||||
import VersionInfo, { modalConfig } from '@/components/version-info';
|
import VersionInfo, { modalConfig } from '@/components/version-info';
|
||||||
import externalLinks from '@/constants/external-links';
|
import externalLinks from '@/constants/external-links';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Divider, Modal, Typography } from 'antd';
|
import { Button, Divider, Modal, Typography } from 'antd';
|
||||||
import { createStyles } from 'antd-style';
|
import { createStyles } from 'antd-style';
|
||||||
import { useAtomValue } from 'jotai';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const CompanyWrapper = styled.div`
|
const CompanyWrapper = styled.div`
|
||||||
@@ -35,7 +35,6 @@ const Footer: React.FC = () => {
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [modal, contextHolder] = Modal.useModal();
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
const { styles } = useStyles();
|
const { styles } = useStyles();
|
||||||
const version = useAtomValue(GPUStackVersionAtom);
|
|
||||||
|
|
||||||
const showVersion = () => {
|
const showVersion = () => {
|
||||||
modal.info({
|
modal.info({
|
||||||
@@ -74,7 +73,7 @@ const Footer: React.FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Divider orientation="vertical" />
|
<Divider orientation="vertical" />
|
||||||
<Button type="link" size="small" onClick={showVersion}>
|
<Button type="link" size="small" onClick={showVersion}>
|
||||||
{version?.version}
|
{getAtomStorage(GPUStackVersionAtom)?.version}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
// Wrapper around core-ui's FullMarkdown that co-locates the KaTeX stylesheet.
|
|
||||||
//
|
|
||||||
// core-ui deliberately does NOT bundle katex.min.css (importing it there
|
|
||||||
// base64-inlines ~1.4MB of fonts into the shared, render-blocking index.css).
|
|
||||||
// Importing it here keeps the KaTeX CSS in the route chunk that actually
|
|
||||||
// renders math, so it loads lazily and never blocks first paint.
|
|
||||||
//
|
|
||||||
// Always import FullMarkdown from this module, not from '@gpustack/core-ui/markdown'.
|
|
||||||
import { FullMarkdown } from '@gpustack/core-ui/markdown';
|
|
||||||
import 'katex/dist/katex.min.css';
|
|
||||||
|
|
||||||
export default FullMarkdown;
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import Logo from '@/assets/images/gpustack-logo.png';
|
import Logo from '@/assets/images/gpustack-logo.png';
|
||||||
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
|
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
|
||||||
import externalLinks from '@/constants/external-links';
|
import externalLinks from '@/constants/external-links';
|
||||||
import { useLogo } from '@/hooks/use-logo';
|
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -19,7 +18,7 @@ const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
|
|||||||
isProd,
|
isProd,
|
||||||
isDev
|
isDev
|
||||||
} = gpuStackVersionAtom;
|
} = gpuStackVersionAtom;
|
||||||
const { sidebarLogo } = useLogo();
|
|
||||||
// user info
|
// user info
|
||||||
const { is_admin } = userDataAtom || {};
|
const { is_admin } = userDataAtom || {};
|
||||||
|
|
||||||
@@ -31,7 +30,7 @@ const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="version-box">
|
<div className="version-box">
|
||||||
<div className="img">
|
<div className="img">
|
||||||
<img src={sidebarLogo || Logo} alt="logo" />
|
<img src={Logo} alt="logo" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="ver">
|
<div className="ver">
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export default {
|
|||||||
rowSelectedBg: 'transparent',
|
rowSelectedBg: 'transparent',
|
||||||
headerSortActiveBg: 'transparent',
|
headerSortActiveBg: 'transparent',
|
||||||
headerSortHoverBg: 'transparent',
|
headerSortHoverBg: 'transparent',
|
||||||
bodySortBg: 'transparent',
|
|
||||||
headerBg: 'none'
|
headerBg: 'none'
|
||||||
},
|
},
|
||||||
Button: {
|
Button: {
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ export default {
|
|||||||
rowSelectedBg: 'transparent',
|
rowSelectedBg: 'transparent',
|
||||||
headerSortActiveBg: 'transparent',
|
headerSortActiveBg: 'transparent',
|
||||||
headerSortHoverBg: 'transparent',
|
headerSortHoverBg: 'transparent',
|
||||||
bodySortBg: 'transparent',
|
|
||||||
headerSplitColor: '#e8e8e8',
|
headerSplitColor: '#e8e8e8',
|
||||||
headerBg: 'none'
|
headerBg: 'none'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export default function useActions<T>(actions: Global.ActionItem<T>[], ctx: T) {
|
||||||
|
return actions
|
||||||
|
.filter((action) => {
|
||||||
|
return action.visible ? action.visible(ctx) : true;
|
||||||
|
})
|
||||||
|
.map((action) => ({
|
||||||
|
...action,
|
||||||
|
disabled: action.disabled?.(ctx)
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { message } from 'antd';
|
||||||
|
|
||||||
|
type MessageType = 'input' | 'select';
|
||||||
|
|
||||||
|
const useAppUtils = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const [messageApi, contextHolder] = message.useMessage();
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param type Array<'input' | 'select'>
|
||||||
|
* @param name
|
||||||
|
* @param locale boolean
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getRuleMessage = (
|
||||||
|
type: MessageType | MessageType[],
|
||||||
|
name: string,
|
||||||
|
locale = true
|
||||||
|
) => {
|
||||||
|
const nameStr = locale ? intl.formatMessage({ id: name }) : name;
|
||||||
|
// transform type to array
|
||||||
|
const typeList = Array.isArray(type) ? type : [type];
|
||||||
|
|
||||||
|
if (typeList.includes('select') && typeList.includes('input')) {
|
||||||
|
return intl.formatMessage(
|
||||||
|
{ id: 'common.form.rule.selectInput' },
|
||||||
|
{ name: nameStr }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeList.includes('input')) {
|
||||||
|
return intl.formatMessage(
|
||||||
|
{ id: 'common.form.rule.input' },
|
||||||
|
{ name: nameStr }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return intl.formatMessage(
|
||||||
|
{ id: 'common.form.rule.select' },
|
||||||
|
{ name: nameStr }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showSuccess = (msg?: string) => {
|
||||||
|
messageApi.success(
|
||||||
|
msg || intl.formatMessage({ id: 'common.message.success' })
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getRuleMessage,
|
||||||
|
showSuccess
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useAppUtils;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// broadcast channel hook
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export const useBroadcast = () => {
|
||||||
|
const broadcastChannel = useRef<BroadcastChannel | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
broadcastChannel.current = new BroadcastChannel('broadcast_channel');
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
broadcastChannel.current?.close();
|
||||||
|
console.log('broadcast channel closed');
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { broadcastChannel };
|
||||||
|
};
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
export default function useContainerScroll(
|
||||||
|
container: any,
|
||||||
|
options?: { toBottom?: boolean }
|
||||||
|
) {
|
||||||
|
const isWheeled = useRef(false);
|
||||||
|
const scroller = useRef(container);
|
||||||
|
const optionsRef = useRef(options);
|
||||||
|
const toBottomFlag = useRef(options?.toBottom);
|
||||||
|
const timerRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const debunceResetWheeled = _.debounce(() => {
|
||||||
|
isWheeled.current = false;
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
const handleContentWheel = (e: any) => {
|
||||||
|
isWheeled.current = true;
|
||||||
|
debunceResetWheeled.cancel?.();
|
||||||
|
debunceResetWheeled();
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollerRun = () => {
|
||||||
|
const scrollerContainer = scroller.current?.current || {};
|
||||||
|
const { scrollHeight, clientHeight, scrollTop } = scrollerContainer;
|
||||||
|
if (
|
||||||
|
optionsRef.current?.toBottom &&
|
||||||
|
toBottomFlag.current &&
|
||||||
|
scrollHeight > clientHeight + scrollTop
|
||||||
|
) {
|
||||||
|
scroller.current.current.scrollTop = scrollHeight;
|
||||||
|
// toBottomFlag.current = false;
|
||||||
|
isWheeled.current = false;
|
||||||
|
} else if (
|
||||||
|
!isWheeled.current &&
|
||||||
|
scrollHeight > clientHeight + scrollTop &&
|
||||||
|
scroller.current?.current
|
||||||
|
) {
|
||||||
|
scroller.current.current.scrollTop += 10;
|
||||||
|
window.requestAnimationFrame(scrollerRun);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateScrollerPosition = () => {
|
||||||
|
if (!isWheeled.current) {
|
||||||
|
window.requestAnimationFrame(scrollerRun);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleContentWheel,
|
||||||
|
updateScrollerPosition,
|
||||||
|
scroller
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
const useCopyToClipboard = () => {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const copyToClipboard = useCallback(async (text: string) => {
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setCopied(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { copied, copyToClipboard };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCopyToClipboard;
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { HandlerOptions } from '@/hooks/use-chunk-fetch';
|
||||||
|
import { useDownloadStream } from '@gpustack/core-ui';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Progress, notification } from 'antd';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
const renderMessage = (title: string) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 280,
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFileName = (name: string) => {
|
||||||
|
const timestamp = dayjs().format('YYYY-MM-DD_HH-mm-ss');
|
||||||
|
const fileName = `${name}_${timestamp}.txt`;
|
||||||
|
return fileName;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useDownloadLogs = () => {
|
||||||
|
const { downloadStream } = useDownloadStream();
|
||||||
|
const intl = useIntl();
|
||||||
|
const [api, contextHolder] = notification.useNotification({
|
||||||
|
stack: { threshold: 1 }
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadNotification = (
|
||||||
|
data: HandlerOptions & {
|
||||||
|
filename: string;
|
||||||
|
duration?: number;
|
||||||
|
chunkRequestRef: any;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
api.open({
|
||||||
|
duration: data.duration,
|
||||||
|
message: renderMessage(data.filename),
|
||||||
|
key: data.filename,
|
||||||
|
closeIcon: (
|
||||||
|
<span>{intl.formatMessage({ id: 'common.button.cancel' })}</span>
|
||||||
|
),
|
||||||
|
description: <Progress percent={data.percent} size="small"></Progress>,
|
||||||
|
onClose() {
|
||||||
|
data.chunkRequestRef?.current?.abort();
|
||||||
|
notification.destroy?.(data.filename);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadLog = async (params: { url: string; name: string }) => {
|
||||||
|
downloadStream({
|
||||||
|
url: params.url,
|
||||||
|
filename: createFileName(params.name),
|
||||||
|
downloadNotification
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onDownloadLog: handleDownloadLog,
|
||||||
|
contextHolder
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDownloadLogs;
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import useSetChunkFetch, { HandlerOptions } from '@/hooks/use-chunk-fetch';
|
||||||
|
import { message } from 'antd';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export default function useDownloadStream() {
|
||||||
|
const chunkRequestRef = useRef<any>(null);
|
||||||
|
const logParseWorker = useRef<any>(null);
|
||||||
|
const clearScreen = useRef(false);
|
||||||
|
const filename = useRef('log');
|
||||||
|
const downloadNotificationRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const { setChunkFetch } = useSetChunkFetch();
|
||||||
|
|
||||||
|
const downloadFile = (content: string) => {
|
||||||
|
const blob = new Blob([content], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename.current;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateContent = (data: string, options?: HandlerOptions) => {
|
||||||
|
const { isComplete, percent } = options || {};
|
||||||
|
|
||||||
|
logParseWorker.current?.postMessage({
|
||||||
|
inputStr: data,
|
||||||
|
reset: clearScreen.current,
|
||||||
|
isComplete: isComplete,
|
||||||
|
percent: percent,
|
||||||
|
chunked: false
|
||||||
|
});
|
||||||
|
clearScreen.current = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleError = (error: any) => {
|
||||||
|
const errorMsg = error?.message || error;
|
||||||
|
const msg =
|
||||||
|
typeof errorMsg === 'string' ? errorMsg : JSON.stringify(errorMsg);
|
||||||
|
message.error(msg);
|
||||||
|
downloadNotificationRef.current?.({
|
||||||
|
duration: 1,
|
||||||
|
percent: 0,
|
||||||
|
filename: filename.current
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadStream = async (props: {
|
||||||
|
data?: any;
|
||||||
|
url: string;
|
||||||
|
params?: any;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
method?: string;
|
||||||
|
headers?: any;
|
||||||
|
filename?: string;
|
||||||
|
downloadNotification?: (data: any) => void;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
clearScreen.current = true;
|
||||||
|
filename.current = props.filename || 'log';
|
||||||
|
downloadNotificationRef.current = props.downloadNotification;
|
||||||
|
const { params, url } = props;
|
||||||
|
|
||||||
|
chunkRequestRef.current?.current?.abort?.();
|
||||||
|
|
||||||
|
chunkRequestRef.current = setChunkFetch({
|
||||||
|
url,
|
||||||
|
params,
|
||||||
|
watch: false,
|
||||||
|
contentType: 'text',
|
||||||
|
errorHandler: handleError,
|
||||||
|
handler: updateContent
|
||||||
|
});
|
||||||
|
downloadNotificationRef.current?.({
|
||||||
|
filename: filename.current,
|
||||||
|
duration: null,
|
||||||
|
chunkRequestRef: chunkRequestRef.current
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
//
|
||||||
|
downloadNotificationRef.current?.({
|
||||||
|
duration: 1,
|
||||||
|
percent: 0,
|
||||||
|
filename: filename.current
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
logParseWorker.current?.terminate?.();
|
||||||
|
|
||||||
|
logParseWorker.current = new Worker(
|
||||||
|
// @ts-ignore
|
||||||
|
new URL('@/components/logs-viewer/parse-worker.ts', import.meta.url),
|
||||||
|
{
|
||||||
|
type: 'module'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
logParseWorker.current.onmessage = (event: any) => {
|
||||||
|
const { result, isComplete, percent } = event.data;
|
||||||
|
|
||||||
|
const isAborted = chunkRequestRef.current?.current?.signal?.aborted;
|
||||||
|
if (!isComplete && !isAborted) {
|
||||||
|
downloadNotificationRef.current?.({
|
||||||
|
percent: percent,
|
||||||
|
duration: null,
|
||||||
|
filename: filename.current,
|
||||||
|
chunkRequestRef: chunkRequestRef.current
|
||||||
|
});
|
||||||
|
} else if (isComplete && !isAborted) {
|
||||||
|
downloadNotificationRef.current?.({
|
||||||
|
duration: 1,
|
||||||
|
percent: 100,
|
||||||
|
filename: filename.current
|
||||||
|
});
|
||||||
|
downloadFile(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (logParseWorker.current) {
|
||||||
|
logParseWorker.current.terminate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
downloadStream
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { driver, type Config } from 'driver.js';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export const useDriver = (config?: Config & { id: string }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const driverRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const handleDoNotShowAgain = () => {};
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
driverRef.current = driver({
|
||||||
|
overlayOpacity: 0.2,
|
||||||
|
animate: false,
|
||||||
|
...config
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const start = () => {
|
||||||
|
if (!driverRef.current) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
driverRef.current.drive();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
driverRef.current?.destroy();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { start, initDriver: init, driver: driverRef.current };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDriver;
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import HotKeys from '@/config/hotkeys';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import { throttle } from 'lodash';
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ css, token }) => ({
|
||||||
|
hintOverlay: css`
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: var(--color-esc-hint-bg);
|
||||||
|
color: ${token.colorTextLightSolid};
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 2000;
|
||||||
|
font-size: 14px;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: fadeInOut 2s ease-in-out;
|
||||||
|
@keyframes fadeInOut {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
10% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
90% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function useEscHint(options?: {
|
||||||
|
enabled?: boolean;
|
||||||
|
message?: string;
|
||||||
|
throttleDelay?: number;
|
||||||
|
}) {
|
||||||
|
const { enabled = true, message, throttleDelay = 3000 } = options || {};
|
||||||
|
const intl = useIntl();
|
||||||
|
const { styles } = useStyles();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const timeoutRef = useRef<any>(null);
|
||||||
|
const isHintActiveRef = useRef(false);
|
||||||
|
|
||||||
|
const showHintThrottled = useMemo(
|
||||||
|
() =>
|
||||||
|
throttle(
|
||||||
|
() => {
|
||||||
|
if (isHintActiveRef.current) return;
|
||||||
|
|
||||||
|
isHintActiveRef.current = true;
|
||||||
|
|
||||||
|
setVisible(true);
|
||||||
|
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
setVisible(false);
|
||||||
|
isHintActiveRef.current = false;
|
||||||
|
}, 2000);
|
||||||
|
},
|
||||||
|
throttleDelay,
|
||||||
|
{
|
||||||
|
leading: true,
|
||||||
|
trailing: false
|
||||||
|
}
|
||||||
|
),
|
||||||
|
[throttleDelay]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
HotKeys.ESC,
|
||||||
|
() => {
|
||||||
|
if (!enabled) return;
|
||||||
|
showHintThrottled();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: enabled
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
showHintThrottled.cancel();
|
||||||
|
};
|
||||||
|
}, [showHintThrottled]);
|
||||||
|
|
||||||
|
const EscHint = visible ? (
|
||||||
|
<div className={styles.hintOverlay}>
|
||||||
|
{message || intl.formatMessage({ id: 'common.tips.escape.disable' })}
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
return { EscHint };
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import qs from 'query-string';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export const createEventSourceURL = (url: string) => {
|
||||||
|
const { host, protocol } = window.location;
|
||||||
|
return `${protocol}://${host}${url}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
0: connecting
|
||||||
|
1: connect successfully
|
||||||
|
2: closed
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function useEventSource() {
|
||||||
|
const eventSourceRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const createEventSourceConnection = (query: {
|
||||||
|
url: string;
|
||||||
|
params: any;
|
||||||
|
onmessage?: (data: any) => void;
|
||||||
|
}) => {
|
||||||
|
eventSourceRef.current?.close?.();
|
||||||
|
|
||||||
|
const { url, params, onmessage = () => {} } = query;
|
||||||
|
|
||||||
|
const sseurl = createEventSourceURL(url);
|
||||||
|
|
||||||
|
eventSourceRef.current = new EventSource(
|
||||||
|
`${url}?${qs.stringify({
|
||||||
|
...params
|
||||||
|
})}`,
|
||||||
|
{
|
||||||
|
withCredentials: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
eventSourceRef.current.onmessage = (res: any) => {
|
||||||
|
try {
|
||||||
|
console.log('event source message: ', { res, resData: res });
|
||||||
|
const data = JSON.parse(res.data);
|
||||||
|
onmessage(data);
|
||||||
|
} catch (error) {
|
||||||
|
// error
|
||||||
|
console.log('event source error: ', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSourceRef.current.onclose = () => {
|
||||||
|
console.log('event source closed...');
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSourceRef.current.onopen = () => {
|
||||||
|
console.log('event source connected...');
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSourceRef.current.onerror = (error: any) => {
|
||||||
|
console.log('event source error: ', error);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
eventSourceRef.current?.close?.();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventSourceRef: eventSourceRef,
|
||||||
|
createEventSourceConnection
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
import { throttle } from 'lodash';
|
||||||
|
import {
|
||||||
|
UseOverlayScrollbarsParams,
|
||||||
|
useOverlayScrollbars
|
||||||
|
} from 'overlayscrollbars-react';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import useUserSettings from './use-user-settings';
|
||||||
|
|
||||||
|
type OverflowBehavior =
|
||||||
|
| 'hidden'
|
||||||
|
| 'scroll'
|
||||||
|
| 'visible'
|
||||||
|
| 'visible-hidden'
|
||||||
|
| 'visible-scroll';
|
||||||
|
export interface OverlayScrollerOptions {
|
||||||
|
oppositeTheme?: boolean;
|
||||||
|
overflow?: {
|
||||||
|
x?: OverflowBehavior;
|
||||||
|
y?: OverflowBehavior;
|
||||||
|
};
|
||||||
|
scrollbars?: {
|
||||||
|
theme?: 'os-theme-light' | 'os-theme-dark';
|
||||||
|
autoHide?: 'never' | 'scroll' | 'leave' | 'move';
|
||||||
|
autoHideDelay?: number;
|
||||||
|
clickScroll?: boolean | 'instant';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const overlaySollerOptions: UseOverlayScrollbarsParams = {
|
||||||
|
options: {
|
||||||
|
update: {
|
||||||
|
debounce: 0
|
||||||
|
},
|
||||||
|
overflow: {
|
||||||
|
x: 'hidden'
|
||||||
|
},
|
||||||
|
scrollbars: {
|
||||||
|
theme: 'os-theme-light',
|
||||||
|
autoHide: 'scroll',
|
||||||
|
autoHideDelay: 600,
|
||||||
|
clickScroll: 'instant'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defer: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const RESETSCROLLDELAY = 5000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param options.theme: if set theme, it will fix the theme
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function useOverlayScroller(data?: {
|
||||||
|
options?: OverlayScrollerOptions;
|
||||||
|
events?: any;
|
||||||
|
defer?: boolean;
|
||||||
|
}) {
|
||||||
|
const { userSettings } = useUserSettings();
|
||||||
|
const { options, events, defer = true } = data || {};
|
||||||
|
const { scrollbars, overflow, oppositeTheme } = options || {};
|
||||||
|
const scrollEventElement = React.useRef<any>(null);
|
||||||
|
const instanceRef = React.useRef<any>(null);
|
||||||
|
const initialized = React.useRef(false);
|
||||||
|
const scrollElementRef = React.useRef<any>(null);
|
||||||
|
const stopUpdatePosition = React.useRef(false);
|
||||||
|
const timerRef = React.useRef<any>(null);
|
||||||
|
const [initialize, instance] = useOverlayScrollbars({
|
||||||
|
options: {
|
||||||
|
update: {
|
||||||
|
debounce: 0
|
||||||
|
},
|
||||||
|
overflow: {
|
||||||
|
x: 'hidden',
|
||||||
|
...overflow
|
||||||
|
},
|
||||||
|
scrollbars: {
|
||||||
|
autoHide: 'scroll',
|
||||||
|
autoHideDelay: 600,
|
||||||
|
clickScroll: 'instant',
|
||||||
|
...scrollbars,
|
||||||
|
theme:
|
||||||
|
scrollbars?.theme ||
|
||||||
|
(userSettings.theme === 'light' || !userSettings.theme
|
||||||
|
? 'os-theme-dark'
|
||||||
|
: 'os-theme-light')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
...events
|
||||||
|
},
|
||||||
|
defer: defer
|
||||||
|
});
|
||||||
|
|
||||||
|
instanceRef.current = instance?.();
|
||||||
|
scrollEventElement.current =
|
||||||
|
instanceRef.current?.elements()?.scrollEventElement;
|
||||||
|
|
||||||
|
const handleOnScroll = () => {
|
||||||
|
const scrollTop = scrollEventElement.current?.scrollTop;
|
||||||
|
const scrollHeight = scrollEventElement.current?.scrollHeight;
|
||||||
|
const clientHeight = scrollEventElement.current?.clientHeight;
|
||||||
|
|
||||||
|
const isBottom = scrollTop + clientHeight + 20 >= scrollHeight;
|
||||||
|
|
||||||
|
if (isBottom) {
|
||||||
|
stopUpdatePosition.current = false;
|
||||||
|
} else {
|
||||||
|
stopUpdatePosition.current = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const throttledScroll = useMemoizedFn(
|
||||||
|
throttle(() => {
|
||||||
|
scrollEventElement.current?.scrollTo?.({
|
||||||
|
top: scrollEventElement.current?.scrollHeight,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
instanceRef.current?.update?.();
|
||||||
|
}, 100)
|
||||||
|
);
|
||||||
|
|
||||||
|
const scrollauto = useMemoizedFn(() => {
|
||||||
|
scrollEventElement.current?.scrollTo?.({
|
||||||
|
top: scrollEventElement.current.scrollHeight,
|
||||||
|
behavior: 'auto'
|
||||||
|
});
|
||||||
|
instanceRef.current?.update?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
// scroll to bottom
|
||||||
|
const throttledUpdateScrollerPosition = useMemoizedFn((delay?: number) => {
|
||||||
|
if (stopUpdatePosition.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (delay === 0) {
|
||||||
|
scrollauto();
|
||||||
|
} else {
|
||||||
|
throttledScroll();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// scroll to top
|
||||||
|
const updateScrollerPositionToTop = useMemoizedFn(() => {
|
||||||
|
scrollEventElement.current?.scrollTo?.({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'auto'
|
||||||
|
});
|
||||||
|
instanceRef.current?.update?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
const generateInstance = () => {
|
||||||
|
instanceRef.current = instance?.();
|
||||||
|
scrollEventElement.current =
|
||||||
|
instanceRef.current?.elements()?.scrollEventElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWheelCallback = useMemoizedFn((e: any) => {
|
||||||
|
handleOnScroll();
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
}
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
stopUpdatePosition.current = false;
|
||||||
|
}, RESETSCROLLDELAY);
|
||||||
|
});
|
||||||
|
|
||||||
|
// add wheel event
|
||||||
|
const handleWheelEvent = () => {
|
||||||
|
scrollElementRef.current?.addEventListener?.('wheel', handleWheelCallback, {
|
||||||
|
passive: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// remove wheel event
|
||||||
|
const removeWheelEvent = () => {
|
||||||
|
scrollElementRef.current?.removeEventListener?.(
|
||||||
|
'wheel',
|
||||||
|
handleWheelCallback,
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createInstance = useMemoizedFn((el: any) => {
|
||||||
|
if (instanceRef.current) {
|
||||||
|
return instanceRef.current;
|
||||||
|
}
|
||||||
|
if (el) {
|
||||||
|
initialize(el);
|
||||||
|
scrollElementRef.current = el;
|
||||||
|
initialized.current = true;
|
||||||
|
instanceRef.current = instance?.();
|
||||||
|
scrollEventElement.current =
|
||||||
|
instanceRef.current?.elements()?.scrollEventElement;
|
||||||
|
handleWheelEvent();
|
||||||
|
}
|
||||||
|
return instanceRef.current;
|
||||||
|
});
|
||||||
|
|
||||||
|
const destroyInstance = () => {
|
||||||
|
instanceRef.current?.destroy?.();
|
||||||
|
removeWheelEvent();
|
||||||
|
instanceRef.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToTarget = (target: any, offset = 100) => {
|
||||||
|
if (!target) return;
|
||||||
|
if (!instanceRef.current || !scrollEventElement.current) {
|
||||||
|
instanceRef.current = instance?.();
|
||||||
|
scrollEventElement.current =
|
||||||
|
instanceRef.current?.elements()?.scrollEventElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewport = instanceRef.current?.elements().viewport;
|
||||||
|
const containerRect = viewport.getBoundingClientRect();
|
||||||
|
const targetRect = target.getBoundingClientRect();
|
||||||
|
|
||||||
|
const scrollerState = instanceRef.current?.state();
|
||||||
|
|
||||||
|
const currentScroll = scrollerState.current?.overflowAmount?.y;
|
||||||
|
|
||||||
|
// const currentScroll = instanceRef.current?.scroll().position.y;
|
||||||
|
const targetPos = targetRect.top - containerRect.top + currentScroll;
|
||||||
|
|
||||||
|
scrollEventElement.current.scroll({
|
||||||
|
y: targetPos - offset,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
instanceRef.current?.update?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScrollElementScrollableHeight = () => {
|
||||||
|
if (!instanceRef.current || !scrollEventElement.current) {
|
||||||
|
instanceRef.current = instance?.();
|
||||||
|
scrollEventElement.current =
|
||||||
|
instanceRef.current?.elements()?.scrollEventElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollOffsetElement = instanceRef.current?.elements().viewport;
|
||||||
|
|
||||||
|
return {
|
||||||
|
scrollTop: scrollOffsetElement?.scrollTop,
|
||||||
|
scrollHeight:
|
||||||
|
scrollOffsetElement?.scrollHeight - scrollOffsetElement?.clientHeight
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScrollElement = () => {
|
||||||
|
if (!instanceRef.current || !scrollEventElement.current) {
|
||||||
|
instanceRef.current = instance?.();
|
||||||
|
scrollEventElement.current =
|
||||||
|
instanceRef.current?.elements()?.scrollEventElement;
|
||||||
|
}
|
||||||
|
return scrollEventElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
instanceRef.current?.destroy?.();
|
||||||
|
removeWheelEvent();
|
||||||
|
};
|
||||||
|
}, [instance]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
initialize: createInstance,
|
||||||
|
instance: instanceRef,
|
||||||
|
scrollEventElement: scrollEventElement,
|
||||||
|
initialized: initialized.current,
|
||||||
|
getScrollElementScrollableHeight,
|
||||||
|
getScrollElement,
|
||||||
|
generateInstance,
|
||||||
|
destroyInstance: destroyInstance,
|
||||||
|
updateScrollerPosition: throttledUpdateScrollerPosition,
|
||||||
|
updateScrollerPositionToTop: updateScrollerPositionToTop,
|
||||||
|
scrollToBottom: scrollauto,
|
||||||
|
scrollToTop: updateScrollerPositionToTop,
|
||||||
|
scrollToTarget
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import VersionInfo, { modalConfig } from '@/components/version-info';
|
|||||||
import externalLinks from '@/constants/external-links';
|
import externalLinks from '@/constants/external-links';
|
||||||
import useBodyScroll from '@/hooks/use-body-scroll';
|
import useBodyScroll from '@/hooks/use-body-scroll';
|
||||||
import { logout } from '@/pages/login/apis';
|
import { logout } from '@/pages/login/apis';
|
||||||
import { getGPUStackPlugin } from '@/plugins';
|
|
||||||
import { useModel } from '@@/plugin-model';
|
import { useModel } from '@@/plugin-model';
|
||||||
import {
|
import {
|
||||||
DiscordOutlined,
|
DiscordOutlined,
|
||||||
@@ -99,7 +98,6 @@ const CustomItem = styled.div`
|
|||||||
|
|
||||||
export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
||||||
const { isDarkTheme } = props;
|
const { isDarkTheme } = props;
|
||||||
const plugin = getGPUStackPlugin();
|
|
||||||
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
|
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
|
||||||
const [modal, contextHolder] = Modal.useModal();
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
const [version] = useAtom(GPUStackVersionAtom);
|
const [version] = useAtom(GPUStackVersionAtom);
|
||||||
@@ -288,7 +286,6 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
|||||||
</NewLabel>
|
</NewLabel>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!plugin && (
|
|
||||||
<DropdownActions menu={{ ...helpMenu }} popupRender={helpPopupRender}>
|
<DropdownActions menu={{ ...helpMenu }} popupRender={helpPopupRender}>
|
||||||
<IconWrapper>
|
<IconWrapper>
|
||||||
<IconFont
|
<IconFont
|
||||||
@@ -298,7 +295,6 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
|||||||
/>
|
/>
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
</DropdownActions>
|
</DropdownActions>
|
||||||
)}
|
|
||||||
<PluginExtraField name="GlobalSettings" />
|
<PluginExtraField name="GlobalSettings" />
|
||||||
<DropdownActions menu={{ ...userMenu }} popupRender={userPopupRender}>
|
<DropdownActions menu={{ ...userMenu }} popupRender={userPopupRender}>
|
||||||
<IconWrapper>
|
<IconWrapper>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import externalLinks from '@/constants/external-links';
|
import externalLinks from '@/constants/external-links';
|
||||||
import { GithubFilled } from '@ant-design/icons';
|
import { GithubFilled } from '@ant-design/icons';
|
||||||
import { nsLocal } from '@gpustack/core-ui/utils';
|
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Tooltip } from 'antd';
|
import { Tooltip } from 'antd';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -67,7 +66,7 @@ type CacheEntry = { value: number; time: number };
|
|||||||
|
|
||||||
const readCache = (): CacheEntry | null => {
|
const readCache = (): CacheEntry | null => {
|
||||||
try {
|
try {
|
||||||
const raw = nsLocal.get(CACHE_KEY);
|
const raw = localStorage.getItem(CACHE_KEY);
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
if (typeof parsed?.value !== 'number' || typeof parsed?.time !== 'number') {
|
if (typeof parsed?.value !== 'number' || typeof parsed?.time !== 'number') {
|
||||||
@@ -81,7 +80,10 @@ const readCache = (): CacheEntry | null => {
|
|||||||
|
|
||||||
const writeCache = (value: number) => {
|
const writeCache = (value: number) => {
|
||||||
try {
|
try {
|
||||||
nsLocal.set(CACHE_KEY, JSON.stringify({ value, time: Date.now() }));
|
localStorage.setItem(
|
||||||
|
CACHE_KEY,
|
||||||
|
JSON.stringify({ value, time: Date.now() })
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
// ignore quota errors
|
// ignore quota errors
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,21 @@ import { ExtraContent } from './extraRender';
|
|||||||
import { patchRoutes } from './runtime';
|
import { patchRoutes } from './runtime';
|
||||||
import SiderMenu from './sider-menu';
|
import SiderMenu from './sider-menu';
|
||||||
|
|
||||||
|
// Pages that use the page container in the page
|
||||||
|
const NO_CONTAINER_PAGES = [
|
||||||
|
'chat',
|
||||||
|
'rerank',
|
||||||
|
'embedding',
|
||||||
|
'speech',
|
||||||
|
'image',
|
||||||
|
'text2images',
|
||||||
|
'clusterDetail',
|
||||||
|
'clusterCreate',
|
||||||
|
'benchmarkDetail',
|
||||||
|
'deployment',
|
||||||
|
'video'
|
||||||
|
];
|
||||||
|
|
||||||
const CHECK_RESOURCE_PATH = [
|
const CHECK_RESOURCE_PATH = [
|
||||||
'/resources/workers',
|
'/resources/workers',
|
||||||
'/resources/clusters/list',
|
'/resources/clusters/list',
|
||||||
@@ -254,6 +269,11 @@ export default (props: any) => {
|
|||||||
[location.pathname]
|
[location.pathname]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isNoContainerPage = useMemo(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
return NO_CONTAINER_PAGES.includes(matchedRoute?.name as string);
|
||||||
|
}, [matchedRoute]);
|
||||||
|
|
||||||
const collapsed = useMemo(() => {
|
const collapsed = useMemo(() => {
|
||||||
return userSettings.collapsed || false;
|
return userSettings.collapsed || false;
|
||||||
}, [userSettings.collapsed]);
|
}, [userSettings.collapsed]);
|
||||||
@@ -449,11 +469,15 @@ export default (props: any) => {
|
|||||||
unAccessible={runtimeConfig?.unAccessible}
|
unAccessible={runtimeConfig?.unAccessible}
|
||||||
noAccessible={runtimeConfig?.noAccessible}
|
noAccessible={runtimeConfig?.noAccessible}
|
||||||
>
|
>
|
||||||
|
{isNoContainerPage ? (
|
||||||
|
<Outlet />
|
||||||
|
) : (
|
||||||
<PageContainerInner>
|
<PageContainerInner>
|
||||||
<div>
|
<div>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</PageContainerInner>
|
</PageContainerInner>
|
||||||
|
)}
|
||||||
</Exception>
|
</Exception>
|
||||||
</div>
|
</div>
|
||||||
{NoResourceModal}
|
{NoResourceModal}
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ export default {
|
|||||||
'On the Worker that needs to be added, run the following command to join it to the cluster.',
|
'On the Worker that needs to be added, run the following command to join it to the cluster.',
|
||||||
'clusters.create.addCommand.k8s.tips':
|
'clusters.create.addCommand.k8s.tips':
|
||||||
'On the Kubernetes cluster that needs to be registered, run the following command to create the Kubernetes resources and register the cluster.',
|
'On the Kubernetes cluster that needs to be registered, run the following command to create the Kubernetes resources and register the cluster.',
|
||||||
'clusters.create.addCommand.k8s.version.warning':
|
|
||||||
'The minimum supported Kubernetes version is 1.23. To use the GPU Service feature, the minimum supported Kubernetes version is 1.27.',
|
|
||||||
'clusters.create.register.tips':
|
'clusters.create.register.tips':
|
||||||
'On the Kubernetes cluster that needs to be added, run the following command to join its nodes to the cluster.',
|
'On the Kubernetes cluster that needs to be added, run the following command to join its nodes to the cluster.',
|
||||||
'cluster.create.checkEnv.tips':
|
'cluster.create.checkEnv.tips':
|
||||||
|
|||||||
@@ -267,10 +267,6 @@ export default {
|
|||||||
'common.select.count': '{count} selected',
|
'common.select.count': '{count} selected',
|
||||||
'common.login.auth': 'Authenticating...',
|
'common.login.auth': 'Authenticating...',
|
||||||
'common.login.auth.failed': 'Authentication failed',
|
'common.login.auth.failed': 'Authentication failed',
|
||||||
'common.login.error.source_conflict':
|
|
||||||
'An account with this username already exists from a different authentication source. Please contact an administrator to link or convert it.',
|
|
||||||
'common.login.error.auth_failed':
|
|
||||||
'Authentication with the identity provider failed. Please try again or contact your administrator.',
|
|
||||||
'common.login.password': 'Log in with Password',
|
'common.login.password': 'Log in with Password',
|
||||||
'common.login.username.holder': 'Please enter username',
|
'common.login.username.holder': 'Please enter username',
|
||||||
'common.login.password.holder': 'Please enter password',
|
'common.login.password.holder': 'Please enter password',
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ export default {
|
|||||||
'gpuservice.template': 'GPU Instance Template',
|
'gpuservice.template': 'GPU Instance Template',
|
||||||
'gpuservice.template.add': 'Add Instance Template',
|
'gpuservice.template.add': 'Add Instance Template',
|
||||||
'gpuservice.template.edit': 'Edit Instance Template',
|
'gpuservice.template.edit': 'Edit Instance Template',
|
||||||
'gpuservice.template.clone': 'Clone Instance Template',
|
|
||||||
'gpuservice.template.filter.name': 'Filter by name',
|
'gpuservice.template.filter.name': 'Filter by name',
|
||||||
'gpuservice.template.filter.vendor': 'Filter by vendor',
|
'gpuservice.template.filter.vendor': 'Filter by vendor',
|
||||||
'gpuservice.template.image': 'Image',
|
'gpuservice.template.image': 'Image',
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default {
|
|||||||
'menu.models.modelCatalog': 'Catalog',
|
'menu.models.modelCatalog': 'Catalog',
|
||||||
'menu.models.catalog': 'Model Catalog',
|
'menu.models.catalog': 'Model Catalog',
|
||||||
'menu.models.deployment': 'Deployments',
|
'menu.models.deployment': 'Deployments',
|
||||||
'menu.models.userModels': 'Models',
|
'menu.models.userModels': 'My Models',
|
||||||
'menu.models.benchmark': 'Benchmarks',
|
'menu.models.benchmark': 'Benchmarks',
|
||||||
'menu.models.benchmarkDetail': 'Benchmark Details',
|
'menu.models.benchmarkDetail': 'Benchmark Details',
|
||||||
'menu.models.providers': 'Providers',
|
'menu.models.providers': 'Providers',
|
||||||
|
|||||||
@@ -12,11 +12,6 @@ export default {
|
|||||||
'users.form.active.description': 'Enable or disable this user account',
|
'users.form.active.description': 'Enable or disable this user account',
|
||||||
'users.form.fullname': 'Full Name',
|
'users.form.fullname': 'Full Name',
|
||||||
'users.form.source': 'Source',
|
'users.form.source': 'Source',
|
||||||
'users.form.source.local': 'Local',
|
|
||||||
'users.form.source.tip.switchToLocal':
|
|
||||||
'Switching to Local requires a new password. The user will sign in via the standard login form.',
|
|
||||||
'users.form.source.tip.switchToExternal':
|
|
||||||
"Switching to an external source clears the user's local password. They will sign in via the configured identity provider.",
|
|
||||||
'users.table.user': 'users',
|
'users.table.user': 'users',
|
||||||
'users.form.admin': 'Admin',
|
'users.form.admin': 'Admin',
|
||||||
'users.form.user': 'User',
|
'users.form.user': 'User',
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ export default {
|
|||||||
'On the Worker that needs to be added, run the following command to join it to the cluster.',
|
'On the Worker that needs to be added, run the following command to join it to the cluster.',
|
||||||
'clusters.create.addCommand.k8s.tips':
|
'clusters.create.addCommand.k8s.tips':
|
||||||
'登録する Kubernetes クラスターで以下のコマンドを実行し、Kubernetes リソースを作成してクラスターを登録します。',
|
'登録する Kubernetes クラスターで以下のコマンドを実行し、Kubernetes リソースを作成してクラスターを登録します。',
|
||||||
'clusters.create.addCommand.k8s.version.warning':
|
|
||||||
'サポートされる Kubernetes の最小バージョンは 1.23 です。GPU Service 機能を使用する場合、サポートされる Kubernetes の最小バージョンは 1.27 です。',
|
|
||||||
'cluster.create.checkEnv.tips':
|
'cluster.create.checkEnv.tips':
|
||||||
'Use the following command to check if the environment is ready.',
|
'Use the following command to check if the environment is ready.',
|
||||||
'clusters.create.register.tips':
|
'clusters.create.register.tips':
|
||||||
|
|||||||
@@ -266,10 +266,6 @@ export default {
|
|||||||
'common.select.count': '{count} selected',
|
'common.select.count': '{count} selected',
|
||||||
'common.login.auth': 'Authenticating...',
|
'common.login.auth': 'Authenticating...',
|
||||||
'common.login.auth.failed': 'Authentication failed',
|
'common.login.auth.failed': 'Authentication failed',
|
||||||
'common.login.error.source_conflict':
|
|
||||||
'このユーザー名のアカウントは別の認証ソースで既に存在します。管理者にリンクまたは変換を依頼してください。',
|
|
||||||
'common.login.error.auth_failed':
|
|
||||||
'ID プロバイダーでの認証に失敗しました。再試行するか、管理者にお問い合わせください。',
|
|
||||||
'common.login.password': 'Log in with Password',
|
'common.login.password': 'Log in with Password',
|
||||||
'common.login.username.holder': 'Please enter username',
|
'common.login.username.holder': 'Please enter username',
|
||||||
'common.login.password.holder': 'Please enter password',
|
'common.login.password.holder': 'Please enter password',
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ export default {
|
|||||||
'gpuservice.template': 'GPU インスタンステンプレート',
|
'gpuservice.template': 'GPU インスタンステンプレート',
|
||||||
'gpuservice.template.add': 'インスタンステンプレートを追加',
|
'gpuservice.template.add': 'インスタンステンプレートを追加',
|
||||||
'gpuservice.template.edit': 'インスタンステンプレートを編集',
|
'gpuservice.template.edit': 'インスタンステンプレートを編集',
|
||||||
'gpuservice.template.clone': 'インスタンステンプレートを複製',
|
|
||||||
'gpuservice.template.filter.name': '名前でフィルター',
|
'gpuservice.template.filter.name': '名前でフィルター',
|
||||||
'gpuservice.template.filter.vendor': 'ベンダーでフィルター',
|
'gpuservice.template.filter.vendor': 'ベンダーでフィルター',
|
||||||
'gpuservice.template.image': 'コンテナイメージ',
|
'gpuservice.template.image': 'コンテナイメージ',
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default {
|
|||||||
'menu.accessControl.organizations': 'Organizations',
|
'menu.accessControl.organizations': 'Organizations',
|
||||||
'menu.resources.clusters': 'Clusters',
|
'menu.resources.clusters': 'Clusters',
|
||||||
'menu.resources.credentials': 'Cloud Credentials',
|
'menu.resources.credentials': 'Cloud Credentials',
|
||||||
'menu.models.userModels': 'Models',
|
'menu.models.userModels': 'My Models',
|
||||||
'menu.resources.clusterDetail': 'Cluster Detail',
|
'menu.resources.clusterDetail': 'Cluster Detail',
|
||||||
'menu.resources.clusterCreate': 'Create Cluster',
|
'menu.resources.clusterCreate': 'Create Cluster',
|
||||||
'menu.models.backendsList': 'Inference Backends',
|
'menu.models.backendsList': 'Inference Backends',
|
||||||
@@ -62,11 +62,11 @@ export default {
|
|||||||
// 6. 'menu.accessControl.apikeys': 'API Keys',
|
// 6. 'menu.accessControl.apikeys': 'API Keys',
|
||||||
// 7. 'menu.accessControl.users': 'Users',
|
// 7. 'menu.accessControl.users': 'Users',
|
||||||
// 8. 'menu.clusterManagement': 'Cluster Management',
|
// 8. 'menu.clusterManagement': 'Cluster Management',
|
||||||
// 9. 'menu.resources.clusters': 'Clusters',
|
// 9. 'menu.clusterManagement.clusters': 'Clusters',
|
||||||
// 10. 'menu.resources.credentials': 'Cloud Credentials',
|
// 10. 'menu.clusterManagement.credentials': 'Cloud Credentials',
|
||||||
// 11. 'menu.models.userModels': 'Models'
|
// 11. 'menu.models.userModels': 'My Models'
|
||||||
// 12. 'menu.resources.clusterDetail': 'Cluster Detail',
|
// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail',
|
||||||
// 13. 'menu.resources.clusterCreate': 'Create Cluster',
|
// 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster',
|
||||||
// 14. 'menu.models.backendsList': 'Inference Backends',
|
// 14. 'menu.models.backendsList': 'Inference Backends',
|
||||||
// 15. 'menu.models.benchmark': 'Benchmarks',
|
// 15. 'menu.models.benchmark': 'Benchmarks',
|
||||||
// 15. 'menu.models.provider': 'Provider',
|
// 15. 'menu.models.provider': 'Provider',
|
||||||
|
|||||||
@@ -13,11 +13,6 @@ export default {
|
|||||||
'このユーザーアカウントを有効または無効にする',
|
'このユーザーアカウントを有効または無効にする',
|
||||||
'users.form.fullname': 'フルネーム',
|
'users.form.fullname': 'フルネーム',
|
||||||
'users.form.source': 'ソース',
|
'users.form.source': 'ソース',
|
||||||
'users.form.source.local': 'ローカル',
|
|
||||||
'users.form.source.tip.switchToLocal':
|
|
||||||
'ローカルに切り替えるには新しいパスワードが必要です。以後、ユーザーは標準のログインフォームからサインインします。',
|
|
||||||
'users.form.source.tip.switchToExternal':
|
|
||||||
'外部ソースに切り替えるとユーザーのローカルパスワードが削除され、設定済みの ID プロバイダーからサインインするようになります。',
|
|
||||||
'users.table.user': 'ユーザー',
|
'users.table.user': 'ユーザー',
|
||||||
'users.form.admin': '管理者',
|
'users.form.admin': '管理者',
|
||||||
'users.form.user': '一般ユーザー',
|
'users.form.user': '一般ユーザー',
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ export default {
|
|||||||
'На воркере, который необходимо добавить, выполните следующую команду, чтобы присоединить его к кластеру.',
|
'На воркере, который необходимо добавить, выполните следующую команду, чтобы присоединить его к кластеру.',
|
||||||
'clusters.create.addCommand.k8s.tips':
|
'clusters.create.addCommand.k8s.tips':
|
||||||
'На Kubernetes-кластере, который необходимо зарегистрировать, выполните следующую команду, чтобы создать ресурсы Kubernetes и зарегистрировать этот кластер.',
|
'На Kubernetes-кластере, который необходимо зарегистрировать, выполните следующую команду, чтобы создать ресурсы Kubernetes и зарегистрировать этот кластер.',
|
||||||
'clusters.create.addCommand.k8s.version.warning':
|
|
||||||
'Минимальная поддерживаемая версия Kubernetes — 1.23. Для использования функции GPU Service минимальная поддерживаемая версия Kubernetes — 1.27.',
|
|
||||||
'cluster.create.checkEnv.tips':
|
'cluster.create.checkEnv.tips':
|
||||||
'Используйте следующую команду для проверки готовности окружения',
|
'Используйте следующую команду для проверки готовности окружения',
|
||||||
'clusters.create.register.tips':
|
'clusters.create.register.tips':
|
||||||
|
|||||||
@@ -265,10 +265,6 @@ export default {
|
|||||||
'common.select.count': '{count} Выбрано',
|
'common.select.count': '{count} Выбрано',
|
||||||
'common.login.auth': 'Аутентификация...',
|
'common.login.auth': 'Аутентификация...',
|
||||||
'common.login.auth.failed': 'Ошибка аутентификации',
|
'common.login.auth.failed': 'Ошибка аутентификации',
|
||||||
'common.login.error.source_conflict':
|
|
||||||
'Учётная запись с таким именем уже существует, но с другим источником аутентификации. Обратитесь к администратору для связывания или преобразования.',
|
|
||||||
'common.login.error.auth_failed':
|
|
||||||
'Не удалось пройти аутентификацию через провайдера идентификации. Попробуйте ещё раз или обратитесь к администратору.',
|
|
||||||
'common.login.password': 'Войти с паролем',
|
'common.login.password': 'Войти с паролем',
|
||||||
'common.login.username.holder': 'Введите имя пользователя',
|
'common.login.username.holder': 'Введите имя пользователя',
|
||||||
'common.login.password.holder': 'Введите пароль',
|
'common.login.password.holder': 'Введите пароль',
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ export default {
|
|||||||
'gpuservice.template': 'Шаблон экземпляра GPU',
|
'gpuservice.template': 'Шаблон экземпляра GPU',
|
||||||
'gpuservice.template.add': 'Добавить шаблон экземпляра',
|
'gpuservice.template.add': 'Добавить шаблон экземпляра',
|
||||||
'gpuservice.template.edit': 'Редактировать шаблон экземпляра',
|
'gpuservice.template.edit': 'Редактировать шаблон экземпляра',
|
||||||
'gpuservice.template.clone': 'Клонировать шаблон экземпляра',
|
|
||||||
'gpuservice.template.filter.name': 'Фильтр по имени',
|
'gpuservice.template.filter.name': 'Фильтр по имени',
|
||||||
'gpuservice.template.filter.vendor': 'Фильтр по производителю',
|
'gpuservice.template.filter.vendor': 'Фильтр по производителю',
|
||||||
'gpuservice.template.image': 'Образ',
|
'gpuservice.template.image': 'Образ',
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default {
|
|||||||
'menu.accessControl.organizations': 'Организации',
|
'menu.accessControl.organizations': 'Организации',
|
||||||
'menu.resources.clusters': 'Кластеры',
|
'menu.resources.clusters': 'Кластеры',
|
||||||
'menu.resources.credentials': 'Облачные аккаунты',
|
'menu.resources.credentials': 'Облачные аккаунты',
|
||||||
'menu.models.userModels': 'Модели',
|
'menu.models.userModels': 'Мои модели',
|
||||||
'menu.resources.clusterDetail': 'Детали кластера',
|
'menu.resources.clusterDetail': 'Детали кластера',
|
||||||
'menu.resources.clusterCreate': 'Создать кластер',
|
'menu.resources.clusterCreate': 'Создать кластер',
|
||||||
'menu.models.backendsList': 'Бэкенды запуска',
|
'menu.models.backendsList': 'Бэкенды запуска',
|
||||||
|
|||||||
@@ -13,11 +13,6 @@ export default {
|
|||||||
'Включить или отключить эту учетную запись пользователя',
|
'Включить или отключить эту учетную запись пользователя',
|
||||||
'users.form.fullname': 'Полное имя',
|
'users.form.fullname': 'Полное имя',
|
||||||
'users.form.source': 'Источник',
|
'users.form.source': 'Источник',
|
||||||
'users.form.source.local': 'Локальный',
|
|
||||||
'users.form.source.tip.switchToLocal':
|
|
||||||
'Переключение на «Локальный» требует ввода нового пароля. После этого пользователь будет входить через стандартную форму входа.',
|
|
||||||
'users.form.source.tip.switchToExternal':
|
|
||||||
'Переключение на внешний источник удаляет локальный пароль пользователя. После этого вход будет выполняться через настроенного провайдера идентификации.',
|
|
||||||
'users.table.user': 'пользователи',
|
'users.table.user': 'пользователи',
|
||||||
'users.form.admin': 'Администратор',
|
'users.form.admin': 'Администратор',
|
||||||
'users.form.user': 'Пользователь',
|
'users.form.user': 'Пользователь',
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ export default {
|
|||||||
'Eklenmesi gereken İşçi Düğümde, kümeye katılması için aşağıdaki komutu çalıştırın.',
|
'Eklenmesi gereken İşçi Düğümde, kümeye katılması için aşağıdaki komutu çalıştırın.',
|
||||||
'clusters.create.addCommand.k8s.tips':
|
'clusters.create.addCommand.k8s.tips':
|
||||||
'Kaydedilmesi gereken Kubernetes kümesinde, Kubernetes kaynaklarını oluşturmak ve kümeyi kaydetmek için aşağıdaki komutu çalıştırın.',
|
'Kaydedilmesi gereken Kubernetes kümesinde, Kubernetes kaynaklarını oluşturmak ve kümeyi kaydetmek için aşağıdaki komutu çalıştırın.',
|
||||||
'clusters.create.addCommand.k8s.version.warning':
|
|
||||||
'Desteklenen minimum Kubernetes sürümü 1.23’tür. GPU Service özelliğini kullanmak için desteklenen minimum Kubernetes sürümü 1.27’dir.',
|
|
||||||
'clusters.create.register.tips':
|
'clusters.create.register.tips':
|
||||||
'Eklenmesi gereken Kubernetes kümesinde, düğümlerini kümeye katılması için aşağıdaki komutu çalıştırın.',
|
'Eklenmesi gereken Kubernetes kümesinde, düğümlerini kümeye katılması için aşağıdaki komutu çalıştırın.',
|
||||||
'cluster.create.checkEnv.tips':
|
'cluster.create.checkEnv.tips':
|
||||||
|
|||||||
@@ -269,10 +269,6 @@ export default {
|
|||||||
'common.select.count': '{count} seçildi',
|
'common.select.count': '{count} seçildi',
|
||||||
'common.login.auth': 'Kimlik doğrulanıyor...',
|
'common.login.auth': 'Kimlik doğrulanıyor...',
|
||||||
'common.login.auth.failed': 'Kimlik doğrulama başarısız',
|
'common.login.auth.failed': 'Kimlik doğrulama başarısız',
|
||||||
'common.login.error.source_conflict':
|
|
||||||
'Bu kullanıcı adıyla farklı bir kimlik doğrulama kaynağından bir hesap zaten mevcut. Bağlamak veya dönüştürmek için lütfen yöneticinize başvurun.',
|
|
||||||
'common.login.error.auth_failed':
|
|
||||||
'Kimlik sağlayıcısı ile kimlik doğrulama başarısız oldu. Lütfen tekrar deneyin veya yöneticinize başvurun.',
|
|
||||||
'common.login.password': 'Şifre ile giriş yap',
|
'common.login.password': 'Şifre ile giriş yap',
|
||||||
'common.login.username.holder': 'Lütfen kullanıcı adını girin',
|
'common.login.username.holder': 'Lütfen kullanıcı adını girin',
|
||||||
'common.login.password.holder': 'Lütfen şifreyi girin',
|
'common.login.password.holder': 'Lütfen şifreyi girin',
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ export default {
|
|||||||
'gpuservice.template': 'GPU Örnek Şablonu',
|
'gpuservice.template': 'GPU Örnek Şablonu',
|
||||||
'gpuservice.template.add': 'Örnek Şablonu Ekle',
|
'gpuservice.template.add': 'Örnek Şablonu Ekle',
|
||||||
'gpuservice.template.edit': 'Örnek Şablonunu Düzenle',
|
'gpuservice.template.edit': 'Örnek Şablonunu Düzenle',
|
||||||
'gpuservice.template.clone': 'Örnek Şablonunu Klonla',
|
|
||||||
'gpuservice.template.filter.name': 'Ada göre filtrele',
|
'gpuservice.template.filter.name': 'Ada göre filtrele',
|
||||||
'gpuservice.template.filter.vendor': 'Tedarikçiye göre filtrele',
|
'gpuservice.template.filter.vendor': 'Tedarikçiye göre filtrele',
|
||||||
'gpuservice.template.image': 'İmaj',
|
'gpuservice.template.image': 'İmaj',
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ export default {
|
|||||||
'menu.models.modelCatalog': 'Katalog',
|
'menu.models.modelCatalog': 'Katalog',
|
||||||
'menu.models.catalog': 'Model Kataloğu',
|
'menu.models.catalog': 'Model Kataloğu',
|
||||||
'menu.models.deployment': 'Dağıtımlar',
|
'menu.models.deployment': 'Dağıtımlar',
|
||||||
'menu.models.userModels': 'Modeller',
|
'menu.models.userModels': 'Modellerim',
|
||||||
'menu.models.benchmark': 'Kıyaslamalar',
|
'menu.models.benchmark': 'Kıyaslamalar',
|
||||||
'menu.models.benchmarkDetail': 'Kıyaslama Detayları',
|
'menu.models.benchmarkDetail': 'Kıyaslama Detayları',
|
||||||
'menu.models.providers': 'Sağlayıcılar',
|
'menu.models.providers': 'Sağlayıcılar',
|
||||||
'menu.models.routes': 'Yönlendirmeler',
|
'menu.models.routes': 'Yönlendirmeler',
|
||||||
'menu.models.usage': 'Kullanım',
|
'menu.models.usage': 'Usage',
|
||||||
'menu.modelCatalog': 'Katalog',
|
'menu.modelCatalog': 'Katalog',
|
||||||
'menu.resources': 'Kaynaklar',
|
'menu.resources': 'Kaynaklar',
|
||||||
'menu.apikeys': 'API Anahtarları',
|
'menu.apikeys': 'API Anahtarları',
|
||||||
|
|||||||
@@ -13,11 +13,6 @@ export default {
|
|||||||
'Bu kullanıcı hesabını etkinleştir veya devre dışı bırak',
|
'Bu kullanıcı hesabını etkinleştir veya devre dışı bırak',
|
||||||
'users.form.fullname': 'Tam Ad',
|
'users.form.fullname': 'Tam Ad',
|
||||||
'users.form.source': 'Kaynak',
|
'users.form.source': 'Kaynak',
|
||||||
'users.form.source.local': 'Yerel',
|
|
||||||
'users.form.source.tip.switchToLocal':
|
|
||||||
'Yerel kaynağa geçmek yeni bir parola gerektirir. Kullanıcı bundan sonra standart oturum açma formunu kullanır.',
|
|
||||||
'users.form.source.tip.switchToExternal':
|
|
||||||
'Harici bir kaynağa geçmek kullanıcının yerel parolasını siler. Kullanıcı bundan sonra yapılandırılmış kimlik sağlayıcı üzerinden oturum açar.',
|
|
||||||
'users.table.user': 'kullanıcılar',
|
'users.table.user': 'kullanıcılar',
|
||||||
'users.form.admin': 'Yönetici',
|
'users.form.admin': 'Yönetici',
|
||||||
'users.form.user': 'Kullanıcı',
|
'users.form.user': 'Kullanıcı',
|
||||||
|
|||||||
@@ -43,8 +43,6 @@ export default {
|
|||||||
'在需要添加的节点上运行以下命令,将其加入到集群中。',
|
'在需要添加的节点上运行以下命令,将其加入到集群中。',
|
||||||
'clusters.create.addCommand.k8s.tips':
|
'clusters.create.addCommand.k8s.tips':
|
||||||
'在需要注册的 Kubernetes 集群中运行以下命令,创建 Kubernetes 资源,注册该集群。',
|
'在需要注册的 Kubernetes 集群中运行以下命令,创建 Kubernetes 资源,注册该集群。',
|
||||||
'clusters.create.addCommand.k8s.version.warning':
|
|
||||||
'Kubernetes 版本最低支持 1.23,如果要使用 GPU Service 功能,Kubernetes 版本最低支持 1.27。',
|
|
||||||
'clusters.create.register.tips':
|
'clusters.create.register.tips':
|
||||||
'在需要添加的 Kubernetes 集群上运行以下命令,将其中的节点加入到集群中。',
|
'在需要添加的 Kubernetes 集群上运行以下命令,将其中的节点加入到集群中。',
|
||||||
'cluster.create.checkEnv.tips': '使用以下命令检查环境是否准备妥当。',
|
'cluster.create.checkEnv.tips': '使用以下命令检查环境是否准备妥当。',
|
||||||
|
|||||||
@@ -257,9 +257,6 @@ export default {
|
|||||||
'common.select.count': '已选 {count} 项',
|
'common.select.count': '已选 {count} 项',
|
||||||
'common.login.auth': '认证中...',
|
'common.login.auth': '认证中...',
|
||||||
'common.login.auth.failed': '认证失败',
|
'common.login.auth.failed': '认证失败',
|
||||||
'common.login.error.source_conflict':
|
|
||||||
'已存在同名账号但来源不同。请联系管理员关联或转换该账号。',
|
|
||||||
'common.login.error.auth_failed': '身份提供商认证失败。请重试或联系管理员。',
|
|
||||||
'common.login.password': '使用密码登录',
|
'common.login.password': '使用密码登录',
|
||||||
'common.login.username.holder': '请输入用户名',
|
'common.login.username.holder': '请输入用户名',
|
||||||
'common.login.password.holder': '请输入密码',
|
'common.login.password.holder': '请输入密码',
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ export default {
|
|||||||
'gpuservice.template': 'GPU 实例模板',
|
'gpuservice.template': 'GPU 实例模板',
|
||||||
'gpuservice.template.add': '添加实例模板',
|
'gpuservice.template.add': '添加实例模板',
|
||||||
'gpuservice.template.edit': '编辑实例模板',
|
'gpuservice.template.edit': '编辑实例模板',
|
||||||
'gpuservice.template.clone': '克隆实例模板',
|
|
||||||
'gpuservice.template.filter.name': '按名称过滤',
|
'gpuservice.template.filter.name': '按名称过滤',
|
||||||
'gpuservice.template.filter.vendor': '按厂商过滤',
|
'gpuservice.template.filter.vendor': '按厂商过滤',
|
||||||
'gpuservice.template.image': '镜像',
|
'gpuservice.template.image': '镜像',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default {
|
|||||||
'menu.models.modelList': '部署与管理',
|
'menu.models.modelList': '部署与管理',
|
||||||
'menu.models.modelCatalog': '模型库',
|
'menu.models.modelCatalog': '模型库',
|
||||||
'menu.models.deployment': '部署',
|
'menu.models.deployment': '部署',
|
||||||
'menu.models.userModels': '模型广场',
|
'menu.models.userModels': '我的模型',
|
||||||
'menu.models.benchmark': '基准测试',
|
'menu.models.benchmark': '基准测试',
|
||||||
'menu.models.benchmarkDetail': '基准测试详情',
|
'menu.models.benchmarkDetail': '基准测试详情',
|
||||||
'menu.models.providers': '提供商',
|
'menu.models.providers': '提供商',
|
||||||
|
|||||||
@@ -12,11 +12,6 @@ export default {
|
|||||||
'users.form.active.description': '启用或禁用此用户账户',
|
'users.form.active.description': '启用或禁用此用户账户',
|
||||||
'users.form.fullname': '全名',
|
'users.form.fullname': '全名',
|
||||||
'users.form.source': '来源',
|
'users.form.source': '来源',
|
||||||
'users.form.source.local': '本地',
|
|
||||||
'users.form.source.tip.switchToLocal':
|
|
||||||
'切换到本地需要设置新密码,之后用户将通过标准登录表单登录。',
|
|
||||||
'users.form.source.tip.switchToExternal':
|
|
||||||
'切换到外部来源会清除该用户的本地密码,之后用户将通过所配置的身份提供商登录。',
|
|
||||||
'users.table.user': '用户',
|
'users.table.user': '用户',
|
||||||
'users.form.admin': '管理员',
|
'users.form.admin': '管理员',
|
||||||
'users.form.user': '普通用户',
|
'users.form.user': '普通用户',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import useCoolColors from '@/hooks/use-cool-colors';
|
import useCoolColors from '@/hooks/use-cool-colors';
|
||||||
import { Chart } from '@gpustack/core-ui/charts';
|
import { Chart } from '@gpustack/core-ui';
|
||||||
import { formatLargeNumber } from '@gpustack/core-ui/utils';
|
import { formatLargeNumber } from '@gpustack/core-ui/utils';
|
||||||
import { Empty, Spin, theme } from 'antd';
|
import { Empty, Spin, theme } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|||||||
@@ -4,53 +4,28 @@ import {
|
|||||||
RouteContext,
|
RouteContext,
|
||||||
type PageContainerProps
|
type PageContainerProps
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import {
|
import { useOverlayScroller } from '@gpustack/core-ui';
|
||||||
HeaderSlotContext,
|
|
||||||
useOverlayScroller,
|
|
||||||
type HeaderSlotContextValue
|
|
||||||
} from '@gpustack/core-ui';
|
|
||||||
import { Divider } from 'antd';
|
import { Divider } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {
|
import { useContext, useEffect, useRef } from 'react';
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState
|
|
||||||
} from 'react';
|
|
||||||
import pageBoxCss from './styles/page-box.less';
|
import pageBoxCss from './styles/page-box.less';
|
||||||
|
|
||||||
// The header-slot bridge (HeaderSlotContext, HeaderLeft, HeaderRight,
|
|
||||||
// usePageContentStyle) lives in @gpustack/core-ui so the host and the
|
|
||||||
// enterprise plugin share the SAME context instance and can portal into this
|
|
||||||
// layout-owned header bar. Re-exported here so existing host import sites
|
|
||||||
// keep working.
|
|
||||||
export {
|
|
||||||
HeaderLeft,
|
|
||||||
HeaderRight,
|
|
||||||
usePageContentStyle
|
|
||||||
} from '@gpustack/core-ui';
|
|
||||||
|
|
||||||
const paddingInlinePageContainerContent = 24;
|
const paddingInlinePageContainerContent = 24;
|
||||||
|
|
||||||
export const PageContainerInner: React.FC<
|
export const PageContainerInner: React.FC<
|
||||||
PageContainerProps & {
|
PageContainerProps & {
|
||||||
|
leftContent?: React.ReactNode;
|
||||||
|
rightContent?: React.ReactNode;
|
||||||
styles?: {
|
styles?: {
|
||||||
containerWrapper?: React.CSSProperties;
|
containerWrapper?: React.CSSProperties;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
> = ({ children, styles, title, ...rest }) => {
|
> = ({ children, styles, title, leftContent, rightContent, ...rest }) => {
|
||||||
const { initialize: initialize } = useOverlayScroller({
|
const { initialize: initialize } = useOverlayScroller({
|
||||||
defer: false
|
defer: false
|
||||||
});
|
});
|
||||||
const pageContext = useContext(RouteContext);
|
const pageContext = useContext(RouteContext);
|
||||||
const contentWrapperRef = useRef<HTMLDivElement>(null);
|
const contentWrapperRef = useRef<HTMLDivElement>(null);
|
||||||
const [leftEl, setLeftEl] = useState<HTMLDivElement | null>(null);
|
|
||||||
const [rightEl, setRightEl] = useState<HTMLDivElement | null>(null);
|
|
||||||
const [contentStyleOverride, setContentStyleOverride] = useState<
|
|
||||||
React.CSSProperties | undefined
|
|
||||||
>(undefined);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (contentWrapperRef.current) {
|
if (contentWrapperRef.current) {
|
||||||
@@ -59,45 +34,7 @@ export const PageContainerInner: React.FC<
|
|||||||
}
|
}
|
||||||
}, [initialize, contentWrapperRef]);
|
}, [initialize, contentWrapperRef]);
|
||||||
|
|
||||||
const setContentStyle = useCallback(
|
|
||||||
(style: React.CSSProperties | undefined) => {
|
|
||||||
setContentStyleOverride(style);
|
|
||||||
return () => setContentStyleOverride(undefined);
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const slotOwnersRef = useRef<{ left: number; right: number }>({
|
|
||||||
left: 0,
|
|
||||||
right: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
const registerSlot = useCallback((slot: 'left' | 'right') => {
|
|
||||||
const owners = slotOwnersRef.current;
|
|
||||||
owners[slot] += 1;
|
|
||||||
if (process.env.NODE_ENV !== 'production' && owners[slot] > 1) {
|
|
||||||
const name = slot === 'left' ? 'HeaderLeft' : 'HeaderRight';
|
|
||||||
console.warn(
|
|
||||||
`[PageContainerInner] ${owners[slot]} <${name}> are mounted at once; their content stacks in the same header slot. Only one page/component should own each slot at a time.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
owners[slot] -= 1;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const slotValue = useMemo<HeaderSlotContextValue>(
|
|
||||||
() => ({
|
|
||||||
leftEl,
|
|
||||||
rightEl,
|
|
||||||
setContentStyle,
|
|
||||||
registerSlot
|
|
||||||
}),
|
|
||||||
[leftEl, rightEl, setContentStyle, registerSlot]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HeaderSlotContext.Provider value={slotValue}>
|
|
||||||
<div className={pageBoxCss.containerWrapper}>
|
<div className={pageBoxCss.containerWrapper}>
|
||||||
<PageContainer
|
<PageContainer
|
||||||
{...rest}
|
{...rest}
|
||||||
@@ -113,31 +50,27 @@ export const PageContainerInner: React.FC<
|
|||||||
>
|
>
|
||||||
<div className={pageBoxCss.title}>
|
<div className={pageBoxCss.title}>
|
||||||
<div className={pageBoxCss.left}>
|
<div className={pageBoxCss.left}>
|
||||||
<div ref={setLeftEl} className={pageBoxCss.leftSlot} />
|
{leftContent || pageContext.title}
|
||||||
<span className={pageBoxCss.defaultTitle}>
|
|
||||||
{pageContext.title}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={pageBoxCss.right}>
|
<div className={pageBoxCss.right}>
|
||||||
<div ref={setRightEl} className={pageBoxCss.rightSlot} />
|
{rightContent && (
|
||||||
<Divider
|
<div>
|
||||||
className={pageBoxCss.divider}
|
{rightContent}
|
||||||
orientation="vertical"
|
<Divider orientation="vertical" style={{ margin: '0 16px' }} />
|
||||||
style={{ margin: '0 16px' }}
|
</div>
|
||||||
/>
|
)}
|
||||||
<ExtraContent />
|
<ExtraContent />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classNames(pageBoxCss.contentWrapper)}
|
className={classNames(pageBoxCss.contentWrapper)}
|
||||||
style={{ ...styles?.containerWrapper, ...contentStyleOverride }}
|
style={styles?.containerWrapper}
|
||||||
ref={contentWrapperRef}
|
ref={contentWrapperRef}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
</div>
|
</div>
|
||||||
</HeaderSlotContext.Provider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||