fix: playground formref is invalid

This commit is contained in:
jialin
2025-11-12 14:57:39 +08:00
parent 11a676be83
commit cbc000d35a
17 changed files with 110 additions and 449 deletions
+7
View File
@@ -54,3 +54,10 @@ export const allRegionInstanceTypeListAtom = atom<
>([]);
export const fromClusterCreationAtom = atom(false);
/**
* for temporary cluster session storage during creation/editing
*/
export const clusterSessionAtom = atom<{
firstAddWorker: boolean;
} | null>(null);
+1 -1
View File
@@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons';
// import './iconfont/iconfont.js';
const IconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_4613488_ga8perwfeom.js'
scriptUrl: '//at.alicdn.com/t/c/font_4613488_9h4e8ttfsau.js'
});
export default IconFont;
+37 -7
View File
@@ -2,15 +2,40 @@ import { Segmented, type SegmentedProps } from 'antd';
import React from 'react';
import styled from 'styled-components';
interface ThemeType {
color: string;
itemColorHover: string;
itemSelectedColor: string;
thumbBgColor: string;
fontWeight: number;
}
interface SegmentLineProps extends SegmentedProps {
height?: number;
theme: 'dark' | 'light';
}
const SegmentedQwrapper = styled.div<{ $height: number }>`
const darkTheme: ThemeType = {
color: 'var(--color-white-tertiary)',
itemColorHover: 'var(--color-white-secondary)',
itemSelectedColor: 'var(--color-white-primary)',
thumbBgColor: 'var(--color-white-primary)',
fontWeight: 400
};
const lightTheme: ThemeType = {
color: 'var(--ant-segmented-item-color)',
itemColorHover: 'var(--ant-segmented-item-hover-color)',
itemSelectedColor: 'var(--ant-segmented-item-selected-color)',
thumbBgColor: 'var(--ant-color-primary)',
fontWeight: 500
};
const SegmentedQwrapper = styled.div<{ $height: number; $theme: ThemeType }>`
.ant-segmented.segment-line {
padding: 0;
background-color: transparent;
color: var(--color-white-tertiary);
color: ${(props) => props.$theme.color};
font-weight: ${(props) => props.$theme.fontWeight};
border: none;
box-shadow: none;
height: ${(props) => props.$height}px;
@@ -21,14 +46,14 @@ const SegmentedQwrapper = styled.div<{ $height: number }>`
height: 100%;
}
.ant-segmented-item:hover {
color: var(--color-white-secondary);
color: ${(props) => props.$theme.itemColorHover};
}
.ant-segmented-thumb {
height: 2px;
padding: 0px;
bottom: 0px;
top: unset;
background-color: var(--color-white-primary) !important;
background-color: ${(props) => props.$theme.thumbBgColor} !important;
}
.ant-segmented-item-label {
display: flex;
@@ -42,7 +67,7 @@ const SegmentedQwrapper = styled.div<{ $height: number }>`
display: flex;
align-items: center;
&::after {
background-color: var(--color-white-primary) !important;
background-color: ${(props) => props.$theme.thumbBgColor} !important;
width: 100%;
height: 0px;
border-radius: 2px;
@@ -53,7 +78,8 @@ const SegmentedQwrapper = styled.div<{ $height: number }>`
}
.ant-segmented-item-selected {
background-color: transparent;
color: var(--color-white-primary);
box-shadow: none;
color: ${(props) => props.$theme.itemSelectedColor};
&::after {
height: 2px;
opacity: 1;
@@ -68,11 +94,15 @@ const SegmentLine: React.FC<SegmentLineProps> = (props) => {
size = 'small',
options = [],
className = 'segment-line',
theme = 'dark',
...rest
} = props;
return (
<SegmentedQwrapper $height={height}>
<SegmentedQwrapper
$height={height}
$theme={theme === 'dark' ? darkTheme : lightTheme}
>
<Segmented
{...rest}
size={size}
@@ -1,7 +1,7 @@
.ant-pro-footer-bar {
padding-inline: 0;
border-block-start: 1px solid rgba(5, 5, 5, 6%);
background-color: #f5f5f5;
// background-color: #f5f5f5;
border-radius: 4px 4px 0 0;
.ant-pro-footer-bar-left {
+5 -2
View File
@@ -1,6 +1,7 @@
import IconFont from '@/components/icon-font';
import ModalFooter from '@/components/modal-footer';
import GSDrawer from '@/components/scroller-modal/gs-drawer';
import SegmentLine from '@/components/segment-line';
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { useIntl } from '@umijs/max';
@@ -33,7 +34,8 @@ const SegmentedInner = styled(Segmented)`
`;
const SegmentedHeader = styled.div`
padding: 16px 24px;
margin: 16px 24px;
border-bottom: 1px solid var(--ant-color-split);
`;
interface AddModalProps {
@@ -203,7 +205,8 @@ const AddModal: React.FC<AddModalProps> = (props) => {
footer={false}
>
<SegmentedHeader>
<SegmentedInner
<SegmentLine
theme="light"
onChange={(value) => setActiveKey(value as string)}
options={[
{
+33 -12
View File
@@ -1,4 +1,4 @@
import { expandKeysAtom } from '@/atoms/clusters';
import { clusterSessionAtom, expandKeysAtom } from '@/atoms/clusters';
import DeleteModal from '@/components/delete-modal';
import IconFont from '@/components/icon-font';
import { FilterBar } from '@/components/page-tools';
@@ -10,7 +10,7 @@ import useExpandedRowKeys from '@/hooks/use-expanded-row-keys';
import useTableFetch from '@/hooks/use-table-fetch';
import useWatchList from '@/hooks/use-watch-list';
import AddWorker from '@/pages/cluster-management/components/add-worker';
import { useIntl, useNavigate, useSearchParams } from '@umijs/max';
import { useIntl, useNavigate } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { Button, message } from 'antd';
import { useAtom } from 'jotai';
@@ -32,7 +32,11 @@ import AddCluster from './components/add-cluster';
import AddPool from './components/add-pool';
import PoolRows from './components/pool-rows';
import RegisterCluster from './components/register-cluster';
import { ProviderType, ProviderValueMap } from './config';
import {
ClusterStatusValueMap,
ProviderType,
ProviderValueMap
} from './config';
import {
ClusterListItem,
ClusterFormData as FormData,
@@ -61,15 +65,10 @@ const Clusters: React.FC = () => {
contentForDelete: 'menu.clusterManagement.clusters'
});
const { watchDataList: allWorkerPoolList } = useWatchList(WORKER_POOLS_API);
const [expandAtom, setExpandAtom] = useAtom(expandKeysAtom);
const {
handleExpandChange,
handleExpandAll,
updateExpandedRowKeys,
removeExpandedRowKey,
expandedRowKeys
} = useExpandedRowKeys(expandAtom);
const [searchParams, setSearchParams] = useSearchParams();
const [expandAtom] = useAtom(expandKeysAtom);
const [clusterSession, setClusterSession] = useAtom(clusterSessionAtom);
const { handleExpandChange, handleExpandAll, expandedRowKeys } =
useExpandedRowKeys(expandAtom);
const navigate = useNavigate();
const intl = useIntl();
const [registerClusterStatus, setRegisterClusterStatus] = useState<{
@@ -309,6 +308,28 @@ const Clusters: React.FC = () => {
fetchCredentialList();
}, []);
/**
* add worker from workers page redirect
*/
useEffect(() => {
if (
clusterSession?.firstAddWorker &&
dataSource.loadend &&
dataSource.dataList?.length > 0
) {
const targetCluster = dataSource.dataList.find(
(cluster) =>
cluster.provider === ProviderValueMap.Docker &&
cluster.state === ClusterStatusValueMap.Ready
);
if (targetCluster) {
handleAddWorker(targetCluster);
// reset session
setClusterSession(null);
}
}
}, [clusterSession, dataSource.loadend, dataSource.dataList]);
const renderChildren = (
list: any,
options: { parent?: any; [key: string]: any }
@@ -29,17 +29,16 @@ const NotesWrapper = styled.ol`
margin: 0 !important;
padding: 0 !important;
line-height: 1.25;
counter-reset: main 2 sub 0;
li {
margin-left: 12px !important;
counter-increment: sub;
margin-left: 20px !important;
&::marker {
content: counters(main, '.') '.' counter(sub) ' ';
}
}
`;
const Title = styled.div`
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
`;
const Line = styled.div`
position: relative;
margin: 24px 0;
@@ -1,390 +0,0 @@
import DropdownButtons from '@/components/drop-down-buttons';
import GaugeChart from '@/components/echarts/gauge';
import IconFont from '@/components/icon-font';
import StatusTag from '@/components/status-tag';
import ThemeTag from '@/components/tags-wrapper/theme-tag';
import Card from '@/components/templates/card';
import { PageAction } from '@/config';
import { Card as ACard, Col, Collapse, Row } from 'antd';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { queryClusterToken } from '../apis';
import {
ClusterStatus,
ClusterStatusLabelMap,
ProviderLabelMap,
ProviderValueMap,
clusterActionList
} from '../config';
import { ClusterListItem as ListItem, NodePoolListItem } from '../config/types';
import AddPool from './add-pool';
import RegisterCluster from './register-cluster';
import WorkerPools from './worker-pools';
const Content = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
.chart-wrapper {
flex: 1;
}
`;
const CardWrapper = styled(ACard)`
text-align: center;
box-shadow: none !important;
flex: 1;
.ant-card {
box-shadow: none;
}
.ant-card-body {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
padding: 6px 10px;
}
.label {
font-weight: 400;
margin-bottom: 12px;
font-size: var(--font-size-middle);
color: var(--ant-color-text-secondary);
}
.value {
font-weight: 600;
color: var(--ant-color-text);
font-size: var(--font-size-large);
}
`;
const CardBox = styled.div`
display: flex;
gap: 16px;
flex: 1;
`;
const Inner = styled.div`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
cursor: default;
.title {
margin-bottom: 12px;
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 8px;
.text {
display: flex;
align-items: center;
font-size: var(--font-size-middle);
font-weight: 500;
color: var(--ant-color-text);
}
}
.content {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
}
`;
const CollapseWrapper = styled(Collapse)`
width: 100%;
border: none;
border-radius: 0;
border-top: 1px solid var(--ant-color-split);
background-color: var(--ant-color-fill-quaternary);
.ant-collapse-content {
border-top: 1px solid var(--ant-color-split);
}
.ant-collapse-header {
font-size: var(--font-size-middle);
padding-block: 10px !important;
.ant-collapse-expand-icon {
margin-left: 0 !important;
}
}
table .ant-table-thead tr {
background-color: transparent !important;
th {
border-bottom: 1px solid var(--ant-color-split) !important;
font-weight: 500 !important;
}
}
`;
const SubTitle = styled.div`
font-size: var(--font-size-middle);
font-weight: 500;
color: var(--ant-color-text);
margin-block: 24px 16px;
`;
interface CardProps {
data: ListItem;
onSelect?: (key: string, row: ListItem) => void;
}
const gaugeConfig = {
radius: '100%',
progress: {
show: true,
roundCap: false,
width: 8
},
axisLine: {
roundCap: false,
lineStyle: {
width: 8,
color: [
[0.5, 'rgba(84, 204, 152, 80%)'],
[0.8, 'rgba(250, 173, 20, 80%)'],
[1, 'rgba(255, 77, 79, 80%)']
]
}
}
};
const CardItem: React.FC<CardProps> = (props) => {
const chartHeight = 160;
const { data, onSelect } = props;
const [show, setShow] = React.useState(false);
const [addPoolStatus, setAddPoolStatus] = React.useState({
open: false,
action: PageAction.CREATE,
title: '',
provider: ProviderValueMap.DigitalOcean
});
const [registerClusterStatus, setRegisterClusterStatus] = React.useState<{
open: boolean;
registrationInfo: {
token: string;
image: string;
server_url: string;
cluster_id: number;
};
}>({
open: false,
registrationInfo: {
token: '',
image: '',
server_url: '',
cluster_id: 0
}
});
const handleRegisterCluster = async () => {
try {
const info = await queryClusterToken({ id: data.id });
setRegisterClusterStatus({
open: true,
registrationInfo: {
...info,
cluster_id: data.id
}
});
} catch (error) {}
};
// cluster action handler
const handleOnSelect = (key: string) => {
if (key === 'addPool') {
setAddPoolStatus({
open: true,
action: PageAction.CREATE,
title: 'Add Worker Pool',
provider: data.provider
});
return;
}
if (key === 'register_cluster') {
handleRegisterCluster();
return;
}
onSelect?.(key, data);
};
// pool action handler
const handleOnAction = (action: string, record: NodePoolListItem) => {
if (action === 'edit') {
setAddPoolStatus({
open: true,
action: PageAction.CREATE,
title: 'Edit Worker Pool',
provider: data.provider
});
}
};
const actions = useMemo(() => {
return clusterActionList.filter((item) => {
if (item.provider) {
return item.provider === data.provider;
}
return true;
});
}, [data.provider]);
return (
<Card
height={'auto'}
clickable={false}
ghost
footerHolder={
<CollapseWrapper
onChange={() => setShow(!show)}
expandIconPosition="end"
expandIcon={({ isActive }) => (
<IconFont type="icon-down" rotate={isActive ? 0 : -90} />
)}
items={[
{
key: '1',
label: 'More Information',
children: (
<>
<div className="chart-wrapper">
<Row gutter={16} style={{ width: '100%' }}>
<Col span={6}>
<GaugeChart
title="GPU Utilization"
value={85}
height={chartHeight}
gaugeConfig={gaugeConfig}
/>
</Col>
<Col span={6}>
<GaugeChart
title="CPU Utilization"
value={50}
height={chartHeight}
gaugeConfig={gaugeConfig}
/>
</Col>
<Col span={6}>
<GaugeChart
title="RAM Utilization"
value={70}
height={chartHeight}
gaugeConfig={gaugeConfig}
/>
</Col>
<Col span={6}>
<GaugeChart
title="VRAM Utilization"
value={60}
height={chartHeight}
gaugeConfig={gaugeConfig}
/>
</Col>
</Row>
</div>
{data.provider === ProviderValueMap.DigitalOcean && (
<>
<SubTitle>Worker Pools</SubTitle>
<WorkerPools
provider={data.provider}
workerPools={data.worker_pools}
height={show ? 'auto' : 0}
onAction={handleOnAction}
/>
</>
)}
</>
)
}
]}
></CollapseWrapper>
}
>
<Inner>
<div className="title">
<span className="flex-center gap-8">
<span className="text">{data.name}</span>
<ThemeTag color="purple">
{ProviderLabelMap[data.provider]}
</ThemeTag>
<StatusTag
statusValue={{
status: ClusterStatus[data.state],
text: ClusterStatusLabelMap[data.state]
}}
/>
</span>
<span>
<DropdownButtons
items={actions}
onSelect={handleOnSelect}
></DropdownButtons>
</span>
</div>
<Content>
<CardBox>
<CardWrapper bordered={false}>
<div className="label">Workers</div>
<div className="value">
{data.ready_workers} / {data.workers}
</div>
</CardWrapper>
<CardWrapper bordered={false}>
<div className="label">GPUs</div>
<div className="value">{data.gpus}</div>
</CardWrapper>
<CardWrapper bordered={false}>
<div className="label">Deployments</div>
<div className="value">{data.models}</div>
</CardWrapper>
</CardBox>
</Content>
</Inner>
<AddPool
provider={addPoolStatus.provider}
open={addPoolStatus.open}
action={addPoolStatus.action}
title={addPoolStatus.title}
onCancel={() => {
setAddPoolStatus({
open: false,
action: PageAction.CREATE,
title: '',
provider: 'digitalocean'
});
}}
onOk={() => {
setAddPoolStatus({
open: false,
action: addPoolStatus.action,
title: '',
provider: 'digitalocean'
});
}}
></AddPool>
<RegisterCluster
title="Register Cluster"
open={registerClusterStatus.open}
registrationInfo={registerClusterStatus.registrationInfo}
onCancel={() => {
setRegisterClusterStatus({
open: false,
registrationInfo: {
token: '',
image: '',
server_url: '',
cluster_id: 0
}
});
}}
></RegisterCluster>
</Card>
);
};
export default CardItem;
@@ -13,19 +13,6 @@ const Wrapper = styled.div`
max-width: 300px !important;
color: var(--ant-color-text-description) !important;
}
.ant-steps-item-content > .ant-steps-item-title {
// font-weight: 600;
}
.ant-steps-item {
.ant-steps-item-container {
// .ant-steps-item-icon {
// margin-inline-start: 0 !important;
// }
// .ant-steps-item-tail {
// margin-inline-start: 0 !important;
// }
}
}
`;
const ClusterSteps: React.FC<{
+2 -1
View File
@@ -60,7 +60,7 @@ const Credentials: React.FC = () => {
deleteAPI: deleteCredential,
contentForDelete: 'menu.clusterManagement.credentials'
});
const [isFromCluster] = useAtom(fromClusterCreationAtom);
const [isFromCluster, setIsFromCluster] = useAtom(fromClusterCreationAtom);
const intl = useIntl();
const navigate = useNavigate();
const [openModalStatus, setOpenModalStatus] = useState<{
@@ -110,6 +110,7 @@ const Credentials: React.FC = () => {
navigate(-1);
}
}
setIsFromCluster(false);
fetchData();
setOpenModalStatus({ ...openModalStatus, open: false });
message.success(intl.formatMessage({ id: 'common.message.success' }));
@@ -454,6 +454,7 @@ const AddModal: FC<AddModalProps> = (props) => {
form.current?.form?.setFieldsValue({
...props.initialValues
});
console.log('handleOnOpen - modelFiles:', props.initialValues);
handleOnValuesChange?.({
changedValues: {},
allValues: {
@@ -530,7 +531,6 @@ const AddModal: FC<AddModalProps> = (props) => {
closeIcon={false}
maskClosable={false}
keyboard={false}
zIndex={2000}
styles={{
body: {
height: 'calc(100vh - 57px)',
+10 -2
View File
@@ -1,3 +1,5 @@
import IconFont from '@/components/icon-font';
import SegmentLine from '@/components/segment-line';
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import CollapsePanel from '@/pages/_components/collapse-panel';
@@ -52,7 +54,8 @@ const SegmentedHeader = styled.div<{ $top?: number }>`
position: sticky;
top: ${(props) => props.$top || 0}px;
z-index: 10;
padding-bottom: 16px;
margin-bottom: 16px;
border-bottom: 1px solid var(--ant-color-split);
background-color: var(--ant-color-bg-elevated);
`;
@@ -112,21 +115,25 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
{
value: TABKeysMap.BASIC,
label: intl.formatMessage({ id: 'common.title.basicInfo' }),
icon: <IconFont type="icon-basic" />,
field: 'name'
},
{
value: TABKeysMap.SCHEDULING,
label: intl.formatMessage({ id: 'models.form.scheduling' }),
icon: <IconFont type="icon-model" />,
field: 'scheduleType'
},
{
value: TABKeysMap.PERFORMANCE,
label: intl.formatMessage({ id: 'models.form.performance' }),
icon: <IconFont type="icon-speed" />,
field: 'extended_kv_cache.enabled'
},
{
value: TABKeysMap.ADVANCED,
label: intl.formatMessage({ id: 'resources.form.advanced' }),
icon: <IconFont type="icon-settings" />,
field: 'categories'
}
];
@@ -404,7 +411,8 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
}}
>
<SegmentedHeader $top={segmentedTop.top}>
<SegmentedInner
<SegmentLine
theme={'light'}
defaultValue="basic"
value={target}
onChange={handleTargetChange}
@@ -52,13 +52,13 @@ const GroundLeft: React.FC<MessageProps> = forwardRef((props, ref) => {
handleAddNewMessage,
handleClear,
setMessageList,
formRef,
tokenResult,
messageList,
loading
} = useChatCompletion(scroller);
const {
handleOnValuesChange,
formRef,
paramsRef,
paramsConfig,
initialValues,
@@ -23,7 +23,6 @@ export default function useChatCompletion(
const currentMessageRef = useRef<any>(null);
const messageListLengthCache = useRef<number>(0);
const reasonContentRef = useRef('');
const formRef = useRef<any>(null);
const setMessageId = () => {
messageId.current = messageId.current + 1;
@@ -217,7 +216,6 @@ export default function useChatCompletion(
loading,
tokenResult,
messageList,
formRef,
setMessageId,
setMessageList,
handleClear,
@@ -52,6 +52,7 @@ export const useInitLLmMeta = (
defaultValues = {},
defaultParamsConfig = []
} = options;
const formRef = useRef<any>(null);
const [searchParams] = useSearchParams();
const [modelMeta, setModelMeta] = useState<any>({});
const [initialValues, setInitialValues] = useState<any>({
@@ -194,6 +195,7 @@ export const useInitLLmMeta = (
setInitialValues,
setParams,
setParamsConfig,
formRef,
paramsConfig,
initialValues,
parameters,
-9
View File
@@ -11,7 +11,6 @@ import classNames from 'classnames';
import _ from 'lodash';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import styled from 'styled-components';
import { PageContainerInner } from '../_components/page-box';
import { queryModelsList } from './apis';
import GroundLeft from './components/ground-left';
@@ -19,14 +18,6 @@ import MultipleChat from './components/multiple-chat';
import ViewCodeButtons from './components/view-code-buttons';
import './style/play-ground.less';
const Header = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 16px;
padding-inline: 32px;
`;
const Playground: React.FC = () => {
const intl = useIntl();
const { size } = useWindowResize();
@@ -1,3 +1,4 @@
import { clusterSessionAtom } from '@/atoms/clusters';
import DeleteModal from '@/components/delete-modal';
import IconFont from '@/components/icon-font';
import { FilterBar } from '@/components/page-tools';
@@ -7,6 +8,7 @@ import { queryClusterList } from '@/pages/cluster-management/apis';
import { useIntl, useNavigate } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { Button, ConfigProvider, Table, message } from 'antd';
import { useAtom } from 'jotai';
import React, { useEffect, useState } from 'react';
import NoResult from '../../_components/no-result';
import TerminalTabs from '../../_components/terminal-tabs';
@@ -44,6 +46,7 @@ const Workers: React.FC = () => {
watch: true,
API: WORKERS_API
});
const [, setClusterSession] = useAtom(clusterSessionAtom);
const navigate = useNavigate();
const intl = useIntl();
@@ -178,6 +181,7 @@ const Workers: React.FC = () => {
});
const handleAddWorker = () => {
setClusterSession({ firstAddWorker: true });
navigate('/cluster-management/clusters/list');
};