Compare commits

..
122 Commits
Author SHA1 Message Date
jialin 75b70a26bd fix: download log failed 2025-07-25 11:26:09 +08:00
jialin 4433e5d8e5 chore: move favicon icon to root 2025-07-23 16:02:58 +08:00
jialin 351b2e1417 chore: adjust the evaluation result statement 2025-07-23 11:13:37 +08:00
jialin 3c7dc5a49e fix: update the instance servers field 2025-07-22 19:26:08 +08:00
jialin 9895231696 fix: miss model category、name on catalog page 2025-07-21 19:51:30 +08:00
jialin 3fa3281c51 fix: progress bar default open 2025-07-18 15:12:18 +08:00
jialin bbf11d0374 style: model file status cell size 2025-07-18 10:35:35 +08:00
jialin 04ee696f6a style: playground bold text 2025-07-17 17:26:34 +08:00
jialin ddbbe92a85 chore: show used vram for a second 2025-07-17 12:01:49 +08:00
jialin 32ba5e1802 fix(style): axis tick value too long 2025-07-16 16:33:22 +08:00
jialin a1c6eb177a chore: remove autoplay for audio and video 2025-07-16 14:38:37 +08:00
jialin 14d60264c7 fix: wav format audio, #2317 2025-07-16 13:06:01 +08:00
jialin f4d6abace7 fix: remove insert action for watching, #2504 2025-07-15 19:33:04 +08:00
jialin a5e1717138 chore: update mindie backend parameters 2025-07-15 15:21:26 +08:00
jialin 472a4d400b chore: installer download link 2025-07-14 17:01:11 +08:00
jialin fda4786737 fix: set null to filename for download non-gguf 2025-07-11 11:07:54 +08:00
jialin c3686579b2 chore: add worker statements 2025-07-11 10:09:01 +08:00
jialin f2dfb2f32d style: uniform margin top for resources pages 2025-07-11 10:09:01 +08:00
jialin 60ee9f1a15 fix: catalog jump to deployments route error 2025-07-10 17:50:21 +08:00
jialin f43d1af6a7 fix(style): menu item text wrap 2025-07-10 15:24:55 +08:00
jialin b631b4c7b7 chore: remove menu data render 2025-07-10 14:52:08 +08:00
jialin e3aba0529f chore: update iconfont files 2025-07-10 10:02:10 +08:00
jialin cf682feae9 fix: menu items for user role 2025-07-10 10:02:10 +08:00
jialin 03d798760c chore: add worker guide 2025-07-10 09:47:12 +08:00
jialin 51450c2b02 style: flatten menu items 2025-07-10 09:47:12 +08:00
EVGENY Mandjialin 751ddbb3f3 Update resources.ts 2025-07-10 09:44:00 +08:00
EVGENY Mandjialin 36b3620d0c Update common.ts 2025-07-10 09:43:16 +08:00
jialin fd3eb513ff chore: update the guide for adding worker 2025-07-09 16:03:07 +08:00
jialin af0aaddbfd fix: update the requestId when switch model 2025-07-09 16:03:07 +08:00
jialin b167170aa9 style: tag tooltip content width 2025-07-09 16:03:07 +08:00
jialin 642ac3ebc4 fix: reset form after switch model 2025-07-07 17:40:09 +08:00
jialin 69cceb4fc7 refactor: update user guide for add worker
chore: update resource locales
2025-07-07 17:40:09 +08:00
jialin ac32e23e87 fix: distributed inference across workers disabled for vox-box 2025-07-04 20:57:57 +08:00
jialin 04de4ff606 fix: enable distributed inference field for mindie 2025-07-04 19:56:47 +08:00
jialin 27761566c9 fix: collpase instance error 2025-07-04 17:49:17 +08:00
jialin 3d93eda92d fix: distribution downloading data do not show 2025-07-04 16:45:15 +08:00
jialin 6943571a6d fix: images did not get cleared after clicking stop 2025-07-04 15:42:03 +08:00
jialin 28994cb9da chore: add iluvatar corex in add worker 2025-07-04 15:42:03 +08:00
jialin 5866634b3f chore: update iconfont files 2025-07-03 11:38:19 +08:00
jialin e3d91f1cc7 style: source icon in model result list 2025-07-03 11:38:19 +08:00
jialin 8ac06259f5 fix: do nothing when clicking the selected model or file 2025-07-03 10:41:22 +08:00
jialin c8933a6669 fix: update form values when switching model、size, #2191 2025-07-02 20:34:48 +08:00
jialin 6c2da264db fix: image generating flickering, #1415 2025-07-02 19:33:33 +08:00
jialin 993f01a301 fix: multiple select ux 2025-07-02 11:39:54 +08:00
jialin 9e29e7283c feat: add simple select component 2025-07-01 20:20:50 +08:00
jialin 7da106798d fix: some fields value do not be updated from default spec 2025-06-30 20:10:20 +08:00
jialin b2e9cbb44a chore: update backend parameters and logs reading frequency 2025-06-30 18:57:13 +08:00
jialin 143f6df2c1 perf: do not handle ansi sequence for parsing logs 2025-06-30 17:07:29 +08:00
jialin a64505fcc1 chore: update mindie parameters 2025-06-29 16:43:03 +08:00
jialin 0aaf919827 fix: switch model clear backend_parameters, env, backend_version 2025-06-27 18:07:30 +08:00
jialin bf8e988488 style: top users chart compact 2025-06-27 17:26:34 +08:00
jialin d26ea28028 fix(style): top users bar name too long 2025-06-27 16:42:24 +08:00
jialin abdd5e08a6 fix(style): top users bar name too long 2025-06-27 16:38:19 +08:00
jialin 47a5ef915b fix: pick backend_parameter, env, backend_version from spec in catalog 2025-06-27 16:38:19 +08:00
jialin 5861508153 fix(style): imgs overflow style on chat 2025-06-27 13:43:40 +08:00
jialin 85754ccfb5 fix(style): chart grid right margin 2025-06-27 10:53:51 +08:00
jialin cbeea23295 style: keep the logs pagination visible 2025-06-27 10:15:11 +08:00
jialin e2d79a6802 style: multiple selection tag 2025-06-26 20:39:07 +08:00
jialin c68d85096b fix(style): usage reponse size 2025-06-26 15:54:59 +08:00
jialin fcafa83111 fix: split line align for two yAxis chart 2025-06-26 15:54:59 +08:00
jialin 47d7418eb1 fix: usage: datepicker item disabled issue 2025-06-26 12:15:09 +08:00
jialin 8751400ae3 fix: distributed servers info 2025-06-25 20:29:48 +08:00
jialin 3889c1fa71 fix: reranker item uid updated, chat image size issue, #2280 2025-06-25 16:49:46 +08:00
jialin a4d1d3b16c style: skeleton for search result 2025-06-25 14:35:00 +08:00
jialin adfdf41e85 fix: do not check the required fields for evaluation, #2191 2025-06-25 14:35:00 +08:00
jialin a6825f7ee3 feat: clean markdown content 2025-06-25 14:35:00 +08:00
jialin 830ee45161 fix: update evaluation result issue 2025-06-24 20:47:27 +08:00
jialin 5f7df3f871 chore: update usage api 2025-06-24 16:54:03 +08:00
jialin 34af730934 feat: model usage 2025-06-24 16:54:03 +08:00
jialin 645801fe86 chore: mindle multiple support 2025-06-23 20:23:05 +08:00
jialin 547f49a978 fix: audio uploader has cache issue 2025-06-23 16:25:22 +08:00
jialin fd3d7a6f29 style: side menu style 2025-06-23 15:13:49 +08:00
jialin 6be7140a47 fix: #2227, #2263 2025-06-20 19:34:04 +08:00
jialin ca7dc02d88 revert: markdown katex parse 2025-06-20 11:18:18 +08:00
jialin 1924c45358 fix: chat request payload error, #2163 2025-06-20 11:18:18 +08:00
jialin be92f92991 fix: for gguf ref not be created, #2227 2025-06-19 15:25:21 +08:00
jialin a9b909cca1 fix: upload a audio will clear imgs, #2163 2025-06-19 13:36:49 +08:00
jialin 175b83dd68 fix: #2191,#2227 2025-06-19 13:36:49 +08:00
jialin df85284a60 style: audio player, usage chart 2025-06-19 13:36:49 +08:00
jialin 17ffa93393 fix: do not modify the custom name 2025-06-18 17:56:19 +08:00
jialin 73c525127f fix(style): simple audio player 2025-06-18 17:45:58 +08:00
jialin 3e76d7c321 fix: embedding params escape 2025-06-18 15:03:35 +08:00
jialin 9295f9cef3 fix: sync and update the evaluation result 2025-06-18 11:48:09 +08:00
jialin 9d1e6df8f0 chore: date ranger picker presets 2025-06-18 11:48:09 +08:00
jialin 0805d70c07 style: usage download button 2025-06-17 16:17:26 +08:00
jialin 0133524f09 fix: debounce for page change in search models 2025-06-17 11:50:33 +08:00
EVGENY Mandjialin 2c5100b2ed Update common.ts 2025-06-17 09:50:38 +08:00
EVGENY Mandjialin 51296cdc15 Update dashboard.ts 2025-06-17 09:50:14 +08:00
EVGENY Mandjialin f261bec248 Update users.ts 2025-06-17 09:49:54 +08:00
jialin 97c7087f6c fix(style): esc hint flickering 2025-06-16 19:54:23 +08:00
jialin 109906d083 fix: usage chart y axis ticks, add esc hint for drawer 2025-06-16 19:28:02 +08:00
jialin 81833c4938 fix: update gpus state by polling 2025-06-12 17:26:34 +08:00
jialin 1b6cfdbdc4 fix: evaluate status add lock state 2025-06-12 09:57:25 +08:00
jialin 281d8bddc1 feat: add pagination for searching models 2025-06-10 19:24:13 +08:00
jialin 0eaa3aeeb7 fix: do not undo when is inverting 2025-06-09 19:39:27 +08:00
jialin 4dcb0bcfed fix: image editor fit view incorrect, do not query gguf files when gguf unchecked 2025-06-09 18:45:47 +08:00
jialin e69b7dfd6a Revert "fix: normaliza embedding data"
This reverts commit 2bfb93a063.
2025-06-09 18:45:47 +08:00
jialin 38d63c71ee fix: normaliza embedding data 2025-06-09 18:45:47 +08:00
jialin 6880fd3f5f fix: user settings locales 2025-06-09 18:45:47 +08:00
jialin 1771ca363a fix: show embedding token usage 2025-06-09 18:45:47 +08:00
jialin c896c88c23 chore: remove ollama model source 2025-06-09 18:45:47 +08:00
jialin 5ef733729b fix: ui conflicts in update of multiple evalute result 2025-06-09 18:45:47 +08:00
jialin f73e29937c chore: export table title 2025-06-09 18:45:47 +08:00
jialin 8383547943 style: simple audio player 2025-06-09 18:45:47 +08:00
jialin 8070d3f1e6 style: usage chart style 2025-06-09 18:45:47 +08:00
jialin a161fa953e feat: usage export data 2025-06-09 18:45:47 +08:00
jialin 471e1ae96f feat: upload audio file in chat 2025-06-03 19:58:08 +08:00
jialin ed28bcdfb0 fix: set default spec to form 2025-06-03 15:16:52 +08:00
gitlawr 31ea81c211 ci: publish v*-dev branches 2025-05-30 14:02:56 +08:00
jialin 283189a17c fix: model instances always update by cache 2025-05-30 11:23:25 +08:00
jialin 40020df865 fix: clear backend parameters when change model file 2025-05-29 11:32:23 +08:00
jialin aa6b5ec24f fix: refresh model list manually 2025-05-28 17:25:23 +08:00
jialin f53fe042a9 fix: update allChildren after stop or start 2025-05-28 14:41:35 +08:00
jialin 034a54bf4b chore: model file components for updating 2025-05-27 19:45:24 +08:00
jialin 894dd1e353 chore: update mindie parameters 2025-05-27 16:50:08 +08:00
jialin ca99db5a97 chore: show llm code with common component 2025-05-27 16:50:08 +08:00
jialin 61e506010c fix: pca calculating takes a long time 2025-05-26 19:56:51 +08:00
jialin 4b4343a9fd chore: add watch for worker and gpu updating 2025-05-26 16:17:17 +08:00
jialin 9efacefba3 fix: remove worker-loader 2025-05-25 19:45:57 +08:00
jialin f288c9a9ad perf: embedding calc in worker process 2025-05-25 11:15:50 +08:00
jialin ec45f23485 chore: adjust the delay for scroller 2025-05-20 19:33:10 +08:00
jialin 1c11e73ff8 fix: playground parameters area resizable 2025-05-20 19:33:10 +08:00
215 changed files with 7540 additions and 3505 deletions
+1
View File
@@ -1,3 +1,4 @@
PORT=9000
UMI_DEV_SERVER_COMPRESS=none
DID_YOU_KNOW=none
+18 -8
View File
@@ -4,11 +4,13 @@ on:
push:
branches:
- 'main'
- 'v*-dev'
tags:
- '*.*.*'
pull_request:
branches:
- 'main'
- 'v*-dev'
env:
NODE_VERSION: '21'
@@ -72,11 +74,6 @@ jobs:
if: github.event_name == 'push'
timeout-minutes: 30
runs-on: ubuntu-22.04
strategy:
matrix:
version: ['latest', '${{ github.ref_name }}']
exclude:
- version: 'main'
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -92,10 +89,23 @@ jobs:
${{ github.workspace }}/node_modules
- name: Build
run: scripts/build
- name: Determine Version
id: version
run: |
if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then
echo "version=latest" >> $GITHUB_OUTPUT
elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
elif [[ "${GITHUB_REF}" == refs/heads/v*-dev ]]; then
echo "version=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT
else
echo "Not a publish target"
exit 1
fi
- name: Package
run: scripts/package
env:
VERSION: '${{ matrix.version }}'
VERSION: '${{ steps.version.outputs.version }}'
- name: Release
uses: TencentCloud/cos-action@b0aa648235fb35a1bdd6a77f529eb0ac4c2f1c25
with:
@@ -103,7 +113,7 @@ jobs:
secret_key: ${{ secrets.CI_TECENTCOS_SECRET_KEY }}
cos_bucket: ${{ secrets.COS_BUCKET }}
cos_region: ${{ secrets.COS_REGION }}
local_path: dist/${{ matrix.version }}.tar.gz
remote_path: releases/${{ matrix.version }}.tar.gz
local_path: dist/${{ steps.version.outputs.version }}.tar.gz
remote_path: releases/${{ steps.version.outputs.version }}.tar.gz
accelerate: true
clean: false
+3 -5
View File
@@ -28,6 +28,9 @@ export default defineConfig({
logLevel: 'info',
defaultSizes: 'parsed' // stat // gzip
},
mfsu: {
exclude: ['lodash', 'ml-pca']
},
base: process.env.npm_config_base || '/',
...(isProduction
? {
@@ -50,11 +53,6 @@ export default defineConfig({
chunkFilename: `css/[name].${t}.chunk.css`
}
]);
config.module
.rule('worker')
.test(/\.worker\.js$/)
.use('worker-loader')
.loader('worker-loader');
config.output
.filename(`js/[name].${t}.js`)
.chunkFilename(`js/[name].${t}.chunk.js`);
-6
View File
@@ -20,12 +20,6 @@ export default function createProxyTable(target?: string) {
ws: true,
log: 'debug',
pathRewrite: (pth: string) => pth.replace(`/^/${api}`, `/${api}`),
// onProxyRes: (proxyRes: any, req: any, res: any) => {
// console.log('headers=========', {
// res: proxyRes.headers,
// req: req.headers
// });
// },
headers: {
origin: newTarget,
Connection: 'keep-alive'
+122 -44
View File
@@ -5,13 +5,18 @@ export default [
name: 'dashboard',
path: '/dashboard',
key: 'dashboard',
icon: 'AppstoreOutlined',
icon: 'icon-dashboard',
selectedIcon: 'icon-dashboard-filled',
defaultIcon: 'icon-dashboard',
access: 'canSeeAdmin',
component: './dashboard'
component: './dashboard',
routes: []
},
{
name: 'playground',
icon: 'ExperimentOutlined',
icon: 'icon-experiment',
selectedIcon: 'icon-experiment-filled',
defaultIcon: 'icon-experiment',
path: '/playground',
key: 'playground',
routes: [
@@ -24,31 +29,19 @@ export default [
title: 'Chat',
path: '/playground/chat',
key: 'chat',
icon: 'Comment',
icon: 'icon-chat',
selectedIcon: 'icon-chat-filled',
defaultIcon: 'icon-chat',
component: './playground/index'
},
{
name: 'text2images',
title: 'Text2Images',
path: keepAliveRoutes.text2images,
key: 'text2images',
icon: 'Comment',
component: './playground/images'
},
{
name: 'speech',
title: 'Speech',
path: keepAliveRoutes.speech,
key: 'speech',
icon: 'Comment',
component: './playground/speech'
},
{
name: 'embedding',
title: 'embedding',
path: '/playground/embedding',
key: 'embedding',
icon: 'Comment',
icon: 'icon-embedding',
selectedIcon: 'icon-embedding-filled',
defaultIcon: 'icon-embedding',
component: './playground/embedding'
},
{
@@ -56,50 +49,135 @@ export default [
title: 'Rerank',
path: '/playground/rerank',
key: 'rerank',
icon: 'Comment',
icon: 'icon-reranker',
selectedIcon: 'icon-reranker-filled',
defaultIcon: 'icon-reranker',
component: './playground/rerank'
},
{
name: 'text2images',
title: 'Text2Images',
path: keepAliveRoutes.text2images,
key: 'text2images',
icon: 'icon-image1',
selectedIcon: 'icon-image-filled',
defaultIcon: 'icon-image1',
component: './playground/images'
},
{
name: 'speech',
title: 'Speech',
path: keepAliveRoutes.speech,
key: 'speech',
icon: 'icon-audio1',
selectedIcon: 'icon-audio-filled',
defaultIcon: 'icon-audio1',
component: './playground/speech'
}
]
},
{
name: 'modelCatalog',
path: '/models/catalog',
key: 'modelsCatalog',
icon: 'icon-catalog',
access: 'canSeeAdmin',
component: './llmodels/catalog'
},
{
name: 'models',
path: '/models/list',
path: '/models',
key: 'models',
icon: 'Block',
access: 'canSeeAdmin',
component: './llmodels/index'
routes: [
{
path: '/models',
redirect: '/models/deployments'
},
{
name: 'modelCatalog',
path: '/models/catalog',
key: 'modelsCatalog',
icon: 'icon-layers',
selectedIcon: 'icon-layers-filled',
defaultIcon: 'icon-layers',
access: 'canSeeAdmin',
component: './llmodels/catalog'
},
{
name: 'deployment',
path: '/models/deployments',
key: 'modelDeployments',
icon: 'icon-model',
selectedIcon: 'icon-model-filled',
defaultIcon: 'icon-model',
access: 'canSeeAdmin',
component: './llmodels/index'
}
]
},
{
name: 'resources',
path: '/resources',
key: 'resources',
icon: 'CloudServer',
access: 'canSeeAdmin',
component: './resources'
routes: [
{
path: '/resources',
redirect: '/resources/workers'
},
{
name: 'workers',
path: '/resources/workers',
key: 'workers',
icon: 'icon-resources',
selectedIcon: 'icon-resources-filled',
defaultIcon: 'icon-resources',
component: './resources/components/workers'
},
{
name: 'gpus',
path: '/resources/gpus',
key: 'gpus',
icon: 'icon-gpu1',
selectedIcon: 'icon-gpu-filled',
defaultIcon: 'icon-gpu1',
component: './resources/components/gpus'
},
{
name: 'modelfiles',
path: '/resources/modelfiles',
key: 'modelfiles',
icon: 'icon-files',
selectedIcon: 'icon-files-filled',
defaultIcon: 'icon-files',
component: './resources/components/model-files'
}
]
},
{
name: 'accessControl',
path: '/access-control',
key: 'accessControl',
access: 'canSeeAdmin',
routes: [
{
path: '/access-control',
redirect: '/access-control/users'
},
{
name: 'users',
path: '/access-control/users',
key: 'users',
icon: 'icon-users',
selectedIcon: 'icon-users-filled',
defaultIcon: 'icon-users',
component: './users'
}
]
},
{
name: 'apikeys',
path: '/api-keys',
key: 'apikeys',
icon: 'KeyOutlined',
hideInMenu: true,
selectedIcon: 'icon-key-filled',
icon: 'icon-key',
defaultIcon: 'icon-key',
component: './api-keys'
},
{
name: 'users',
path: '/users',
key: 'users',
icon: 'Team',
access: 'canSeeAdmin',
component: './users'
},
{
name: 'profile',
path: '/profile',
+10 -1
View File
@@ -29,17 +29,20 @@
"@umijs/max": "^4.2.11",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"ahooks": "^3.8.5",
"ansi-to-html": "^0.7.2",
"antd": "^5.21.6",
"antd-style": "^3.6.2",
"axios": "^1.7.2",
"axios": "^1.8.2",
"classnames": "^2.5.1",
"clipboard": "^2.0.11",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.11",
"dompurify": "^3.2.6",
"driver.js": "^1.3.1",
"echarts": "^5.5.1",
"epubjs": "^0.3.93",
"file-saver": "^2.0.5",
"has-ansi": "^5.0.1",
"highlight.js": "^11.10.0",
"jotai": "^2.8.4",
@@ -78,6 +81,7 @@
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0",
"@umijs/case-sensitive-paths-webpack-plugin": "^1.0.1",
"@umijs/plugins": "^4.4.11",
"babel-plugin-named-asset-import": "^0.3.8",
"case-sensitive-paths-webpack-plugin": "^2.4.0",
"compression-webpack-plugin": "^11.1.0",
@@ -99,5 +103,10 @@
"url-loader": "^4.1.1",
"webpack-bundle-analyzer": "^4.10.2",
"worker-loader": "^3.0.8"
},
"pnpm": {
"overrides": {
"elliptic": "^6.6.1"
}
}
}
+187 -700
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

+1 -1
View File
@@ -23,7 +23,7 @@ const checkDefaultPage = async (userInfo: any) => {
if (isFirstLogin === null && isOnline()) {
writeState(IS_FIRST_LOGIN, true);
if (userInfo && userInfo?.is_admin) {
history.push('/models/list');
history.push('/models/deployments');
}
}
};
+4
View File
@@ -204,6 +204,10 @@
font-size: 20px;
}
.font-size-18 {
font-size: 18px;
}
.font-size-24 {
font-size: 24px;
}
+15 -1
View File
@@ -1,4 +1,18 @@
import { atom } from 'jotai';
import { atom, getDefaultStore } from 'jotai';
// models expand keys: create, update , delete,
export const modelsExpandKeysAtom = atom<string[]>([]);
export const requestIdAtom = atom<number>(0);
export const setRquestId = () => {
const store = getDefaultStore();
const id = Date.now();
store.set(requestIdAtom, id);
return id;
};
export const getRequestId = () => {
const store = getDefaultStore();
return store.get(requestIdAtom);
};
+3 -1
View File
@@ -7,13 +7,15 @@ type UserSettings = {
mode: 'light' | 'realDark' | 'auto';
colorPrimary: string;
isDarkTheme: boolean;
collapsed: boolean;
};
const defaultSettings: UserSettings = {
theme: 'light',
mode: 'auto',
isDarkTheme: false,
colorPrimary: colorPrimary
colorPrimary: colorPrimary,
collapsed: false
};
export const getStorageUserSettings = () => {
+10 -3
View File
@@ -3,7 +3,7 @@ import { Typography } from 'antd';
import classNames from 'classnames';
import React from 'react';
import styled from 'styled-components';
import OverlayScroller from '../overlay-scroller';
import OverlayScroller, { OverlayScrollerOptions } from '../overlay-scroller';
import './block.less';
interface AlertInfoProps {
type: Global.MessageType;
@@ -15,6 +15,7 @@ interface AlertInfoProps {
contentStyle?: React.CSSProperties;
title?: React.ReactNode;
maxHeight?: number;
overlayScrollerProps?: OverlayScrollerOptions;
}
const TitleWrapper = styled.div`
@@ -29,6 +30,7 @@ const ContentWrapper = styled.div<{ $hasTitle: boolean }>`
? 'var(--ant-color-text-secondary)'
: 'var(--ant-color-text)'};
font-weight: var(--font-weight-500);
white-space: pre-line;
`;
const AlertInfo: React.FC<AlertInfoProps> = (props) => {
@@ -41,7 +43,8 @@ const AlertInfo: React.FC<AlertInfoProps> = (props) => {
title,
contentStyle,
icon,
maxHeight = 86
maxHeight = 86,
overlayScrollerProps = {}
} = props;
return (
@@ -67,7 +70,11 @@ const AlertInfo: React.FC<AlertInfoProps> = (props) => {
{title && (
<TitleWrapper className="title-text">{title}</TitleWrapper>
)}
<OverlayScroller maxHeight={maxHeight} style={{ ...contentStyle }}>
<OverlayScroller
maxHeight={maxHeight}
style={{ ...contentStyle }}
{...overlayScrollerProps}
>
<ContentWrapper
$hasTitle={!!title}
className={classNames('content', type)}
@@ -0,0 +1,22 @@
import React from 'react';
import styled from 'styled-components';
const AudioWrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: flex-start;
`;
const AudioElement: React.FC<any> = (props) => {
return (
<div>
<AudioWrapper>
<audio {...props} controls></audio>
</AudioWrapper>
</div>
);
};
export default AudioElement;
@@ -0,0 +1,406 @@
import { formatTime } from '@/utils/index';
import { DeleteOutlined, DownloadOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Dropdown, Slider, type MenuProps } from 'antd';
import { createStyles } from 'antd-style';
import { round } from 'lodash';
import React, {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo
} from 'react';
import styled from 'styled-components';
import AutoTooltip from '../auto-tooltip';
import IconFont from '../icon-font';
type ActionItem = 'download' | 'delete' | 'speed';
interface AudioPlayerProps {
autoplay?: boolean;
url: string;
speed?: number;
ref?: any;
name: string;
height?: number;
width?: number;
duration?: number;
actions?: ActionItem[];
onDelete?: () => void;
}
const SliderWrapper = styled.div`
width: 100%;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
.ant-slider {
flex: 1;
}
.time {
color: var(--ant-color-text-tertiary);
}
`;
const useStyles = createStyles(({ css, token }) => {
// @ts-ignore
const isDarkMode = token.darkMode as boolean;
return {
wrapper: css`
position: relative;
min-width: 360px;
height: 54px;
display: flex;
align-items: center;
justify-content: flex-start;
padding: 8px 10px;
background-color: ${isDarkMode
? 'var(--ant-color-fill-secondary)'
: '#F1F3F4'};
border-radius: 28px;
.inner {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-start;
flex: 1;
gap: 8px;
.slider {
display: flex;
flex-direction: column;
justify-content: center;
flex: 1;
.ant-slider {
margin: 0;
}
&:hover {
.ant-slider-handle {
opacity: 1;
transition: opacity 0.3s ease-in-out;
}
}
&:focus-within {
.ant-slider-handle {
opacity: 1;
}
}
}
.ant-slider-handle {
opacity: 0;
&::before {
background-color: var(--ant-color-bg-spotlight);
border-radius: 50%;
}
&::after {
display: none;
}
}
}
`
};
});
const sliderStyles = {
rail: {
borderRadius: '4px',
backgroundColor: 'var(--ant-color-fill-secondary)'
},
track: {
borderRadius: '4px',
backgroundColor: 'var(--ant-color-bg-spotlight)'
}
};
const speedOptions = [
{ label: '1x', value: 1 },
{ label: '2x', value: 2 },
{ label: '3x', value: 3 },
{ label: '4x', value: 4 }
];
const speedConfig = {
min: 0.5,
max: 2,
step: 0.25
};
const AudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
const intl = useIntl();
const { styles } = useStyles();
const {
autoplay = false,
speed: defaultSpeed = 1,
actions = ['delete'],
name,
onDelete
} = props;
const audioRef = React.useRef<HTMLAudioElement>(null);
const [audioState, setAudioState] = React.useState<{
currentTime: number;
duration: number;
}>({
currentTime: 0,
duration: 0
});
console.log('audioState', name);
const [playOn, setPlayOn] = React.useState<boolean>(false);
const [speakerOn, setSpeakerOn] = React.useState<boolean>(false);
const [volume, setVolume] = React.useState<number>(1);
const [speed, setSpeed] = React.useState<number>(defaultSpeed);
const timer = React.useRef<any>(null);
useImperativeHandle(ref, () => ({
play: () => {
audioRef.current?.play();
},
pause: () => {
audioRef.current?.pause();
}
}));
const handleShowVolume = useCallback(() => {
setSpeakerOn(!speakerOn);
}, [speakerOn]);
const handleSeepdChange = useCallback((value: number | string) => {
setSpeed(value as number);
audioRef.current!.playbackRate = value as number;
}, []);
const handleAudioOnPlay = useCallback(() => {
timer.current = setInterval(() => {
setAudioState((prestate) => {
return {
currentTime: Math.ceil(audioRef.current?.currentTime || 0),
duration:
prestate.duration || Math.ceil(audioRef.current?.duration || 0)
};
});
if (audioRef.current?.paused || audioRef.current?.ended) {
clearInterval(timer.current);
setPlayOn(false);
setAudioState((prestate: any) => {
return {
currentTime: audioRef.current?.ended ? 0 : prestate.currentTime,
duration: prestate.duration
};
});
}
}, 500);
}, []);
const handlePlay = useCallback(() => {
setPlayOn(!playOn);
if (playOn) {
audioRef.current?.pause();
} else {
audioRef.current?.play();
}
}, [playOn]);
const handleFormatVolume = (val?: number) => {
if (val === undefined) {
return `${round(volume * 100)}%`;
}
return `${round(val * 100)}%`;
};
const handleVolumeChange = useCallback((value: number) => {
audioRef.current!.volume = round(value, 2);
setVolume(round(value, 2));
}, []);
const initPlayerConfig = () => {
if (audioRef.current) {
audioRef.current!.volume = volume;
audioRef.current!.playbackRate = speed;
}
};
const handleLoadedMetadata = useCallback(
(data: any) => {
const duration = Math.ceil(audioRef.current?.duration || 0);
setAudioState({
currentTime: 0,
duration:
duration && duration !== Infinity ? duration : props.duration || 0
});
setPlayOn(autoplay);
},
[autoplay, props.duration]
);
const handleCurrentChange = useCallback((val: number) => {
audioRef.current!.currentTime = val;
setAudioState((prestate) => {
return {
currentTime: val,
duration: prestate.duration
};
});
}, []);
const handleReduceSpeed = () => {
setSpeed((pre) => {
if (pre - speedConfig.step < speedConfig.min) {
return speedConfig.min;
}
const next = pre - speedConfig.step;
audioRef.current!.playbackRate = next;
return next;
});
};
const handleAddSpeed = () => {
setSpeed((pre) => {
if (pre + speedConfig.step > speedConfig.max) {
return speedConfig.max;
}
const next = pre + speedConfig.step;
audioRef.current!.playbackRate = next;
return next;
});
};
const handleOnLoad = (e: any) => {
console.log('onload', e);
};
const onDownload = useCallback(() => {
const url = props.url || '';
const filename = props.name;
const link = document.createElement('a');
link.href = url;
link.download = filename || 'audio.mp3'; // Default filename
document.body.appendChild(link);
link.click();
link.remove();
}, [props.url, props.name]);
const items: MenuProps['items'] = useMemo(() => {
return [
{
key: 'download',
label: intl.formatMessage({ id: 'common.button.download' }),
icon: <DownloadOutlined />,
onClick: onDownload
},
{
key: 'speed',
label: intl.formatMessage({ id: 'playground.params.speed' }),
icon: <IconFont type="icon-play-speed"></IconFont>,
children: speedOptions.map((item) => ({
key: item.value,
label: item.label,
onClick: () => handleSeepdChange(item.value)
}))
},
{
key: 'delete',
label: intl.formatMessage({ id: 'common.button.delete' }),
icon: <DeleteOutlined />,
danger: true,
onClick: () => {
if (audioRef.current) {
audioRef.current.pause();
audioRef.current.src = '';
audioRef.current.load();
}
setAudioState({ currentTime: 0, duration: 0 });
setPlayOn(false);
onDelete?.();
}
}
].filter((item) => actions.includes(item.key as ActionItem));
}, [actions, intl, onDownload, onDelete, handleSeepdChange]);
useEffect(() => {
if (audioRef.current) {
initPlayerConfig();
}
}, [audioRef.current]);
useEffect(() => {
return () => {
clearInterval(timer.current);
};
}, []);
return (
<div
className={styles.wrapper}
style={{
width: props.width || '100%',
height: props.height || '60px',
position: 'relative'
}}
>
<div className="inner">
<Button
size="middle"
type="text"
onClick={handlePlay}
shape="circle"
disabled={!audioState?.duration}
icon={
!playOn ? (
<IconFont
type="icon-playcircle-fill"
style={{ fontSize: '24px' }}
></IconFont>
) : (
<IconFont
type="icon-stopcircle-fill"
style={{ fontSize: '24px' }}
></IconFont>
)
}
></Button>
<div className="slider">
<div className="flex-center flex-between file-name">
<AutoTooltip ghost maxWidth={200}>
<span>{name}</span>
</AutoTooltip>
</div>
<SliderWrapper>
<span className="time">{formatTime(audioState.currentTime)}</span>
<Slider
tooltip={{ open: false }}
min={0}
step={1}
styles={sliderStyles}
max={audioState.duration}
value={audioState.currentTime}
onChange={handleCurrentChange}
/>
</SliderWrapper>
</div>
<Dropdown menu={{ items }} trigger={['click']}>
<Button
icon={<IconFont type="icon-more"></IconFont>}
type="text"
size="middle"
shape="circle"
></Button>
</Dropdown>
</div>
<audio
crossOrigin="anonymous"
autoPlay={autoplay}
src={props.url}
ref={audioRef}
preload="metadata"
style={{ opacity: 0, position: 'absolute', left: '-9999px' }}
onPlay={handleAudioOnPlay}
onLoadedMetadata={handleLoadedMetadata}
></audio>
</div>
);
});
export default React.memo(AudioPlayer);
+1 -1
View File
@@ -87,7 +87,7 @@
}
.single-image {
height: 100%;
// height: 100%;
width: inherit;
display: flex;
justify-content: center;
+18 -4
View File
@@ -8,6 +8,7 @@ import React, {
useRef,
useState
} from 'react';
import styled from 'styled-components';
import { TooltipOverlayScroller } from '../overlay-scroller';
// type TagProps = React.ComponentProps<typeof Tag>;
@@ -22,9 +23,18 @@ interface AutoTooltipProps extends Omit<TagProps, 'title'> {
title?: React.ReactNode;
showTitle?: boolean;
closable?: boolean;
radius?: number | string;
filled?: boolean;
tooltipProps?: React.ComponentProps<typeof Tooltip>;
}
const StyledTag = styled(Tag)`
&.tag-filled {
border: none;
background-color: var(--ant-color-fill-secondary);
}
`;
const AutoTooltip: React.FC<AutoTooltipProps> = ({
children,
maxWidth = '100%',
@@ -33,6 +43,8 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
title,
showTitle = false,
tooltipProps,
radius = 12,
filled = false,
...tagProps
}) => {
const contentRef = useRef<HTMLDivElement>(null);
@@ -103,13 +115,14 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
{children}
</div>
) : (
<Tag
<StyledTag
{...tagProps}
className={`${tagProps.className || ''} ${filled ? 'tag-filled' : ''}`}
ref={contentRef}
style={{
...tagStyle,
paddingInline: tagProps.closable ? '8px 22px' : 8,
borderRadius: 12
borderRadius: radius
}}
closeIcon={
tagProps.closable ? (
@@ -117,7 +130,8 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
style={{
position: 'absolute',
right: 8,
top: 6
top: '50%',
transform: 'translateY(-50%)'
}}
/>
) : (
@@ -126,7 +140,7 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
}
>
{children}
</Tag>
</StyledTag>
)}
</TooltipOverlayScroller>
);
+109
View File
@@ -0,0 +1,109 @@
import { createStyles } from 'antd-style';
import React from 'react';
import styled from 'styled-components';
const SimpleCardItemWrapper = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 100%;
gap: 16px;
`;
const useStyles = createStyles(({ css, token }) => ({
wrapper: css`
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
background: ${token.colorBgContainer};
border-radius: ${token.borderRadius}px;
padding: ${token.padding}px;
justify-content: center;
align-items: center;
gap: ${token.padding}px;
&.bordered {
border: 1px solid ${token.colorBorder};
}
.title {
font-size: ${token.fontSize}px;
font-weight: var(--font-weight-medium);
}
.content {
display: flex;
justify-content: center;
align-items: center;
font-size: ${token.fontSize}px;
color: ${token.colorTextSecondary};
gap: 8px;
.icon {
display: inline-block;
width: 10px;
height: 10px;
gap: 10px;
&.roundRect {
border-radius: 2px;
}
&.circle {
border-radius: 50%;
}
}
}
`
}));
export const SimpleCardItem: React.FC<{
title?: string;
content?: React.ReactNode;
style?: React.CSSProperties;
bordered?: boolean;
color?: string;
iconType?: string;
}> = (props) => {
const { styles, cx } = useStyles();
const { title, content, style, bordered, iconType, color } = props;
return (
<div className={cx({ bordered: bordered }, styles.wrapper)} style={style}>
<div className="title">{title}</div>
<div className="content">
<span
className={cx([iconType], 'icon')}
style={{
backgroundColor: color || 'transparent'
}}
></span>
<span>{content}</span>
</div>
</div>
);
};
export const SimpleCard: React.FC<{
dataList: {
label: string;
value: React.ReactNode;
color: string;
iconType: string;
}[];
height?: string | number;
bordered?: boolean;
}> = (props) => {
const { dataList, bordered } = props;
return (
<SimpleCardItemWrapper style={{ height: props.height || '100%' }}>
{dataList.map((item, index) => (
<SimpleCardItem
key={index}
title={item.label}
content={item.value}
bordered={bordered}
color={item.color}
iconType={item.iconType}
></SimpleCardItem>
))}
</SimpleCardItemWrapper>
);
};
-22
View File
@@ -1,22 +0,0 @@
:local(.delete-modal-content) {
display: flex;
font-size: var(--font-size-middle);
:global {
.anticon {
font-size: 20px;
margin-right: 10px;
color: var(--ant-color-warning);
}
.title {
display: flex;
align-items: center;
}
}
}
:local(.content) {
padding-top: 15px;
padding-left: 30px;
}
+85 -41
View File
@@ -9,9 +9,38 @@ import {
message,
type ModalFuncProps
} from 'antd';
import { FC, forwardRef, useImperativeHandle, useState } from 'react';
import { createStyles } from 'antd-style';
import { forwardRef, useImperativeHandle, useState } from 'react';
import styled from 'styled-components';
import Styles from './index.less';
const useStyles = createStyles(({ css }) => ({
'delete-modal-content': css`
display: flex;
font-size: var(--font-size-middle);
.anticon {
font-size: 20px;
margin-right: 10px;
color: var(--ant-color-warning);
}
.title {
display: flex;
align-items: center;
font-weight: var(--font-weight-500);
}
`,
content: css`
padding-top: 15px;
padding-left: 30px;
color: var(--ant-color-text-secondary);
white-space: pre-line;
word-break: break-all;
span {
color: var(--ant-color-text);
display: flex;
margin-top: 8px;
}
`
}));
const CheckboxWrapper = styled.div`
margin-top: 20px;
@@ -25,12 +54,8 @@ const CheckboxWrapper = styled.div`
}
`;
interface DeleteModalProps {
ref: any;
}
interface DataOptions {
content: string;
content?: string;
selection?: boolean;
name?: string;
okText?: string;
@@ -43,28 +68,33 @@ interface DataOptions {
};
}
const DeleteModal: FC<DeleteModalProps> = forwardRef((props, ref) => {
interface Configuration {
checked: boolean;
}
const DeleteModal = forwardRef((props, ref) => {
const intl = useIntl();
const { styles } = useStyles();
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const [visible, setVisible] = useState(false);
const [checked, setChecked] = useState(false);
const [configuration, setConfiguration] = useState<Configuration>({
checked: false
});
const [config, setConfig] = useState<ModalFuncProps & DataOptions>({} as any);
useImperativeHandle(ref, () => ({
show: (data: ModalFuncProps & DataOptions) => {
saveScrollHeight();
setConfig(data);
setChecked(data.checkConfig?.defautlChecked || false);
setVisible(true);
},
hide: () => {
setVisible(false);
restoreScrollHeight();
},
configuration: {
checked: checked
}
}));
const show = (data: ModalFuncProps & DataOptions) => {
saveScrollHeight();
setConfig(data);
setConfiguration({
checked: data.checkConfig?.defautlChecked || false
});
setVisible(true);
};
const hide = () => {
setVisible(false);
restoreScrollHeight();
};
const handleCancel = () => {
setVisible(false);
@@ -94,6 +124,12 @@ const DeleteModal: FC<DeleteModalProps> = forwardRef((props, ref) => {
}
};
useImperativeHandle(ref, () => ({
show,
hide,
configuration
}));
return (
<Modal
style={{
@@ -102,12 +138,16 @@ const DeleteModal: FC<DeleteModalProps> = forwardRef((props, ref) => {
open={visible}
onOk={handleOk}
onCancel={handleCancel}
destroyOnClose={true}
destroyOnClose={false}
closeIcon={false}
maskClosable={false}
keyboard={false}
width={460}
styles={{}}
styles={{
footer: {
marginTop: '20px'
}
}}
footer={
<Space size={20}>
<Button onClick={handleCancel} size="middle">
@@ -123,7 +163,7 @@ const DeleteModal: FC<DeleteModalProps> = forwardRef((props, ref) => {
</Space>
}
>
<div className={Styles['delete-modal-content']}>
<div className={styles['delete-modal-content']}>
<span className="title">
<ExclamationCircleFilled />
<span>
@@ -134,26 +174,30 @@ const DeleteModal: FC<DeleteModalProps> = forwardRef((props, ref) => {
</span>
</div>
<div
className={Styles['content']}
className={styles['content']}
dangerouslySetInnerHTML={{
__html:
config.content &&
intl.formatMessage(
{
id: config.operation || ''
},
{
type: intl.formatMessage({ id: config.content }),
name: config.name
}
)
__html: config.content
? intl.formatMessage(
{
id: config.operation || ''
},
{
type: intl.formatMessage({ id: config.content }),
name: config.name
}
)
: ''
}}
></div>
{config.checkConfig && (
<CheckboxWrapper>
<Checkbox
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
checked={configuration.checked}
onChange={(e) =>
setConfiguration({
checked: e.target.checked
})
}
>
<span className="check-text">
{intl.formatMessage({ id: config.checkConfig?.checkText })}
+76 -20
View File
@@ -1,7 +1,6 @@
import { throttle } from 'lodash';
import _, { throttle } from 'lodash';
import React, {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useRef
@@ -18,6 +17,7 @@ const Chart: React.FC<{
const chart = useRef<echarts.EChartsType>();
const resizeable = useRef(false);
const resizeObserver = useRef<ResizeObserver>();
const finished = useRef(false);
useImperativeHandle(ref, () => {
return {
@@ -25,40 +25,96 @@ const Chart: React.FC<{
};
});
const init = useCallback(() => {
const init = () => {
if (container.current) {
chart.current?.clear();
chart.current = echarts.init(container.current);
}
}, []);
};
const resize = useCallback(() => {
chart.current?.resize();
}, []);
const setOption = useCallback(
(options: ECOption) => {
chart.current?.clear();
chart.current?.setOption(options, {
notMerge: true,
lazyUpdate: true
});
},
[options]
);
const setOption = (options: ECOption) => {
console.log('setOption', options);
chart.current?.clear();
chart.current?.setOption(options, {
notMerge: true,
lazyUpdate: true
});
if (Array.isArray(options.yAxis) && options.yAxis.length > 1) {
chart.current?.resize();
}
};
useEffect(() => {
const handleOnFinished = () => {
if (!chart.current || finished.current) return;
const currentChart = chart.current;
const optionsYAxis = currentChart.getOption()?.yAxis;
if (
!optionsYAxis ||
!Array.isArray(optionsYAxis) ||
optionsYAxis.length < 2
)
return;
// @ts-ignore
const model = currentChart.getModel();
const yAxisModels = [
model.getComponent('yAxis', 0),
model.getComponent('yAxis', 1)
];
if (!yAxisModels[0] || !yAxisModels[1]) return;
const axes = yAxisModels.map((m) => m.axis);
const intervals = axes.map((axis) => axis.scale.getInterval());
const ticksList = axes.map((axis) => axis.scale.getTicks());
const counts = ticksList.map((t) => t.length);
const unifiedCount = Math.max(counts[0], counts[1]);
const newMax0 = intervals[0] * (unifiedCount - 1);
const newMax1 = intervals[1] * (unifiedCount - 1);
// if newMax0 equal to maxValue0, and newMax1 equal to maxValue1, do not update yAxis
if (counts[0] === counts[1]) return;
const yAxis: any[] = [{}, {}];
if (counts[0] < unifiedCount) {
yAxis[0].max = _.round(newMax0, 2);
yAxis[0].interval = intervals[0];
yAxis[0].splitNumber = unifiedCount;
}
if (counts[1] < unifiedCount) {
yAxis[1].max = _.round(newMax1, 2);
yAxis[1].interval = intervals[1];
yAxis[1].splitNumber = unifiedCount;
}
finished.current = true;
currentChart.setOption({
yAxis: yAxis
});
};
if (container.current) {
init();
chart.current?.on('finished', handleOnFinished);
}
return () => {
chart.current?.dispose();
chart.current?.off('finished', handleOnFinished);
};
}, [init]);
}, []);
useEffect(() => {
resizeable.current = false;
resize();
finished.current = false;
setOption(options);
resizeable.current = true;
}, [options]);
+12 -24
View File
@@ -1,27 +1,12 @@
import useUserSettings from '@/hooks/use-user-settings';
import { formatLargeNumber } from '@/utils';
import { theme } from 'antd';
import { isFunction } from 'lodash';
import { useMemo } from 'react';
const formatLargeNumber = (value: number) => {
if (typeof value !== 'number' || isNaN(value)) {
return value;
}
if (value >= 1e9) {
return (value / 1e9).toFixed(1).replace(/\.0$/, '') + 'B';
} else if (value >= 1e6) {
return (value / 1e6).toFixed(1).replace(/\.0$/, '') + 'M';
} else if (value >= 1e3) {
return (value / 1e3).toFixed(1).replace(/\.0$/, '') + 'K';
} else {
return value;
}
};
export const grid = {
left: 0,
right: 20,
right: 0,
bottom: 20,
containLabel: true
};
@@ -56,18 +41,23 @@ export default function useChartConfig() {
borderColor: 'transparent',
formatter(params: any, callback?: (val: any) => any) {
let result = `<span class="tooltip-x-name">${params[0].axisValue}</span>`;
params.forEach((item: any) => {
let value = isFunction(callback)
? callback?.(item.data.value)
: item.data.value;
const borderRadius = item.seriesType === 'bar' ? '2px' : '8px';
result += `<span class="tooltip-item">
<span class="tooltip-item-name">
<span style="display:inline-block;margin-right:5px;border-radius:8px;width:8px;height:8px;background-color:${item.color};"></span>
<span class="tooltip-title">${item.seriesName}</span>:
</span>
<span class="tooltip-value">${value}</span>
<span class="tooltip-item-name">
<span style="display:inline-block;margin-right:5px;border-radius:${borderRadius};width:8px;height:8px;background-color:${item.color};"></span>
<span class="tooltip-title">${item.seriesName}</span>:
</span>
<span class="tooltip-value">${value}</span>
</span>`;
});
return `<div class="tooltip-wrapper">${result}</div>`;
}
};
@@ -98,8 +88,6 @@ export default function useChartConfig() {
};
const yAxis = {
// max: 100,
// min: 0,
nameTextStyle: {
padding: [0, 0, 0, -20]
},
+18 -3
View File
@@ -5,7 +5,7 @@ import _ from 'lodash';
import React, { memo, useMemo } from 'react';
import { ChartProps } from './types';
const BarChart: React.FC<ChartProps> = (props) => {
const BarChart: React.FC<ChartProps & { maxItems?: number }> = (props) => {
const {
seriesData,
xAxisData,
@@ -13,6 +13,7 @@ const BarChart: React.FC<ChartProps> = (props) => {
width,
labelFormatter,
legendData,
maxItems,
title
} = props;
const {
@@ -30,7 +31,13 @@ const BarChart: React.FC<ChartProps> = (props) => {
...titleConfig,
left: 'start'
},
grid,
grid: {
...grid,
top: 0,
bottom: maxItems
? `${(1 / maxItems) * (maxItems - xAxisData.length) * 100}%`
: 0
},
tooltip: {
...tooltip
},
@@ -41,7 +48,15 @@ const BarChart: React.FC<ChartProps> = (props) => {
formatter: labelFormatter
}
},
yAxis,
yAxis: {
...yAxis,
axisLabel: {
...yAxis.axisLabel,
overflow: 'truncate',
width: 75,
ellipsis: '...'
}
},
legend: {
...legend,
data: []
+11 -1
View File
@@ -28,6 +28,16 @@ const LineChart: React.FC<ChartProps> = (props) => {
yAxis
} = useChartConfig();
const axisLabelFormatter = (value: string, index: number) => {
if (labelFormatter) {
return labelFormatter(value, index);
}
if (index === xAxisData.length - 1) {
return '';
}
return value;
};
const options = {
title: {
text: ''
@@ -45,7 +55,7 @@ const LineChart: React.FC<ChartProps> = (props) => {
...xAxis,
axisLabel: {
...xAxis.axisLabel,
formatter: labelFormatter
formatter: axisLabelFormatter
}
},
yAxis,
+152
View File
@@ -0,0 +1,152 @@
import Chart from '@/components/echarts/chart';
import useChartConfig from '@/components/echarts/config';
import EmptyData from '@/components/empty-data';
import _ from 'lodash';
import React, { useMemo } from 'react';
import { ChartProps } from './types';
const MixLineBarChart: React.FC<
ChartProps & {
chartData: {
line: any[];
bar: any[];
};
}
> = (props) => {
const {
seriesData,
xAxisData,
yAxisName,
height,
width,
labelFormatter,
tooltipValueFormatter = null,
legendData = [],
smooth,
title,
chartData
} = props;
const {
grid,
legend,
lineItemConfig,
barItemConfig,
title: titleConfig,
tooltip,
xAxis,
yAxis
} = useChartConfig();
const { line: lineSeriesData, bar: barSeriesData } = chartData;
const options = {
title: {
text: ''
},
grid: {
...grid,
right: 0,
top: 20,
bottom: 10
},
tooltip: {
...tooltip,
formatter(params: any) {
return tooltipValueFormatter
? tooltip.formatter(params, tooltipValueFormatter)
: tooltip.formatter(params);
}
},
xAxis: {
...xAxis,
axisLabel: {
...xAxis.axisLabel,
formatter: labelFormatter
}
},
yAxis,
legend: {
...legend,
data: legendData,
itemGap: 20,
bottom: 5,
show: false
},
series: []
};
const dataOptions = useMemo((): any => {
const linedata = _.map(lineSeriesData, (item: any) => {
return {
...item,
...lineItemConfig,
smooth: smooth,
itemStyle: {
...lineItemConfig.itemStyle,
color: item.color
},
yAxisIndex: 1,
lineStyle: {
...lineItemConfig.lineStyle,
color: item.color
}
};
});
const barData = _.map(barSeriesData, (item: any) => {
return {
...item,
...barItemConfig,
stack: 'total',
yAxisIndex: 0,
itemStyle: {
...item.itemStyle,
color: item.color
}
};
});
return {
...options,
animation: false,
title: {
...titleConfig,
text: title
},
yAxis: [
{
...options.yAxis
},
{
...options.yAxis,
nameTextStyle: {
fontSize: 12,
align: 'right'
}
}
],
xAxis: {
...options.xAxis,
data: xAxisData
},
series: [...barData, ...linedata]
};
}, [seriesData, xAxisData, yAxisName, title, smooth, legendData, options]);
return (
<>
{!lineSeriesData.length && !barSeriesData.length ? (
<EmptyData height={height} title={title}></EmptyData>
) : (
<Chart
height={height}
options={dataOptions}
width={width || '100%'}
></Chart>
)}
</>
);
};
export default MixLineBarChart;
+1 -1
View File
@@ -181,7 +181,7 @@ const Scatter: React.FC<ChartProps> = (props) => {
<span class="tooltip-value">${item.text}</span>
</span>`;
});
return `<div class="tooltip-wrapper">${result}</div>`;
return `<div class="tooltip-wrapper scatter">${result}</div>`;
}
},
title: {
+3 -2
View File
@@ -1,9 +1,10 @@
import type { LegendComponentOption } from 'echarts/components';
export interface ChartProps {
seriesData: any[];
showEmpty?: boolean;
xAxisData: string[];
legendData?: string[];
labelFormatter?: (val?: any) => string;
legendData?: LegendComponentOption['data'];
labelFormatter?: (val?: any, index?: number) => string;
tooltipValueFormatter?: (val: any) => string;
height: string | number;
width?: string | number;
+207 -11
View File
@@ -1,8 +1,8 @@
@font-face {
font-family: iconfont; /* Project id 4613488 */
src: url('iconfont.woff2?t=1747231573342') format('woff2'),
url('iconfont.woff?t=1747231573342') format('woff'),
url('iconfont.ttf?t=1747231573342') format('truetype');
src: url('iconfont.woff2?t=1752112673765') format('woff2'),
url('iconfont.woff?t=1752112673765') format('woff'),
url('iconfont.ttf?t=1752112673765') format('truetype');
}
.iconfont {
@@ -13,6 +13,210 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-huggingface1::before {
content: "\e605";
}
.icon-modelscope_light::before {
content: "\e696";
}
.icon-catalog1::before {
content: "\e691";
}
.icon-chat-filled::before {
content: "\e692";
}
.icon-chat::before {
content: "\e693";
}
.icon-files-filled::before {
content: "\e694";
}
.icon-files::before {
content: "\e695";
}
.icon-models-filled::before {
content: "\e682";
}
.icon-image-filled::before {
content: "\e683";
}
.icon-audio1::before {
content: "\e684";
}
.icon-image1::before {
content: "\e685";
}
.icon-audio-filled::before {
content: "\e686";
}
.icon-reranker-filled::before {
content: "\e687";
}
.icon-embedding-filled::before {
content: "\e68a";
}
.icon-models::before {
content: "\e68b";
}
.icon-reranker::before {
content: "\e68c";
}
.icon-embedding::before {
content: "\e68d";
}
.icon-gpu-filled::before {
content: "\e68e";
}
.icon-catalog-filled::before {
content: "\e68f";
}
.icon-gpu1::before {
content: "\e690";
}
.icon-language::before {
content: "\e679";
}
.icon-help::before {
content: "\e67a";
}
.icon-key-filled::before {
content: "\e67b";
}
.icon-key::before {
content: "\e67d";
}
.icon-resources::before {
content: "\e67e";
}
.icon-users::before {
content: "\e67f";
}
.icon-resources-filled::before {
content: "\e680";
}
.icon-users-filled::before {
content: "\e681";
}
.icon-model::before {
content: "\e676";
}
.icon-model-filled::before {
content: "\e678";
}
.icon-layers-filled::before {
content: "\e671";
}
.icon-layers::before {
content: "\e673";
}
.icon-experiment::before {
content: "\e674";
}
.icon-experiment-filled::before {
content: "\e675";
}
.icon-dashboard::before {
content: "\e66d";
}
.icon-dashboard-filled::before {
content: "\e66e";
}
.icon-expand-left::before {
content: "\ea48";
}
.icon-expand-right::before {
content: "\ea49";
}
.icon-users-fill::before {
content: "\e677";
}
.icon-layers-fill::before {
content: "\e89d";
}
.icon-key-fill::before {
content: "\e80e";
}
.icon-server-fill::before {
content: "\e7a3";
}
.icon-model-fill::before {
content: "\e7b7";
}
.icon-left_panel_close::before {
content: "\e668";
}
.icon-left_panel_open::before {
content: "\e669";
}
.icon-playcircle-fill::before {
content: "\e665";
}
.icon-stopcircle-fill::before {
content: "\e666";
}
.icon-play-speed::before {
content: "\e856";
}
.icon-more::before {
content: "\e62e";
}
.icon-play::before {
content: "\e9f9";
}
.icon-pause::before {
content: "\e713";
}
.icon-dark_theme::before {
content: "\e646";
}
@@ -237,14 +441,6 @@
content: "\e62c";
}
.icon-stopcircle-fill::before {
content: "\e7cc";
}
.icon-playcircle-fill::before {
content: "\e7cd";
}
.icon-SpeakerSlash::before {
content: "\e661";
}
File diff suppressed because one or more lines are too long
+357 -14
View File
@@ -5,6 +5,363 @@
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "40865292",
"name": "huggingface",
"font_class": "huggingface1",
"unicode": "e605",
"unicode_decimal": 58885
},
{
"icon_id": "43616582",
"name": "modelscope_light",
"font_class": "modelscope_light",
"unicode": "e696",
"unicode_decimal": 59030
},
{
"icon_id": "44776845",
"name": "catalog",
"font_class": "catalog1",
"unicode": "e691",
"unicode_decimal": 59025
},
{
"icon_id": "44776848",
"name": "chat-filled",
"font_class": "chat-filled",
"unicode": "e692",
"unicode_decimal": 59026
},
{
"icon_id": "44776846",
"name": "chat",
"font_class": "chat",
"unicode": "e693",
"unicode_decimal": 59027
},
{
"icon_id": "44776841",
"name": "files-filled",
"font_class": "files-filled",
"unicode": "e694",
"unicode_decimal": 59028
},
{
"icon_id": "44776840",
"name": "files",
"font_class": "files",
"unicode": "e695",
"unicode_decimal": 59029
},
{
"icon_id": "44776851",
"name": "models-filled",
"font_class": "models-filled",
"unicode": "e682",
"unicode_decimal": 59010
},
{
"icon_id": "44776857",
"name": "image-filled",
"font_class": "image-filled",
"unicode": "e683",
"unicode_decimal": 59011
},
{
"icon_id": "44776856",
"name": "audio",
"font_class": "audio1",
"unicode": "e684",
"unicode_decimal": 59012
},
{
"icon_id": "44776855",
"name": "image",
"font_class": "image1",
"unicode": "e685",
"unicode_decimal": 59013
},
{
"icon_id": "44776850",
"name": "audio-filled",
"font_class": "audio-filled",
"unicode": "e686",
"unicode_decimal": 59014
},
{
"icon_id": "44776849",
"name": "reranker-filled",
"font_class": "reranker-filled",
"unicode": "e687",
"unicode_decimal": 59015
},
{
"icon_id": "44776852",
"name": "embedding-filled",
"font_class": "embedding-filled",
"unicode": "e68a",
"unicode_decimal": 59018
},
{
"icon_id": "44776853",
"name": "models",
"font_class": "models",
"unicode": "e68b",
"unicode_decimal": 59019
},
{
"icon_id": "44776844",
"name": "reranker",
"font_class": "reranker",
"unicode": "e68c",
"unicode_decimal": 59020
},
{
"icon_id": "44776854",
"name": "embedding",
"font_class": "embedding",
"unicode": "e68d",
"unicode_decimal": 59021
},
{
"icon_id": "44776843",
"name": "gpu-filled",
"font_class": "gpu-filled",
"unicode": "e68e",
"unicode_decimal": 59022
},
{
"icon_id": "44776847",
"name": "catalog-filled",
"font_class": "catalog-filled",
"unicode": "e68f",
"unicode_decimal": 59023
},
{
"icon_id": "44776842",
"name": "gpu",
"font_class": "gpu1",
"unicode": "e690",
"unicode_decimal": 59024
},
{
"icon_id": "44685190",
"name": "language",
"font_class": "language",
"unicode": "e679",
"unicode_decimal": 59001
},
{
"icon_id": "44685189",
"name": "help",
"font_class": "help",
"unicode": "e67a",
"unicode_decimal": 59002
},
{
"icon_id": "44684306",
"name": "key-filled",
"font_class": "key-filled",
"unicode": "e67b",
"unicode_decimal": 59003
},
{
"icon_id": "44684302",
"name": "key",
"font_class": "key",
"unicode": "e67d",
"unicode_decimal": 59005
},
{
"icon_id": "44684304",
"name": "resources",
"font_class": "resources",
"unicode": "e67e",
"unicode_decimal": 59006
},
{
"icon_id": "44684305",
"name": "users",
"font_class": "users",
"unicode": "e67f",
"unicode_decimal": 59007
},
{
"icon_id": "44684303",
"name": "resources-filled",
"font_class": "resources-filled",
"unicode": "e680",
"unicode_decimal": 59008
},
{
"icon_id": "44684301",
"name": "users-filled",
"font_class": "users-filled",
"unicode": "e681",
"unicode_decimal": 59009
},
{
"icon_id": "44684150",
"name": "model",
"font_class": "model",
"unicode": "e676",
"unicode_decimal": 58998
},
{
"icon_id": "44684149",
"name": "model-filled",
"font_class": "model-filled",
"unicode": "e678",
"unicode_decimal": 59000
},
{
"icon_id": "44684090",
"name": "layers-filled",
"font_class": "layers-filled",
"unicode": "e671",
"unicode_decimal": 58993
},
{
"icon_id": "44684091",
"name": "layers",
"font_class": "layers",
"unicode": "e673",
"unicode_decimal": 58995
},
{
"icon_id": "44684086",
"name": "experiment",
"font_class": "experiment",
"unicode": "e674",
"unicode_decimal": 58996
},
{
"icon_id": "44684085",
"name": "experiment",
"font_class": "experiment-filled",
"unicode": "e675",
"unicode_decimal": 58997
},
{
"icon_id": "44684021",
"name": "dashboard",
"font_class": "dashboard",
"unicode": "e66d",
"unicode_decimal": 58989
},
{
"icon_id": "44684020",
"name": "dashboard-filled",
"font_class": "dashboard-filled",
"unicode": "e66e",
"unicode_decimal": 58990
},
{
"icon_id": "40068272",
"name": "expand-left",
"font_class": "expand-left",
"unicode": "ea48",
"unicode_decimal": 59976
},
{
"icon_id": "40068278",
"name": "expand-right",
"font_class": "expand-right",
"unicode": "ea49",
"unicode_decimal": 59977
},
{
"icon_id": "1350497",
"name": "sq-users",
"font_class": "users-fill",
"unicode": "e677",
"unicode_decimal": 58999
},
{
"icon_id": "13691715",
"name": "layers",
"font_class": "layers-fill",
"unicode": "e89d",
"unicode_decimal": 59549
},
{
"icon_id": "14267903",
"name": "key-skeleton-alt",
"font_class": "key-fill",
"unicode": "e80e",
"unicode_decimal": 59406
},
{
"icon_id": "14341823",
"name": "cloud-server-solid",
"font_class": "server-fill",
"unicode": "e7a3",
"unicode_decimal": 59299
},
{
"icon_id": "40764011",
"name": "icon-model-selected",
"font_class": "model-fill",
"unicode": "e7b7",
"unicode_decimal": 59319
},
{
"icon_id": "44668359",
"name": "left_panel_close_36dp_1F1F1F_FILL0_wght400_GRAD0_o",
"font_class": "left_panel_close",
"unicode": "e668",
"unicode_decimal": 58984
},
{
"icon_id": "44668360",
"name": "left_panel_open_36dp_1F1F1F_FILL0_wght400_GRAD0_op",
"font_class": "left_panel_open",
"unicode": "e669",
"unicode_decimal": 58985
},
{
"icon_id": "44650419",
"name": "play circle-fill",
"font_class": "playcircle-fill",
"unicode": "e665",
"unicode_decimal": 58981
},
{
"icon_id": "44650418",
"name": "stop circle-fill",
"font_class": "stopcircle-fill",
"unicode": "e666",
"unicode_decimal": 58982
},
{
"icon_id": "8592899",
"name": "play-speed",
"font_class": "play-speed",
"unicode": "e856",
"unicode_decimal": 59478
},
{
"icon_id": "10941162",
"name": "more",
"font_class": "more",
"unicode": "e62e",
"unicode_decimal": 58926
},
{
"icon_id": "703373",
"name": "play",
"font_class": "play",
"unicode": "e9f9",
"unicode_decimal": 59897
},
{
"icon_id": "736882",
"name": "pause",
"font_class": "pause",
"unicode": "e713",
"unicode_decimal": 59155
},
{
"icon_id": "44028047",
"name": "dark_theme",
@@ -397,20 +754,6 @@
"unicode": "e62c",
"unicode_decimal": 58924
},
{
"icon_id": "6151140",
"name": "stop circle-fill",
"font_class": "stopcircle-fill",
"unicode": "e7cc",
"unicode_decimal": 59340
},
{
"icon_id": "6151141",
"name": "play circle-fill",
"font_class": "playcircle-fill",
"unicode": "e7cd",
"unicode_decimal": 59341
},
{
"icon_id": "23563264",
"name": "SpeakerSlash",
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -107,7 +107,6 @@ export default function useZoom(props: {
if (isLoadingMaskRef.current) {
return;
}
event.preventDefault();
// stop
handleZoom(event);
updateCursorSize();
+13 -4
View File
@@ -433,7 +433,6 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
fitView();
setActiveScale(autoScale.current);
updateCursorSize();
redrawStrokes(strokesRef.current);
};
const handleBrushSizeChange = (value: number) => {
@@ -454,11 +453,22 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
useEffect(() => {
const handleUndoShortcut = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
if (
(e.ctrlKey || e.metaKey) &&
e.key === 'z' &&
!negativeMaskRef.current
) {
undo();
}
};
window.addEventListener('keydown', handleUndoShortcut);
return () => {
window.removeEventListener('keydown', handleUndoShortcut);
};
}, []);
useEffect(() => {
const handleMouseDown = (e: MouseEvent) => {
mouseDownState.current = true;
};
@@ -467,7 +477,6 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
mouseDownState.current = false;
};
window.addEventListener('keydown', handleUndoShortcut);
// mouse down
window.addEventListener('mousedown', handleMouseDown);
@@ -475,7 +484,7 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
window.addEventListener('mouseup', handleMouseUp);
return () => {
clearTimeout(timer.current);
window.removeEventListener('keydown', handleUndoShortcut);
window.removeEventListener('mousedown', handleMouseDown);
window.removeEventListener('mouseup', handleMouseUp);
};
+1 -1
View File
@@ -1,7 +1,7 @@
export const controlSeqRegex = /\x1b\[(\d*);?(\d*)?([A-DJKHfm])/g;
export const replaceLineRegex = /\r\n/g;
export const PageSize = 500;
export const PageSize = 1000;
export const throttle = <T extends (...args: any[]) => void>(
func: T,
+1 -1
View File
@@ -72,7 +72,7 @@ const LogsList: React.FC<LogsListProps> = forwardRef((props, ref) => {
const isBottom = scrollTop + clientHeight + 150 >= scrollHeight;
// is scroll to top
if (scrollTop === 0) {
if (scrollTop <= 10) {
onScroll?.({
isTop: true,
isBottom: false
@@ -106,4 +106,4 @@ const LogsPagination: React.FC<LogsPaginationProps> = (props) => {
);
};
export default React.memo(LogsPagination);
export default LogsPagination;
+33 -4
View File
@@ -30,6 +30,8 @@ class AnsiParser {
private percent: number = 0;
private isComplete: boolean = false;
private chunked: boolean = true; // true: send data in chunks, false: send all data at once
private reminder: string = '';
private lines: string[] = [];
private pageSize: number = 500;
private colorMap = {
'30': 'black',
@@ -52,6 +54,8 @@ class AnsiParser {
this.screen = [['']];
this.rawDataRows = 0;
this.uid = this.uid + 1;
this.lines = [];
this.reminder = '';
this.page = 1;
}
@@ -109,7 +113,6 @@ class AnsiParser {
const n = parseInt(match[1] || '1', 10);
const m = parseInt(match[2] || '1', 10);
const command = match[3];
switch (command) {
case 'A':
this.cursorRow = Math.max(0, this.cursorRow - n);
@@ -183,6 +186,31 @@ class AnsiParser {
return result;
}
private processInputByLine(input: string): {
data: string[];
lines: number;
remainder: string;
} {
const lines = input?.split(/\r?\n/) || [];
const remainder = lines.pop() || '';
// const data = lines.join('\n');
this.rawDataRows += lines.length;
lines.forEach((line) => {
this.lines.push(line);
});
return {
data: this.lines,
lines: this.rawDataRows,
remainder
};
}
private getAllLines() {
return this.lines.join('\n');
}
private async processQueue(): Promise<void> {
if (this.isProcessing) {
return;
@@ -191,11 +219,12 @@ class AnsiParser {
this.isProcessing = true;
while (this.taskQueue.length > 0) {
const input = this.taskQueue.shift();
const input = this.reminder + this.taskQueue.shift();
if (input) {
try {
const result = this.processInput(input);
const result = this.processInputByLine(input);
this.reminder = result.remainder;
if (this.chunked) {
self.postMessage({ result: result.data, lines: result.lines });
} else if (!this.isComplete) {
@@ -220,7 +249,7 @@ class AnsiParser {
this.processQueue();
} else if (this.isComplete && !this.chunked) {
self.postMessage({
result: this.getScreenText(),
result: this.getAllLines(),
percent: this.percent,
isComplete: true
});
+1 -1
View File
@@ -17,7 +17,7 @@
top: 0;
bottom: 0;
right: -18px;
width: 80px;
// width: 80px;
height: 185px;
&:hover {
@@ -15,6 +15,7 @@
.text {
min-height: 22px;
padding-inline-end: 80px;
&.numable {
position: relative;
@@ -12,7 +12,7 @@
}
.ant-btn {
background-color: rgba(71, 71, 71, 100%) !important;
background-color: rgba(71, 71, 71, 70%) !important;
}
.pages {
@@ -26,6 +26,7 @@ interface LogsViewerProps {
enableScorllLoad?: boolean;
diffHeight?: number;
}
const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
const { diffHeight, url, tail: defaultTail, enableScorllLoad = true } = props;
const { pageSize, page, setPage, setTotalPage, totalPage } =
@@ -43,7 +44,7 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
const pageRef = useRef<any>(page);
const totalPageRef = useRef<any>(totalPage);
const isLoadingMoreRef = useRef(false);
const [currentData, setCurrentData] = useState<any[]>([]);
const [currentData, setCurrentPageData] = useState<any[]>([]);
const scrollPosRef = useRef<any>({
pos: 'bottom',
page: 1
@@ -58,6 +59,21 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
}
}));
const removeBracketsFromLine = (row: string) => {
return row.startsWith('(…)') ? row.slice(3) : row;
};
const setCurrentData = (lines: string[]) => {
const dataList = lines.map((line, index) => {
return {
content: removeBracketsFromLine(line),
uid: `${pageRef.current}-${index}`
};
});
setCurrentPageData(dataList);
};
const debounceLoading = _.debounce(() => {
setLoading(false);
isLoadingMoreRef.current = false;
@@ -313,7 +329,7 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
<div className="pg">
<div
className={classNames('pg-inner', {
'at-top': isAtTop
'at-top': true
})}
>
<LogsPagination
+4 -7
View File
@@ -32,7 +32,6 @@
h2,
h3,
h4 {
font-weight: var(--font-weight-bold);
margin-top: 1em;
}
@@ -58,10 +57,6 @@
margin-top: 16px;
}
strong {
font-weight: var(--font-weight-bold);
}
ul {
margin-bottom: 0;
padding-left: 20px;
@@ -87,7 +82,7 @@
font-weight: var(--font-weight-bold);
line-height: 1.5;
padding-inline: 6px;
border-bottom: 1px solid var(--ant-color-split);
border: 1px solid var(--ant-color-split);
word-break: break-word;
text-align: left !important;
}
@@ -95,7 +90,7 @@
td {
line-height: 1.5;
padding-inline: 6px;
border-bottom: 1px solid var(--ant-color-split);
border: 1px solid var(--ant-color-split);
word-break: break-word;
text-align: left !important;
}
@@ -118,6 +113,8 @@
img[src^="https://img.shields.io/"], img[src*="badge.svg"]
{
margin-bottom: 6px;
width: unset;
max-width: 100%;
}
video {
+23 -3
View File
@@ -1,6 +1,7 @@
import { EyeOutlined } from '@ant-design/icons';
import { sanitizeUrl } from '@braintree/sanitize-url';
import { Checkbox, Image, Typography } from 'antd';
import DOMPurify from 'dompurify';
import { unescape } from 'lodash';
import { TokensList, marked } from 'marked';
import React, { Fragment, useCallback, useEffect } from 'react';
@@ -16,6 +17,15 @@ interface MarkdownViewerProps {
generateImgLink?: (src: string) => string;
}
const dompurifyOptions = {
FORBID_TAGS: ['meta', 'style', 'script', 'iframe'],
FORBID_ATTR: ['onerror', 'onclick', 'onload', 'style', 'autoplay']
};
const cleanHtml = (html: string): string => {
return DOMPurify?.sanitize?.(html, dompurifyOptions);
};
const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
content,
generateImgLink,
@@ -68,11 +78,15 @@ const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
const renderItem = useCallback(
(token: any, render: any) => {
if (token.type === 'script' || token.type === 'style') {
return null;
}
if (!reDefineTypes.includes(token.type)) {
return (
<span
dangerouslySetInnerHTML={{
__html: marked.parser([token], { renderer })
__html: cleanHtml(marked.parser([token], { renderer }))
}}
></span>
);
@@ -89,7 +103,13 @@ const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
}
if (token.type === 'html') {
htmlstr = <div dangerouslySetInnerHTML={{ __html: token.text }} />;
htmlstr = (
<div
dangerouslySetInnerHTML={{
__html: cleanHtml(token.text)
}}
/>
);
}
if (token.type === 'list') {
htmlstr = token.order ? (
@@ -158,7 +178,7 @@ const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
if (token.type === 'link') {
htmlstr = (
<Link
href={token.href}
href={sanitizeUrl(token.href || '')}
title={token.title || ''}
target="_blank"
rel="noopener noreferrer"
@@ -12,6 +12,9 @@ const Wrapper = styled.div<{ $maxHeight?: number }>`
width: 100%;
`;
// export OverlayScrollerOptions
export type { OverlayScrollerOptions };
export const OverlayScroller: React.FC<
OverlayScrollerOptions & {
maxHeight?: number;
+9 -3
View File
@@ -1,9 +1,9 @@
import { Progress, Tooltip } from 'antd';
import React, { memo, useMemo } from 'react';
import React, { memo, useEffect, useMemo } from 'react';
const RenderProgress = memo(
(props: {
open?: boolean;
defaultOpen?: boolean;
percent: number;
steps?: number;
download?: boolean;
@@ -11,7 +11,8 @@ const RenderProgress = memo(
successPercent?: number;
successColor?: string;
}) => {
const { open, percent, download, label, successPercent } = props;
const { defaultOpen, percent, download, label, successPercent } = props;
const [open, setOpen] = React.useState(false);
const strokeColor = useMemo(() => {
if (download) {
@@ -27,6 +28,10 @@ const RenderProgress = memo(
return 'var(--ant-color-error)';
}, [percent]);
useEffect(() => {
setOpen(defaultOpen || false);
}, [defaultOpen]);
const renderProgress = useMemo(() => {
return (
<Progress
@@ -59,6 +64,7 @@ const RenderProgress = memo(
<Tooltip
title={label}
open={open}
onOpenChange={setOpen}
overlayInnerStyle={{ paddingInline: 12 }}
>
{renderProgress}
@@ -0,0 +1,19 @@
import { useEscHint } from '@/hooks/use-esc-hint';
import { Drawer, type DrawerProps } from 'antd';
const ScrollerModal = (props: DrawerProps) => {
const { EscHint } = useEscHint({
enabled: !props.keyboard && props.open
});
return (
<>
<Drawer {...props}>
{props.children}
{EscHint}
</Drawer>
</>
);
};
export default ScrollerModal;
@@ -0,0 +1,38 @@
import AutoTooltip from '@/components/auto-tooltip';
import React, { useMemo } from 'react';
interface SelectRenderProps {
maxTagWidth?: number;
radius?: number | string;
style?: React.CSSProperties;
filled?: boolean;
}
export default function useSelectRender(config?: SelectRenderProps) {
const { maxTagWidth = 100, radius, filled, style } = config || {};
const TagRender = (props: any) => {
const { label } = props;
const labelText = useMemo(() => {
if (!props.isMaxTag) {
return label;
}
return label.slice(0, -3);
}, [label, props.isMaxTag]);
return (
<AutoTooltip
maxWidth={maxTagWidth}
closable={props.closable}
onClose={props.onClose}
radius={radius}
filled={filled}
style={style}
>
{labelText}
</AutoTooltip>
);
};
return {
TagRender
};
}
+1 -1
View File
@@ -148,4 +148,4 @@ const RowTextarea: React.FC<SystemMessageProps> = (props) => {
);
};
export default React.memo(RowTextarea);
export default RowTextarea;
+2 -1
View File
@@ -11,7 +11,7 @@ import SelectWrapper from './wrapper/select';
const tag = (props: any) => {
if (props.isMaxTag) {
return props.label;
return props.label?.slice(0, -3);
}
const parent = _.split(props.value, '__RC_CASCADER_SPLIT__')?.[0];
return `${parent} / ${props?.label}`;
@@ -24,6 +24,7 @@ const renderTag = (props: any) => {
closable={props.closable}
onClose={props.onClose}
maxWidth={240}
filled
>
{tag(props)}
</AutoTooltip>
+259
View File
@@ -0,0 +1,259 @@
import { useIntl } from '@umijs/max';
import type { SelectProps } from 'antd';
import { Checkbox, Select, Tag } from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import React, { useEffect } from 'react';
import styled from 'styled-components';
import AutoTooltip from '../auto-tooltip';
const OptionWrapper = styled.div`
display: flex;
align-items: center;
gap: 8px;
`;
const DropdownWrapper = styled.div`
.ant-select-item {
padding-inline-start: 8px;
&:hover {
background-color: var(--ant-select-option-active-bg);
}
}
.ant-select-item-option-selected:not(.ant-select-item-option-disabled) {
background-color: unset;
font-weight: unset;
&:hover {
background-color: var(--ant-select-option-active-bg);
}
}
`;
const SelectAllWrapper = styled.div`
margin-bottom: 10px;
padding: 5px 8px;
font-size: 12px;
border-bottom: 1px solid var(--ant-color-split);
.ant-checkbox-wrapper {
color: var(--ant-color-text-tertiary);
}
`;
const TagWrapper = styled(Tag)`
border-radius: 12px;
`;
const SimpleSelect: React.FC<SelectProps> = (props) => {
const intl = useIntl();
const { options = [], ...restProps } = props;
const [allSelection, setAllSelection] = React.useState<{
checked: boolean;
indeterminate: boolean;
}>({
checked: false,
indeterminate: false
});
const [optionsList, setOptionsList] = React.useState<any[]>(options || []);
const selectRef = React.useRef<any>(null);
useEffect(() => {
setOptionsList(options || []);
}, [options]);
const optionRender = (option: any, info: any) => {
const { value, label } = option;
return (
<OptionWrapper>
{restProps.value?.includes(value) ? (
<Checkbox checked></Checkbox>
) : (
<Checkbox></Checkbox>
)}
<AutoTooltip ghost>{label}</AutoTooltip>
</OptionWrapper>
);
};
const handleOnCheckboxChange = (e: CheckboxChangeEvent) => {
const isChecked = e.target.checked;
const allValues = optionsList?.map((opt: any) => opt.value) || [];
setAllSelection({
checked: isChecked,
indeterminate: false
});
let allSelectedValues = [...(restProps.value || [])];
if (isChecked) {
// Select all options
allSelectedValues = Array.from(
new Set([...allSelectedValues, ...allValues])
);
} else {
// Deselect all options
allSelectedValues = allSelectedValues.filter(
(value) => !allValues.includes(value)
);
}
restProps.onChange?.(allSelectedValues, optionsList || []);
};
const dropdownRender = (originPanel: React.ReactNode) => {
return (
<DropdownWrapper>
{restProps.mode === 'multiple' && (
<SelectAllWrapper>
<Checkbox
checked={allSelection.checked}
indeterminate={allSelection.indeterminate}
onChange={handleOnCheckboxChange}
>
{intl.formatMessage({ id: 'common.checbox.all' })}
</Checkbox>
</SelectAllWrapper>
)}
{originPanel}
</DropdownWrapper>
);
};
const handleOnChange = (value: any, option: any) => {
const selectedValues = Array.isArray(value) ? value : [value];
const allSelected = optionsList?.map((opt: any) => opt.value) || [];
const isAllSelected = selectedValues.length === allSelected?.length;
setAllSelection({
checked: isAllSelected,
indeterminate: !isAllSelected && selectedValues.length > 0
});
restProps.onChange?.(selectedValues, option);
};
const filterOption = (inputValue: string, option: any) => {
if (!option || !option.label) return false;
return option.label.toLowerCase().includes(inputValue.toLowerCase());
};
const checkAllSelection = (list: Global.BaseOption<string | number>[]) => {
if (
!restProps.value ||
!Array.isArray(restProps.value) ||
list.length === 0
) {
setAllSelection({
checked: false,
indeterminate: false
});
return;
}
const selectedValues = new Set(restProps.value);
const allValues = list?.map((opt: any) => opt.value) || [];
const isAllSelected = allValues.every((val: any) =>
selectedValues.has(val)
);
const isSomeSelected = allValues.some((val: any) =>
selectedValues.has(val)
);
setAllSelection({
checked: isAllSelected,
indeterminate: isSomeSelected && !isAllSelected
});
};
const TagRender = (props: any) => {
const { label } = props;
const count = props.isMaxTag ? label.slice(0, -3).slice(1) : label;
return (
<TagWrapper
bordered={false}
style={{
height: 24,
backgroundColor: 'var(--ant-color-fill-tertiary)',
fontSize: 'var(--ant-font-size)'
}}
className="flex-center"
>
{intl.formatMessage({ id: 'common.select.count' }, { count: count })}
</TagWrapper>
);
};
const handleOnSearch = (value: string) => {
if (restProps.onSearch) {
restProps.onSearch(value);
} else {
const filteredOptions = options?.filter((option: any) =>
option.label.toLowerCase().includes(value.toLowerCase())
) as Global.BaseOption<string | number>[];
setOptionsList(filteredOptions || []);
checkAllSelection(filteredOptions || []);
}
};
const handleOnBlur = (e: any) => {
restProps.onBlur?.(e);
};
const handleOnFocus = (e: any) => {
restProps.onFocus?.(e);
};
const handleOnOpenChange = (open: boolean) => {
if (!open) {
checkAllSelection(options as Global.BaseOption<string | number>[]);
setOptionsList(options || []);
}
};
useEffect(() => {
const input = selectRef.current?.querySelector?.('input');
if (!input) return;
const handler = (event: KeyboardEvent) => {
if (
event.key === 'Backspace' &&
(input as HTMLInputElement).value === ''
) {
event.stopPropagation();
event.preventDefault();
}
};
input.addEventListener('keydown', handler);
return () => {
input.removeEventListener('keydown', handler);
};
}, [selectRef.current]);
return (
<div ref={selectRef}>
<Select
{...restProps}
options={optionsList}
maxTagCount={0}
defaultActiveFirstOption={false}
dropdownRender={dropdownRender}
optionRender={optionRender}
menuItemSelectedIcon={false}
onChange={handleOnChange}
tagRender={TagRender}
onBlur={handleOnBlur}
onFocus={handleOnFocus}
onSearch={handleOnSearch}
filterOption={filterOption}
onDropdownVisibleChange={handleOnOpenChange}
></Select>
</div>
);
};
export default SimpleSelect;
@@ -1,5 +1,6 @@
import IconFont from '@/components/icon-font';
import { RightOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Checkbox } from 'antd';
import _ from 'lodash';
import React from 'react';
@@ -29,10 +30,20 @@ const HeaderPrefix: React.FC<HeaderPrefixProps> = (props) => {
disabled
} = props;
const intl = useIntl();
const handleToggleExpand = () => {
onExpandAll?.(!expandAll);
};
const handleUnCheckAll = () => {
onSelectAll?.({
target: {
checked: false
}
});
};
if (!hasColumns) {
return null;
}
@@ -98,4 +109,4 @@ const HeaderPrefix: React.FC<HeaderPrefixProps> = (props) => {
return null;
};
export default React.memo(HeaderPrefix);
export default HeaderPrefix;
@@ -45,4 +45,4 @@ const Header: React.FC<HeaderProps> = (props) => {
);
};
export default React.memo(Header);
export default Header;
@@ -1,8 +1,7 @@
import { Pagination, type PaginationProps } from 'antd';
import { memo } from 'react';
const PaginationComponent: React.FC<PaginationProps> = (props) => {
return <Pagination {...props} />;
};
export default memo(PaginationComponent);
export default PaginationComponent;
@@ -60,4 +60,4 @@ const TableHeader: React.FC<TableHeaderProps> = (props) => {
);
};
export default React.memo(TableHeader);
export default TableHeader;
@@ -27,7 +27,7 @@ const TableRow: React.FC<
rowIndex,
expandable,
rowSelection,
expandedRowKeys,
expandedRowKeys = [],
rowKey,
childParentKey,
columns,
@@ -41,7 +41,6 @@ const TableRow: React.FC<
} = props;
const tableContext: any = React.useContext<{
allChildren?: any[];
allSubChildren?: any[];
setDisableExpand?: (record: any) => boolean;
}>(TableContext);
const { setChunkRequest } = useSetChunkRequest();
@@ -80,6 +79,12 @@ const TableRow: React.FC<
return rowSelection?.selectedRowKeys?.includes(record[rowKey]);
}, [rowSelection?.selectedRowKeys, record, rowKey]);
useEffect(() => {
if (expandedRowKeys?.length === 0) {
setCurrentExpand(false);
}
}, [expandedRowKeys.length]);
const renderChildrenData = () => {
if (childrenData.length === 0) {
// return (
@@ -131,9 +136,6 @@ const TableRow: React.FC<
}, [record, loadChildren]);
const filterUpdateChildrenHandler = () => {
if (!expandedRowKeys?.includes(record[rowKey])) {
return;
}
const dataList = _.filter(tableContext.allChildren, (data: any) => {
return _.get(data, [childParentKey]) === _.get(record, [rowKey]);
});
@@ -269,7 +271,7 @@ const TableRow: React.FC<
})}
</Row>
</div>
{expanded && (
{expanded && !disableExpand && (
<div className="expanded-row">
<Spin spinning={loading}>{renderChildrenData()}</Spin>
</div>
@@ -279,4 +281,4 @@ const TableRow: React.FC<
);
};
export default React.memo(TableRow);
export default TableRow;
+2 -2
View File
@@ -22,7 +22,7 @@ const InfoColumn: React.FC<InfoColumnProps> = (props) => {
<span className="flex" style={style}>
{fieldList.map((item, index) => {
return (
<>
<span key={item.key || index} className="flex-center">
<span className="flex-column flex-center">
<span>
{' '}
@@ -46,7 +46,7 @@ const InfoColumn: React.FC<InfoColumnProps> = (props) => {
}}
></Divider>
) : null}
</>
</span>
);
})}
</span>
+18 -11
View File
@@ -29,6 +29,7 @@ type StatusTagProps = {
download?: {
percent: number;
};
maxTooltipWidth?: number;
extra?: React.ReactNode;
actions?: {
label: string;
@@ -44,6 +45,7 @@ const StatusTag: React.FC<StatusTagProps> = ({
download,
extra,
actions = [],
maxTooltipWidth = 250,
type = 'tag'
}) => {
const { text, status } = statusValue;
@@ -56,16 +58,21 @@ const StatusTag: React.FC<StatusTagProps> = ({
return StatusColorMap[status];
}, [status]);
const statusMessage = useMemo(() => {
return statusValue.message?.replace(linkReg, '');
const hasLink = useMemo(() => {
if (!statusValue.message) return false;
return linkReg.test(statusValue.message || '');
}, [statusValue.message]);
const messageLink = useMemo(() => {
const statusMessage = useMemo<string>(() => {
if (!statusValue.message) return '';
const link = statusValue.message?.match(linkReg);
if (link) {
return link?.[0].replace(linkReg, '<a $1 target="_blank">$2</a>');
return statusValue.message?.replace(
linkReg,
'<a $1 target="_blank">$2</a>'
);
}
return null;
return statusValue.message;
}, [statusValue.message]);
const renderContent = () => {
@@ -118,17 +125,17 @@ const StatusTag: React.FC<StatusTagProps> = ({
<div
style={{
width: 'max-content',
maxWidth: 250,
maxWidth: maxTooltipWidth,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
}}
>
{statusMessage}
{statusMessage && <span className="m-r-5"></span>}
{extra}
{messageLink && (
<span dangerouslySetInnerHTML={{ __html: messageLink }}></span>
{hasLink ? (
<span dangerouslySetInnerHTML={{ __html: statusMessage }}></span>
) : (
statusMessage
)}
{extra && <span className="m-l-5">{extra}</span>}
</div>
</div>
);
+33 -9
View File
@@ -1,27 +1,49 @@
import { UploadOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Tooltip, Upload } from 'antd';
import { Button, Tooltip, Upload, message } from 'antd';
import React from 'react';
import { convertFileSize } from '../../utils';
interface UploadAudioProps {
accept?: string;
maxCount?: number;
type?: 'text' | 'primary' | 'default';
icon?: React.ReactNode;
size?: 'small' | 'middle' | 'large';
shape?: 'circle' | 'round' | 'default';
maxFileSize?: number; // in bytes
onChange?: (data: { file: any; fileList: any[] }) => void;
}
const UploadAudio: React.FC<UploadAudioProps> = (props) => {
const [messageApi, contextHolder] = message.useMessage();
const { icon, accept, type, size = 'large', shape = 'circle' } = props;
const intl = useIntl();
const beforeUpload = (file: any) => {
return false;
};
const handleOnChange = React.useCallback(
(data: { file: any; fileList: any }) => {
props.onChange?.(data);
},
[]
);
const isFileSizeValid = (file: File) => {
if (props.maxFileSize && file.size > props.maxFileSize) {
messageApi.open({
type: 'warning',
content: intl.formatMessage(
{ id: 'playground.uploadfile.sizeError' },
{ size: `${convertFileSize(props.maxFileSize)}` } // Convert bytes to MB
)
});
return false;
}
return true;
};
const handleOnChange = (data: { file: any; fileList: any }) => {
if (!isFileSizeValid(data.file)) {
return;
}
props.onChange?.(data);
};
return (
<Tooltip
overlayInnerStyle={{ maxWidth: 290, width: 'max-content' }}
@@ -48,12 +70,14 @@ const UploadAudio: React.FC<UploadAudioProps> = (props) => {
}}
>
<Button
icon={<UploadOutlined />}
size={size}
icon={icon ?? <UploadOutlined />}
type={props.type ?? 'text'}
shape="circle"
shape={shape}
></Button>
</div>
</Upload>
{contextHolder}
</Tooltip>
);
};
+1 -1
View File
@@ -38,7 +38,7 @@ declare namespace Global {
interface HintOptions {
label: string;
value: string;
opts?: Array<BaseOption<string>>;
opts?: Array<BaseOption<string | number>>;
}
type SearchParams = Pagination & { search?: string };
+2 -1
View File
@@ -27,7 +27,8 @@ const KeybindingsMap = {
NEW3: ['Ctrl+3', 'Meta+3'],
NEW4: ['Ctrl+4', 'Meta+4'],
FOCUS: ['/', '/'],
ADD: ['Alt+Ctrl+Enter', 'Alt+Meta+Enter']
ADD: ['Alt+Ctrl+Enter', 'Alt+Meta+Enter'],
ESC: ['Esc', 'Esc']
};
type KeyBindingType = keyof typeof KeybindingsMap;
+5 -2
View File
@@ -24,13 +24,15 @@ export default {
},
Tabs: {
titleFontSizeLG: 14
// cardBg: '#1D1E20'
},
DatePicker: {
fontSizeLG: 14
},
Menu: {
iconSize: 16,
iconMarginInlineEnd: 12,
itemBorderRadius: 4,
itemHeight: 44,
itemHeight: 32,
itemSelectedColor: '#007BFF',
darkItemSelectedBg: '#141414',
darkItemHoverBg: 'rgba(255, 255, 255, 0.03)',
@@ -69,6 +71,7 @@ export default {
}
},
token: {
darkMode: true,
fontFamily:
"Helvetica Neue, -apple-system, BlinkMacSystemFont, Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'",
colorText: '#ccc',
+1 -1
View File
@@ -6,5 +6,5 @@ export const colorPrimary = '#007BFF';
export default {
light,
dark,
colorPrimary: '#007BFF'
colorPrimary: colorPrimary
};
+5 -1
View File
@@ -25,12 +25,15 @@ export default {
Tabs: {
titleFontSizeLG: 14
},
DatePicker: {
fontSizeLG: 14
},
Menu: {
iconSize: 16,
iconMarginInlineEnd: 12,
itemBorderRadius: 4,
itemSelectedColor: '#007BFF',
itemHeight: 44,
itemHeight: 32,
groupTitleColor: 'rgba(0,0,0,1)',
itemHoverColor: 'rgba(0,0,0,1)',
itemColor: 'rgba(0,0,0,1)',
@@ -66,6 +69,7 @@ export default {
}
},
token: {
darkMode: false,
fontFamily:
"Helvetica Neue, -apple-system, BlinkMacSystemFont, Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'",
colorText: 'rgba(0,0,0,1)',
+39 -11
View File
@@ -72,7 +72,7 @@ html {
--color-green-fill-light: rgb(243 251 248);
--ant-rate-star-color: #fadb14;
--color-fill-spin-bg: rgba(255, 255, 255, 15%);
--width-tooltip-max: 300px;
--width-tooltip-max: 420px;
--color-bg-tooltip: '#fff';
--color-modal-content-bg: rgba(255, 255, 255, 90%);
--color-modal-box-shadow: 0 4px 16px rgba(0, 0, 0, 10%);
@@ -84,6 +84,7 @@ html {
--ant-color-border: #d9d9d9;
--ant-line-type: solid;
--ant-line-width: 1px;
--color-esc-hint-bg: rgba(0, 0, 0, 75%);
}
html[data-theme='realDark'] {
@@ -99,6 +100,7 @@ html[data-theme='realDark'] {
--color-modal-content-bg: #1f1f1f;
--color-modal-box-shadow: none;
--color-spotlight-bg: #3e3e3e;
--color-esc-hint-bg: rgba(150, 150, 150, 75%);
background: #141414;
@@ -250,7 +252,8 @@ body {
}
.ant-pro-sider .ant-layout-sider-children {
background-color: var(--color-fill-sider);
// background-color: var(--color-fill-sider);
border-right: 1px solid var(--ant-color-split);
}
}
@@ -275,6 +278,12 @@ body {
display: none;
}
.ant-pro-base-menu-vertical-item-title,
.ant-pro-base-menu-vertical-item-title-collapsed {
height: var(--ant-menu-item-height) !important;
line-height: var(--ant-menu-item-height) !important;
}
.ant-pro-layout {
height: 100vh;
@@ -284,6 +293,10 @@ body {
}
}
.ant-pro-sider-actions-list-collapsed {
margin-block-end: 0;
}
.ant-pro-page-container-children-container {
padding-block: var(--layout-content-blockpadding);
padding-inline: var(--layout-content-inlinepadding);
@@ -311,31 +324,37 @@ body {
.ant-menu {
.ant-menu-item {
color: var(--ant-color-text) !important;
// color: var(--ant-color-text) !important;
&:active {
background-color: var(--ant-menu-item-selected-bg);
// background-color: var(--ant-menu-item-selected-bg);
}
}
.ant-menu-item:not(.ant-menu-item-selected) {
color: var(--ant-color-text);
// color: var(--ant-color-text);
}
.ant-menu-item.ant-menu-item-selected {
color: var(--ant-color-primary) !important;
// color: var(--ant-color-primary) !important;
font-weight: 400;
}
.ant-menu-item-group-title.hide-submenu + .ant-menu-item-group-list {
display: none;
}
}
}
}
// ======== menu style end ============
.ant-menu-submenu-popup {
.ant-menu-sub {
.ant-menu-item-only-child {
height: 40px;
line-height: 40px;
color: var(--ant-color-text);
// color: var(--ant-color-text);
.anticon {
font-size: var(--font-size-middle);
@@ -366,7 +385,7 @@ body {
.ant-menu {
.ant-menu-submenu-title {
color: var(--ant-color-text) !important;
// color: var(--ant-color-text) !important;
}
}
@@ -374,7 +393,7 @@ body {
.user-avatar.ant-menu-submenu {
.ant-menu-submenu-title {
padding-left: 10px;
color: var(--ant-color-text) !important;
// color: var(--ant-color-text) !important;
}
}
@@ -383,7 +402,7 @@ body {
display: flex;
justify-content: center;
align-items: center;
color: var(--ant-color-text) !important;
// color: var(--ant-color-text) !important;
padding-inline: unset !important;
width: 100%;
margin-inline: 0 !important;
@@ -410,7 +429,7 @@ body {
display: flex;
justify-content: flex-start;
align-items: center;
color: var(--ant-color-text) !important;
// color: var(--ant-color-text) !important;
}
}
@@ -746,9 +765,18 @@ body {
min-width: 100px;
max-width: 360px;
&.scatter {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(48%, 1fr));
gap: 8px;
}
.tooltip-x-name {
font-size: var(--font-size-small);
color: var(--ant-color-text-tertiary);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.tooltip-item {
+54 -20
View File
@@ -1,3 +1,4 @@
import { convertFileSize } from '@/utils';
import { throttle } from 'lodash';
import qs from 'query-string';
import { useEffect, useRef } from 'react';
@@ -42,13 +43,19 @@ const useSetChunkFetch = () => {
const readTextEventStreamData = async (
response: Response,
callback: HandlerFunction,
delay = 200
delay = 100
) => {
class BufferManager {
private buffer: any[] = [];
private contentLength: number | null = null;
private progress: number = 0;
private percent: number = 0;
private speedHistory: number[] = [];
private maxHistory = 5;
private lastTime: number = performance.now();
private lastBytes: number = 0;
private totalBytes: number = 0;
private avgSpeed: number = 0;
constructor(private options: { contentLength?: string | null }) {
this.contentLength = options.contentLength
@@ -63,24 +70,49 @@ const useSetChunkFetch = () => {
}
}
private logSpeed(speedBps: number) {
this.speedHistory.push(speedBps);
if (this.speedHistory.length > this.maxHistory) {
this.speedHistory.shift();
}
this.avgSpeed =
this.speedHistory.reduce((a, b) => a + b, 0) /
this.speedHistory.length;
console.log(`瞬时均值: ${convertFileSize(this.avgSpeed)}/s`);
}
public updateSpeed(bytes: number) {
const now = performance.now();
const elapsed = (now - this.lastTime) / 1000;
if (elapsed > 0.3) {
const speed = (this.totalBytes + bytes - this.lastBytes) / elapsed;
this.logSpeed(speed);
this.lastTime = now;
this.lastBytes = this.totalBytes + bytes;
}
}
public add(data: any) {
this.buffer.push(data);
this.updateProgress(data);
}
public flush(done?: boolean) {
public async flush(done?: boolean) {
if (this.buffer.length > 0) {
const currentBuffer = [...this.buffer];
this.buffer = [];
currentBuffer.forEach((item, i) => {
const isComplete = i === currentBuffer.length - 1 && done;
callback(item, {
isComplete: isComplete || this.percent === 100,
percent: this.percent,
progress: this.progress,
contentLength: this.contentLength
while (this.buffer.length > 0) {
const item = this.buffer.shift()!;
const isComplete = this.buffer.length === 0 && done;
await new Promise<void>((resolve) => {
callback(item, {
isComplete: isComplete || this.percent === 100,
percent: this.percent,
progress: this.progress,
contentLength: this.contentLength
});
resolve();
});
});
}
}
}
@@ -98,8 +130,8 @@ const useSetChunkFetch = () => {
contentLength: contentLength
});
const throttledCallback = throttle(() => {
bufferManager.flush();
const throttledCallback = throttle(async () => {
await bufferManager.flush();
}, delay);
let isReading = true;
@@ -107,12 +139,6 @@ const useSetChunkFetch = () => {
while (isReading) {
const { done, value } = await reader.read();
if (done) {
isReading = false;
bufferManager.flush(done);
break;
}
try {
const chunk = decoder.decode(value, { stream: true });
bufferManager.add(chunk);
@@ -120,6 +146,14 @@ const useSetChunkFetch = () => {
} catch (error) {
// handle error
}
if (done) {
isReading = false;
await bufferManager.flush(done);
throttledCallback.cancel();
reader.releaseLock();
break;
}
}
};
+18
View File
@@ -0,0 +1,18 @@
import { useRequest } from 'ahooks';
export default function useDeferredRequest<P extends any[], R>(
requestFn: (...args: P) => Promise<R>,
delay = 100
) {
return useRequest(
async (...args: P) => {
await new Promise((resolve) => {
setTimeout(resolve, delay);
});
return await requestFn(...args);
},
{
manual: true
}
);
}
-26
View File
@@ -1,26 +0,0 @@
import { useIntl } from '@umijs/max';
import { Modal, message, type ModalFuncProps } from 'antd';
import { useRef } from 'react';
export default function useDeleteModal() {
const intl = useIntl();
const modalRef = useRef<any>();
const showDeleteModal = (config: ModalFuncProps = {}) => {
modalRef.current = Modal.confirm({
...config,
okText: intl.formatMessage({
id: 'common.button.delete'
}),
onCancel: () => {
config.onCancel?.();
modalRef.current.destroy?.();
},
onOk: async () => {
await config.onOk?.();
message.success(intl.formatMessage({ id: 'common.message.success' }));
}
});
};
return { showDeleteModal };
}
+106
View File
@@ -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 };
}
+16 -3
View File
@@ -6,8 +6,18 @@ import {
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';
@@ -34,6 +44,8 @@ export const overlaySollerOptions: UseOverlayScrollbarsParams = {
defer: true
};
const RESETSCROLLDELAY = 5000;
/**
*
* @param options.theme: if set theme, it will fix the theme
@@ -46,7 +58,7 @@ export default function useOverlayScroller(data?: {
}) {
const { userSettings } = useUserSettings();
const { options, events, defer = true } = data || {};
const { scrollbars, oppositeTheme } = options || {};
const { scrollbars, overflow, oppositeTheme } = options || {};
const scrollEventElement = React.useRef<any>(null);
const instanceRef = React.useRef<any>(null);
const initialized = React.useRef(false);
@@ -59,7 +71,8 @@ export default function useOverlayScroller(data?: {
debounce: 0
},
overflow: {
x: 'hidden'
x: 'hidden',
...overflow
},
scrollbars: {
autoHide: 'scroll',
@@ -151,7 +164,7 @@ export default function useOverlayScroller(data?: {
}
timerRef.current = setTimeout(() => {
stopUpdatePosition.current = false;
}, 1500);
}, RESETSCROLLDELAY);
}, []);
// add wheel event
+35 -7
View File
@@ -11,19 +11,17 @@ export function useCancelToken() {
const { source } = axiso.CancelToken;
const requestToken = useRef<any>(null);
const updateCancelToken = () => {
if (requestToken.current) {
requestToken.current.cancel();
}
requestToken.current = source();
};
const cancelRequest = () => {
if (requestToken.current) {
requestToken.current.cancel();
}
};
const updateCancelToken = () => {
cancelRequest();
requestToken.current = source();
};
const getCanceltToken = () => {
return requestToken.current.token;
};
@@ -38,3 +36,33 @@ export function useCancelToken() {
return { updateCancelToken, cancelRequest, getCanceltToken, source };
}
export function useAbortController() {
const controller = useRef<AbortController | null>(null);
const abortController = () => {
if (controller.current) {
controller.current.abort();
}
};
const getController = () => {
if (!controller.current) {
controller.current = new AbortController();
}
return controller.current;
};
const updateController = () => {
abortController();
controller.current = new AbortController();
};
useEffect(() => {
return () => {
abortController();
};
}, []);
return { getController, updateController, abortController, controller };
}
+69 -17
View File
@@ -7,28 +7,42 @@ import _ from 'lodash';
import qs from 'query-string';
import { useEffect, useRef, useState } from 'react';
export default function useTableFetch<ListItem>(options: {
API?: string;
watch?: boolean;
fetchAPI: (params: any) => Promise<Global.PageResponse<ListItem>>;
deleteAPI?: (id: number, params?: any) => Promise<any>;
contentForDelete?: string;
defaultData?: any[];
}) {
type EventsType = 'CREATE' | 'UPDATE' | 'DELETE' | 'INSERT';
type WatchConfig =
| { watch?: false | undefined; API?: string; polling?: boolean }
| { watch: true; API: string; polling?: false | undefined }
| { polling: true; watch: false | undefined; API?: string };
export default function useTableFetch<ListItem>(
options: {
fetchAPI: (params: any) => Promise<Global.PageResponse<ListItem>>;
deleteAPI?: (id: number, params?: any) => Promise<any>;
contentForDelete?: string;
defaultData?: any[];
events?: EventsType[];
} & WatchConfig
) {
const {
fetchAPI,
deleteAPI,
contentForDelete,
API,
polling = false,
watch,
defaultData = []
defaultData = [],
events = ['UPDATE', 'DELETE']
} = options;
const pollingRef = useRef<any>(null);
const chunkRequedtRef = useRef<any>(null);
const modalRef = useRef<any>(null);
const rowSelection = useTableRowSelection();
const { sortOrder, setSortOrder } = useTableSort({
defaultSortOrder: 'descend'
});
const [extraStatus, setExtraStatus] = useState<Record<string, any>>({
firstLoad: true
});
const [dataSource, setDataSource] = useState<{
dataList: ListItem[];
@@ -51,7 +65,7 @@ export default function useTableFetch<ListItem>(options: {
const { setChunkRequest } = useSetChunkRequest();
const { updateChunkedList, cacheDataListRef } = useUpdateChunkedList({
events: ['UPDATE', 'DELETE'],
events: events,
dataList: dataSource.dataList,
setDataList(list, opts?: any) {
setDataSource((pre) => {
@@ -67,6 +81,8 @@ export default function useTableFetch<ListItem>(options: {
}
});
const debounceSetExtraStatus = _.debounce(setExtraStatus, 3000);
const updateHandler = (list: any) => {
_.each(list, (data: any) => {
updateChunkedList(data);
@@ -88,12 +104,14 @@ export default function useTableFetch<ListItem>(options: {
}
};
const fetchData = async (params?: { query: any }) => {
const fetchData = async (params?: { query: any }, polling = false) => {
if (!polling) {
setDataSource((pre) => {
pre.loading = true;
return { ...pre };
});
}
const { query } = params || {};
setDataSource((pre) => {
pre.loading = true;
return { ...pre };
});
try {
const params = {
..._.pickBy(query || queryParams, (val: any) => !!val)
@@ -136,9 +154,31 @@ export default function useTableFetch<ListItem>(options: {
total: dataSource.total,
totalPage: dataSource.totalPage
});
} finally {
debounceSetExtraStatus({
firstLoad: false
});
}
};
const fetchAPIWithPolling = async (params: any) => {
if (!polling || watch || !fetchAPI) return;
if (pollingRef.current) {
clearInterval(pollingRef.current);
}
// fetch data with polling, 1s interval
pollingRef.current = setInterval(async () => {
fetchData(
{
query: params
},
true
);
}, 5000);
};
const handleQueryChange = (params: any) => {
setQueryParams({
...queryParams,
@@ -176,7 +216,7 @@ export default function useTableFetch<ListItem>(options: {
row: ListItem & { name: string; id: number },
options?: any
) => {
modalRef.current.show({
modalRef.current?.show({
content: contentForDelete,
operation: 'common.delete.single.confirm',
name: row.name,
@@ -192,7 +232,7 @@ export default function useTableFetch<ListItem>(options: {
};
const handleDeleteBatch = (options = {}) => {
modalRef.current.show({
modalRef.current?.show({
content: contentForDelete,
operation: 'common.delete.confirm',
selection: true,
@@ -216,6 +256,17 @@ export default function useTableFetch<ListItem>(options: {
});
};
useEffect(() => {
if (dataSource.loadend) {
fetchAPIWithPolling(queryParams);
}
return () => {
if (pollingRef.current) {
clearInterval(pollingRef.current);
}
};
}, [dataSource.loadend, queryParams]);
useEffect(() => {
const init = async () => {
await fetchData();
@@ -236,6 +287,7 @@ export default function useTableFetch<ListItem>(options: {
sortOrder,
queryParams,
modalRef,
extraStatus,
setQueryParams,
handleDelete,
handleDeleteBatch,
+5 -2
View File
@@ -16,14 +16,17 @@ interface ErrorResultProps {
function isChunkLoadError(msg?: string): boolean {
if (typeof msg !== 'string') return false;
return msg.includes('Loading chunk') && msg.includes('failed');
const jsChunkFailed = msg.includes('Loading chunk');
const cssChunkFailed = msg.includes('Loading CSS chunk');
return (jsChunkFailed || cssChunkFailed) && msg.includes('failed');
}
const ErrorResult: React.FC<ErrorResultProps> = ({ extra }) => {
const intl = useIntl();
return (
<Result
status="error"
status="warning"
title={
isChunkLoadError(extra)
? intl.formatMessage({ id: 'common.page.refresh.tips' })
+170 -106
View File
@@ -3,6 +3,7 @@
import { routeCacheAtom, setRouteCache } from '@/atoms/route-cache';
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
import DarkMask from '@/components/dark-mask';
import IconFont from '@/components/icon-font';
import ShortCuts, {
modalConfig as ShortCutsConfig
} from '@/components/short-cuts';
@@ -14,7 +15,6 @@ import useUserSettings from '@/hooks/use-user-settings';
import { logout } from '@/pages/login/apis';
import { useAccessMarkedRoutes } from '@@/plugin-access';
import { useModel } from '@@/plugin-model';
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
import { ProLayout } from '@ant-design/pro-components';
import {
Link,
@@ -28,17 +28,18 @@ import {
useNavigate,
type IRoute
} from '@umijs/max';
import { Button, ConfigProvider, Modal, theme } from 'antd';
import { Button, ConfigProvider, Modal, Tooltip, theme } from 'antd';
import 'driver.js/dist/driver.css';
import { useAtom } from 'jotai';
import 'overlayscrollbars/overlayscrollbars.css';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import Exception from './Exception';
import './Layout.css';
import { LogoIcon, SLogoIcon } from './Logo';
import ErrorBoundary from './error-boundary';
import { getRightRenderContent } from './rightRender';
import { patchRoutes } from './runtime';
import SiderMenu from './sider-menu';
const loginPath = '/login';
@@ -102,7 +103,8 @@ export default (props: any) => {
defer: false
});
const [modal, contextHolder] = Modal.useModal();
const { themeData, setTheme, userSettings, isDarkTheme } = useUserSettings();
const { themeData, setTheme, setUserSettings, userSettings, isDarkTheme } =
useUserSettings();
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const { initialize: initializeMenu } = useOverlayScroller();
const [userInfo] = useAtom(userAtom);
@@ -113,8 +115,9 @@ export default (props: any) => {
const navigate = useNavigate();
const intl = useIntl();
const { clientRoutes, pluginManager } = useAppData();
const [collapsed, setCollapsed] = useState(false);
// const [collapsed, setCollapsed] = useState(userSettings.collapsed || false);
const [collapseValue, setCollapseValue] = useState(false);
const [collapseKeys, setCollapseKeys] = useState<Set<string>>(new Set());
const initialInfo = (useModel && useModel('@@initialState')) || {
initialState: undefined,
@@ -187,7 +190,11 @@ export default (props: any) => {
const handleToggleCollapse = (e: any) => {
e.stopPropagation();
setCollapsed(!collapsed);
// setCollapsed(!collapsed);
setUserSettings({
...userSettings,
collapsed: !userSettings.collapsed
});
};
const newRoutes = filterRoutes(
@@ -203,6 +210,8 @@ export default (props: any) => {
const role = initialState?.currentUser?.is_admin ? 'admin' : 'user';
const [route] = useAccessMarkedRoutes(mapRoutes(newRoutes, role));
console.log('route============', route);
patchRoutes({
routes: route.children,
initialState: initialInfo.initialState
@@ -213,6 +222,24 @@ export default (props: any) => {
[location.pathname]
);
const allRouteKeys = useMemo(() => {
const keys = new Set<string>();
const childrenRoutes = route?.children || [];
const traverseRoutes = (routes) => {
routes.forEach((r) => {
if (r.path) {
keys.add(r.path);
}
if (r.children) {
traverseRoutes(r.children);
}
});
};
traverseRoutes(childrenRoutes);
return keys;
}, [route?.children]);
const showUpgrade = useMemo(() => {
return (
initialState?.currentUser?.is_admin &&
@@ -256,122 +283,159 @@ export default (props: any) => {
return () => clearTimeout(timeout);
}, [initializeMenu, matchedRoute, location]);
const renderMenuHeader = useCallback(
(logo, title) => {
return (
<>
{logo}
<div className="collapse-wrap" onClick={handleToggleCollapse}>
<Button
style={{ marginRight: collapsed ? 0 : -14 }}
size="small"
type={collapsed ? 'default' : 'text'}
>
<>
<MenuUnfoldOutlined
style={{ display: collapsed ? 'block' : 'none' }}
/>
<MenuFoldOutlined
style={{ display: !collapsed ? 'block' : 'none' }}
/>
</>
</Button>
</div>
</>
);
},
[collapsed]
);
const collapsed = useMemo(() => {
return userSettings.collapsed || false;
}, [userSettings.collapsed]);
const actionRender = useCallback(
(layoutProps) => {
const dom = getRightRenderContent({
runtimeConfig,
loading,
initialState,
setInitialState,
intl,
isDarkTheme: userSettings.isDarkTheme,
siderWidth: layoutProps.siderWidth,
collapsed: layoutProps.collapsed,
showUpgrade
});
const renderMenuHeader = (logo, title) => {
return (
<>
{logo}
<div className="collapse-wrap" onClick={handleToggleCollapse}>
<Button
style={{ marginRight: collapsed ? 0 : -14, border: 'none' }}
size="small"
type={collapsed ? 'default' : 'text'}
>
<>
<IconFont
type="icon-expand-left"
className="font-size-18 text-secondary"
style={{ display: collapsed ? 'block' : 'none' }}
/>
<IconFont
type="icon-expand-right"
className="font-size-18 text-secondary"
style={{ display: !collapsed ? 'block' : 'none' }}
/>
</>
</Button>
</div>
</>
);
};
return dom;
},
[intl, showUpgrade, userSettings.theme, userSettings.isDarkTheme]
);
const handleToggleGroup = (menuItemProps, e) => {
e.stopPropagation();
const itemRender = useCallback((route, _, routes) => {
const { breadcrumbName, title, path } = route;
const label = title || breadcrumbName;
const last = routes[routes.length - 1];
if (last) {
if (last.path === path || last.linkPath === path) {
return <span>{label}</span>;
}
if (collapseKeys.has(menuItemProps.key)) {
collapseKeys.delete(menuItemProps.key);
} else {
collapseKeys.add(menuItemProps.key);
}
return <Link to={path}>{label}</Link>;
}, []);
setCollapseKeys(new Set(collapseKeys));
};
const menuItemRender = useCallback(
(menuItemProps, defaultDom) => {
if (menuItemProps.isUrl || menuItemProps.children) {
return defaultDom;
}
if (menuItemProps.path && location.pathname !== menuItemProps.path) {
return (
const menuContentRender = (menuProps, defaultDom) => {
return <SiderMenu {...menuProps}></SiderMenu>;
};
const actionRender = (layoutProps) => {
console.log('actionRender', layoutProps);
const dom = getRightRenderContent({
runtimeConfig,
loading,
initialState,
setInitialState,
intl,
isDarkTheme: userSettings.isDarkTheme,
siderWidth: layoutProps.siderWidth,
collapsed: layoutProps.collapsed,
showUpgrade
});
return dom;
};
const menuItemRender = (menuItemProps, defaultDom) => {
console.log('defaultdom==========', menuItemProps, defaultDom);
if (menuItemProps.isUrl || menuItemProps.children) {
return defaultDom;
}
if (menuItemProps.path && location.pathname !== menuItemProps.path) {
return (
<Tooltip
title={collapsed ? menuItemProps.name : false}
placement="right"
>
<Link
to={menuItemProps.path.replace('/*', '')}
target={menuItemProps.target}
>
{defaultDom}
</Link>
</Tooltip>
);
}
return (
<Tooltip title={collapsed ? menuItemProps.name : false} placement="right">
{defaultDom}
</Tooltip>
);
};
const menuDataRender = (menuData) => {
const currentItem = menuData.find((s) => location.pathname === s.path);
const result = menuData.map((item) => {
const newItem = { ...item };
const selected =
location.pathname === newItem.path ||
location.pathname.indexOf(newItem.path) > -1;
if (newItem.icon) {
newItem.icon = selected ? (
<IconFont type={newItem.selectedIcon} />
) : (
<IconFont type={newItem.defaultIcon} />
);
}
return <>{defaultDom}</>;
},
[location.pathname]
);
const onPageChange = useCallback(
(route) => {
const { location } = history;
const { pathname } = location;
initRouteCacheValue(pathname);
dropRouteCache(pathname);
// if user is not change password, redirect to change password page
if (
location.pathname !== loginPath &&
userInfo?.require_password_change
) {
history.push(loginPath);
return;
if (newItem.children) {
newItem.children = menuDataRender(newItem.children);
}
return newItem;
});
// if user is not logged in, redirect to login page
if (!initialState?.currentUser && location.pathname !== loginPath) {
history.push(loginPath);
} else if (location.pathname === '/') {
const pathname = initialState?.currentUser?.is_admin
? '/dashboard'
: '/playground';
history.push(pathname);
}
},
[userInfo?.require_password_change, initialState?.currentUser]
);
return result;
};
const onMenuHeaderClick = useCallback((e) => {
const onPageChange = (route) => {
const { location } = history;
const { pathname } = location;
console.log('onPageChange', pathname, route);
initRouteCacheValue(pathname);
dropRouteCache(pathname);
// if user is not change password, redirect to change password page
if (location.pathname !== loginPath && userInfo?.require_password_change) {
history.push(loginPath);
return;
}
// if user is not logged in, redirect to login page
if (!initialState?.currentUser && location.pathname !== loginPath) {
history.push(loginPath);
} else if (location.pathname === '/') {
const pathname = initialState?.currentUser?.is_admin
? '/dashboard'
: '/playground';
history.push(pathname);
}
};
const onMenuHeaderClick = (e) => {
e.stopPropagation();
e.preventDefault();
navigate('/dashboard');
}, []);
};
const onCollapse = (value) => {
setCollapsed(value);
// setCollapsed(value);
setUserSettings({
...userSettings,
collapsed: value
});
};
useEffect(() => {
@@ -417,7 +481,6 @@ export default (props: any) => {
: theme.defaultAlgorithm,
...themeData
};
console.log('currentTheme====', data);
return data;
}, [userSettings.isDarkTheme, themeData]);
@@ -452,15 +515,16 @@ export default (props: any) => {
onCollapse={onCollapse}
onMenuHeaderClick={onMenuHeaderClick}
menuHeaderRender={renderMenuHeader}
collapsed={collapsed}
collapsed={userSettings.collapsed}
onPageChange={onPageChange}
formatMessage={formatMessage}
menu={{
locale: true
locale: true,
type: 'group'
}}
splitMenus={true}
logo={collapsed ? SLogoIcon : LogoIcon}
menuItemRender={menuItemRender}
itemRender={itemRender}
menuContentRender={menuContentRender}
disableContentMargin
fixSiderbar
fixedHeader
+40 -37
View File
@@ -6,12 +6,11 @@ import langConfigMap from '@/locales/lang-config-map';
import {
DiscordOutlined,
GithubOutlined,
GlobalOutlined,
HomeOutlined,
InfoCircleOutlined,
LogoutOutlined,
MoonOutlined,
QuestionCircleOutlined,
MoreOutlined,
ReadOutlined,
SettingOutlined,
SunOutlined
@@ -21,23 +20,14 @@ import { Avatar, Menu, Spin } from 'antd';
import _ from 'lodash';
import React from 'react';
const themeConfig = [
{
key: 'realDark',
label: 'common.appearance.dark',
icon: <MoonOutlined />
},
{
key: 'light',
label: 'common.appearance.light',
icon: <SunOutlined />
}
// {
// key: 'auto',
// label: 'common.appearance.system',
// icon: <IconFont type="icon-theme-auto" />
// }
];
const getMenuStyle = (
collapsed: boolean,
siderWidth: number,
extraStyle: React.CSSProperties = {}
) => ({
width: collapsed ? 40 : `calc(${siderWidth}px - 16px)`,
...extraStyle
});
export const getRightRenderContent = (opts: {
runtimeConfig: any;
@@ -140,7 +130,7 @@ export const getRightRenderContent = (opts: {
items: [
{
key: 'help',
icon: <QuestionCircleOutlined />,
icon: <IconFont type="icon-help" />,
label: (
<span className="sub-title ">
<span className="flex-center">
@@ -213,7 +203,7 @@ export const getRightRenderContent = (opts: {
items: [
{
key: 'lang',
icon: <GlobalOutlined />,
icon: <IconFont type="icon-language" />,
label: (
<span className="sub-title">
{intl?.formatMessage?.({ id: 'common.settings.language' })}
@@ -265,7 +255,7 @@ export const getRightRenderContent = (opts: {
? 'user-menu-container user-menu-collapsed'
: 'user-menu-container',
mode: 'vertical',
expandIcon: false,
expandIcon: collapsed ? false : <MoreOutlined />,
// inlineCollapsed: collapsed,
triggerSubMenuAction: 'hover',
items: [
@@ -274,7 +264,7 @@ export const getRightRenderContent = (opts: {
key: 'user',
className: 'user-avatar',
icon: (
<span>
<span className="avatar-container">
<Avatar
size={28}
style={{
@@ -287,7 +277,13 @@ export const getRightRenderContent = (opts: {
{!collapsed && (
<span
className="m-l-8 font-size-14"
style={{ fontWeight: 'var(--font-weight-normal)' }}
style={{
fontWeight: 'var(--font-weight-normal)',
maxWidth: 100,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
{opts.initialState?.currentUser?.username}
</span>
@@ -295,6 +291,20 @@ export const getRightRenderContent = (opts: {
</span>
),
children: [
{
key: 'apikeys',
label: (
<span className="flex flex-center">
<IconFont type="icon-key" />
<span className="m-l-8" style={{ marginLeft: 8 }}>
{intl?.formatMessage?.({ id: 'menu.apikeys' })}
</span>
</span>
),
onClick: () => {
history.push('/api-keys');
}
},
{
key: 'settings',
label: (
@@ -328,23 +338,16 @@ export const getRightRenderContent = (opts: {
]
};
const getMenuStyle = (
collapsed: boolean,
siderWidth: number,
extraStyle: React.CSSProperties = {}
) => ({
width: collapsed ? 40 : `calc(${siderWidth}px - 16px)`,
...extraStyle
});
return (
<div>
<>
<Menu {...helpMenu} style={getMenuStyle(collapsed, siderWidth)} />
<Menu {...langMenu} style={getMenuStyle(collapsed, siderWidth)} />
<Menu
{...userMenu}
style={getMenuStyle(collapsed, siderWidth, { marginTop: 20 })}
style={getMenuStyle(collapsed, siderWidth, {
marginTop: 8,
marginBottom: 0
})}
/>
</div>
</>
);
};
+248
View File
@@ -0,0 +1,248 @@
import IconFont from '@/components/icon-font';
import { CaretDownOutlined } from '@ant-design/icons';
import { Link, useLocation } from '@umijs/max';
import { Divider, Tooltip } from 'antd';
import { createStyles } from 'antd-style';
import React, { useMemo, useState } from 'react';
interface MenuItem {
icon?: string;
selectedIcon?: string;
defaultIcon?: string;
children?: MenuItem[];
[key: string]: any;
}
interface SiderMenuProps {
menuData: MenuItem[];
collapsed?: boolean;
}
const useStyles = createStyles(({ css, token }) => {
console.log('useStyles', token);
// @ts-ignore
const { Menu } = token;
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 {
color: var(--ant-color-text);
}
}
.anticon {
transform: scale(0.8);
}
.group-title-text {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--ant-color-text-tertiary);
font-weight: 400;
}
&.menu-item-group-title-collapsed {
height: 1px;
padding-block: 0;
padding-inline: 0;
}
`,
menuItemContent: css`
margin: 4px;
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-secondary);
&: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;
}
`
};
});
const SiderMenu: React.FC<SiderMenuProps> = (props) => {
const { menuData, collapsed } = props;
const { styles, cx } = useStyles();
const location = useLocation();
const [collapseKeys, setCollapseKeys] = useState<Set<string>>(new Set());
console.log('SiderMenu', props);
const dividerStyles = useMemo(() => {
if (collapsed) {
return {
margin: '6px 0'
};
}
return {
margin: '6px 16px',
width: 'unset',
minWidth: 'unset',
maxWidth: 'unset'
};
}, [collapsed]);
const handleToggleGroup = (e: any, menuGroup: any) => {
e.stopPropagation();
console.log('handleToggleGroup', menuGroup.key);
if (collapseKeys.has(menuGroup.key)) {
collapseKeys.delete(menuGroup.key);
} else {
collapseKeys.add(menuGroup.key);
}
setCollapseKeys(new Set(collapseKeys));
};
const menuItemRender = (menuItem: MenuItem, key: string) => {
return (
<div
className={cx(styles.menuItemContent, 'menu-item-content')}
key={key}
>
<Link
to={menuItem.path.replace('/*', '')}
target={menuItem.target}
className={cx(styles.menuItemWrapper, 'menu-item', {
'menu-item-selected': location.pathname === menuItem.path
})}
>
{collapsed ? (
<Tooltip title={menuItem.name} placement="right">
<span className="icon-wrapper">
<IconFont
type={
location.pathname === menuItem.path
? menuItem.selectedIcon || ''
: menuItem.defaultIcon || ''
}
></IconFont>
</span>
</Tooltip>
) : (
<>
<IconFont
type={
location.pathname === menuItem.path
? menuItem.selectedIcon || ''
: menuItem.defaultIcon || ''
}
></IconFont>
<span>{menuItem.name}</span>
</>
)}
</Link>
</div>
);
};
return (
<div
className={cx(styles.siderMenu, 'sider-menu', {
'sider-menu-collapsed': collapsed
})}
>
{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>
) : (
<Divider style={dividerStyles} />
)}
</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>
))}
</div>
);
};
export default SiderMenu;
+11 -7
View File
@@ -97,7 +97,7 @@ export default {
'common.form.field.input.required': 'required',
'common.form.field.select.required': 'required',
'common.select.option': 'All',
'common.checbox.all': 'All',
'common.checbox.all': 'Select all',
'common.select.all': 'All {type}',
'common.data.unkonwn': 'Unknown',
'common.data.none': 'No Data',
@@ -105,7 +105,7 @@ export default {
'common.button.addfile': 'Add File',
'common.logout.msg': 'Log out succeeded',
'common.button.logout': 'Log out',
'common.form.rule.select': 'please select {name}',
'common.form.rule.select': 'Please select a {name}',
'common.form.rule.input': '{name} is required',
'common.form.key': 'the same key exists',
'common.date.utc': 'UTC time',
@@ -194,13 +194,13 @@ export default {
'common.delete.confirm':
'Are you sure you want to delete the selected {type}?',
'common.delete.single.confirm':
'Are you sure you want to delete <span style="font-size: 13px;font-weight: 700">{name}</span>?',
'Are you sure you want to delete? \n <span style="font-size: 13px;font-weight: 700">{name}</span>',
'common.stop.confirm': 'Are you sure you want to stop the selected {type}?',
'common.stop.single.confirm':
'Are you sure you want to stop <span style="font-size: 13px;font-weight: 700">{name}</span>?',
'Are you sure you want to stop? \n <span style="font-size: 13px;font-weight: 700">{name}</span>',
'common.start.confirm': 'Are you sure you want to start the selected {type}?',
'common.start.single.confirm':
'Are you sure you want to start <span style="font-size: 13px;font-weight: 700">{name}</span>?',
'Are you sure you want to start? \n <span style="font-size: 13px;font-weight: 700">{name}</span>',
'common.filter.name': 'Filter by name',
'common.form.password': 'Password',
'common.form.username': 'Username',
@@ -210,7 +210,7 @@ export default {
'common.button.feedback': 'Feedback',
'common.button.docs': 'Documentation',
'common.button.version': 'Version',
'common.title.delete.confirm': 'Confirm delete',
'common.title.delete.confirm': 'Confirm deletion',
'common.title.stop.confirm': 'Confirm stop',
'common.title.start.confirm': 'Confirm start',
'common.title.recreate.confirm': 'Confirm recreate',
@@ -247,5 +247,9 @@ export default {
'common.appearance.theme': 'Theme',
'common.page.wentwrong': 'Something went wrong.',
'common.page.refresh.tips':
'Oops! Something went wrong. Try refreshing the page.'
'Oops! Something went wrong. Try refreshing the page.',
'common.tips.escape.disable':
'Click Cancel or the X at the top right to close.',
'common.button.clearSelection': 'Clear Selection',
'common.select.count': '{count} selected'
};
+10 -1
View File
@@ -21,5 +21,14 @@ export default {
'dashboard.activeModels': 'Active Models',
'dashboard.runninginstances': 'Running Instances',
'dashboard.activeModels.name': 'Model Name',
'dashboard.allocatevram': 'Allocated VRAM / RAM'
'dashboard.allocatevram': 'Allocated VRAM / RAM',
'dashboard.usage.selectuser': 'Select users',
'dashboard.usage.selectmodel': 'Select models',
'dashboard.usage.export': 'Export Data',
'dashboard.usage.export.user': 'User',
'dashboard.usage.export.model': 'Model',
'dashboard.usage.export.date': 'Date',
'dashboard.usage.datePicker.last7days': 'Last 7 Days',
'dashboard.usage.datePicker.last30days': 'Last 30 Days',
'dashboard.usage.datePicker.last60days': 'Last 60 Days'
};
+7
View File
@@ -11,10 +11,17 @@ export default {
'menu.models.modelList': 'Deploy & Manage',
'menu.models.modelCatalog': 'Catalog',
'menu.models.catalog': 'Model Catalog',
'menu.models.deployment': 'Deployments',
'menu.modelCatalog': 'Catalog',
'menu.resources': 'Resources',
'menu.apikeys': 'API Keys',
'menu.users': 'Users',
'menu.resources.workers': 'Workers',
'menu.resources.gpus': 'GPUs',
'menu.resources.modelfiles': 'Model Files',
'menu.accessControl': 'Access Control',
'menu.accessControl.apikeys': 'API Keys',
'menu.accessControl.users': 'Users',
'menu.profile': 'Profile',
'menu.login': 'Login',
'menu.usage': 'Usage',
+6 -4
View File
@@ -85,7 +85,7 @@ export default {
'models.form.backend.llamabox':
'For GGUF format models, supports Linux, macOS, and Windows.',
'models.form.backend.vllm':
'For non-GGUF format models, supported only on Linux (amd64/x86_64).',
'For non-GGUF format models, supported only on Linux.',
'models.form.backend.voxbox':
'For non-GGUF format audio models, supported only on NVIDIA GPUs and CPUs.',
'models.form.backend.mindie':
@@ -133,9 +133,11 @@ export default {
'models.form.check.params': 'Checking configuration...',
'models.form.check.passed': 'Compatibility Check Passed',
'models.form.check.claims':
'The model requires approximately {vram} VRAM and {ram} RAM.',
'models.form.check.claims2': 'The model requires approximately {vram} VRAM.',
'models.form.check.claims3': 'The model requires approximately {ram} RAM.',
'The model will consume approximately {vram} VRAM and {ram} RAM.',
'models.form.check.claims2':
'The model will consume approximately {vram} VRAM.',
'models.form.check.claims3':
'The model will consume approximately {ram} RAM.',
'models.form.update.tips':
'Changes will only apply after you delete and recreate the instance.',
'models.table.download.progress': 'Download Progress',
+8 -3
View File
@@ -21,7 +21,7 @@ export default {
'playground.params.temperature.tips':
'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive.',
'playground.params.maxtokens.tips':
"The maximum number of tokens to generated. The total length of input tokens and generated tokens is limited by the model's context length.",
"The maximum number of tokens to generate. The total length of input tokens and generated tokens is limited by the model's context length.",
'playground.params.topp.tips':
'Controls diversity via nucleus sampling: 0.5 means half of all likelihood-weighted options are considered.',
'playground.params.seed.tips':
@@ -90,7 +90,7 @@ export default {
'playground.audio.stoprecord': 'Stop Recording',
'playground.audio.generating.tips': 'Generated text will appear here.',
'playground.audio.uploadfile.tips':
'Please upload an audio file, supported formats: {formats}',
'Upload an audio file, supported formats: {formats}',
'playground.input.multiplePaste': 'Batch Input Mode',
'playground.input.multiplePaste.tips':
'When enabled, pasted multi-line text will be automatically split by newline into separate entries in the form.',
@@ -151,5 +151,10 @@ export default {
'playground.image.negativeMask.tips':
'1. After selection, no further masking can be drawn; therefore, you should draw the mask first and then check the option.\n 2. Once a mask image is uploaded, no further masks can be generated.',
'playground.model.noavailable.tips2':
'If the expected model isnt showing up, make sure its running and correctly categorized. If the category is incorrect, you can manually adjust it in the models settings.'
'If the expected model isnt showing up, make sure its running and correctly categorized. If the category is incorrect, you can manually adjust it in the models settings.',
'playground.rerank.query.validate': 'The query is required.',
'playground.image.generate.error':
'Something went wrong. The image could not be generated.',
'playground.uploadfile.sizeError':
'File size exceeds the limit. Maximum allowed: {size}.'
};
+21 -2
View File
@@ -70,7 +70,26 @@ export default {
'Waiting for the download to complete...',
'resources.filter.worker': 'Filter by worker',
'resources.filter.source': 'Filter by Source',
'resources.modelfiles.delete.tips': 'Also delete the file from disk!',
'resources.modelfiles.delete.tips': 'Also delete the file from disk',
'resources.modelfiles.copy.tips': 'Copy Full Path',
'resources.filter.path': 'Filter by path'
'resources.filter.path': 'Filter by path',
'resources.register.worker.step1':
'Click the <span class="bold-text">Copy Token</span> menu in the application.',
'resources.register.worker.step2':
'Click the <span class="bold-text">Quick Config</span> menu in the application.',
'resources.register.worker.step3':
'Click the <span class="bold-text">General</span> tab.',
'resources.register.worker.step4':
'Select <span class="bold-text">Worker</span> as the service role.',
'resources.register.worker.step5':
'Enter the <span class="bold-text">Server URL</span>: {url}.',
'resources.register.worker.step6':
'Paste the <span class="bold-text">Token</span>.',
'resources.register.worker.step7':
'Click <span class="bold-text">Restart</span> to apply the settings.',
'resources.register.install.title': 'Install GPUStack on {os}',
'resources.register.download':
'Download and install the <a href={url} target="_blank">installer</a>. Only supported: {versions}.',
'resource.register.maos.support': 'Apple Silicon (M series), macOS 14+',
'resource.register.windows.support': 'win 10, win 11'
};
+2 -1
View File
@@ -27,5 +27,6 @@ export default {
'users.password.confirm.error': 'The two passwords entered do not match.',
'users.login.title': 'Log in to',
'users.version.islatest': 'GPUStack {version} is the latest version',
'users.version.update': 'GPUStack {version} is available'
'users.version.update': 'GPUStack {version} is available',
'users.settings.title': 'User Settings'
};
+8 -1
View File
@@ -247,7 +247,11 @@ export default {
'common.appearance.theme': 'Theme',
'common.page.wentwrong': 'Something went wrong.',
'common.page.refresh.tips':
'Oops! Something went wrong. Try refreshing the page.'
'Oops! Something went wrong. Try refreshing the page.',
'common.tips.escape.disable':
'Click Cancel or the X at the top right to close.',
'common.button.clearSelection': 'Clear Selection',
'common.select.count': '{count} selected'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -263,4 +267,7 @@ export default {
// 10. 'common.appearance.theme': 'Theme',
// 11. 'common.page.wentwrong': 'Something went wrong.',
// 12. 'common.page.refresh.tips': 'Oops! Something went wrong. Try refreshing the page.'
// 13. 'common.tips.escape.disable': 'Click Cancel or the X at the top right to close.'
// 14. 'common.button.clearSelection': 'Clear Selection',
// 15. 'common.select.count': '{count} selected'
// ========== End of To-Do List ==========
+22 -1
View File
@@ -21,5 +21,26 @@ export default {
'dashboard.activeModels': 'アクティブなモデル',
'dashboard.runninginstances': '稼働中のインスタンス',
'dashboard.activeModels.name': 'モデル名',
'dashboard.allocatevram': '割り当て済みVRAM / メモリ'
'dashboard.allocatevram': '割り当て済みVRAM / メモリ',
'dashboard.usage.selectuser': 'Select users',
'dashboard.usage.selectmodel': 'Select models',
'dashboard.usage.export': 'Export Data',
'dashboard.usage.export.user': 'User',
'dashboard.usage.export.model': 'Model',
'dashboard.usage.export.date': 'Date',
'dashboard.usage.datePicker.last7days': 'Last 7 Days',
'dashboard.usage.datePicker.last30days': 'Last 30 Days',
'dashboard.usage.datePicker.last60days': 'Last 60 Days'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'dashboard.usage.selectuser': 'Select users',
// 2. 'dashboard.usage.selectmodel': 'Select models',
// 3. 'dashboard.usage.export': 'Export Data',
// 4. 'dashboard.usage.export.user': 'User',
// 5. 'dashboard.usage.export.model': 'Model',
// 6. 'dashboard.usage.export.date': 'Date',
// 7. 'dashboard.usage.datePicker.last7days': 'Last 7 Days',
// 8.'dashboard.usage.datePicker.last30days': 'Last 30 Days',
// 9. 'dashboard.usage.datePicker.last60days': 'Last 60 Days'
// ========== End of To-Do List ==========
+18 -1
View File
@@ -11,6 +11,7 @@ export default {
'menu.models.modelList': 'デプロイと管理',
'menu.models.modelCatalog': 'カタログ',
'menu.models.catalog': 'モデルカタログ',
'menu.models.deployment': 'Deployment',
'menu.modelCatalog': 'カタログ',
'menu.resources': 'リソース',
'menu.apikeys': 'APIキー',
@@ -18,5 +19,21 @@ export default {
'menu.profile': 'プロフィール',
'menu.login': 'ログイン',
'menu.usage': '使用状況',
'menu.404': '404'
'menu.404': '404',
'menu.resources.workers': 'Workers',
'menu.resources.gpus': 'GPUs',
'menu.resources.modelfiles': 'Model Files',
'menu.accessControl': 'Access Control',
'menu.accessControl.apikeys': 'API Keys',
'menu.accessControl.users': 'Users'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'menu.models.deployment': 'Deployment',
// 2. 'menu.resources.workers': 'Workers',
// 3. 'menu.resources.gpus': 'GPUs',
// 4. 'menu.resources.modelfiles': 'Model Files',
// 5. 'menu.accessControl': 'Access Control',
// 6. 'menu.accessControl.apikeys': 'API Keys',
// 7. 'menu.accessControl.users': 'Users',
// ========== End of To-Do List ==========
+1 -2
View File
@@ -83,8 +83,7 @@ export default {
'models.form.gpuselector': 'GPUセレクター',
'models.form.backend.llamabox':
'GGUF形式のモデル用(Linux、macOS、Windowsをサポート)。',
'models.form.backend.vllm':
'非GGUF形式のモデル用。Linuxamd64/x86_64)のみ対応。',
'models.form.backend.vllm': '非GGUF形式のモデル用。Linux のみ対応。',
'models.form.backend.voxbox':
'非GGUF形式の音声モデル用。NVIDIA GPUおよびCPUのみ対応。',
'models.form.backend.mindie':
+12 -1
View File
@@ -154,5 +154,16 @@ export default {
'playground.image.negativeMask.tips':
'1. 選択後は追加のマスクを描画できません。そのため、最初にマスクを描画してからオプションを選択してください。\n 2. マスク画像をアップロードすると、追加のマスクを生成することはできません。',
'playground.model.noavailable.tips2':
'期待するモデルが表示されない場合は、モデルが実行中で正しく分類されていることを確認してください。カテゴリが間違っている場合は、モデルの設定で手動で調整できます。'
'期待するモデルが表示されない場合は、モデルが実行中で正しく分類されていることを確認してください。カテゴリが間違っている場合は、モデルの設定で手動で調整できます。',
'playground.rerank.query.validate': 'The query is required.',
'playground.image.generate.error':
'Something went wrong. The image could not be generated.',
'playground.uploadfile.sizeError':
'File size exceeds the limit. Maximum allowed: {size}.'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'playground.rerank.query.validate': 'The query is required.'
// 2. 'playground.image.generate.error': 'Something went wrong. The image could not be generated.',
// 3. 'playground.uploadfile.sizeError': 'File size exceeds the limit. Maximum allowed: {size}.'
// ========== End of To-Do List ==========
+35 -2
View File
@@ -71,7 +71,40 @@ export default {
'ダウンロード完了を待っています...',
'resources.filter.worker': 'ワーカーでフィルタ',
'resources.filter.source': 'ソースでフィルタ',
'resources.modelfiles.delete.tips': 'ディスクからファイルも削除します',
'resources.modelfiles.delete.tips': 'ディスクからファイルも削除します',
'resources.modelfiles.copy.tips': 'フルパスをコピー',
'resources.filter.path': 'パスでフィルタ'
'resources.filter.path': 'パスでフィルタ',
'resources.register.worker.step1':
'Click the <span class="bold-text">Copy Token</span> menu in the application.',
'resources.register.worker.step2':
'Click the <span class="bold-text">Quick Config</span> menu in the application.',
'resources.register.worker.step3':
'Click the <span class="bold-text">General</span> tab.',
'resources.register.worker.step4':
'Select <span class="bold-text">Worker</span> as the service role.',
'resources.register.worker.step5':
'Enter the <span class="bold-text">Server URL</span>: {url}.',
'resources.register.worker.step6':
'Paste the <span class="bold-text">Token</span>.',
'resources.register.worker.step7':
'Click <span class="bold-text">Restart</span> to apply the settings.',
'resources.register.install.title': 'Install GPUStack on {os}',
'resources.register.download':
'Download and install the <a href={url} target="_blank">installer</a>. Only supported: {versions}.',
'resource.register.maos.support': 'Apple Silicon (M series), macOS 14+',
'resource.register.windows.support': 'win 10, win 11'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'resources.register.worker.step1': 'Click the <span class="bold-text">Copy Token</span> menu in the application.',
// 2. 'resources.register.worker.step2': 'Click the <span class="bold-text">Quick Config</span> menu in the application.',
// 3. 'resources.register.worker.step3': 'Click the <span class="bold-text">General</span> tab.',
// 4. 'resources.register.worker.step4': 'Select <span class="bold-text">Worker</span> as the service role.',
// 5. 'resources.register.worker.step5': 'Enter the <span class="bold-text">Server URL</span>: {url}.',
// 6. 'resources.register.worker.step6': 'Paste the <span class="bold-text">Token</span>.',
// 7. 'resources.register.worker.step7': 'Click <span class="bold-text">Restart</span> to apply the settings.',
// 8. 'resources.register.install.title': 'Install GPUStack on {os}',
// 9. 'resources.register.download':'Download and install the <a>installer</a>. Only supported: {versions}.',
// 10. 'resource.register.maos.support': 'Apple Silicon (M series), macOS 14+',
// 11. 'resource.register.windows.support': 'win 10, win 11'
// ========== End of To-Do List ==========
+6 -1
View File
@@ -27,5 +27,10 @@ export default {
'users.password.confirm.error': '入力された2つのパスワードが一致しません。',
'users.login.title': 'ログイン',
'users.version.islatest': 'GPUStack {version} は最新バージョンです',
'users.version.update': 'GPUStack {version} が利用可能です'
'users.version.update': 'GPUStack {version} が利用可能です',
'users.settings.title': 'User Settings'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'users.settings.title': 'User Settings'
// ========== End of To-Do List ==========
+6 -1
View File
@@ -245,7 +245,12 @@ export default {
'common.button.forgotpassword': 'Забыли пароль?',
'common.appearance.theme': 'Тема',
'common.page.wentwrong': 'Что-то пошло не так.',
'common.page.refresh.tips': 'Упс! Что-то пошло не так. Попробуйте обновить страницу.'
'common.page.refresh.tips':
'Упс! Что-то пошло не так. Попробуйте обновить страницу.',
'common.tips.escape.disable':
'Чтобы закрыть, нажмите "Отмена" или крестик (X) в правом верхнем углу.',
'common.button.clearSelection': 'Сбросить выбор',
'common.select.count': '{count} Выбрано'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
+16 -1
View File
@@ -21,5 +21,20 @@ export default {
'dashboard.activeModels': 'Активные модели',
'dashboard.runninginstances': 'Запущенные инстансы',
'dashboard.activeModels.name': 'Название модели',
'dashboard.allocatevram': 'Выделено VRAM / ОЗУ'
'dashboard.allocatevram': 'Выделено VRAM / ОЗУ',
'dashboard.usage.selectuser': 'Выбрать пользователей',
'dashboard.usage.selectmodel': 'Выбрать модели',
'dashboard.usage.export': 'Экспорт данных',
'dashboard.usage.export.user': 'Пользователь',
'dashboard.usage.export.model': 'Модель',
'dashboard.usage.export.date': 'Дата',
'dashboard.usage.datePicker.last7days': 'Last 7 Days',
'dashboard.usage.datePicker.last30days': 'Last 30 Days',
'dashboard.usage.datePicker.last60days': 'Last 60 Days'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'dashboard.usage.datePicker.last7days': 'Last 7 Days',
// 2. 'dashboard.usage.datePicker.last30days': 'Last 30 Days',
// 3. 'dashboard.usage.datePicker.last60days': 'Last 60 Days'
// ========== End of To-Do List ==========
+18 -1
View File
@@ -11,6 +11,7 @@ export default {
'menu.models.modelList': 'Развертывание и управление',
'menu.models.modelCatalog': 'Каталог',
'menu.models.catalog': 'Каталог моделей',
'menu.models.deployment': 'Deployment',
'menu.modelCatalog': 'Каталог',
'menu.resources': 'Ресурсы',
'menu.apikeys': 'API-ключи',
@@ -18,5 +19,21 @@ export default {
'menu.profile': 'Профиль',
'menu.login': 'Авторизация',
'menu.usage': 'Использование',
'menu.404': '404'
'menu.404': '404',
'menu.resources.workers': 'Workers',
'menu.resources.gpus': 'GPUs',
'menu.resources.modelfiles': 'Model Files',
'menu.accessControl': 'Access Control',
'menu.accessControl.apikeys': 'API Keys',
'menu.accessControl.users': 'Users'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'menu.models.deployment': 'Deployment',
// 2. 'menu.resources.workers': 'Workers',
// 3. 'menu.resources.gpus': 'GPUs',
// 4. 'menu.resources.modelfiles': 'Model Files',
// 5. 'menu.accessControl': 'Access Control',
// 6. 'menu.accessControl.apikeys': 'API Keys',
// 7. 'menu.accessControl.users': 'Users',
// ========== End of To-Do List ==========
+1 -1
View File
@@ -85,7 +85,7 @@ export default {
'models.form.backend.llamabox':
'Для моделей формата GGUF. Поддержка Linux, macOS и Windows.',
'models.form.backend.vllm':
'Для моделей не-GGUF формата, поддерживается только в Linux (amd64/x86_64).',
'Для моделей не-GGUF формата, поддерживается только в Linux.',
'models.form.backend.voxbox':
'Для аудиомоделей не-GGUF формата, поддерживается только на GPU NVIDIA и CPU.',
'models.form.backend.mindie':
+14 -4
View File
@@ -139,15 +139,25 @@ export default {
'playground.chat.aithought': 'Рассуждение (CoT)',
'playground.chat.thinking': 'Рассуждение...',
'playground.image.mask.uploaded': 'Маска загружена',
'playground.image.mask.upload': 'Маска загрузки: Дополнительное редактирование запрещено после загрузки.',
'playground.image.mask.upload':
'Маска загрузки: Дополнительное редактирование запрещено после загрузки.',
'playground.params.frequency_penalty.tips': `Число от -2.0 до 2.0. Положительные значения снижают вероятность повторения токенов, уже часто встречающихся в тексте, уменьшая склонность модели дословно повторять одни и те же фразы.`,
'playground.params.presence_penalty.tips': `Число от -2.0 до 2.0. Положительные значения снижают вероятность повторения любых токенов, присутствующих в тексте, повышая склонность модели к обсуждению новых тем.`,
'playground.image.origin': 'Оригинал',
'playground.image.mask': 'Маска',
'playground.image.negativeMask.tips': '1. После выделения области создание маски станет недоступным; поэтому сначала нарисуйте маску, а затем активируйте опцию. \n 2. После загрузки изображения маски создание новых масок невозможно.',
'playground.model.noavailable.tips2': 'Если нужная модель не отображается, убедитесь, что она запущена и ей присвоена правильная категория. Если категория указана неверно, её можно изменить вручную в настройках модели.' // Translated
'playground.image.negativeMask.tips':
'1. После выделения области создание маски станет недоступным; поэтому сначала нарисуйте маску, а затем активируйте опцию. \n 2. После загрузки изображения маски создание новых масок невозможно.',
'playground.model.noavailable.tips2':
'Если нужная модель не отображается, убедитесь, что она запущена и ей присвоена правильная категория. Если категория указана неверно, её можно изменить вручную в настройках модели.',
'playground.rerank.query.validate': 'The query is required.',
'playground.image.generate.error':
'Something went wrong. The image could not be generated.',
'playground.uploadfile.sizeError':
'File size exceeds the limit. Maximum allowed: {size}.'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'playground.rerank.query.validate': 'The query is required.'
// 2. 'playground.image.generate.error': 'Something went wrong. The image could not be generated.',
// 3. 'playground.uploadfile.sizeError': 'File size exceeds the limit. Maximum allowed: {size}.'
// ========== End of To-Do List ==========
+21 -2
View File
@@ -69,9 +69,28 @@ export default {
'resources.modelfiles.storagePath.holder': 'Ожидание завершения загрузки...',
'resources.filter.worker': 'Фильтровать по узлу',
'resources.filter.source': 'Фильтровать по источнику',
'resources.modelfiles.delete.tips': 'Также удалить файл с диска!',
'resources.modelfiles.delete.tips': 'Также удалить файл с диска',
'resources.modelfiles.copy.tips': 'Скопировать полный путь',
'resources.filter.path': 'Фильтрация по пути'
'resources.filter.path': 'Фильтрация по пути',
'resources.register.worker.step1':
'В меню выберите <span class="bold-text">Скопировать токен</span>.',
'resources.register.worker.step2':
'В меню выберите <span class="bold-text">Быстрая настройка</span>.',
'resources.register.worker.step3':
'Перейдите на вкладку <span class="bold-text">Общие</span>.',
'resources.register.worker.step4':
'Выберите роль сервиса: <span class="bold-text">Воркер</span>.',
'resources.register.worker.step5':
'Введите <span class="bold-text">URL сервера</span>: {url}.',
'resources.register.worker.step6':
'Вставьте <span class="bold-text">Токен</span>.',
'resources.register.worker.step7':
'Нажмите <span class="bold-text">Перезапуск</span> для применения настроек.',
'resources.register.install.title': 'Установка GPUStack на {os}',
'resources.register.download':
'Скачайте и установите <a href={url} target="_blank">инсталлятор</a>. Поддерживаемые версии: {versions}.',
'resource.register.maos.support': 'Apple Silicon (серия M), macOS 14+',
'resource.register.windows.support': 'Windows 10, Windows 11'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
+6 -1
View File
@@ -27,5 +27,10 @@ export default {
'users.password.confirm.error': 'Пароли не совпадают',
'users.login.title': 'Вход в',
'users.version.islatest': 'GPUStack {version} — последняя версия',
'users.version.update': 'Доступно обновление GPUStack {version}'
'users.version.update': 'Доступно обновление GPUStack {version}',
'users.settings.title': 'Настройки пользователя'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// ========== End of To-Do List ==========

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