Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9cf111a86 | ||
|
|
99f8914830 | ||
|
|
6da877709d | ||
|
|
c4948dc11f | ||
|
|
3f243129a4 | ||
|
|
eb2cae7a77 | ||
|
|
a2bbcd3939 | ||
|
|
0650c4bc26 | ||
|
|
7c32bf97ae | ||
|
|
70aecc1f0c | ||
|
|
5bd5c7d2c5 | ||
|
|
d6dce4021d | ||
|
|
ccfdb99348 | ||
|
|
2ca712435a | ||
|
|
0af8e34a8f | ||
|
|
f2583e152a | ||
|
|
cf70bd2dc1 | ||
|
|
8f50af917f | ||
|
|
4bdde365cf | ||
|
|
7f1bc33eed | ||
|
|
d0d24aba4e | ||
|
|
0d68c0d683 | ||
|
|
e1ab735823 | ||
|
|
bb67a8cbbb | ||
|
|
c8a7ac7dc6 | ||
|
|
788a294200 | ||
|
|
f34935a634 | ||
|
|
a7331256fc | ||
|
|
8fa3941db9 | ||
|
|
ada120c1b9 | ||
|
|
72f7168893 |
+3
-3
@@ -100,9 +100,9 @@ export default [
|
||||
name: 'deployment',
|
||||
path: '/models/deployments',
|
||||
key: 'modelDeployments',
|
||||
icon: 'icon-model',
|
||||
selectedIcon: 'icon-model-filled',
|
||||
defaultIcon: 'icon-model',
|
||||
icon: 'icon-rocket-launch1',
|
||||
selectedIcon: 'icon-rocket-launch-fill',
|
||||
defaultIcon: 'icon-rocket-launch1',
|
||||
access: 'canSeeAdmin',
|
||||
component: './llmodels/index'
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"react-hotkeys-hook": "^4.5.0",
|
||||
"react-markdown": "^9.0.3",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"simplebar-react": "^3.2.6",
|
||||
|
||||
Generated
+18
@@ -165,6 +165,9 @@ dependencies:
|
||||
rehype-katex:
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1
|
||||
remark-breaks:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
remark-gfm:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
@@ -13121,6 +13124,13 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/mdast-util-newline-to-break@2.0.0:
|
||||
resolution: {integrity: sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==, tarball: https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz}
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
mdast-util-find-and-replace: 3.0.2
|
||||
dev: false
|
||||
|
||||
/mdast-util-phrasing@4.1.0:
|
||||
resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==, tarball: https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz}
|
||||
dependencies:
|
||||
@@ -17456,6 +17466,14 @@ packages:
|
||||
engines: {node: '>= 0.10'}
|
||||
dev: false
|
||||
|
||||
/remark-breaks@4.0.0:
|
||||
resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==, tarball: https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz}
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
mdast-util-newline-to-break: 2.0.0
|
||||
unified: 11.0.5
|
||||
dev: false
|
||||
|
||||
/remark-gfm@4.0.1:
|
||||
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==, tarball: https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz}
|
||||
dependencies:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1755020187776" class="icon" viewBox="0 0 1105 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7392" xmlns:xlink="http://www.w3.org/1999/xlink" width="138.125" height="128"><path d="M507.2896 133.44768l130.98496-71.4496v820.9408l-130.97984 69.71392V133.44768z" fill="#F38019" p-id="7393"></path><path d="M479.39072 323.968a724.30592 724.30592 0 0 0-77.056 16.6144C118.35904 417.01376 37.77536 603.8016 67.95776 701.696c30.11584 97.8944 161.03424 263.2192 439.2704 250.19904 0.13824-27.41248 0.13824-57.73824 0-90.89536-200.49408-26.37824-302.67904-88.82688-306.55488-187.40736-5.74464-147.74272 134.1696-216.90368 217.87136-229.0176 16.82432-2.49344 37.66272-5.67808 61.19936-8.6528l-0.27648-111.9488h-0.07168z m185.12384 107.65312c52.1984 5.6832 103.57248 18.69312 147.0464 44.44672-10.65984 5.19168-34.26816 19.73248-70.82496 43.47904l290.49344 66.39104-16.54272-206.58176-78.37184 44.30336c-116.10112-57.8048-205.19936-91.0336-267.29984-99.6864l-4.50048 107.648z" fill="#ADAFB3" p-id="7394"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M140-180v-88.92q0-29.39 15.96-54.43 15.96-25.03 42.66-38.49 59.3-29.08 119.65-43.62Q378.62-420 440-420q21.06 0 42.12 1.89 21.05 1.88 42.11 5.65-1.69 51.46 20.81 96.81Q567.54-270.31 609-240v60H140ZM753.85-72.31l-53.35-53.14v-164.73q-39.11-11.51-63.85-43.97-24.73-32.46-24.73-74.7 0-51.46 36.38-87.84t87.85-36.38q51.46 0 87.66 36.39Q860-460.28 860-408.79q0 39.94-22.42 70.71-22.43 30.77-57.2 44.23l44.23 44.23-53.07 53.2 53.07 53.19-70.76 70.92ZM440-484.62q-57.75 0-98.87-41.12Q300-566.86 300-624.61q0-57.75 41.13-98.88 41.12-41.12 98.87-41.12 57.75 0 98.87 41.12Q580-682.36 580-624.61q0 57.75-41.13 98.87-41.12 41.12-98.87 41.12Zm296.15 93.47q14.7 0 25.04-10.54 10.35-10.54 10.35-25.23 0-14.69-10.35-25.04-10.34-10.35-25.04-10.35-14.69 0-25.23 10.35-10.54 10.35-10.54 25.04t10.54 25.23q10.54 10.54 25.23 10.54Z"/></svg>
|
||||
|
After Width: | Height: | Size: 934 B |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
|
||||
␍<g fill="#C22E33">
|
||||
␍<path d="M7.754 2l.463.41c.343.304.687.607 1.026.915C11.44 5.32 13.3 7.565 14.7 10.149c.072.132.137.268.202.403l.098.203-.108.057-.081-.115-.21-.299-.147-.214c-1.019-1.479-2.04-2.96-3.442-4.145a6.563 6.563 0 00-1.393-.904c-1.014-.485-1.916-.291-2.69.505-.736.757-1.118 1.697-1.463 2.653-.045.123-.092.245-.139.367l-.082.215-.172-.055c.1-.348.192-.698.284-1.049.21-.795.42-1.59.712-2.356.31-.816.702-1.603 1.093-2.39.169-.341.338-.682.5-1.025h.092z"/>
|
||||
␍<path d="M8.448 11.822c-1.626.77-5.56 1.564-7.426 1.36C.717 11.576 3.71 4.05 5.18 2.91l-.095.218a4.638 4.638 0 01-.138.303l-.066.129c-.76 1.462-1.519 2.926-1.908 4.53a7.482 7.482 0 00-.228 1.689c-.01 1.34.824 2.252 2.217 2.309.67.027 1.347-.043 2.023-.114.294-.03.587-.061.88-.084.108-.008.214-.021.352-.039l.231-.028z"/>
|
||||
␍<path d="M3.825 14.781c-.445.034-.89.068-1.333.108 4.097.39 8.03-.277 11.91-1.644-1.265-2.23-2.97-3.991-4.952-5.522.026.098.084.169.141.239l.048.06c.17.226.348.448.527.67.409.509.818 1.018 1.126 1.578.778 1.42.356 2.648-1.168 3.296-1.002.427-2.097.718-3.18.892-1.03.164-2.075.243-3.119.323z"/>
|
||||
␍</g>
|
||||
␍</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -33,10 +33,9 @@ export const getStorageUserSettings = () => {
|
||||
}
|
||||
};
|
||||
|
||||
export const userSettingsAtom = atomWithStorage<UserSettings>(
|
||||
'userSettings',
|
||||
defaultSettings
|
||||
);
|
||||
export const userSettingsAtom = atomWithStorage<UserSettings>('userSettings', {
|
||||
...getStorageUserSettings()
|
||||
});
|
||||
|
||||
export const userSettingsHelperAtom = atom(
|
||||
(get) => get(userSettingsAtom),
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
.alert-info-block {
|
||||
padding-block: 6px;
|
||||
padding-inline: 10px 16px;
|
||||
position: relative;
|
||||
padding-left: 32px;
|
||||
text-align: left;
|
||||
border-radius: var(--border-radius-base);
|
||||
margin: 0;
|
||||
border: 1px solid transparent;
|
||||
|
||||
.ant-typography {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
border-color: var(--ant-color-error-border);
|
||||
background-color: var(--ant-color-error-bg);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
border-color: var(--ant-color-warning-border);
|
||||
background-color: var(--ant-color-warning-bg);
|
||||
}
|
||||
|
||||
&.transition {
|
||||
color: var(--ant-geekblue-7);
|
||||
background: var(--ant-geekblue-1);
|
||||
border-color: var(--ant-geekblue-3);
|
||||
}
|
||||
|
||||
&.success {
|
||||
border: 1px solid var(--ant-color-success);
|
||||
color: var(--ant-color-success);
|
||||
background: var(--ant-color-success-bg);
|
||||
|
||||
.content.success {
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
height: 32px;
|
||||
padding: 5px 10px;
|
||||
border-radius: var(--border-radius-base) var(--border-radius-base) 0 0;
|
||||
|
||||
.info-icon {
|
||||
&.danger {
|
||||
color: var(--ant-color-error);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: var(--ant-color-warning);
|
||||
}
|
||||
|
||||
&.transition {
|
||||
color: var(--ant-geekblue-7);
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: var(--ant-color-success);
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { WarningFilled } from '@ant-design/icons';
|
||||
import { Typography } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import OverlayScroller, { OverlayScrollerOptions } from '../overlay-scroller';
|
||||
import './block.less';
|
||||
interface AlertInfoProps {
|
||||
type: Global.MessageType;
|
||||
message: React.ReactNode;
|
||||
@@ -18,6 +18,81 @@ interface AlertInfoProps {
|
||||
overlayScrollerProps?: OverlayScrollerOptions;
|
||||
}
|
||||
|
||||
const useStyles = createStyles(({ token, css }) => {
|
||||
return {
|
||||
alertBlockInfo: css`
|
||||
padding-block: 6px;
|
||||
padding-inline: 10px 16px;
|
||||
position: relative;
|
||||
padding-left: 32px;
|
||||
text-align: left;
|
||||
border-radius: ${token.borderRadius}px;
|
||||
margin: 0;
|
||||
border: 1px solid transparent;
|
||||
.ant-typography {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
border-color: ${token.colorErrorBorder};
|
||||
background-color: ${token.colorErrorBg};
|
||||
}
|
||||
|
||||
&.warning {
|
||||
border-color: ${token.colorWarningBorder};
|
||||
background-color: ${token.colorWarningBg};
|
||||
}
|
||||
|
||||
&.transition {
|
||||
color: ${token.geekblue7};
|
||||
background: ${token.geekblue1};
|
||||
border-color: ${token.geekblue3};
|
||||
}
|
||||
|
||||
&.success {
|
||||
border: 1px solid ${token.colorSuccess};
|
||||
color: ${token.colorSuccessText};
|
||||
background: ${token.colorSuccessBg};
|
||||
|
||||
.content.success {
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
}
|
||||
.title {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
height: 32px;
|
||||
padding: 5px 10px;
|
||||
border-radius: ${token.borderRadius}px ${token.borderRadius}px 0 0;
|
||||
|
||||
.info-icon {
|
||||
&.danger {
|
||||
color: ${token.colorErrorText};
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: ${token.colorWarningText};
|
||||
}
|
||||
|
||||
&.transition {
|
||||
color: ${token.geekblue7};
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: ${token.colorSuccessText};
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
}
|
||||
`
|
||||
};
|
||||
});
|
||||
|
||||
const TitleWrapper = styled.div`
|
||||
font-weight: 700;
|
||||
color: var(--ant-color-text);
|
||||
@@ -46,12 +121,12 @@ const AlertInfo: React.FC<AlertInfoProps> = (props) => {
|
||||
maxHeight = 86,
|
||||
overlayScrollerProps = {}
|
||||
} = props;
|
||||
|
||||
const { styles } = useStyles();
|
||||
return (
|
||||
<>
|
||||
{message ? (
|
||||
<div
|
||||
className={classNames('alert-info-block', type)}
|
||||
className={classNames(styles.alertBlockInfo, type)}
|
||||
style={{ ...style }}
|
||||
>
|
||||
<Typography.Paragraph
|
||||
|
||||
@@ -15,7 +15,7 @@ const fadeOut = keyframes`
|
||||
}
|
||||
`;
|
||||
|
||||
const Mask = styled.div<{ isHiding: boolean }>`
|
||||
const Mask = styled.div<{ $isHiding: boolean }>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -26,7 +26,7 @@ const Mask = styled.div<{ isHiding: boolean }>`
|
||||
height: 100vh;
|
||||
background-color: rgba(59, 59, 59, 1);
|
||||
z-index: 9999;
|
||||
animation: ${({ isHiding }) => (isHiding ? fadeOut : 'none')} ${duration}s
|
||||
animation: ${({ $isHiding }) => ($isHiding ? fadeOut : 'none')} ${duration}s
|
||||
ease forwards;
|
||||
`;
|
||||
|
||||
@@ -79,7 +79,7 @@ const DarkModeMask = () => {
|
||||
return () => el.removeEventListener('animationend', handleAnimationEnd);
|
||||
}, [visible]);
|
||||
|
||||
return visible ? <Mask ref={maskRef} isHiding={isHiding}></Mask> : null;
|
||||
return visible ? <Mask ref={maskRef} $isHiding={isHiding}></Mask> : null;
|
||||
};
|
||||
|
||||
export default DarkModeMask;
|
||||
|
||||
@@ -129,9 +129,15 @@ const BarChart: React.FC<ChartProps & { maxItems?: number }> = (props) => {
|
||||
legend
|
||||
]);
|
||||
|
||||
const isEmpty = useMemo(() => {
|
||||
return seriesData?.every?.((item: any) => {
|
||||
return !item?.data?.length;
|
||||
});
|
||||
}, [seriesData]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!seriesData.length ? (
|
||||
{isEmpty ? (
|
||||
<EmptyData height={height} title={title}></EmptyData>
|
||||
) : (
|
||||
<Chart
|
||||
|
||||
@@ -56,4 +56,4 @@ const HighlightCode: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(HighlightCode);
|
||||
export default HighlightCode;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: iconfont; /* Project id 4613488 */
|
||||
src: url('iconfont.woff2?t=1752112673765') format('woff2'),
|
||||
url('iconfont.woff?t=1752112673765') format('woff'),
|
||||
url('iconfont.ttf?t=1752112673765') format('truetype');
|
||||
src: url('iconfont.woff2?t=1755160439217') format('woff2'),
|
||||
url('iconfont.woff?t=1755160439217') format('woff'),
|
||||
url('iconfont.ttf?t=1755160439217') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,50 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-admin-user::before {
|
||||
content: "\e69d";
|
||||
}
|
||||
|
||||
.icon-user::before {
|
||||
content: "\e69e";
|
||||
}
|
||||
|
||||
.icon-quick_reference_24dp_1F1F1F_FILL0_wght300_GRAD0_op::before {
|
||||
content: "\e69b";
|
||||
}
|
||||
|
||||
.icon-docker::before {
|
||||
content: "\e69c";
|
||||
}
|
||||
|
||||
.icon-digitalocean::before {
|
||||
content: "\eb79";
|
||||
}
|
||||
|
||||
.icon-rocket-launch1::before {
|
||||
content: "\e699";
|
||||
}
|
||||
|
||||
.icon-rocket-launch-fill::before {
|
||||
content: "\e69a";
|
||||
}
|
||||
|
||||
.icon-credential-filled::before {
|
||||
content: "\e697";
|
||||
}
|
||||
|
||||
.icon-credential-outline::before {
|
||||
content: "\e698";
|
||||
}
|
||||
|
||||
.icon-k8s-filled::before {
|
||||
content: "\e63e";
|
||||
}
|
||||
|
||||
.icon-k8s-outline::before {
|
||||
content: "\e64b";
|
||||
}
|
||||
|
||||
.icon-huggingface1::before {
|
||||
content: "\e605";
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -5,6 +5,83 @@
|
||||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "45240470",
|
||||
"name": "admin-user",
|
||||
"font_class": "admin-user",
|
||||
"unicode": "e69d",
|
||||
"unicode_decimal": 59037
|
||||
},
|
||||
{
|
||||
"icon_id": "45240469",
|
||||
"name": "user",
|
||||
"font_class": "user",
|
||||
"unicode": "e69e",
|
||||
"unicode_decimal": 59038
|
||||
},
|
||||
{
|
||||
"icon_id": "45221182",
|
||||
"name": "quick_reference_24dp_1F1F1F_FILL0_wght300_GRAD0_op",
|
||||
"font_class": "quick_reference_24dp_1F1F1F_FILL0_wght300_GRAD0_op",
|
||||
"unicode": "e69b",
|
||||
"unicode_decimal": 59035
|
||||
},
|
||||
{
|
||||
"icon_id": "45102077",
|
||||
"name": "Docker",
|
||||
"font_class": "docker",
|
||||
"unicode": "e69c",
|
||||
"unicode_decimal": 59036
|
||||
},
|
||||
{
|
||||
"icon_id": "15378290",
|
||||
"name": "digitalocean",
|
||||
"font_class": "digitalocean",
|
||||
"unicode": "eb79",
|
||||
"unicode_decimal": 60281
|
||||
},
|
||||
{
|
||||
"icon_id": "45040302",
|
||||
"name": "rocket-launch",
|
||||
"font_class": "rocket-launch1",
|
||||
"unicode": "e699",
|
||||
"unicode_decimal": 59033
|
||||
},
|
||||
{
|
||||
"icon_id": "45040301",
|
||||
"name": "rocket-launch-fill",
|
||||
"font_class": "rocket-launch-fill",
|
||||
"unicode": "e69a",
|
||||
"unicode_decimal": 59034
|
||||
},
|
||||
{
|
||||
"icon_id": "45028316",
|
||||
"name": "credential",
|
||||
"font_class": "credential-filled",
|
||||
"unicode": "e697",
|
||||
"unicode_decimal": 59031
|
||||
},
|
||||
{
|
||||
"icon_id": "45028315",
|
||||
"name": "credential",
|
||||
"font_class": "credential-outline",
|
||||
"unicode": "e698",
|
||||
"unicode_decimal": 59032
|
||||
},
|
||||
{
|
||||
"icon_id": "37601405",
|
||||
"name": "k8s",
|
||||
"font_class": "k8s-filled",
|
||||
"unicode": "e63e",
|
||||
"unicode_decimal": 58942
|
||||
},
|
||||
{
|
||||
"icon_id": "41673174",
|
||||
"name": "k8s",
|
||||
"font_class": "k8s-outline",
|
||||
"unicode": "e64b",
|
||||
"unicode_decimal": 58955
|
||||
},
|
||||
{
|
||||
"icon_id": "40865292",
|
||||
"name": "huggingface",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,32 @@
|
||||
import IconFont from '@/components/icon-font';
|
||||
import {
|
||||
ApiOutlined,
|
||||
DeleteOutlined,
|
||||
DownloadOutlined,
|
||||
EditOutlined,
|
||||
ExperimentOutlined,
|
||||
RetweetOutlined,
|
||||
ThunderboltOutlined
|
||||
} from '@ant-design/icons';
|
||||
import React from 'react';
|
||||
|
||||
const icons = {
|
||||
EditOutlined: React.createElement(EditOutlined),
|
||||
ExperimentOutlined: React.createElement(ExperimentOutlined),
|
||||
DeleteOutlined: React.createElement(DeleteOutlined),
|
||||
ThunderboltOutlined: React.createElement(ThunderboltOutlined),
|
||||
RetweetOutlined: React.createElement(RetweetOutlined),
|
||||
DownloadOutlined: React.createElement(DownloadOutlined),
|
||||
ApiOutlined: React.createElement(ApiOutlined),
|
||||
Stop: React.createElement(IconFont, { type: 'icon-stop1' }),
|
||||
Play: React.createElement(IconFont, { type: 'icon-outline-play' }),
|
||||
Catalog: React.createElement(IconFont, { type: 'icon-catalog' }),
|
||||
HF: React.createElement(IconFont, { type: 'icon-huggingface' }),
|
||||
Ollama: React.createElement(IconFont, { type: 'icon-ollama' }),
|
||||
ModelScope: React.createElement(IconFont, { type: 'icon-tu2' }),
|
||||
LocalPath: React.createElement(IconFont, { type: 'icon-hard-disk' }),
|
||||
Launch: React.createElement(IconFont, { type: 'icon-rocket-launch' }),
|
||||
Deployment: React.createElement(IconFont, { type: 'icon-rocket-launch1' })
|
||||
};
|
||||
|
||||
export default icons;
|
||||
@@ -2,9 +2,27 @@ import langConfigMap from '@/locales/lang-config-map';
|
||||
import { GlobalOutlined } from '@ant-design/icons';
|
||||
import { getAllLocales, setLocale } from '@umijs/max';
|
||||
import { Dropdown } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { get } from 'lodash';
|
||||
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
button: css`
|
||||
color: ${token.colorText};
|
||||
padding: 0 12px;
|
||||
cursor: pointer;
|
||||
.anticon {
|
||||
color: ${token.colorText};
|
||||
}
|
||||
&:hover {
|
||||
.anticon {
|
||||
color: ${token.colorTextTertiary};
|
||||
}
|
||||
}
|
||||
`
|
||||
}));
|
||||
|
||||
const LangSelect = () => {
|
||||
const { styles } = useStyles();
|
||||
const allLocals = getAllLocales();
|
||||
const items = allLocals.map((key) => {
|
||||
return {
|
||||
@@ -25,7 +43,7 @@ const LangSelect = () => {
|
||||
|
||||
return (
|
||||
<Dropdown menu={{ items }}>
|
||||
<span style={{ padding: '0 12px', cursor: 'pointer' }}>
|
||||
<span className={styles.button}>
|
||||
<GlobalOutlined />
|
||||
</span>
|
||||
</Dropdown>
|
||||
|
||||
@@ -16,6 +16,7 @@ interface MessageProps {
|
||||
chunked?: boolean;
|
||||
progress?: number;
|
||||
percent?: number;
|
||||
isDownloading?: boolean;
|
||||
}
|
||||
class AnsiParser {
|
||||
private cursorRow: number = 0;
|
||||
@@ -32,6 +33,7 @@ class AnsiParser {
|
||||
private chunked: boolean = true; // true: send data in chunks, false: send all data at once
|
||||
private reminder: string = '';
|
||||
private lines: string[] = [];
|
||||
isDownloading: boolean = false;
|
||||
private pageSize: number = 500;
|
||||
private colorMap = {
|
||||
'30': 'black',
|
||||
@@ -79,6 +81,10 @@ class AnsiParser {
|
||||
this.chunked = chunked ?? true;
|
||||
}
|
||||
|
||||
public setIsDownloading(isDownloading: boolean) {
|
||||
this.isDownloading = isDownloading;
|
||||
}
|
||||
|
||||
private setId() {
|
||||
this.uid += 1;
|
||||
return this.uid;
|
||||
@@ -168,14 +174,18 @@ class AnsiParser {
|
||||
const remainingText = input.slice(lastIndex);
|
||||
this.handleText(remainingText);
|
||||
|
||||
const result = this.screen.map((row, index) => ({
|
||||
content: removeBracketsFromLine(row.join('')),
|
||||
uid: `${this.page}-${index}`
|
||||
}));
|
||||
// const result = this.screen.map((row, index) => ({
|
||||
// content: removeBracketsFromLine(row.join('')),
|
||||
// uid: `${this.page}-${index}`
|
||||
// }));
|
||||
const result = this.screen.map((row, index) =>
|
||||
removeBracketsFromLine(row.join(''))
|
||||
);
|
||||
|
||||
return {
|
||||
data: result,
|
||||
lines: this.rawDataRows
|
||||
lines: this.rawDataRows,
|
||||
remainder: ''
|
||||
};
|
||||
}
|
||||
|
||||
@@ -219,12 +229,23 @@ class AnsiParser {
|
||||
this.isProcessing = true;
|
||||
|
||||
while (this.taskQueue.length > 0) {
|
||||
const input = this.reminder + this.taskQueue.shift();
|
||||
let input = '';
|
||||
|
||||
if (this.isDownloading) {
|
||||
input = this.taskQueue.shift() || '';
|
||||
} else {
|
||||
input = this.reminder + this.taskQueue.shift();
|
||||
}
|
||||
|
||||
if (input) {
|
||||
try {
|
||||
const result = this.processInputByLine(input);
|
||||
this.reminder = result.remainder;
|
||||
const result = this.isDownloading
|
||||
? this.processInput(input)
|
||||
: this.processInputByLine(input);
|
||||
if (!this.isDownloading) {
|
||||
this.reminder = result.remainder;
|
||||
}
|
||||
|
||||
if (this.chunked) {
|
||||
self.postMessage({ result: result.data, lines: result.lines });
|
||||
} else if (!this.isComplete) {
|
||||
@@ -273,9 +294,11 @@ self.onmessage = function (event: MessageEvent<MessageProps>) {
|
||||
page,
|
||||
isComplete = false,
|
||||
chunked = true,
|
||||
percent = 0
|
||||
percent = 0,
|
||||
isDownloading = false
|
||||
} = event.data;
|
||||
|
||||
parser.setIsDownloading(isDownloading);
|
||||
parser.setPage(page);
|
||||
parser.setIsCompelete(isComplete);
|
||||
parser.setChunked(chunked);
|
||||
|
||||
@@ -4,7 +4,6 @@ import classNames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
import React, {
|
||||
forwardRef,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
@@ -25,10 +24,17 @@ interface LogsViewerProps {
|
||||
tail?: number;
|
||||
enableScorllLoad?: boolean;
|
||||
diffHeight?: number;
|
||||
isDownloading?: boolean;
|
||||
}
|
||||
|
||||
const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
|
||||
const { diffHeight, url, tail: defaultTail, enableScorllLoad = true } = props;
|
||||
const {
|
||||
diffHeight,
|
||||
url,
|
||||
tail: defaultTail,
|
||||
enableScorllLoad = true,
|
||||
isDownloading
|
||||
} = props;
|
||||
const { pageSize, page, setPage, setTotalPage, totalPage } =
|
||||
useLogsPagination();
|
||||
const { setChunkFetch } = useSetChunkFetch();
|
||||
@@ -64,9 +70,10 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
|
||||
};
|
||||
|
||||
const setCurrentData = (lines: string[]) => {
|
||||
console.log('setCurrentData', lines);
|
||||
const dataList = lines.map((line, index) => {
|
||||
return {
|
||||
content: removeBracketsFromLine(line),
|
||||
content: line,
|
||||
uid: `${pageRef.current}-${index}`
|
||||
};
|
||||
});
|
||||
@@ -150,7 +157,8 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
|
||||
logParseWorker.current.postMessage({
|
||||
inputStr: data,
|
||||
page: pageRef.current,
|
||||
reset: clearScreen.current
|
||||
reset: clearScreen.current,
|
||||
isDownloading: isDownloading
|
||||
});
|
||||
clearScreen.current = false;
|
||||
};
|
||||
@@ -247,7 +255,7 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
|
||||
return () => {
|
||||
chunkRequedtRef.current?.current?.abort?.();
|
||||
};
|
||||
}, [url]);
|
||||
}, [url, isDownloading]);
|
||||
|
||||
useEffect(() => {
|
||||
debouncedScroll();
|
||||
@@ -349,4 +357,4 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
|
||||
);
|
||||
});
|
||||
|
||||
export default memo(LogsViewer);
|
||||
export default LogsViewer;
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'katex/dist/katex.min.css';
|
||||
import React, { useMemo } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import remarkBreaks from 'remark-breaks';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
import styled from 'styled-components';
|
||||
@@ -17,9 +18,7 @@ interface FullMarkdownProps {
|
||||
|
||||
const Wrapper = styled.div.attrs(() => ({
|
||||
className: 'markdown-viewer'
|
||||
}))`
|
||||
line-height: 2;
|
||||
`;
|
||||
}))``;
|
||||
|
||||
const CodeViewer = (props: any) => {
|
||||
const { children, className, node, theme, ...rest } = props;
|
||||
@@ -41,7 +40,7 @@ const FullMarkdown: React.FC<FullMarkdownProps> = ({ content, theme }) => {
|
||||
return (
|
||||
<Wrapper>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[[remarkMath], remarkGfm]}
|
||||
remarkPlugins={[[remarkMath], remarkGfm, remarkBreaks]}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
components={{
|
||||
code(props) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.markdown-viewer {
|
||||
word-break: break-word;
|
||||
line-height: 2;
|
||||
font-size: var(--font-size-base);
|
||||
|
||||
.hr {
|
||||
@@ -60,13 +61,11 @@
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
padding-left: 20px;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
ol {
|
||||
margin-bottom: 0;
|
||||
padding-left: 20px;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
a * {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import { createStyles } from 'antd-style/lib/functions';
|
||||
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
errorIcon: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background-color: ${token.colorErrorBgActive};
|
||||
color: ${token.colorError};
|
||||
`
|
||||
}));
|
||||
|
||||
const ErrorIcon: React.FC<{ size?: number }> = ({ size }) => {
|
||||
const { styles } = useStyles();
|
||||
return (
|
||||
<div className={styles.errorIcon}>
|
||||
<CloseOutlined style={{ fontSize: size || 30 }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorIcon;
|
||||
@@ -15,9 +15,13 @@ const useStyles = createStyles(({ token, css }) => ({
|
||||
padding: 0 8px;
|
||||
&:hover {
|
||||
color: ${token.colorTextTertiary};
|
||||
.anticon {
|
||||
color: ${token.colorTextTertiary};
|
||||
}
|
||||
}
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
color: ${token.colorText};
|
||||
}
|
||||
.icon-auto {
|
||||
font-size: 20px;
|
||||
|
||||
Vendored
+2
@@ -20,6 +20,8 @@ declare namespace Global {
|
||||
full_name: string;
|
||||
require_password_change: boolean;
|
||||
id: number;
|
||||
source: string;
|
||||
avatar_url?: string;
|
||||
}
|
||||
|
||||
interface BaseListItem<T> {
|
||||
|
||||
+1
-2
@@ -570,7 +570,6 @@ body {
|
||||
|
||||
.ant-message-notice-content {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
line-height: 2;
|
||||
max-width: 300px;
|
||||
text-align: left;
|
||||
word-break: break-word;
|
||||
@@ -579,7 +578,7 @@ body {
|
||||
overflow-y: auto;
|
||||
|
||||
.anticon {
|
||||
padding-top: 5px;
|
||||
padding-top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ export default function useUserSettings() {
|
||||
const { light, dark, colorPrimary } = themeConfig;
|
||||
const [userSettings, setUserSettings] = useAtom(userSettingsHelperAtom);
|
||||
|
||||
console.log('userSettings===', userSettings);
|
||||
|
||||
const getCurrentTheme = (mode: Theme): 'light' | 'realDark' => {
|
||||
if (mode === 'auto') {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
@@ -46,22 +48,32 @@ export default function useUserSettings() {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setHtmlThemeAttr(userSettings.theme);
|
||||
}, [userSettings.theme]);
|
||||
const initTheme = () => {
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
.matches
|
||||
? 'realDark'
|
||||
: 'light';
|
||||
if (userSettings.mode === 'auto' && systemTheme !== userSettings.theme) {
|
||||
setTheme('auto');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (userSettings.mode !== 'auto') return;
|
||||
initTheme();
|
||||
}, [userSettings.theme, userSettings.mode]);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const handleChange = () => {
|
||||
// The theme follows the system preference, when the mode is set to 'auto'
|
||||
if (userSettings.mode !== 'auto') return;
|
||||
setTheme('auto');
|
||||
};
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
return () => {
|
||||
mediaQuery.removeEventListener('change', handleChange);
|
||||
};
|
||||
}, [userSettings.mode]);
|
||||
}, [userSettings.mode, userSettings.theme]);
|
||||
|
||||
return {
|
||||
userSettings,
|
||||
|
||||
@@ -267,6 +267,7 @@ export const getRightRenderContent = (opts: {
|
||||
<span className="avatar-container">
|
||||
<Avatar
|
||||
size={28}
|
||||
src={opts.initialState?.currentUser?.avatar_url}
|
||||
style={{
|
||||
color: 'var(--ant-color-text)',
|
||||
fontSize: 16
|
||||
|
||||
@@ -17,6 +17,7 @@ interface MenuItem {
|
||||
interface SiderMenuProps {
|
||||
menuData: MenuItem[];
|
||||
collapsed?: boolean;
|
||||
initialState: Global.InitialState;
|
||||
}
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => {
|
||||
@@ -122,7 +123,8 @@ const useStyles = createStyles(({ css, token }) => {
|
||||
});
|
||||
|
||||
const SiderMenu: React.FC<SiderMenuProps> = (props) => {
|
||||
const { menuData, collapsed } = props;
|
||||
const { menuData, collapsed, initialState } = props;
|
||||
const is_admin = initialState?.currentUser?.is_admin || false;
|
||||
const { styles, cx } = useStyles();
|
||||
const location = useLocation();
|
||||
const [collapseKeys, setCollapseKeys] = useState<Set<string>>(new Set());
|
||||
@@ -220,9 +222,9 @@ const SiderMenu: React.FC<SiderMenuProps> = (props) => {
|
||||
rotate={collapseKeys.has(item.key) ? -90 : 0}
|
||||
></CaretDownOutlined>
|
||||
</span>
|
||||
) : (
|
||||
) : is_admin ? (
|
||||
<Divider style={dividerStyles} />
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
className={cx(styles.menuItemGroup, {
|
||||
|
||||
@@ -5,7 +5,7 @@ export default {
|
||||
'common.button.editmode': 'Edit mode',
|
||||
'common.button.shortcut': 'Keyboard Shortcut',
|
||||
'common.button.add': 'Add',
|
||||
'common.button.login': 'Log In',
|
||||
'common.button.login': 'Log in',
|
||||
'common.button.select': 'Select',
|
||||
'common.button.selected': 'Selected',
|
||||
'common.button.continue': 'Continue',
|
||||
@@ -251,5 +251,11 @@ export default {
|
||||
'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'
|
||||
'common.select.count': '{count} selected',
|
||||
'common.login.auth': 'Authenticating...',
|
||||
'common.login.auth.failed': 'Authentication failed',
|
||||
'common.login.password': 'Log in with Password',
|
||||
'common.external.login': 'Log in with {type}',
|
||||
'common.sso.noConfig':
|
||||
'Single sign-on is not enabled on this system. Please contact your administrator.'
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ export default {
|
||||
'users.table.username': 'User Name',
|
||||
'users.table.role': 'Role',
|
||||
'users.form.fullname': 'Full Name',
|
||||
'users.form.source': 'Source',
|
||||
'users.table.user': 'users',
|
||||
'users.form.admin': 'Admin',
|
||||
'users.form.user': 'User',
|
||||
|
||||
@@ -251,7 +251,13 @@ export default {
|
||||
'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'
|
||||
'common.select.count': '{count} selected',
|
||||
'common.login.auth': 'Authenticating...',
|
||||
'common.login.auth.failed': 'Authentication failed',
|
||||
'common.login.password': 'Log in with Password',
|
||||
'common.external.login': 'Log in with {type}',
|
||||
'common.sso.noConfig':
|
||||
'Single sign-on is not enabled on this system. Please contact your administrator.'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
@@ -269,5 +275,10 @@ export default {
|
||||
// 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'
|
||||
// 15. 'common.select.count': '{count} selected',
|
||||
// 16. 'common.login.auth': 'Authenticating...',
|
||||
// 17. 'common.login.auth.failed': 'Authentication failed',
|
||||
// 18. 'common.external.login': 'Log in with {type}'
|
||||
// 19. 'common.login.password': 'Log in with Password',
|
||||
// 20. 'common.sso.noConfig': 'Single sign-on is not enabled on this system. Please contact your administrator.'
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -6,6 +6,7 @@ export default {
|
||||
'users.table.username': 'ユーザー名',
|
||||
'users.table.role': '役割',
|
||||
'users.form.fullname': 'フルネーム',
|
||||
'users.form.source': 'ソース',
|
||||
'users.table.user': 'ユーザー',
|
||||
'users.form.admin': '管理者',
|
||||
'users.form.user': '一般ユーザー',
|
||||
|
||||
@@ -250,9 +250,17 @@ export default {
|
||||
'common.tips.escape.disable':
|
||||
'Чтобы закрыть, нажмите "Отмена" или крестик (X) в правом верхнем углу.',
|
||||
'common.button.clearSelection': 'Сбросить выбор',
|
||||
'common.select.count': '{count} Выбрано'
|
||||
'common.select.count': '{count} Выбрано',
|
||||
'common.login.auth': 'Аутентификация...',
|
||||
'common.login.auth.failed': 'Ошибка аутентификации',
|
||||
'common.login.password': 'Log in with Password',
|
||||
'common.external.login': 'Log in with {type}',
|
||||
'common.sso.noConfig':
|
||||
'Single sign-on is not enabled on this system. Please contact your administrator.'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
|
||||
// 1. 'common.external.login': 'Log in with {type}'
|
||||
// 2. 'common.login.password': 'Log in with Password'
|
||||
// 3. 'common.sso.noConfig': 'Single sign-on is not enabled on this system. Please contact your administrator.'
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -28,13 +28,11 @@ export default {
|
||||
'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'
|
||||
'dashboard.usage.datePicker.last7days': 'Последние 7 Days',
|
||||
'dashboard.usage.datePicker.last30days': 'Последние 30 Days',
|
||||
'dashboard.usage.datePicker.last60days': 'Последние 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 ==========
|
||||
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
'menu.models.modelList': 'Развертывание и управление',
|
||||
'menu.models.modelCatalog': 'Каталог',
|
||||
'menu.models.catalog': 'Каталог моделей',
|
||||
'menu.models.deployment': 'Deployment',
|
||||
'menu.models.deployment': 'Запуск',
|
||||
'menu.modelCatalog': 'Каталог',
|
||||
'menu.resources': 'Ресурсы',
|
||||
'menu.apikeys': 'API-ключи',
|
||||
@@ -19,21 +19,15 @@ export default {
|
||||
'menu.profile': 'Профиль',
|
||||
'menu.login': 'Авторизация',
|
||||
'menu.usage': 'Использование',
|
||||
'menu.404': '404',
|
||||
'menu.resources.workers': 'Workers',
|
||||
'menu.404': 'Ошибка 404',
|
||||
'menu.resources.workers': 'Воркеры',
|
||||
'menu.resources.gpus': 'GPUs',
|
||||
'menu.resources.modelfiles': 'Model Files',
|
||||
'menu.accessControl': 'Access Control',
|
||||
'menu.accessControl.apikeys': 'API Keys',
|
||||
'menu.accessControl.users': 'Users'
|
||||
'menu.resources.modelfiles': 'Файлы модлей',
|
||||
'menu.accessControl': 'Управление доступом',
|
||||
'menu.accessControl.apikeys': 'API Ключи',
|
||||
'menu.accessControl.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 ==========
|
||||
|
||||
@@ -150,7 +150,7 @@ export default {
|
||||
'models.table.apiAccessInfo.gotoCreate': 'Перейти к созданию',
|
||||
'models.search.parts': '{n} частей',
|
||||
'models.search.evaluate.error': 'Возникла ошибка при вычислении: ',
|
||||
'models.ollama.deprecated.title': 'Deprecation Notice',
|
||||
'models.ollama.deprecated.title': 'Объявление об устаревании',
|
||||
'models.ollama.deprecated.current':
|
||||
'<span class="bold-text">Текущая версия (v0.6.1): </span>Модели Ollama в настоящее время доступны для использования.',
|
||||
'models.ollama.deprecated.upcoming':
|
||||
@@ -159,13 +159,11 @@ export default {
|
||||
'<span class="bold-text">После обновления до версии (v0.7.0),</span> все ранее развёрнутые модели продолжат работать в обычном режиме.',
|
||||
'models.ollama.deprecated.issue':
|
||||
'См. связанную проблему: <a href="https://github.com/gpustack/gpustack/issues/1979" target="_blank">#1979 on GitHub</a>.',
|
||||
'models.ollama.deprecated.notice': `The Ollama model source has been deprecated as of v0.6.1. For more information, see the <a href="https://github.com/gpustack/gpustack/issues/1979" target="_blank">related GitHub issue</a>.`,
|
||||
'models.ollama.deprecated.notice': `Источник моделей Ollama объявлен устаревшим начиная с версии v0.6.1. Подробности см. в <a href="https://github.com/gpustack/gpustack/issues/1979" target="_blank">соответствующем issue на GitHub</a>.`,
|
||||
'models.backend.mindie.310p':
|
||||
'Ascend 310P only supports FP16, so you need to set --dtype=float16.'
|
||||
'Ascend 310P поддерживает только FP16, поэтому необходимо установить --dtype=float16.'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
// 1. 'models.ollama.deprecated.title': 'Deprecation Notice',
|
||||
// 2. 'models.ollama.deprecated.notice': `The Ollama model source has been deprecated as of v0.6.1. For more information, see the <a href="https://github.com/gpustack/gpustack/issues/1979" target="_blank">related GitHub issue</a>.`
|
||||
// 3. 'models.backend.mindie.310p':'Ascend 310P only supports FP16, so you need to set --dtype=float16.'
|
||||
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -6,6 +6,7 @@ export default {
|
||||
'users.table.username': 'Имя пользователя',
|
||||
'users.table.role': 'Роль',
|
||||
'users.form.fullname': 'Полное имя',
|
||||
'users.form.source': 'Источник',
|
||||
'users.table.user': 'пользователи',
|
||||
'users.form.admin': 'Администратор',
|
||||
'users.form.user': 'Пользователь',
|
||||
|
||||
@@ -244,5 +244,10 @@ export default {
|
||||
'common.page.refresh.tips': '出了点问题,试试刷新页面吧!',
|
||||
'common.tips.escape.disable': '请点击「取消」按钮或右上角 X 关闭窗口',
|
||||
'common.button.clearSelection': '清除选择',
|
||||
'common.select.count': '已选 {count} 项'
|
||||
'common.select.count': '已选 {count} 项',
|
||||
'common.login.auth': '认证中...',
|
||||
'common.login.auth.failed': '认证失败',
|
||||
'common.login.password': '使用密码登录',
|
||||
'common.external.login': '使用 {type} 登录',
|
||||
'common.sso.noConfig': '该系统未启用单点登录,请联系管理员。'
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ export default {
|
||||
'users.table.username': '用户名',
|
||||
'users.table.role': '角色',
|
||||
'users.form.fullname': '全名',
|
||||
'users.form.source': '来源',
|
||||
'users.table.user': '用户',
|
||||
'users.form.admin': '管理员',
|
||||
'users.form.user': '普通用户',
|
||||
|
||||
@@ -161,6 +161,11 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
|
||||
return filterRegGGUF.test(file.path) || _.includes(file.path, '.gguf');
|
||||
};
|
||||
|
||||
const isNormalGGUFModelFile = (filename: string) => {
|
||||
const file = filename?.toLowerCase() ?? '';
|
||||
return file.indexOf('mmproj') === -1 && file.indexOf('imatrix') === -1;
|
||||
};
|
||||
|
||||
// hugging face files
|
||||
const getHuggingfaceFiles = async () => {
|
||||
try {
|
||||
@@ -175,7 +180,7 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
|
||||
});
|
||||
|
||||
const list = _.filter(fileList, (file: any) => {
|
||||
return hfFileFilter(file) && file.path.indexOf('mmproj') === -1;
|
||||
return hfFileFilter(file) && isNormalGGUFModelFile(file.path);
|
||||
});
|
||||
|
||||
return list;
|
||||
@@ -188,7 +193,7 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
|
||||
return (
|
||||
filterRegGGUF.test(file.Path) &&
|
||||
file.Type === 'blob' &&
|
||||
file.Path.indexOf('mmproj') === -1
|
||||
isNormalGGUFModelFile(file.Path)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ const IncompatibleInfo = styled.div`
|
||||
li {
|
||||
position: relative;
|
||||
line-height: 20px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
li.normal::before {
|
||||
position: absolute;
|
||||
|
||||
@@ -877,6 +877,7 @@ const Models: React.FC<ModelsProps> = ({
|
||||
onOk={handleCreateModel}
|
||||
></DeployModal>
|
||||
<ViewLogsModal
|
||||
status={currentInstance.status}
|
||||
url={currentInstance.url}
|
||||
tail={currentInstance.tail}
|
||||
id={currentInstance.id}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useIntl } from '@umijs/max';
|
||||
import { Modal } from 'antd';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { MODELS_API } from '../apis';
|
||||
import { InstanceRealtimeLogStatus } from '../config';
|
||||
import { InstanceRealtimeLogStatus, InstanceStatusMap } from '../config';
|
||||
|
||||
type ViewModalProps = {
|
||||
open: boolean;
|
||||
@@ -12,14 +12,18 @@ type ViewModalProps = {
|
||||
id?: number | string;
|
||||
modelId?: number | string;
|
||||
tail?: number;
|
||||
status?: string;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
|
||||
const intl = useIntl();
|
||||
const { setChunkRequest } = useSetChunkRequest();
|
||||
const { open, url, onCancel, tail } = props || {};
|
||||
const { open, url, onCancel, tail, status } = props || {};
|
||||
const [enableScorllLoad, setEnableScorllLoad] = useState(true);
|
||||
const [isDownloading, setIsDownloading] = useState<boolean>(
|
||||
status === InstanceStatusMap.Downloading
|
||||
);
|
||||
const logsViewerRef = React.useRef<any>(null);
|
||||
const requestRef = React.useRef<any>(null);
|
||||
const contentRef = React.useRef<any>(null);
|
||||
@@ -32,7 +36,9 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
|
||||
const updateHandler = (list: any) => {
|
||||
const data = list?.find((item: any) => item.data?.id === props.id);
|
||||
// state in InstanceRealtimeLogStatus will not enable scorll load, because it is in the trasisition state
|
||||
|
||||
if (data) {
|
||||
setIsDownloading(data?.data?.state === InstanceStatusMap.Downloading);
|
||||
setEnableScorllLoad(
|
||||
() => !InstanceRealtimeLogStatus.includes(data?.data?.state)
|
||||
);
|
||||
@@ -70,6 +76,9 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
|
||||
url: `${MODELS_API}/${props.modelId}/instances`,
|
||||
handler: updateHandler
|
||||
});
|
||||
} else {
|
||||
logsViewerRef.current?.abort();
|
||||
requestRef.current?.current?.cancel?.();
|
||||
}
|
||||
|
||||
return () => {
|
||||
@@ -111,6 +120,7 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
|
||||
url={url}
|
||||
tail={tail}
|
||||
enableScorllLoad={enableScorllLoad}
|
||||
isDownloading={isDownloading}
|
||||
params={{
|
||||
follow: true
|
||||
}}
|
||||
|
||||
@@ -1,36 +1,9 @@
|
||||
import IconFont from '@/components/icon-font';
|
||||
import icons from '@/components/icon-font/icons';
|
||||
import HotKeys from '@/config/hotkeys';
|
||||
import {
|
||||
ApiOutlined,
|
||||
DeleteOutlined,
|
||||
DownloadOutlined,
|
||||
EditOutlined,
|
||||
ExperimentOutlined,
|
||||
RetweetOutlined,
|
||||
ThunderboltOutlined
|
||||
} from '@ant-design/icons';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { modelCategoriesMap, modelSourceMap } from './index';
|
||||
|
||||
const icons = {
|
||||
EditOutlined: React.createElement(EditOutlined),
|
||||
ExperimentOutlined: React.createElement(ExperimentOutlined),
|
||||
DeleteOutlined: React.createElement(DeleteOutlined),
|
||||
ThunderboltOutlined: React.createElement(ThunderboltOutlined),
|
||||
RetweetOutlined: React.createElement(RetweetOutlined),
|
||||
DownloadOutlined: React.createElement(DownloadOutlined),
|
||||
ApiOutlined: React.createElement(ApiOutlined),
|
||||
Stop: React.createElement(IconFont, { type: 'icon-stop1' }),
|
||||
Play: React.createElement(IconFont, { type: 'icon-outline-play' }),
|
||||
Catalog: React.createElement(IconFont, { type: 'icon-catalog' }),
|
||||
HF: React.createElement(IconFont, { type: 'icon-huggingface' }),
|
||||
Ollama: React.createElement(IconFont, { type: 'icon-ollama' }),
|
||||
ModelScope: React.createElement(IconFont, { type: 'icon-tu2' }),
|
||||
LocalPath: React.createElement(IconFont, { type: 'icon-hard-disk' }),
|
||||
Launch: React.createElement(IconFont, { type: 'icon-rocket-launch' })
|
||||
};
|
||||
|
||||
export const modalConfig: Record<
|
||||
string,
|
||||
{ show: boolean; width: string | number; source: any; isGGUF?: boolean }
|
||||
@@ -196,7 +169,7 @@ export const modelFileActions = [
|
||||
{
|
||||
label: 'common.button.deploy',
|
||||
key: 'deploy',
|
||||
icon: icons.Launch
|
||||
icon: icons.Deployment
|
||||
},
|
||||
{
|
||||
label: 'resources.modelfiles.retry.download',
|
||||
|
||||
@@ -517,7 +517,7 @@ export const getBackendParamsTips = (backend: string) => {
|
||||
return {
|
||||
backend: 'vLLM',
|
||||
releases: 'https://github.com/vllm-project/vllm/releases',
|
||||
link: 'https://docs.vllm.ai/en/latest/configuration/engine_args.html',
|
||||
link: 'https://docs.vllm.ai/en/stable/cli/serve.html',
|
||||
version: 'v0.8.5'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,15 +113,25 @@ const options = [
|
||||
label: '--tool-call-parser',
|
||||
value: '--tool-call-parser',
|
||||
options: [
|
||||
'mistral',
|
||||
'hermes',
|
||||
'internlm',
|
||||
'jamba',
|
||||
'llama3_json',
|
||||
'granite-20b-fc',
|
||||
'phi4_mini_json',
|
||||
'llama3_json',
|
||||
'llama4_json',
|
||||
'pythonic',
|
||||
'jamba',
|
||||
'xlam',
|
||||
'llama4_pythonic',
|
||||
'hunyuan_a13b',
|
||||
'mistral',
|
||||
'deepseek_v3',
|
||||
'kimi_k2',
|
||||
'step3',
|
||||
'qwen3_coder',
|
||||
'hermes',
|
||||
'glm45',
|
||||
'granite',
|
||||
'pythonic'
|
||||
'minimax',
|
||||
'granite-20b-fc',
|
||||
'internlm'
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -229,7 +239,16 @@ const options = [
|
||||
{
|
||||
label: '--reasoning-parser',
|
||||
value: '--reasoning-parser',
|
||||
options: ['deepseek_r1']
|
||||
options: [
|
||||
'deepseek_r1',
|
||||
'glm45',
|
||||
'GptOss',
|
||||
'granite',
|
||||
'hunyuan_a13b',
|
||||
'mistral',
|
||||
'qwen3',
|
||||
'step3'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '--config-format',
|
||||
|
||||
@@ -535,13 +535,22 @@ export const useCheckCompatibility = () => {
|
||||
cacheFormValuesRef.current = {};
|
||||
};
|
||||
|
||||
const noLocalPathValue = (allValues: any) => {
|
||||
return (
|
||||
allValues.source === modelSourceMap.local_path_value &&
|
||||
!allValues.local_path
|
||||
);
|
||||
};
|
||||
const handleOnValuesChange = async (params: {
|
||||
changedValues: any;
|
||||
allValues: any;
|
||||
source: string;
|
||||
}) => {
|
||||
const { allValues, source } = params;
|
||||
if (_.isEqual(cacheFormValuesRef.current, allValues)) {
|
||||
if (
|
||||
_.isEqual(cacheFormValuesRef.current, allValues) ||
|
||||
noLocalPathValue(allValues)
|
||||
) {
|
||||
console.log('No changes detected, skipping evaluation.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@ import qs from 'query-string';
|
||||
|
||||
export const AUTH_API = '/auth';
|
||||
|
||||
export const AUTH_CONFIG_API = '/auth-config';
|
||||
|
||||
export const AUTH_OIDC_LOGIN_API = '/auth/oidc/login';
|
||||
export const AUTH_SAML_LOGIN_API = '/auth/saml/login';
|
||||
|
||||
export const login = async (
|
||||
params: { username: string; password: string },
|
||||
options?: any
|
||||
@@ -38,3 +43,7 @@ export const updatePassword = async (params: any) => {
|
||||
data: params
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchAuthConfig = async () => {
|
||||
return request(AUTH_CONFIG_API);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import SealInput from '@/components/seal-form/seal-input';
|
||||
import externalLinks from '@/constants/external-links';
|
||||
import { LockOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Checkbox, Form, FormInstance } from 'antd';
|
||||
|
||||
interface LocalUserFormProps {
|
||||
handleLogin: (values: any) => void;
|
||||
form: FormInstance;
|
||||
}
|
||||
|
||||
const LocalUserForm: React.FC<LocalUserFormProps> = (props) => {
|
||||
const { handleLogin, form } = props;
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
style={{ width: '360px', margin: '0 auto' }}
|
||||
onFinish={handleLogin}
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage(
|
||||
{ id: 'common.form.rule.input' },
|
||||
{
|
||||
name: intl.formatMessage({ id: 'common.form.username' })
|
||||
}
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Input
|
||||
label={intl.formatMessage({ id: 'common.form.username' })}
|
||||
prefix={<UserOutlined />}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage(
|
||||
{ id: 'common.form.rule.input' },
|
||||
{
|
||||
name: intl.formatMessage({ id: 'common.form.password' })
|
||||
}
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Password
|
||||
prefix={<LockOutlined />}
|
||||
label={intl.formatMessage({ id: 'common.form.password' })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div
|
||||
className="flex-center flex-between"
|
||||
style={{
|
||||
marginBottom: 24
|
||||
}}
|
||||
>
|
||||
<Form.Item noStyle name="autoLogin" valuePropName="checked">
|
||||
<Checkbox style={{ marginLeft: 5 }}>
|
||||
<span style={{ color: 'var(--ant-color-text-secondary)' }}>
|
||||
{intl.formatMessage({ id: 'common.login.rember' })}
|
||||
</span>
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
href={externalLinks.resetPassword}
|
||||
target="_blank"
|
||||
style={{ padding: 0 }}
|
||||
>
|
||||
{intl.formatMessage({ id: 'common.button.forgotpassword' })}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
block
|
||||
style={{ height: '48px', fontSize: '14px' }}
|
||||
>
|
||||
{intl.formatMessage({ id: 'common.button.login' })}
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocalUserForm;
|
||||
@@ -1,67 +1,108 @@
|
||||
import LogoIcon from '@/assets/images/gpustack-logo.png';
|
||||
import { initialPasswordAtom, userAtom } from '@/atoms/user';
|
||||
import LangSelect from '@/components/lang-select';
|
||||
import SealInput from '@/components/seal-form/seal-input';
|
||||
import ThemeDropActions from '@/components/theme-toggle/theme-drop-actions';
|
||||
import externalLinks from '@/constants/external-links';
|
||||
import {
|
||||
CRYPT_TEXT,
|
||||
REMEMBER_ME_KEY,
|
||||
getRememberMe,
|
||||
rememberMe,
|
||||
removeRememberMe
|
||||
} from '@/utils/localstore/index';
|
||||
import { LockOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { userAtom } from '@/atoms/user';
|
||||
import { useIntl, useModel } from '@umijs/max';
|
||||
import { Button, Checkbox, Form } from 'antd';
|
||||
import { Button, Divider, Form, Spin, message } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import CryptoJS from 'crypto-js';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { login } from '../apis';
|
||||
import styled from 'styled-components';
|
||||
import { useLocalAuth } from '../hooks/use-local-auth';
|
||||
import { useSSOAuth } from '../hooks/use-sso-auth';
|
||||
import { checkDefaultPage } from '../utils';
|
||||
import LocalUserForm from './local-user-form';
|
||||
|
||||
const SpinContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 300px;
|
||||
.spin {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const Buttons = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
gap: 24px;
|
||||
width: 360px;
|
||||
margin-top: 52px;
|
||||
`;
|
||||
|
||||
const BackButton = styled(Button).attrs({
|
||||
type: 'link',
|
||||
size: 'small',
|
||||
block: true
|
||||
})`
|
||||
margin-top: 20px;
|
||||
`;
|
||||
|
||||
const ButtonWrapper = styled(Button).attrs({
|
||||
type: 'primary',
|
||||
block: true
|
||||
})`
|
||||
height: 48px;
|
||||
`;
|
||||
|
||||
const ButtonText = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const DividerWrapper = styled(Divider)`
|
||||
margin-block: 24px !important;
|
||||
.ant-divider-inner-text {
|
||||
color: var(--ant-color-text-secondary);
|
||||
}
|
||||
`;
|
||||
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
header: css`
|
||||
errorMessage: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 60px;
|
||||
padding: 20px;
|
||||
.anticon-global {
|
||||
color: ${token.colorText};
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
color: ${token.colorText};
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
.anticon:hover {
|
||||
color: ${token.colorTextTertiary};
|
||||
`,
|
||||
welcome: css`
|
||||
display: flex;
|
||||
margin-bottom: 32px;
|
||||
font-size: 20px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.text {
|
||||
color: ${token.colorText};
|
||||
}
|
||||
`
|
||||
}));
|
||||
|
||||
const LoginForm = () => {
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const { styles } = useStyles();
|
||||
const [userInfo, setUserInfo] = useAtom(userAtom);
|
||||
const [initialPassword, setInitialPassword] = useAtom(initialPasswordAtom);
|
||||
const { initialState, setInitialState } = useModel('@@initialState') || {};
|
||||
const [authError, setAuthError] = useState<Error | null>(null);
|
||||
const intl = useIntl();
|
||||
const [form] = Form.useForm();
|
||||
const [isPassword, setIsPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const renderWelCome = useMemo(() => {
|
||||
const renderWelCome = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
marginBottom: 32,
|
||||
fontSize: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<div className={styles.welcome}>
|
||||
<div className="flex-center">
|
||||
<span>{intl?.formatMessage({ id: 'users.login.title' })}</span>
|
||||
<span className="text">
|
||||
{intl?.formatMessage({ id: 'users.login.title' })}
|
||||
</span>
|
||||
<img
|
||||
src={LogoIcon}
|
||||
alt="logo"
|
||||
@@ -70,7 +111,7 @@ const LoginForm = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [intl]);
|
||||
};
|
||||
|
||||
const gotoDefaultPage = async (userInfo: any) => {
|
||||
checkDefaultPage(userInfo, true);
|
||||
@@ -90,147 +131,138 @@ const LoginForm = () => {
|
||||
return userInfo;
|
||||
};
|
||||
|
||||
const encryptPassword = (password: string) => {
|
||||
const psw = CryptoJS.AES?.encrypt?.(password, CRYPT_TEXT).toString();
|
||||
return psw;
|
||||
};
|
||||
const decryptPassword = (password: string) => {
|
||||
const bytes = CryptoJS.AES?.decrypt?.(password, CRYPT_TEXT);
|
||||
const res = bytes.toString(CryptoJS.enc.Utf8);
|
||||
return res;
|
||||
// error handling for authentication
|
||||
const handleOnError = (error: Error) => {
|
||||
setAuthError(error);
|
||||
messageApi.error({
|
||||
duration: 5,
|
||||
content: (
|
||||
<div className={styles.errorMessage}>
|
||||
<div className="title">
|
||||
{intl.formatMessage({ id: 'common.login.auth.failed' })}
|
||||
</div>
|
||||
<div className="message">{error?.message || 'Unknown error'}</div>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
const callRememberMe = async (values: any) => {
|
||||
const { autoLogin } = values;
|
||||
if (autoLogin) {
|
||||
await rememberMe(REMEMBER_ME_KEY, {
|
||||
um: encryptPassword(values.username),
|
||||
pw: encryptPassword(values.password),
|
||||
f: true
|
||||
});
|
||||
} else {
|
||||
await removeRememberMe(REMEMBER_ME_KEY);
|
||||
}
|
||||
};
|
||||
|
||||
const callGetRememberMe = async () => {
|
||||
const rememberMe = await getRememberMe(REMEMBER_ME_KEY);
|
||||
|
||||
if (rememberMe?.f) {
|
||||
const username = decryptPassword(rememberMe?.um);
|
||||
const password = decryptPassword(rememberMe?.pw);
|
||||
form.setFieldsValue({ username, password, autoLogin: true });
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogin = async (values: any) => {
|
||||
try {
|
||||
await login({
|
||||
username: values.username,
|
||||
password: values.password
|
||||
});
|
||||
|
||||
const userInfo = await fetchUserInfo();
|
||||
// local user authentication
|
||||
const { handleLogin } = useLocalAuth({
|
||||
fetchUserInfo,
|
||||
form,
|
||||
onSuccess: async (userInfo) => {
|
||||
setUserInfo(userInfo);
|
||||
if (values.autoLogin) {
|
||||
await callRememberMe(values);
|
||||
} else {
|
||||
await removeRememberMe(REMEMBER_ME_KEY);
|
||||
}
|
||||
if (!userInfo?.require_password_change) {
|
||||
gotoDefaultPage(userInfo);
|
||||
} else {
|
||||
setInitialPassword(encryptPassword(values.password));
|
||||
}
|
||||
} catch (error) {
|
||||
// to do something
|
||||
},
|
||||
|
||||
onError: (error) => {
|
||||
// gpustack handle in the interceptor
|
||||
}
|
||||
});
|
||||
|
||||
// SSO hook
|
||||
const SSOAuth = useSSOAuth({
|
||||
fetchUserInfo,
|
||||
onSuccess: (userInfo) => {
|
||||
setUserInfo(userInfo);
|
||||
gotoDefaultPage({});
|
||||
},
|
||||
onLoading: (loading) => {
|
||||
setLoading(loading);
|
||||
},
|
||||
onError: handleOnError
|
||||
});
|
||||
|
||||
const handleLoginWithPassword = () => {
|
||||
setIsPassword(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
callGetRememberMe();
|
||||
}, []);
|
||||
const handleLoginWithThirdParty = () => {
|
||||
if (SSOAuth.options.oidc) {
|
||||
SSOAuth.loginWithOIDC();
|
||||
} else if (SSOAuth.options.saml) {
|
||||
SSOAuth.loginWithSAML();
|
||||
}
|
||||
setLoading(true);
|
||||
setAuthError(null);
|
||||
};
|
||||
|
||||
const hasThirdPartyLogin = useMemo(() => {
|
||||
return SSOAuth.options.oidc || SSOAuth.options.saml;
|
||||
}, [SSOAuth.options]);
|
||||
|
||||
const renderLoginButtons = () => {
|
||||
// do not render login buttons if using password login or no third-party login
|
||||
if (!hasThirdPartyLogin || isPassword) return null;
|
||||
|
||||
return (
|
||||
<Buttons>
|
||||
{SSOAuth.options.oidc && (
|
||||
<ButtonWrapper onClick={SSOAuth.loginWithOIDC}>
|
||||
<ButtonText>
|
||||
{intl.formatMessage(
|
||||
{ id: 'common.external.login' },
|
||||
{ type: 'SSO' }
|
||||
)}
|
||||
</ButtonText>
|
||||
</ButtonWrapper>
|
||||
)}
|
||||
{SSOAuth.options.saml && (
|
||||
<ButtonWrapper onClick={SSOAuth.loginWithSAML}>
|
||||
<ButtonText>
|
||||
{intl.formatMessage(
|
||||
{ id: 'common.external.login' },
|
||||
{ type: 'SSO' }
|
||||
)}
|
||||
</ButtonText>
|
||||
</ButtonWrapper>
|
||||
)}
|
||||
<Button type="link" block onClick={handleLoginWithPassword}>
|
||||
<ButtonText>
|
||||
{intl.formatMessage({ id: 'common.login.password' })}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</Buttons>
|
||||
);
|
||||
};
|
||||
|
||||
const isThirdPartyAuthHandling = useMemo(() => {
|
||||
return loading && !authError;
|
||||
}, [loading, authError]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<ThemeDropActions></ThemeDropActions>
|
||||
<LangSelect />
|
||||
</div>
|
||||
{contextHolder}
|
||||
<div>
|
||||
<Form
|
||||
form={form}
|
||||
style={{ width: '360px', margin: '0 auto' }}
|
||||
onFinish={handleLogin}
|
||||
>
|
||||
{renderWelCome}
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage(
|
||||
{ id: 'common.form.rule.input' },
|
||||
{ name: intl.formatMessage({ id: 'common.form.username' }) }
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Input
|
||||
label={intl.formatMessage({ id: 'common.form.username' })}
|
||||
prefix={<UserOutlined />}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage(
|
||||
{ id: 'common.form.rule.input' },
|
||||
{ name: intl.formatMessage({ id: 'common.form.password' }) }
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Password
|
||||
prefix={<LockOutlined />}
|
||||
label={intl.formatMessage({ id: 'common.form.password' })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div
|
||||
className="flex-center flex-between"
|
||||
style={{
|
||||
marginBottom: 24
|
||||
}}
|
||||
>
|
||||
<Form.Item noStyle name="autoLogin" valuePropName="checked">
|
||||
<Checkbox style={{ marginLeft: 5 }}>
|
||||
<span style={{ color: 'var(--ant-color-text-secondary)' }}>
|
||||
{intl.formatMessage({ id: 'common.login.rember' })}
|
||||
</span>
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
href={externalLinks.resetPassword}
|
||||
target="_blank"
|
||||
style={{ padding: 0 }}
|
||||
>
|
||||
{intl.formatMessage({ id: 'common.button.forgotpassword' })}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
block
|
||||
style={{ height: '48px', fontSize: '14px' }}
|
||||
>
|
||||
{intl.formatMessage({ id: 'common.button.login' })}
|
||||
</Button>
|
||||
</Form>
|
||||
{isThirdPartyAuthHandling ? (
|
||||
<SpinContainer>
|
||||
{renderWelCome()}
|
||||
<div className="spin">
|
||||
<Spin tip={intl.formatMessage({ id: 'common.login.auth' })}>
|
||||
<div style={{ width: 300 }}></div>
|
||||
</Spin>
|
||||
</div>
|
||||
</SpinContainer>
|
||||
) : (
|
||||
<>
|
||||
{renderWelCome()}
|
||||
{renderLoginButtons()}
|
||||
{(!hasThirdPartyLogin || isPassword) && (
|
||||
<LocalUserForm handleLogin={handleLogin} form={form} />
|
||||
)}
|
||||
{hasThirdPartyLogin && isPassword && (
|
||||
<BackButton onClick={handleLoginWithThirdParty}>
|
||||
{intl.formatMessage(
|
||||
{ id: 'common.external.login' },
|
||||
{ type: 'SSO' }
|
||||
)}
|
||||
</BackButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,8 @@ import { initialPasswordAtom, userAtom } from '@/atoms/user';
|
||||
import SealInput from '@/components/seal-form/seal-input';
|
||||
import { PasswordReg } from '@/config';
|
||||
import { CRYPT_TEXT } from '@/utils/localstore/index';
|
||||
import { GlobalOutlined, LockOutlined } from '@ant-design/icons';
|
||||
import { SelectLang, useIntl } from '@umijs/max';
|
||||
import { LockOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Form, message } from 'antd';
|
||||
import CryptoJS from 'crypto-js';
|
||||
import { useAtom } from 'jotai';
|
||||
@@ -47,9 +47,6 @@ const PasswordForm: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ position: 'fixed', right: 0, top: 0, padding: '0 20px' }}>
|
||||
<SelectLang icon={<GlobalOutlined />} reload={false} />
|
||||
</div>
|
||||
<Form
|
||||
form={form}
|
||||
style={{ width: '360px', margin: '0 auto' }}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { initialPasswordAtom } from '@/atoms/user';
|
||||
import {
|
||||
CRYPT_TEXT,
|
||||
REMEMBER_ME_KEY,
|
||||
getRememberMe,
|
||||
rememberMe,
|
||||
removeRememberMe
|
||||
} from '@/utils/localstore/index';
|
||||
import { FormInstance } from 'antd';
|
||||
import CryptoJS from 'crypto-js';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useEffect } from 'react';
|
||||
import { login } from '../apis';
|
||||
|
||||
interface UseLocalAuthOptions {
|
||||
fetchUserInfo: () => Promise<any>;
|
||||
onSuccess?: (userInfo: any) => void;
|
||||
onError?: (error: Error) => void;
|
||||
form: FormInstance;
|
||||
}
|
||||
|
||||
export const useLocalAuth = ({
|
||||
fetchUserInfo,
|
||||
onSuccess,
|
||||
onError,
|
||||
form
|
||||
}: UseLocalAuthOptions) => {
|
||||
const [initialPassword, setInitialPassword] = useAtom(initialPasswordAtom);
|
||||
|
||||
// Encrypt password before storing
|
||||
const encryptPassword = (password: string) => {
|
||||
const psw = CryptoJS.AES?.encrypt?.(password, CRYPT_TEXT).toString();
|
||||
return psw;
|
||||
};
|
||||
|
||||
const decryptPassword = (password: string) => {
|
||||
const bytes = CryptoJS.AES?.decrypt?.(password, CRYPT_TEXT);
|
||||
const res = bytes.toString(CryptoJS.enc.Utf8);
|
||||
return res;
|
||||
};
|
||||
|
||||
const callGetRememberMe = async () => {
|
||||
const rememberMe = await getRememberMe(REMEMBER_ME_KEY);
|
||||
|
||||
if (rememberMe?.f) {
|
||||
const username = decryptPassword(rememberMe?.um);
|
||||
const password = decryptPassword(rememberMe?.pw);
|
||||
form.setFieldsValue({ username, password, autoLogin: true });
|
||||
}
|
||||
};
|
||||
|
||||
// remember me
|
||||
const callRememberMe = async (values: any) => {
|
||||
const { autoLogin } = values;
|
||||
if (autoLogin) {
|
||||
await rememberMe(REMEMBER_ME_KEY, {
|
||||
um: encryptPassword(values.username),
|
||||
pw: encryptPassword(values.password),
|
||||
f: true
|
||||
});
|
||||
} else {
|
||||
await removeRememberMe(REMEMBER_ME_KEY);
|
||||
}
|
||||
};
|
||||
|
||||
// click login button
|
||||
const handleLogin = async (values: any) => {
|
||||
try {
|
||||
await login({
|
||||
username: values.username,
|
||||
password: values.password
|
||||
});
|
||||
|
||||
const userInfo = await fetchUserInfo();
|
||||
if (values.autoLogin) {
|
||||
await callRememberMe(values);
|
||||
} else {
|
||||
await removeRememberMe(REMEMBER_ME_KEY);
|
||||
}
|
||||
|
||||
if (userInfo?.require_password_change) {
|
||||
setInitialPassword(encryptPassword(values.password));
|
||||
}
|
||||
|
||||
onSuccess?.(userInfo);
|
||||
} catch (error: any) {
|
||||
onError?.(error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
callGetRememberMe();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
handleLogin
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
// hooks/useSSOAuth.ts
|
||||
import { history, useIntl } from '@umijs/max';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
AUTH_OIDC_LOGIN_API,
|
||||
AUTH_SAML_LOGIN_API,
|
||||
fetchAuthConfig
|
||||
} from '../apis';
|
||||
|
||||
type LoginOption = {
|
||||
saml: boolean;
|
||||
oidc: boolean;
|
||||
};
|
||||
|
||||
export function useSSOAuth({
|
||||
fetchUserInfo,
|
||||
onSuccess,
|
||||
onLoading,
|
||||
onError
|
||||
}: {
|
||||
fetchUserInfo: () => Promise<any>;
|
||||
onSuccess?: (userInfo: any) => void;
|
||||
onError?: (err: Error) => void;
|
||||
onLoading?: (loading: boolean) => void;
|
||||
}) {
|
||||
const [loginOption, setLoginOption] = useState<LoginOption>({
|
||||
saml: false,
|
||||
oidc: false
|
||||
});
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
const { location } = history;
|
||||
const params = new URLSearchParams(location.search);
|
||||
const sso = params.get('sso');
|
||||
|
||||
const oidcLogin = () => {
|
||||
window.location.href = AUTH_OIDC_LOGIN_API;
|
||||
};
|
||||
|
||||
const samlLogin = () => {
|
||||
window.location.href = AUTH_SAML_LOGIN_API;
|
||||
};
|
||||
|
||||
const init = async () => {
|
||||
try {
|
||||
const authConfig = await fetchAuthConfig();
|
||||
setLoginOption({
|
||||
oidc: !!authConfig.is_oidc,
|
||||
saml: !!authConfig.is_saml
|
||||
});
|
||||
if (sso) {
|
||||
onLoading?.(true);
|
||||
if (authConfig.is_oidc) {
|
||||
oidcLogin();
|
||||
} else if (authConfig.is_saml) {
|
||||
samlLogin();
|
||||
} else {
|
||||
onError?.(
|
||||
new Error(intl.formatMessage({ id: 'common.sso.noConfig' }))
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
setLoginOption({ oidc: false, saml: false });
|
||||
onLoading?.(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
init();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isSSOLogin: !!sso,
|
||||
options: loginOption,
|
||||
loginWithOIDC: oidcLogin,
|
||||
loginWithSAML: samlLogin
|
||||
};
|
||||
}
|
||||
@@ -2,9 +2,12 @@ import Bg2 from '@/assets/images/bg-2.png';
|
||||
import { userAtom } from '@/atoms/user';
|
||||
import DarkMask from '@/components/dark-mask';
|
||||
import Footer from '@/components/footer';
|
||||
import LangSelect from '@/components/lang-select';
|
||||
import ThemeDropActions from '@/components/theme-toggle/theme-drop-actions';
|
||||
import useUserSettings from '@/hooks/use-user-settings';
|
||||
import { useModel } from '@umijs/max';
|
||||
import { ConfigProvider, theme } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
@@ -12,6 +15,29 @@ import LoginForm from './components/login-form';
|
||||
import PasswordForm from './components/password-form';
|
||||
import { checkDefaultPage } from './utils';
|
||||
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
header: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 60px;
|
||||
padding: 20px;
|
||||
.anticon-global {
|
||||
color: ${token.colorText};
|
||||
}
|
||||
.anticon:hover {
|
||||
color: ${token.colorTextTertiary};
|
||||
}
|
||||
`,
|
||||
formContainer: css`
|
||||
height: calc(100vh - 64px);
|
||||
width: 100%;
|
||||
`
|
||||
}));
|
||||
|
||||
const Wrapper = styled.div<{ $isDarkTheme: boolean }>`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -30,7 +56,6 @@ const Wrapper = styled.div<{ $isDarkTheme: boolean }>`
|
||||
`;
|
||||
|
||||
const Box = styled.div`
|
||||
padding-top: 10%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
@@ -40,12 +65,17 @@ const Box = styled.div`
|
||||
const FormWrapper = styled.div`
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
z-index: 999;
|
||||
border-radius: var(--border-radius-modal);
|
||||
width: max-content;
|
||||
height: max-content;
|
||||
max-width: 800px;
|
||||
max-height: 600px;
|
||||
padding: 40px;
|
||||
background-color: var(--color-modal-content-bg);
|
||||
box-shadow: var(--color-modal-box-shadow);
|
||||
margin-top: 50vh;
|
||||
transform: translateY(-50%);
|
||||
.field-wrapper {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
@@ -56,6 +86,7 @@ const FormWrapper = styled.div`
|
||||
`;
|
||||
|
||||
const Login = () => {
|
||||
const { styles } = useStyles();
|
||||
const { themeData, userSettings, isDarkTheme } = useUserSettings();
|
||||
const [userInfo, setUserInfo] = useAtom(userAtom);
|
||||
const { initialState, setInitialState } = useModel('@@initialState') || {};
|
||||
@@ -93,10 +124,14 @@ const Login = () => {
|
||||
...themeData
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<DarkMask></DarkMask>
|
||||
<Wrapper $isDarkTheme={isDarkTheme}></Wrapper>
|
||||
<Box>
|
||||
<div className={styles.header}>
|
||||
<ThemeDropActions></ThemeDropActions>
|
||||
<LangSelect />
|
||||
</div>
|
||||
<DarkMask></DarkMask>
|
||||
<Wrapper $isDarkTheme={isDarkTheme}></Wrapper>
|
||||
<Box>
|
||||
<div className={styles.formContainer}>
|
||||
<FormWrapper>
|
||||
{userInfo?.require_password_change ? (
|
||||
<PasswordForm />
|
||||
@@ -104,9 +139,9 @@ const Login = () => {
|
||||
<LoginForm />
|
||||
)}
|
||||
</FormWrapper>
|
||||
<Footer />
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</Box>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -168,11 +168,11 @@ export default function useChatCompletion(
|
||||
if (chunk?.error) {
|
||||
setTokenResult({
|
||||
error: true,
|
||||
errorMessage: extractErrorMessage(chunk)
|
||||
errorMessage: extractErrorMessage(chunk.error)
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
joinMessage(chunk);
|
||||
}
|
||||
joinMessage(chunk);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('error:', error);
|
||||
|
||||
+28
-14
@@ -1,8 +1,8 @@
|
||||
import useTabActive from '@/hooks/use-tab-active';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { useIntl, useModel } from '@umijs/max';
|
||||
import { TabsProps } from 'antd';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Appearance from './components/appearance';
|
||||
import ModifyPasswordn from './components/modify-password';
|
||||
@@ -15,23 +15,37 @@ const Wrapper = styled.div`
|
||||
|
||||
const Profile: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { initialState, setInitialState } = useModel('@@initialState') || {};
|
||||
const { setTabActive, getTabActive, tabsMap } = useTabActive();
|
||||
const [activeKey, setActiveKey] = useState(
|
||||
getTabActive(tabsMap.userSettings) || 'modify-password'
|
||||
initialState?.currentUser?.source === 'Local'
|
||||
? 'modify-password'
|
||||
: 'appearance'
|
||||
);
|
||||
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
key: 'modify-password',
|
||||
label: intl.formatMessage({ id: 'users.form.updatepassword' }),
|
||||
children: <ModifyPasswordn />
|
||||
},
|
||||
{
|
||||
key: 'appearance',
|
||||
label: intl.formatMessage({ id: 'common.appearance' }),
|
||||
children: <Appearance />
|
||||
const items: TabsProps['items'] = useMemo(() => {
|
||||
if (initialState?.currentUser?.source !== 'Local') {
|
||||
return [
|
||||
{
|
||||
key: 'appearance',
|
||||
label: intl.formatMessage({ id: 'common.appearance' }),
|
||||
children: <Appearance />
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
return [
|
||||
{
|
||||
key: 'modify-password',
|
||||
label: intl.formatMessage({ id: 'users.form.updatepassword' }),
|
||||
children: <ModifyPasswordn />
|
||||
},
|
||||
{
|
||||
key: 'appearance',
|
||||
label: intl.formatMessage({ id: 'common.appearance' }),
|
||||
children: <Appearance />
|
||||
}
|
||||
];
|
||||
}, [initialState?.currentUser?.source]);
|
||||
|
||||
const handleChangeTab = useCallback((key: string) => {
|
||||
setActiveKey(key);
|
||||
|
||||
@@ -3,6 +3,8 @@ export interface ListItem {
|
||||
is_admin: boolean;
|
||||
full_name: string;
|
||||
id: number;
|
||||
source: string;
|
||||
avatar_url: string;
|
||||
username: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import AutoTooltip from '@/components/auto-tooltip';
|
||||
import DeleteModal from '@/components/delete-modal';
|
||||
import DropdownButtons from '@/components/drop-down-buttons';
|
||||
import IconFont from '@/components/icon-font';
|
||||
import PageTools from '@/components/page-tools';
|
||||
import { PageAction } from '@/config';
|
||||
import HotKeys from '@/config/hotkeys';
|
||||
@@ -10,9 +11,7 @@ import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
PlusOutlined,
|
||||
SyncOutlined,
|
||||
UserOutlined,
|
||||
UserSwitchOutlined
|
||||
SyncOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { useIntl } from '@umijs/max';
|
||||
@@ -86,6 +85,7 @@ const Users: React.FC = () => {
|
||||
|
||||
const handleModalOk = async (data: FormData) => {
|
||||
const params = {
|
||||
...currentData,
|
||||
...data,
|
||||
is_admin: data.is_admin === 'admin'
|
||||
};
|
||||
@@ -247,14 +247,17 @@ const Users: React.FC = () => {
|
||||
render={(text, record: ListItem) => {
|
||||
return record.is_admin ? (
|
||||
<AutoTooltip ghost minWidth={50}>
|
||||
<UserSwitchOutlined className="size-16" />
|
||||
<IconFont
|
||||
type="icon-admin-user"
|
||||
className="size-16"
|
||||
></IconFont>
|
||||
<span className="m-l-5">
|
||||
{intl.formatMessage({ id: 'users.form.admin' })}
|
||||
</span>
|
||||
</AutoTooltip>
|
||||
) : (
|
||||
<AutoTooltip ghost minWidth={50}>
|
||||
<UserOutlined className="size-16" />
|
||||
<IconFont type="icon-user" className="size-16"></IconFont>
|
||||
<span className="m-l-5">
|
||||
{intl.formatMessage({ id: 'users.form.user' })}
|
||||
</span>
|
||||
@@ -277,6 +280,21 @@ const Users: React.FC = () => {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
title={intl.formatMessage({ id: 'users.form.source' })}
|
||||
dataIndex="source"
|
||||
key="source"
|
||||
ellipsis={{
|
||||
showTitle: false
|
||||
}}
|
||||
render={(text, record) => {
|
||||
return (
|
||||
<AutoTooltip ghost minWidth={20}>
|
||||
{text}
|
||||
</AutoTooltip>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
title={intl.formatMessage({ id: 'common.table.createTime' })}
|
||||
dataIndex="created_at"
|
||||
|
||||
@@ -3,6 +3,7 @@ import { clearAtomStorage } from '@/atoms/utils';
|
||||
import { RequestConfig, history } from '@umijs/max';
|
||||
import { message } from 'antd';
|
||||
|
||||
// no base URL APIs
|
||||
const NoBaseURLAPIs = ['/auth', '/v1-openai', '/version', '/proxy', '/update'];
|
||||
|
||||
export const requestConfig: RequestConfig = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
export async function queryCurrentUserState(opts?: Record<string, any>) {
|
||||
return request(`/users/me`, {
|
||||
return request<Global.UserInfo>(`/users/me`, {
|
||||
method: 'GET',
|
||||
...opts
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { throttle } from 'lodash';
|
||||
import qs from 'query-string';
|
||||
|
||||
const extractStreamRegx = /data:\s*({.*?})(?=\n|$)/g;
|
||||
const extractStreamRegx = /(data|error):\s*({.*?})(?=\n|$)/g;
|
||||
|
||||
const extractJSON = (dataStr: string) => {
|
||||
let match;
|
||||
@@ -12,11 +12,16 @@ const extractJSON = (dataStr: string) => {
|
||||
}
|
||||
while ((match = extractStreamRegx.exec(dataStr)) !== null) {
|
||||
try {
|
||||
const jsonData = JSON.parse(match[1]);
|
||||
results.push(jsonData);
|
||||
} catch (error) {
|
||||
console.error('JSON parse error:', error, 'for match:', match[1]);
|
||||
|
||||
const type = match[1]; // "data" or "error"
|
||||
console.log('type========', type);
|
||||
const jsonData = JSON.parse(match[2]);
|
||||
if (type === 'error') {
|
||||
results.push({ error: jsonData });
|
||||
} else {
|
||||
results.push(jsonData);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('JSON parse error:', err, 'for match:', match[2]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -162,33 +167,40 @@ export const readStreamData = async (
|
||||
}, throttleDelay);
|
||||
|
||||
let isReading = true;
|
||||
let textBuffer = ''; // cache incomplete line
|
||||
|
||||
while (isReading) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
isReading = false;
|
||||
bufferManager.flush();
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
textBuffer += decoder.decode(value, { stream: true });
|
||||
|
||||
if (chunk.startsWith('error:')) {
|
||||
const errorStr = chunk.slice(7).trim();
|
||||
if (textBuffer.startsWith('error:')) {
|
||||
const errorStr = textBuffer.slice(7).trim();
|
||||
const jsonData = JSON.parse(errorStr);
|
||||
bufferManager.add({ error: jsonData });
|
||||
textBuffer = ''; // Clear buffer after processing error
|
||||
} else {
|
||||
extractJSON(chunk).forEach((data) => {
|
||||
extractJSON(textBuffer).forEach((data) => {
|
||||
bufferManager.add(data);
|
||||
});
|
||||
textBuffer = ''; // Clear buffer after processing
|
||||
}
|
||||
|
||||
throttledCallback();
|
||||
} catch (error) {
|
||||
bufferManager.add({ error });
|
||||
}
|
||||
|
||||
if (done) {
|
||||
textBuffer += decoder.decode();
|
||||
if (textBuffer) {
|
||||
extractJSON(textBuffer).forEach((data) => bufferManager.add(data));
|
||||
}
|
||||
isReading = false;
|
||||
bufferManager.flush();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+3
-2
@@ -22,7 +22,7 @@ export const handleBatchRequest = async (
|
||||
};
|
||||
|
||||
export const convertFileSize = (
|
||||
sizeInBytes?: number,
|
||||
sizeInBytes: number,
|
||||
prec = 1,
|
||||
allowEmpty = false
|
||||
): string | number => {
|
||||
@@ -31,6 +31,7 @@ export const convertFileSize = (
|
||||
const fmt = 1024;
|
||||
|
||||
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
|
||||
const precs = [0, 1, 1, 2, 2]; // precision for each unit
|
||||
let size = sizeInBytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
@@ -39,7 +40,7 @@ export const convertFileSize = (
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${_.round(size, prec)} ${units[unitIndex]}`;
|
||||
return `${_.round(size, precs[unitIndex])} ${units[unitIndex]}`;
|
||||
};
|
||||
|
||||
export const platformCall = () => {
|
||||
|
||||
Reference in New Issue
Block a user