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',
|
name: 'deployment',
|
||||||
path: '/models/deployments',
|
path: '/models/deployments',
|
||||||
key: 'modelDeployments',
|
key: 'modelDeployments',
|
||||||
icon: 'icon-model',
|
icon: 'icon-rocket-launch1',
|
||||||
selectedIcon: 'icon-model-filled',
|
selectedIcon: 'icon-rocket-launch-fill',
|
||||||
defaultIcon: 'icon-model',
|
defaultIcon: 'icon-rocket-launch1',
|
||||||
access: 'canSeeAdmin',
|
access: 'canSeeAdmin',
|
||||||
component: './llmodels/index'
|
component: './llmodels/index'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@
|
|||||||
"react-hotkeys-hook": "^4.5.0",
|
"react-hotkeys-hook": "^4.5.0",
|
||||||
"react-markdown": "^9.0.3",
|
"react-markdown": "^9.0.3",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
|
"remark-breaks": "^4.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"simplebar-react": "^3.2.6",
|
"simplebar-react": "^3.2.6",
|
||||||
|
|||||||
Generated
+18
@@ -165,6 +165,9 @@ dependencies:
|
|||||||
rehype-katex:
|
rehype-katex:
|
||||||
specifier: ^7.0.1
|
specifier: ^7.0.1
|
||||||
version: 7.0.1
|
version: 7.0.1
|
||||||
|
remark-breaks:
|
||||||
|
specifier: ^4.0.0
|
||||||
|
version: 4.0.0
|
||||||
remark-gfm:
|
remark-gfm:
|
||||||
specifier: ^4.0.1
|
specifier: ^4.0.1
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
@@ -13121,6 +13124,13 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
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:
|
/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}
|
resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==, tarball: https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -17456,6 +17466,14 @@ packages:
|
|||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
dev: false
|
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:
|
/remark-gfm@4.0.1:
|
||||||
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==, tarball: https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz}
|
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==, tarball: https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz}
|
||||||
dependencies:
|
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>(
|
export const userSettingsAtom = atomWithStorage<UserSettings>('userSettings', {
|
||||||
'userSettings',
|
...getStorageUserSettings()
|
||||||
defaultSettings
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const userSettingsHelperAtom = atom(
|
export const userSettingsHelperAtom = atom(
|
||||||
(get) => get(userSettingsAtom),
|
(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 { WarningFilled } from '@ant-design/icons';
|
||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import OverlayScroller, { OverlayScrollerOptions } from '../overlay-scroller';
|
import OverlayScroller, { OverlayScrollerOptions } from '../overlay-scroller';
|
||||||
import './block.less';
|
|
||||||
interface AlertInfoProps {
|
interface AlertInfoProps {
|
||||||
type: Global.MessageType;
|
type: Global.MessageType;
|
||||||
message: React.ReactNode;
|
message: React.ReactNode;
|
||||||
@@ -18,6 +18,81 @@ interface AlertInfoProps {
|
|||||||
overlayScrollerProps?: OverlayScrollerOptions;
|
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`
|
const TitleWrapper = styled.div`
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--ant-color-text);
|
color: var(--ant-color-text);
|
||||||
@@ -46,12 +121,12 @@ const AlertInfo: React.FC<AlertInfoProps> = (props) => {
|
|||||||
maxHeight = 86,
|
maxHeight = 86,
|
||||||
overlayScrollerProps = {}
|
overlayScrollerProps = {}
|
||||||
} = props;
|
} = props;
|
||||||
|
const { styles } = useStyles();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{message ? (
|
{message ? (
|
||||||
<div
|
<div
|
||||||
className={classNames('alert-info-block', type)}
|
className={classNames(styles.alertBlockInfo, type)}
|
||||||
style={{ ...style }}
|
style={{ ...style }}
|
||||||
>
|
>
|
||||||
<Typography.Paragraph
|
<Typography.Paragraph
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const fadeOut = keyframes`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Mask = styled.div<{ isHiding: boolean }>`
|
const Mask = styled.div<{ $isHiding: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -26,7 +26,7 @@ const Mask = styled.div<{ isHiding: boolean }>`
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-color: rgba(59, 59, 59, 1);
|
background-color: rgba(59, 59, 59, 1);
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
animation: ${({ isHiding }) => (isHiding ? fadeOut : 'none')} ${duration}s
|
animation: ${({ $isHiding }) => ($isHiding ? fadeOut : 'none')} ${duration}s
|
||||||
ease forwards;
|
ease forwards;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ const DarkModeMask = () => {
|
|||||||
return () => el.removeEventListener('animationend', handleAnimationEnd);
|
return () => el.removeEventListener('animationend', handleAnimationEnd);
|
||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
return visible ? <Mask ref={maskRef} isHiding={isHiding}></Mask> : null;
|
return visible ? <Mask ref={maskRef} $isHiding={isHiding}></Mask> : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DarkModeMask;
|
export default DarkModeMask;
|
||||||
|
|||||||
@@ -129,9 +129,15 @@ const BarChart: React.FC<ChartProps & { maxItems?: number }> = (props) => {
|
|||||||
legend
|
legend
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const isEmpty = useMemo(() => {
|
||||||
|
return seriesData?.every?.((item: any) => {
|
||||||
|
return !item?.data?.length;
|
||||||
|
});
|
||||||
|
}, [seriesData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!seriesData.length ? (
|
{isEmpty ? (
|
||||||
<EmptyData height={height} title={title}></EmptyData>
|
<EmptyData height={height} title={title}></EmptyData>
|
||||||
) : (
|
) : (
|
||||||
<Chart
|
<Chart
|
||||||
|
|||||||
@@ -56,4 +56,4 @@ const HighlightCode: React.FC<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(HighlightCode);
|
export default HighlightCode;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: iconfont; /* Project id 4613488 */
|
font-family: iconfont; /* Project id 4613488 */
|
||||||
src: url('iconfont.woff2?t=1752112673765') format('woff2'),
|
src: url('iconfont.woff2?t=1755160439217') format('woff2'),
|
||||||
url('iconfont.woff?t=1752112673765') format('woff'),
|
url('iconfont.woff?t=1755160439217') format('woff'),
|
||||||
url('iconfont.ttf?t=1752112673765') format('truetype');
|
url('iconfont.ttf?t=1755160439217') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@@ -13,6 +13,50 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-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 {
|
.icon-huggingface1::before {
|
||||||
content: "\e605";
|
content: "\e605";
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -5,6 +5,83 @@
|
|||||||
"css_prefix_text": "icon-",
|
"css_prefix_text": "icon-",
|
||||||
"description": "",
|
"description": "",
|
||||||
"glyphs": [
|
"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",
|
"icon_id": "40865292",
|
||||||
"name": "huggingface",
|
"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 { GlobalOutlined } from '@ant-design/icons';
|
||||||
import { getAllLocales, setLocale } from '@umijs/max';
|
import { getAllLocales, setLocale } from '@umijs/max';
|
||||||
import { Dropdown } from 'antd';
|
import { Dropdown } from 'antd';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
import { get } from 'lodash';
|
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 LangSelect = () => {
|
||||||
|
const { styles } = useStyles();
|
||||||
const allLocals = getAllLocales();
|
const allLocals = getAllLocales();
|
||||||
const items = allLocals.map((key) => {
|
const items = allLocals.map((key) => {
|
||||||
return {
|
return {
|
||||||
@@ -25,7 +43,7 @@ const LangSelect = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown menu={{ items }}>
|
<Dropdown menu={{ items }}>
|
||||||
<span style={{ padding: '0 12px', cursor: 'pointer' }}>
|
<span className={styles.button}>
|
||||||
<GlobalOutlined />
|
<GlobalOutlined />
|
||||||
</span>
|
</span>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ interface MessageProps {
|
|||||||
chunked?: boolean;
|
chunked?: boolean;
|
||||||
progress?: number;
|
progress?: number;
|
||||||
percent?: number;
|
percent?: number;
|
||||||
|
isDownloading?: boolean;
|
||||||
}
|
}
|
||||||
class AnsiParser {
|
class AnsiParser {
|
||||||
private cursorRow: number = 0;
|
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 chunked: boolean = true; // true: send data in chunks, false: send all data at once
|
||||||
private reminder: string = '';
|
private reminder: string = '';
|
||||||
private lines: string[] = [];
|
private lines: string[] = [];
|
||||||
|
isDownloading: boolean = false;
|
||||||
private pageSize: number = 500;
|
private pageSize: number = 500;
|
||||||
private colorMap = {
|
private colorMap = {
|
||||||
'30': 'black',
|
'30': 'black',
|
||||||
@@ -79,6 +81,10 @@ class AnsiParser {
|
|||||||
this.chunked = chunked ?? true;
|
this.chunked = chunked ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setIsDownloading(isDownloading: boolean) {
|
||||||
|
this.isDownloading = isDownloading;
|
||||||
|
}
|
||||||
|
|
||||||
private setId() {
|
private setId() {
|
||||||
this.uid += 1;
|
this.uid += 1;
|
||||||
return this.uid;
|
return this.uid;
|
||||||
@@ -168,14 +174,18 @@ class AnsiParser {
|
|||||||
const remainingText = input.slice(lastIndex);
|
const remainingText = input.slice(lastIndex);
|
||||||
this.handleText(remainingText);
|
this.handleText(remainingText);
|
||||||
|
|
||||||
const result = this.screen.map((row, index) => ({
|
// const result = this.screen.map((row, index) => ({
|
||||||
content: removeBracketsFromLine(row.join('')),
|
// content: removeBracketsFromLine(row.join('')),
|
||||||
uid: `${this.page}-${index}`
|
// uid: `${this.page}-${index}`
|
||||||
}));
|
// }));
|
||||||
|
const result = this.screen.map((row, index) =>
|
||||||
|
removeBracketsFromLine(row.join(''))
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: result,
|
data: result,
|
||||||
lines: this.rawDataRows
|
lines: this.rawDataRows,
|
||||||
|
remainder: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,12 +229,23 @@ class AnsiParser {
|
|||||||
this.isProcessing = true;
|
this.isProcessing = true;
|
||||||
|
|
||||||
while (this.taskQueue.length > 0) {
|
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) {
|
if (input) {
|
||||||
try {
|
try {
|
||||||
const result = this.processInputByLine(input);
|
const result = this.isDownloading
|
||||||
|
? this.processInput(input)
|
||||||
|
: this.processInputByLine(input);
|
||||||
|
if (!this.isDownloading) {
|
||||||
this.reminder = result.remainder;
|
this.reminder = result.remainder;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.chunked) {
|
if (this.chunked) {
|
||||||
self.postMessage({ result: result.data, lines: result.lines });
|
self.postMessage({ result: result.data, lines: result.lines });
|
||||||
} else if (!this.isComplete) {
|
} else if (!this.isComplete) {
|
||||||
@@ -273,9 +294,11 @@ self.onmessage = function (event: MessageEvent<MessageProps>) {
|
|||||||
page,
|
page,
|
||||||
isComplete = false,
|
isComplete = false,
|
||||||
chunked = true,
|
chunked = true,
|
||||||
percent = 0
|
percent = 0,
|
||||||
|
isDownloading = false
|
||||||
} = event.data;
|
} = event.data;
|
||||||
|
|
||||||
|
parser.setIsDownloading(isDownloading);
|
||||||
parser.setPage(page);
|
parser.setPage(page);
|
||||||
parser.setIsCompelete(isComplete);
|
parser.setIsCompelete(isComplete);
|
||||||
parser.setChunked(chunked);
|
parser.setChunked(chunked);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import classNames from 'classnames';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, {
|
import React, {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
memo,
|
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
@@ -25,10 +24,17 @@ interface LogsViewerProps {
|
|||||||
tail?: number;
|
tail?: number;
|
||||||
enableScorllLoad?: boolean;
|
enableScorllLoad?: boolean;
|
||||||
diffHeight?: number;
|
diffHeight?: number;
|
||||||
|
isDownloading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
|
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 } =
|
const { pageSize, page, setPage, setTotalPage, totalPage } =
|
||||||
useLogsPagination();
|
useLogsPagination();
|
||||||
const { setChunkFetch } = useSetChunkFetch();
|
const { setChunkFetch } = useSetChunkFetch();
|
||||||
@@ -64,9 +70,10 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setCurrentData = (lines: string[]) => {
|
const setCurrentData = (lines: string[]) => {
|
||||||
|
console.log('setCurrentData', lines);
|
||||||
const dataList = lines.map((line, index) => {
|
const dataList = lines.map((line, index) => {
|
||||||
return {
|
return {
|
||||||
content: removeBracketsFromLine(line),
|
content: line,
|
||||||
uid: `${pageRef.current}-${index}`
|
uid: `${pageRef.current}-${index}`
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -150,7 +157,8 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
|
|||||||
logParseWorker.current.postMessage({
|
logParseWorker.current.postMessage({
|
||||||
inputStr: data,
|
inputStr: data,
|
||||||
page: pageRef.current,
|
page: pageRef.current,
|
||||||
reset: clearScreen.current
|
reset: clearScreen.current,
|
||||||
|
isDownloading: isDownloading
|
||||||
});
|
});
|
||||||
clearScreen.current = false;
|
clearScreen.current = false;
|
||||||
};
|
};
|
||||||
@@ -247,7 +255,7 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
|
|||||||
return () => {
|
return () => {
|
||||||
chunkRequedtRef.current?.current?.abort?.();
|
chunkRequedtRef.current?.current?.abort?.();
|
||||||
};
|
};
|
||||||
}, [url]);
|
}, [url, isDownloading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
debouncedScroll();
|
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 React, { useMemo } from 'react';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import rehypeKatex from 'rehype-katex';
|
import rehypeKatex from 'rehype-katex';
|
||||||
|
import remarkBreaks from 'remark-breaks';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import remarkMath from 'remark-math';
|
import remarkMath from 'remark-math';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@@ -17,9 +18,7 @@ interface FullMarkdownProps {
|
|||||||
|
|
||||||
const Wrapper = styled.div.attrs(() => ({
|
const Wrapper = styled.div.attrs(() => ({
|
||||||
className: 'markdown-viewer'
|
className: 'markdown-viewer'
|
||||||
}))`
|
}))``;
|
||||||
line-height: 2;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const CodeViewer = (props: any) => {
|
const CodeViewer = (props: any) => {
|
||||||
const { children, className, node, theme, ...rest } = props;
|
const { children, className, node, theme, ...rest } = props;
|
||||||
@@ -41,7 +40,7 @@ const FullMarkdown: React.FC<FullMarkdownProps> = ({ content, theme }) => {
|
|||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[[remarkMath], remarkGfm]}
|
remarkPlugins={[[remarkMath], remarkGfm, remarkBreaks]}
|
||||||
rehypePlugins={[rehypeKatex]}
|
rehypePlugins={[rehypeKatex]}
|
||||||
components={{
|
components={{
|
||||||
code(props) {
|
code(props) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
.markdown-viewer {
|
.markdown-viewer {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
line-height: 2;
|
||||||
font-size: var(--font-size-base);
|
font-size: var(--font-size-base);
|
||||||
|
|
||||||
.hr {
|
.hr {
|
||||||
@@ -60,13 +61,11 @@
|
|||||||
ul {
|
ul {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
line-height: 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ol {
|
ol {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
line-height: 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a * {
|
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;
|
padding: 0 8px;
|
||||||
&:hover {
|
&:hover {
|
||||||
color: ${token.colorTextTertiary};
|
color: ${token.colorTextTertiary};
|
||||||
|
.anticon {
|
||||||
|
color: ${token.colorTextTertiary};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.anticon {
|
.anticon {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
color: ${token.colorText};
|
||||||
}
|
}
|
||||||
.icon-auto {
|
.icon-auto {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
|||||||
Vendored
+2
@@ -20,6 +20,8 @@ declare namespace Global {
|
|||||||
full_name: string;
|
full_name: string;
|
||||||
require_password_change: boolean;
|
require_password_change: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
|
source: string;
|
||||||
|
avatar_url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BaseListItem<T> {
|
interface BaseListItem<T> {
|
||||||
|
|||||||
+1
-2
@@ -570,7 +570,6 @@ body {
|
|||||||
|
|
||||||
.ant-message-notice-content {
|
.ant-message-notice-content {
|
||||||
border-radius: var(--border-radius-base) !important;
|
border-radius: var(--border-radius-base) !important;
|
||||||
line-height: 2;
|
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
@@ -579,7 +578,7 @@ body {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
.anticon {
|
.anticon {
|
||||||
padding-top: 5px;
|
padding-top: 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ export default function useUserSettings() {
|
|||||||
const { light, dark, colorPrimary } = themeConfig;
|
const { light, dark, colorPrimary } = themeConfig;
|
||||||
const [userSettings, setUserSettings] = useAtom(userSettingsHelperAtom);
|
const [userSettings, setUserSettings] = useAtom(userSettingsHelperAtom);
|
||||||
|
|
||||||
|
console.log('userSettings===', userSettings);
|
||||||
|
|
||||||
const getCurrentTheme = (mode: Theme): 'light' | 'realDark' => {
|
const getCurrentTheme = (mode: Theme): 'light' | 'realDark' => {
|
||||||
if (mode === 'auto') {
|
if (mode === 'auto') {
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
@@ -46,22 +48,32 @@ export default function useUserSettings() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const initTheme = () => {
|
||||||
setHtmlThemeAttr(userSettings.theme);
|
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
}, [userSettings.theme]);
|
.matches
|
||||||
|
? 'realDark'
|
||||||
|
: 'light';
|
||||||
|
if (userSettings.mode === 'auto' && systemTheme !== userSettings.theme) {
|
||||||
|
setTheme('auto');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userSettings.mode !== 'auto') return;
|
initTheme();
|
||||||
|
}, [userSettings.theme, userSettings.mode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
const handleChange = () => {
|
const handleChange = () => {
|
||||||
|
// The theme follows the system preference, when the mode is set to 'auto'
|
||||||
|
if (userSettings.mode !== 'auto') return;
|
||||||
setTheme('auto');
|
setTheme('auto');
|
||||||
};
|
};
|
||||||
mediaQuery.addEventListener('change', handleChange);
|
mediaQuery.addEventListener('change', handleChange);
|
||||||
return () => {
|
return () => {
|
||||||
mediaQuery.removeEventListener('change', handleChange);
|
mediaQuery.removeEventListener('change', handleChange);
|
||||||
};
|
};
|
||||||
}, [userSettings.mode]);
|
}, [userSettings.mode, userSettings.theme]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userSettings,
|
userSettings,
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ export const getRightRenderContent = (opts: {
|
|||||||
<span className="avatar-container">
|
<span className="avatar-container">
|
||||||
<Avatar
|
<Avatar
|
||||||
size={28}
|
size={28}
|
||||||
|
src={opts.initialState?.currentUser?.avatar_url}
|
||||||
style={{
|
style={{
|
||||||
color: 'var(--ant-color-text)',
|
color: 'var(--ant-color-text)',
|
||||||
fontSize: 16
|
fontSize: 16
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ interface MenuItem {
|
|||||||
interface SiderMenuProps {
|
interface SiderMenuProps {
|
||||||
menuData: MenuItem[];
|
menuData: MenuItem[];
|
||||||
collapsed?: boolean;
|
collapsed?: boolean;
|
||||||
|
initialState: Global.InitialState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = createStyles(({ css, token }) => {
|
const useStyles = createStyles(({ css, token }) => {
|
||||||
@@ -122,7 +123,8 @@ const useStyles = createStyles(({ css, token }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const SiderMenu: React.FC<SiderMenuProps> = (props) => {
|
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 { styles, cx } = useStyles();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [collapseKeys, setCollapseKeys] = useState<Set<string>>(new Set());
|
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}
|
rotate={collapseKeys.has(item.key) ? -90 : 0}
|
||||||
></CaretDownOutlined>
|
></CaretDownOutlined>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : is_admin ? (
|
||||||
<Divider style={dividerStyles} />
|
<Divider style={dividerStyles} />
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cx(styles.menuItemGroup, {
|
className={cx(styles.menuItemGroup, {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export default {
|
|||||||
'common.button.editmode': 'Edit mode',
|
'common.button.editmode': 'Edit mode',
|
||||||
'common.button.shortcut': 'Keyboard Shortcut',
|
'common.button.shortcut': 'Keyboard Shortcut',
|
||||||
'common.button.add': 'Add',
|
'common.button.add': 'Add',
|
||||||
'common.button.login': 'Log In',
|
'common.button.login': 'Log in',
|
||||||
'common.button.select': 'Select',
|
'common.button.select': 'Select',
|
||||||
'common.button.selected': 'Selected',
|
'common.button.selected': 'Selected',
|
||||||
'common.button.continue': 'Continue',
|
'common.button.continue': 'Continue',
|
||||||
@@ -251,5 +251,11 @@ export default {
|
|||||||
'common.tips.escape.disable':
|
'common.tips.escape.disable':
|
||||||
'Click Cancel or the X at the top right to close.',
|
'Click Cancel or the X at the top right to close.',
|
||||||
'common.button.clearSelection': 'Clear Selection',
|
'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.username': 'User Name',
|
||||||
'users.table.role': 'Role',
|
'users.table.role': 'Role',
|
||||||
'users.form.fullname': 'Full Name',
|
'users.form.fullname': 'Full Name',
|
||||||
|
'users.form.source': 'Source',
|
||||||
'users.table.user': 'users',
|
'users.table.user': 'users',
|
||||||
'users.form.admin': 'Admin',
|
'users.form.admin': 'Admin',
|
||||||
'users.form.user': 'User',
|
'users.form.user': 'User',
|
||||||
|
|||||||
@@ -251,7 +251,13 @@ export default {
|
|||||||
'common.tips.escape.disable':
|
'common.tips.escape.disable':
|
||||||
'Click Cancel or the X at the top right to close.',
|
'Click Cancel or the X at the top right to close.',
|
||||||
'common.button.clearSelection': 'Clear Selection',
|
'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) ==========
|
// ========== 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.'
|
// 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.'
|
// 13. 'common.tips.escape.disable': 'Click Cancel or the X at the top right to close.'
|
||||||
// 14. 'common.button.clearSelection': 'Clear Selection',
|
// 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 ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export default {
|
|||||||
'users.table.username': 'ユーザー名',
|
'users.table.username': 'ユーザー名',
|
||||||
'users.table.role': '役割',
|
'users.table.role': '役割',
|
||||||
'users.form.fullname': 'フルネーム',
|
'users.form.fullname': 'フルネーム',
|
||||||
|
'users.form.source': 'ソース',
|
||||||
'users.table.user': 'ユーザー',
|
'users.table.user': 'ユーザー',
|
||||||
'users.form.admin': '管理者',
|
'users.form.admin': '管理者',
|
||||||
'users.form.user': '一般ユーザー',
|
'users.form.user': '一般ユーザー',
|
||||||
|
|||||||
@@ -250,9 +250,17 @@ export default {
|
|||||||
'common.tips.escape.disable':
|
'common.tips.escape.disable':
|
||||||
'Чтобы закрыть, нажмите "Отмена" или крестик (X) в правом верхнем углу.',
|
'Чтобы закрыть, нажмите "Отмена" или крестик (X) в правом верхнем углу.',
|
||||||
'common.button.clearSelection': 'Сбросить выбор',
|
'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) ==========
|
// ========== 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 ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -28,13 +28,11 @@ export default {
|
|||||||
'dashboard.usage.export.user': 'Пользователь',
|
'dashboard.usage.export.user': 'Пользователь',
|
||||||
'dashboard.usage.export.model': 'Модель',
|
'dashboard.usage.export.model': 'Модель',
|
||||||
'dashboard.usage.export.date': 'Дата',
|
'dashboard.usage.export.date': 'Дата',
|
||||||
'dashboard.usage.datePicker.last7days': 'Last 7 Days',
|
'dashboard.usage.datePicker.last7days': 'Последние 7 Days',
|
||||||
'dashboard.usage.datePicker.last30days': 'Last 30 Days',
|
'dashboard.usage.datePicker.last30days': 'Последние 30 Days',
|
||||||
'dashboard.usage.datePicker.last60days': 'Last 60 Days'
|
'dashboard.usage.datePicker.last60days': 'Последние 60 Days'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== 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 ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default {
|
|||||||
'menu.models.modelList': 'Развертывание и управление',
|
'menu.models.modelList': 'Развертывание и управление',
|
||||||
'menu.models.modelCatalog': 'Каталог',
|
'menu.models.modelCatalog': 'Каталог',
|
||||||
'menu.models.catalog': 'Каталог моделей',
|
'menu.models.catalog': 'Каталог моделей',
|
||||||
'menu.models.deployment': 'Deployment',
|
'menu.models.deployment': 'Запуск',
|
||||||
'menu.modelCatalog': 'Каталог',
|
'menu.modelCatalog': 'Каталог',
|
||||||
'menu.resources': 'Ресурсы',
|
'menu.resources': 'Ресурсы',
|
||||||
'menu.apikeys': 'API-ключи',
|
'menu.apikeys': 'API-ключи',
|
||||||
@@ -19,21 +19,15 @@ export default {
|
|||||||
'menu.profile': 'Профиль',
|
'menu.profile': 'Профиль',
|
||||||
'menu.login': 'Авторизация',
|
'menu.login': 'Авторизация',
|
||||||
'menu.usage': 'Использование',
|
'menu.usage': 'Использование',
|
||||||
'menu.404': '404',
|
'menu.404': 'Ошибка 404',
|
||||||
'menu.resources.workers': 'Workers',
|
'menu.resources.workers': 'Воркеры',
|
||||||
'menu.resources.gpus': 'GPUs',
|
'menu.resources.gpus': 'GPUs',
|
||||||
'menu.resources.modelfiles': 'Model Files',
|
'menu.resources.modelfiles': 'Файлы модлей',
|
||||||
'menu.accessControl': 'Access Control',
|
'menu.accessControl': 'Управление доступом',
|
||||||
'menu.accessControl.apikeys': 'API Keys',
|
'menu.accessControl.apikeys': 'API Ключи',
|
||||||
'menu.accessControl.users': 'Users'
|
'menu.accessControl.users': 'Пользователи'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== 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 ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export default {
|
|||||||
'models.table.apiAccessInfo.gotoCreate': 'Перейти к созданию',
|
'models.table.apiAccessInfo.gotoCreate': 'Перейти к созданию',
|
||||||
'models.search.parts': '{n} частей',
|
'models.search.parts': '{n} частей',
|
||||||
'models.search.evaluate.error': 'Возникла ошибка при вычислении: ',
|
'models.search.evaluate.error': 'Возникла ошибка при вычислении: ',
|
||||||
'models.ollama.deprecated.title': 'Deprecation Notice',
|
'models.ollama.deprecated.title': 'Объявление об устаревании',
|
||||||
'models.ollama.deprecated.current':
|
'models.ollama.deprecated.current':
|
||||||
'<span class="bold-text">Текущая версия (v0.6.1): </span>Модели Ollama в настоящее время доступны для использования.',
|
'<span class="bold-text">Текущая версия (v0.6.1): </span>Модели Ollama в настоящее время доступны для использования.',
|
||||||
'models.ollama.deprecated.upcoming':
|
'models.ollama.deprecated.upcoming':
|
||||||
@@ -159,13 +159,11 @@ export default {
|
|||||||
'<span class="bold-text">После обновления до версии (v0.7.0),</span> все ранее развёрнутые модели продолжат работать в обычном режиме.',
|
'<span class="bold-text">После обновления до версии (v0.7.0),</span> все ранее развёрнутые модели продолжат работать в обычном режиме.',
|
||||||
'models.ollama.deprecated.issue':
|
'models.ollama.deprecated.issue':
|
||||||
'См. связанную проблему: <a href="https://github.com/gpustack/gpustack/issues/1979" target="_blank">#1979 on GitHub</a>.',
|
'См. связанную проблему: <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':
|
'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) ==========
|
// ========== 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 ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export default {
|
|||||||
'users.table.username': 'Имя пользователя',
|
'users.table.username': 'Имя пользователя',
|
||||||
'users.table.role': 'Роль',
|
'users.table.role': 'Роль',
|
||||||
'users.form.fullname': 'Полное имя',
|
'users.form.fullname': 'Полное имя',
|
||||||
|
'users.form.source': 'Источник',
|
||||||
'users.table.user': 'пользователи',
|
'users.table.user': 'пользователи',
|
||||||
'users.form.admin': 'Администратор',
|
'users.form.admin': 'Администратор',
|
||||||
'users.form.user': 'Пользователь',
|
'users.form.user': 'Пользователь',
|
||||||
|
|||||||
@@ -244,5 +244,10 @@ export default {
|
|||||||
'common.page.refresh.tips': '出了点问题,试试刷新页面吧!',
|
'common.page.refresh.tips': '出了点问题,试试刷新页面吧!',
|
||||||
'common.tips.escape.disable': '请点击「取消」按钮或右上角 X 关闭窗口',
|
'common.tips.escape.disable': '请点击「取消」按钮或右上角 X 关闭窗口',
|
||||||
'common.button.clearSelection': '清除选择',
|
'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.username': '用户名',
|
||||||
'users.table.role': '角色',
|
'users.table.role': '角色',
|
||||||
'users.form.fullname': '全名',
|
'users.form.fullname': '全名',
|
||||||
|
'users.form.source': '来源',
|
||||||
'users.table.user': '用户',
|
'users.table.user': '用户',
|
||||||
'users.form.admin': '管理员',
|
'users.form.admin': '管理员',
|
||||||
'users.form.user': '普通用户',
|
'users.form.user': '普通用户',
|
||||||
|
|||||||
@@ -161,6 +161,11 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
|
|||||||
return filterRegGGUF.test(file.path) || _.includes(file.path, '.gguf');
|
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
|
// hugging face files
|
||||||
const getHuggingfaceFiles = async () => {
|
const getHuggingfaceFiles = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -175,7 +180,7 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const list = _.filter(fileList, (file: any) => {
|
const list = _.filter(fileList, (file: any) => {
|
||||||
return hfFileFilter(file) && file.path.indexOf('mmproj') === -1;
|
return hfFileFilter(file) && isNormalGGUFModelFile(file.path);
|
||||||
});
|
});
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
@@ -188,7 +193,7 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
|
|||||||
return (
|
return (
|
||||||
filterRegGGUF.test(file.Path) &&
|
filterRegGGUF.test(file.Path) &&
|
||||||
file.Type === 'blob' &&
|
file.Type === 'blob' &&
|
||||||
file.Path.indexOf('mmproj') === -1
|
isNormalGGUFModelFile(file.Path)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const IncompatibleInfo = styled.div`
|
|||||||
li {
|
li {
|
||||||
position: relative;
|
position: relative;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
li.normal::before {
|
li.normal::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -877,6 +877,7 @@ const Models: React.FC<ModelsProps> = ({
|
|||||||
onOk={handleCreateModel}
|
onOk={handleCreateModel}
|
||||||
></DeployModal>
|
></DeployModal>
|
||||||
<ViewLogsModal
|
<ViewLogsModal
|
||||||
|
status={currentInstance.status}
|
||||||
url={currentInstance.url}
|
url={currentInstance.url}
|
||||||
tail={currentInstance.tail}
|
tail={currentInstance.tail}
|
||||||
id={currentInstance.id}
|
id={currentInstance.id}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useIntl } from '@umijs/max';
|
|||||||
import { Modal } from 'antd';
|
import { Modal } from 'antd';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { MODELS_API } from '../apis';
|
import { MODELS_API } from '../apis';
|
||||||
import { InstanceRealtimeLogStatus } from '../config';
|
import { InstanceRealtimeLogStatus, InstanceStatusMap } from '../config';
|
||||||
|
|
||||||
type ViewModalProps = {
|
type ViewModalProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -12,14 +12,18 @@ type ViewModalProps = {
|
|||||||
id?: number | string;
|
id?: number | string;
|
||||||
modelId?: number | string;
|
modelId?: number | string;
|
||||||
tail?: number;
|
tail?: number;
|
||||||
|
status?: string;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
|
const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { setChunkRequest } = useSetChunkRequest();
|
const { setChunkRequest } = useSetChunkRequest();
|
||||||
const { open, url, onCancel, tail } = props || {};
|
const { open, url, onCancel, tail, status } = props || {};
|
||||||
const [enableScorllLoad, setEnableScorllLoad] = useState(true);
|
const [enableScorllLoad, setEnableScorllLoad] = useState(true);
|
||||||
|
const [isDownloading, setIsDownloading] = useState<boolean>(
|
||||||
|
status === InstanceStatusMap.Downloading
|
||||||
|
);
|
||||||
const logsViewerRef = React.useRef<any>(null);
|
const logsViewerRef = React.useRef<any>(null);
|
||||||
const requestRef = React.useRef<any>(null);
|
const requestRef = React.useRef<any>(null);
|
||||||
const contentRef = 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 updateHandler = (list: any) => {
|
||||||
const data = list?.find((item: any) => item.data?.id === props.id);
|
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
|
// state in InstanceRealtimeLogStatus will not enable scorll load, because it is in the trasisition state
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
|
setIsDownloading(data?.data?.state === InstanceStatusMap.Downloading);
|
||||||
setEnableScorllLoad(
|
setEnableScorllLoad(
|
||||||
() => !InstanceRealtimeLogStatus.includes(data?.data?.state)
|
() => !InstanceRealtimeLogStatus.includes(data?.data?.state)
|
||||||
);
|
);
|
||||||
@@ -70,6 +76,9 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
|
|||||||
url: `${MODELS_API}/${props.modelId}/instances`,
|
url: `${MODELS_API}/${props.modelId}/instances`,
|
||||||
handler: updateHandler
|
handler: updateHandler
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
logsViewerRef.current?.abort();
|
||||||
|
requestRef.current?.current?.cancel?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -111,6 +120,7 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
|
|||||||
url={url}
|
url={url}
|
||||||
tail={tail}
|
tail={tail}
|
||||||
enableScorllLoad={enableScorllLoad}
|
enableScorllLoad={enableScorllLoad}
|
||||||
|
isDownloading={isDownloading}
|
||||||
params={{
|
params={{
|
||||||
follow: true
|
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 HotKeys from '@/config/hotkeys';
|
||||||
import {
|
|
||||||
ApiOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
DownloadOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
ExperimentOutlined,
|
|
||||||
RetweetOutlined,
|
|
||||||
ThunderboltOutlined
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { modelCategoriesMap, modelSourceMap } from './index';
|
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<
|
export const modalConfig: Record<
|
||||||
string,
|
string,
|
||||||
{ show: boolean; width: string | number; source: any; isGGUF?: boolean }
|
{ show: boolean; width: string | number; source: any; isGGUF?: boolean }
|
||||||
@@ -196,7 +169,7 @@ export const modelFileActions = [
|
|||||||
{
|
{
|
||||||
label: 'common.button.deploy',
|
label: 'common.button.deploy',
|
||||||
key: 'deploy',
|
key: 'deploy',
|
||||||
icon: icons.Launch
|
icon: icons.Deployment
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'resources.modelfiles.retry.download',
|
label: 'resources.modelfiles.retry.download',
|
||||||
|
|||||||
@@ -517,7 +517,7 @@ export const getBackendParamsTips = (backend: string) => {
|
|||||||
return {
|
return {
|
||||||
backend: 'vLLM',
|
backend: 'vLLM',
|
||||||
releases: 'https://github.com/vllm-project/vllm/releases',
|
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'
|
version: 'v0.8.5'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,15 +113,25 @@ const options = [
|
|||||||
label: '--tool-call-parser',
|
label: '--tool-call-parser',
|
||||||
value: '--tool-call-parser',
|
value: '--tool-call-parser',
|
||||||
options: [
|
options: [
|
||||||
'mistral',
|
|
||||||
'hermes',
|
|
||||||
'internlm',
|
|
||||||
'jamba',
|
|
||||||
'llama3_json',
|
|
||||||
'granite-20b-fc',
|
|
||||||
'phi4_mini_json',
|
'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',
|
'granite',
|
||||||
'pythonic'
|
'minimax',
|
||||||
|
'granite-20b-fc',
|
||||||
|
'internlm'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -229,7 +239,16 @@ const options = [
|
|||||||
{
|
{
|
||||||
label: '--reasoning-parser',
|
label: '--reasoning-parser',
|
||||||
value: '--reasoning-parser',
|
value: '--reasoning-parser',
|
||||||
options: ['deepseek_r1']
|
options: [
|
||||||
|
'deepseek_r1',
|
||||||
|
'glm45',
|
||||||
|
'GptOss',
|
||||||
|
'granite',
|
||||||
|
'hunyuan_a13b',
|
||||||
|
'mistral',
|
||||||
|
'qwen3',
|
||||||
|
'step3'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '--config-format',
|
label: '--config-format',
|
||||||
|
|||||||
@@ -535,13 +535,22 @@ export const useCheckCompatibility = () => {
|
|||||||
cacheFormValuesRef.current = {};
|
cacheFormValuesRef.current = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const noLocalPathValue = (allValues: any) => {
|
||||||
|
return (
|
||||||
|
allValues.source === modelSourceMap.local_path_value &&
|
||||||
|
!allValues.local_path
|
||||||
|
);
|
||||||
|
};
|
||||||
const handleOnValuesChange = async (params: {
|
const handleOnValuesChange = async (params: {
|
||||||
changedValues: any;
|
changedValues: any;
|
||||||
allValues: any;
|
allValues: any;
|
||||||
source: string;
|
source: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { allValues, source } = params;
|
const { allValues, source } = params;
|
||||||
if (_.isEqual(cacheFormValuesRef.current, allValues)) {
|
if (
|
||||||
|
_.isEqual(cacheFormValuesRef.current, allValues) ||
|
||||||
|
noLocalPathValue(allValues)
|
||||||
|
) {
|
||||||
console.log('No changes detected, skipping evaluation.');
|
console.log('No changes detected, skipping evaluation.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import qs from 'query-string';
|
|||||||
|
|
||||||
export const AUTH_API = '/auth';
|
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 (
|
export const login = async (
|
||||||
params: { username: string; password: string },
|
params: { username: string; password: string },
|
||||||
options?: any
|
options?: any
|
||||||
@@ -38,3 +43,7 @@ export const updatePassword = async (params: any) => {
|
|||||||
data: params
|
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 LogoIcon from '@/assets/images/gpustack-logo.png';
|
||||||
import { initialPasswordAtom, userAtom } from '@/atoms/user';
|
import { 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 { useIntl, useModel } from '@umijs/max';
|
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 { createStyles } from 'antd-style';
|
||||||
import CryptoJS from 'crypto-js';
|
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { flushSync } from 'react-dom';
|
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 { checkDefaultPage } from '../utils';
|
||||||
|
import LocalUserForm from './local-user-form';
|
||||||
|
|
||||||
const useStyles = createStyles(({ token, css }) => ({
|
const SpinContainer = styled.div`
|
||||||
header: css`
|
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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
position: fixed;
|
`;
|
||||||
right: 0;
|
|
||||||
top: 0;
|
const DividerWrapper = styled(Divider)`
|
||||||
height: 60px;
|
margin-block: 24px !important;
|
||||||
padding: 20px;
|
.ant-divider-inner-text {
|
||||||
.anticon-global {
|
color: var(--ant-color-text-secondary);
|
||||||
color: ${token.colorText};
|
|
||||||
}
|
}
|
||||||
.anticon:hover {
|
`;
|
||||||
color: ${token.colorTextTertiary};
|
|
||||||
|
const useStyles = createStyles(({ token, css }) => ({
|
||||||
|
errorMessage: css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
color: ${token.colorText};
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
welcome: css`
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
font-size: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
.text {
|
||||||
|
color: ${token.colorText};
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const LoginForm = () => {
|
const LoginForm = () => {
|
||||||
|
const [messageApi, contextHolder] = message.useMessage();
|
||||||
const { styles } = useStyles();
|
const { styles } = useStyles();
|
||||||
const [userInfo, setUserInfo] = useAtom(userAtom);
|
const [userInfo, setUserInfo] = useAtom(userAtom);
|
||||||
const [initialPassword, setInitialPassword] = useAtom(initialPasswordAtom);
|
|
||||||
const { initialState, setInitialState } = useModel('@@initialState') || {};
|
const { initialState, setInitialState } = useModel('@@initialState') || {};
|
||||||
|
const [authError, setAuthError] = useState<Error | null>(null);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
const [isPassword, setIsPassword] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const renderWelCome = useMemo(() => {
|
const renderWelCome = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={styles.welcome}>
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
marginBottom: 32,
|
|
||||||
fontSize: 20,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex-center">
|
<div className="flex-center">
|
||||||
<span>{intl?.formatMessage({ id: 'users.login.title' })}</span>
|
<span className="text">
|
||||||
|
{intl?.formatMessage({ id: 'users.login.title' })}
|
||||||
|
</span>
|
||||||
<img
|
<img
|
||||||
src={LogoIcon}
|
src={LogoIcon}
|
||||||
alt="logo"
|
alt="logo"
|
||||||
@@ -70,7 +111,7 @@ const LoginForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, [intl]);
|
};
|
||||||
|
|
||||||
const gotoDefaultPage = async (userInfo: any) => {
|
const gotoDefaultPage = async (userInfo: any) => {
|
||||||
checkDefaultPage(userInfo, true);
|
checkDefaultPage(userInfo, true);
|
||||||
@@ -90,147 +131,138 @@ const LoginForm = () => {
|
|||||||
return userInfo;
|
return userInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
const encryptPassword = (password: string) => {
|
// error handling for authentication
|
||||||
const psw = CryptoJS.AES?.encrypt?.(password, CRYPT_TEXT).toString();
|
const handleOnError = (error: Error) => {
|
||||||
return psw;
|
setAuthError(error);
|
||||||
};
|
messageApi.error({
|
||||||
const decryptPassword = (password: string) => {
|
duration: 5,
|
||||||
const bytes = CryptoJS.AES?.decrypt?.(password, CRYPT_TEXT);
|
content: (
|
||||||
const res = bytes.toString(CryptoJS.enc.Utf8);
|
<div className={styles.errorMessage}>
|
||||||
return res;
|
<div className="title">
|
||||||
};
|
{intl.formatMessage({ id: 'common.login.auth.failed' })}
|
||||||
|
</div>
|
||||||
const callRememberMe = async (values: any) => {
|
<div className="message">{error?.message || 'Unknown error'}</div>
|
||||||
const { autoLogin } = values;
|
</div>
|
||||||
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 () => {
|
// local user authentication
|
||||||
const rememberMe = await getRememberMe(REMEMBER_ME_KEY);
|
const { handleLogin } = useLocalAuth({
|
||||||
|
fetchUserInfo,
|
||||||
if (rememberMe?.f) {
|
form,
|
||||||
const username = decryptPassword(rememberMe?.um);
|
onSuccess: async (userInfo) => {
|
||||||
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();
|
|
||||||
setUserInfo(userInfo);
|
setUserInfo(userInfo);
|
||||||
if (values.autoLogin) {
|
|
||||||
await callRememberMe(values);
|
|
||||||
} else {
|
|
||||||
await removeRememberMe(REMEMBER_ME_KEY);
|
|
||||||
}
|
|
||||||
if (!userInfo?.require_password_change) {
|
if (!userInfo?.require_password_change) {
|
||||||
gotoDefaultPage(userInfo);
|
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(() => {
|
const handleLoginWithThirdParty = () => {
|
||||||
callGetRememberMe();
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.header}>
|
{contextHolder}
|
||||||
<ThemeDropActions></ThemeDropActions>
|
|
||||||
<LangSelect />
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<Form
|
{isThirdPartyAuthHandling ? (
|
||||||
form={form}
|
<SpinContainer>
|
||||||
style={{ width: '360px', margin: '0 auto' }}
|
{renderWelCome()}
|
||||||
onFinish={handleLogin}
|
<div className="spin">
|
||||||
>
|
<Spin tip={intl.formatMessage({ id: 'common.login.auth' })}>
|
||||||
{renderWelCome}
|
<div style={{ width: 300 }}></div>
|
||||||
<Form.Item
|
</Spin>
|
||||||
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>
|
</div>
|
||||||
<Button
|
</SpinContainer>
|
||||||
htmlType="submit"
|
) : (
|
||||||
type="primary"
|
<>
|
||||||
block
|
{renderWelCome()}
|
||||||
style={{ height: '48px', fontSize: '14px' }}
|
{renderLoginButtons()}
|
||||||
>
|
{(!hasThirdPartyLogin || isPassword) && (
|
||||||
{intl.formatMessage({ id: 'common.button.login' })}
|
<LocalUserForm handleLogin={handleLogin} form={form} />
|
||||||
</Button>
|
)}
|
||||||
</Form>
|
{hasThirdPartyLogin && isPassword && (
|
||||||
|
<BackButton onClick={handleLoginWithThirdParty}>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: 'common.external.login' },
|
||||||
|
{ type: 'SSO' }
|
||||||
|
)}
|
||||||
|
</BackButton>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { initialPasswordAtom, userAtom } from '@/atoms/user';
|
|||||||
import SealInput from '@/components/seal-form/seal-input';
|
import SealInput from '@/components/seal-form/seal-input';
|
||||||
import { PasswordReg } from '@/config';
|
import { PasswordReg } from '@/config';
|
||||||
import { CRYPT_TEXT } from '@/utils/localstore/index';
|
import { CRYPT_TEXT } from '@/utils/localstore/index';
|
||||||
import { GlobalOutlined, LockOutlined } from '@ant-design/icons';
|
import { LockOutlined } from '@ant-design/icons';
|
||||||
import { SelectLang, useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Form, message } from 'antd';
|
import { Button, Form, message } from 'antd';
|
||||||
import CryptoJS from 'crypto-js';
|
import CryptoJS from 'crypto-js';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
@@ -47,9 +47,6 @@ const PasswordForm: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ position: 'fixed', right: 0, top: 0, padding: '0 20px' }}>
|
|
||||||
<SelectLang icon={<GlobalOutlined />} reload={false} />
|
|
||||||
</div>
|
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
style={{ width: '360px', margin: '0 auto' }}
|
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 { userAtom } from '@/atoms/user';
|
||||||
import DarkMask from '@/components/dark-mask';
|
import DarkMask from '@/components/dark-mask';
|
||||||
import Footer from '@/components/footer';
|
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 useUserSettings from '@/hooks/use-user-settings';
|
||||||
import { useModel } from '@umijs/max';
|
import { useModel } from '@umijs/max';
|
||||||
import { ConfigProvider, theme } from 'antd';
|
import { ConfigProvider, theme } from 'antd';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@@ -12,6 +15,29 @@ import LoginForm from './components/login-form';
|
|||||||
import PasswordForm from './components/password-form';
|
import PasswordForm from './components/password-form';
|
||||||
import { checkDefaultPage } from './utils';
|
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 }>`
|
const Wrapper = styled.div<{ $isDarkTheme: boolean }>`
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -30,7 +56,6 @@ const Wrapper = styled.div<{ $isDarkTheme: boolean }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Box = styled.div`
|
const Box = styled.div`
|
||||||
padding-top: 10%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -40,12 +65,17 @@ const Box = styled.div`
|
|||||||
const FormWrapper = styled.div`
|
const FormWrapper = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
z-index: 999;
|
||||||
border-radius: var(--border-radius-modal);
|
border-radius: var(--border-radius-modal);
|
||||||
width: max-content;
|
width: max-content;
|
||||||
height: max-content;
|
height: max-content;
|
||||||
|
max-width: 800px;
|
||||||
|
max-height: 600px;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
background-color: var(--color-modal-content-bg);
|
background-color: var(--color-modal-content-bg);
|
||||||
box-shadow: var(--color-modal-box-shadow);
|
box-shadow: var(--color-modal-box-shadow);
|
||||||
|
margin-top: 50vh;
|
||||||
|
transform: translateY(-50%);
|
||||||
.field-wrapper {
|
.field-wrapper {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
@@ -56,6 +86,7 @@ const FormWrapper = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
|
const { styles } = useStyles();
|
||||||
const { themeData, userSettings, isDarkTheme } = useUserSettings();
|
const { themeData, userSettings, isDarkTheme } = useUserSettings();
|
||||||
const [userInfo, setUserInfo] = useAtom(userAtom);
|
const [userInfo, setUserInfo] = useAtom(userAtom);
|
||||||
const { initialState, setInitialState } = useModel('@@initialState') || {};
|
const { initialState, setInitialState } = useModel('@@initialState') || {};
|
||||||
@@ -93,10 +124,14 @@ const Login = () => {
|
|||||||
...themeData
|
...themeData
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div className={styles.header}>
|
||||||
|
<ThemeDropActions></ThemeDropActions>
|
||||||
|
<LangSelect />
|
||||||
|
</div>
|
||||||
<DarkMask></DarkMask>
|
<DarkMask></DarkMask>
|
||||||
<Wrapper $isDarkTheme={isDarkTheme}></Wrapper>
|
<Wrapper $isDarkTheme={isDarkTheme}></Wrapper>
|
||||||
<Box>
|
<Box>
|
||||||
|
<div className={styles.formContainer}>
|
||||||
<FormWrapper>
|
<FormWrapper>
|
||||||
{userInfo?.require_password_change ? (
|
{userInfo?.require_password_change ? (
|
||||||
<PasswordForm />
|
<PasswordForm />
|
||||||
@@ -104,9 +139,9 @@ const Login = () => {
|
|||||||
<LoginForm />
|
<LoginForm />
|
||||||
)}
|
)}
|
||||||
</FormWrapper>
|
</FormWrapper>
|
||||||
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -168,11 +168,11 @@ export default function useChatCompletion(
|
|||||||
if (chunk?.error) {
|
if (chunk?.error) {
|
||||||
setTokenResult({
|
setTokenResult({
|
||||||
error: true,
|
error: true,
|
||||||
errorMessage: extractErrorMessage(chunk)
|
errorMessage: extractErrorMessage(chunk.error)
|
||||||
});
|
});
|
||||||
return;
|
} else {
|
||||||
}
|
|
||||||
joinMessage(chunk);
|
joinMessage(chunk);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('error:', error);
|
console.log('error:', error);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import useTabActive from '@/hooks/use-tab-active';
|
import useTabActive from '@/hooks/use-tab-active';
|
||||||
import { PageContainer } from '@ant-design/pro-components';
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl, useModel } from '@umijs/max';
|
||||||
import { TabsProps } from 'antd';
|
import { TabsProps } from 'antd';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import Appearance from './components/appearance';
|
import Appearance from './components/appearance';
|
||||||
import ModifyPasswordn from './components/modify-password';
|
import ModifyPasswordn from './components/modify-password';
|
||||||
@@ -15,12 +15,25 @@ const Wrapper = styled.div`
|
|||||||
|
|
||||||
const Profile: React.FC = () => {
|
const Profile: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const { initialState, setInitialState } = useModel('@@initialState') || {};
|
||||||
const { setTabActive, getTabActive, tabsMap } = useTabActive();
|
const { setTabActive, getTabActive, tabsMap } = useTabActive();
|
||||||
const [activeKey, setActiveKey] = useState(
|
const [activeKey, setActiveKey] = useState(
|
||||||
getTabActive(tabsMap.userSettings) || 'modify-password'
|
initialState?.currentUser?.source === 'Local'
|
||||||
|
? 'modify-password'
|
||||||
|
: 'appearance'
|
||||||
);
|
);
|
||||||
|
|
||||||
const items: TabsProps['items'] = [
|
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',
|
key: 'modify-password',
|
||||||
label: intl.formatMessage({ id: 'users.form.updatepassword' }),
|
label: intl.formatMessage({ id: 'users.form.updatepassword' }),
|
||||||
@@ -32,6 +45,7 @@ const Profile: React.FC = () => {
|
|||||||
children: <Appearance />
|
children: <Appearance />
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
}, [initialState?.currentUser?.source]);
|
||||||
|
|
||||||
const handleChangeTab = useCallback((key: string) => {
|
const handleChangeTab = useCallback((key: string) => {
|
||||||
setActiveKey(key);
|
setActiveKey(key);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ export interface ListItem {
|
|||||||
is_admin: boolean;
|
is_admin: boolean;
|
||||||
full_name: string;
|
full_name: string;
|
||||||
id: number;
|
id: number;
|
||||||
|
source: string;
|
||||||
|
avatar_url: string;
|
||||||
username: string;
|
username: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import AutoTooltip from '@/components/auto-tooltip';
|
import AutoTooltip from '@/components/auto-tooltip';
|
||||||
import DeleteModal from '@/components/delete-modal';
|
import DeleteModal from '@/components/delete-modal';
|
||||||
import DropdownButtons from '@/components/drop-down-buttons';
|
import DropdownButtons from '@/components/drop-down-buttons';
|
||||||
|
import IconFont from '@/components/icon-font';
|
||||||
import PageTools from '@/components/page-tools';
|
import PageTools from '@/components/page-tools';
|
||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import HotKeys from '@/config/hotkeys';
|
import HotKeys from '@/config/hotkeys';
|
||||||
@@ -10,9 +11,7 @@ import {
|
|||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
SyncOutlined,
|
SyncOutlined
|
||||||
UserOutlined,
|
|
||||||
UserSwitchOutlined
|
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { PageContainer } from '@ant-design/pro-components';
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
@@ -86,6 +85,7 @@ const Users: React.FC = () => {
|
|||||||
|
|
||||||
const handleModalOk = async (data: FormData) => {
|
const handleModalOk = async (data: FormData) => {
|
||||||
const params = {
|
const params = {
|
||||||
|
...currentData,
|
||||||
...data,
|
...data,
|
||||||
is_admin: data.is_admin === 'admin'
|
is_admin: data.is_admin === 'admin'
|
||||||
};
|
};
|
||||||
@@ -247,14 +247,17 @@ const Users: React.FC = () => {
|
|||||||
render={(text, record: ListItem) => {
|
render={(text, record: ListItem) => {
|
||||||
return record.is_admin ? (
|
return record.is_admin ? (
|
||||||
<AutoTooltip ghost minWidth={50}>
|
<AutoTooltip ghost minWidth={50}>
|
||||||
<UserSwitchOutlined className="size-16" />
|
<IconFont
|
||||||
|
type="icon-admin-user"
|
||||||
|
className="size-16"
|
||||||
|
></IconFont>
|
||||||
<span className="m-l-5">
|
<span className="m-l-5">
|
||||||
{intl.formatMessage({ id: 'users.form.admin' })}
|
{intl.formatMessage({ id: 'users.form.admin' })}
|
||||||
</span>
|
</span>
|
||||||
</AutoTooltip>
|
</AutoTooltip>
|
||||||
) : (
|
) : (
|
||||||
<AutoTooltip ghost minWidth={50}>
|
<AutoTooltip ghost minWidth={50}>
|
||||||
<UserOutlined className="size-16" />
|
<IconFont type="icon-user" className="size-16"></IconFont>
|
||||||
<span className="m-l-5">
|
<span className="m-l-5">
|
||||||
{intl.formatMessage({ id: 'users.form.user' })}
|
{intl.formatMessage({ id: 'users.form.user' })}
|
||||||
</span>
|
</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
|
<Column
|
||||||
title={intl.formatMessage({ id: 'common.table.createTime' })}
|
title={intl.formatMessage({ id: 'common.table.createTime' })}
|
||||||
dataIndex="created_at"
|
dataIndex="created_at"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { clearAtomStorage } from '@/atoms/utils';
|
|||||||
import { RequestConfig, history } from '@umijs/max';
|
import { RequestConfig, history } from '@umijs/max';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
|
|
||||||
|
// no base URL APIs
|
||||||
const NoBaseURLAPIs = ['/auth', '/v1-openai', '/version', '/proxy', '/update'];
|
const NoBaseURLAPIs = ['/auth', '/v1-openai', '/version', '/proxy', '/update'];
|
||||||
|
|
||||||
export const requestConfig: RequestConfig = {
|
export const requestConfig: RequestConfig = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { request } from '@umijs/max';
|
import { request } from '@umijs/max';
|
||||||
|
|
||||||
export async function queryCurrentUserState(opts?: Record<string, any>) {
|
export async function queryCurrentUserState(opts?: Record<string, any>) {
|
||||||
return request(`/users/me`, {
|
return request<Global.UserInfo>(`/users/me`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
...opts
|
...opts
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
import qs from 'query-string';
|
import qs from 'query-string';
|
||||||
|
|
||||||
const extractStreamRegx = /data:\s*({.*?})(?=\n|$)/g;
|
const extractStreamRegx = /(data|error):\s*({.*?})(?=\n|$)/g;
|
||||||
|
|
||||||
const extractJSON = (dataStr: string) => {
|
const extractJSON = (dataStr: string) => {
|
||||||
let match;
|
let match;
|
||||||
@@ -12,11 +12,16 @@ const extractJSON = (dataStr: string) => {
|
|||||||
}
|
}
|
||||||
while ((match = extractStreamRegx.exec(dataStr)) !== null) {
|
while ((match = extractStreamRegx.exec(dataStr)) !== null) {
|
||||||
try {
|
try {
|
||||||
const jsonData = JSON.parse(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);
|
results.push(jsonData);
|
||||||
} catch (error) {
|
}
|
||||||
console.error('JSON parse error:', error, 'for match:', match[1]);
|
} catch (err) {
|
||||||
|
console.error('JSON parse error:', err, 'for match:', match[2]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,33 +167,40 @@ export const readStreamData = async (
|
|||||||
}, throttleDelay);
|
}, throttleDelay);
|
||||||
|
|
||||||
let isReading = true;
|
let isReading = true;
|
||||||
|
let textBuffer = ''; // cache incomplete line
|
||||||
|
|
||||||
while (isReading) {
|
while (isReading) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
|
|
||||||
if (done) {
|
|
||||||
isReading = false;
|
|
||||||
bufferManager.flush();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const chunk = decoder.decode(value, { stream: true });
|
textBuffer += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
if (chunk.startsWith('error:')) {
|
if (textBuffer.startsWith('error:')) {
|
||||||
const errorStr = chunk.slice(7).trim();
|
const errorStr = textBuffer.slice(7).trim();
|
||||||
const jsonData = JSON.parse(errorStr);
|
const jsonData = JSON.parse(errorStr);
|
||||||
bufferManager.add({ error: jsonData });
|
bufferManager.add({ error: jsonData });
|
||||||
|
textBuffer = ''; // Clear buffer after processing error
|
||||||
} else {
|
} else {
|
||||||
extractJSON(chunk).forEach((data) => {
|
extractJSON(textBuffer).forEach((data) => {
|
||||||
bufferManager.add(data);
|
bufferManager.add(data);
|
||||||
});
|
});
|
||||||
|
textBuffer = ''; // Clear buffer after processing
|
||||||
}
|
}
|
||||||
|
|
||||||
throttledCallback();
|
throttledCallback();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
bufferManager.add({ 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 = (
|
export const convertFileSize = (
|
||||||
sizeInBytes?: number,
|
sizeInBytes: number,
|
||||||
prec = 1,
|
prec = 1,
|
||||||
allowEmpty = false
|
allowEmpty = false
|
||||||
): string | number => {
|
): string | number => {
|
||||||
@@ -31,6 +31,7 @@ export const convertFileSize = (
|
|||||||
const fmt = 1024;
|
const fmt = 1024;
|
||||||
|
|
||||||
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
|
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
|
||||||
|
const precs = [0, 1, 1, 2, 2]; // precision for each unit
|
||||||
let size = sizeInBytes;
|
let size = sizeInBytes;
|
||||||
let unitIndex = 0;
|
let unitIndex = 0;
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ export const convertFileSize = (
|
|||||||
unitIndex++;
|
unitIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${_.round(size, prec)} ${units[unitIndex]}`;
|
return `${_.round(size, precs[unitIndex])} ${units[unitIndex]}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const platformCall = () => {
|
export const platformCall = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user