fix(style): k8s form
This commit is contained in:
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
"@ant-design/pro-components": "3.1.0-0",
|
"@ant-design/pro-components": "3.1.0-0",
|
||||||
"@antv/g6": "^5.0.51",
|
"@antv/g6": "^5.0.51",
|
||||||
"@braintree/sanitize-url": "^7.1.1",
|
"@braintree/sanitize-url": "^7.1.1",
|
||||||
"@gpustack/core-ui": "^1.0.20",
|
"@gpustack/core-ui": "^1.0.21",
|
||||||
"@huggingface/gguf": "^0.1.7",
|
"@huggingface/gguf": "^0.1.7",
|
||||||
"@huggingface/hub": "^0.15.1",
|
"@huggingface/hub": "^0.15.1",
|
||||||
"@huggingface/tasks": "^0.11.6",
|
"@huggingface/tasks": "^0.11.6",
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default {
|
|||||||
'menu.usage': '使用量',
|
'menu.usage': '使用量',
|
||||||
'menu.404': '404',
|
'menu.404': '404',
|
||||||
'menu.resources.workers': '节点',
|
'menu.resources.workers': '节点',
|
||||||
'menu.resources.gpus': 'GPUs',
|
'menu.resources.gpus': 'GPU',
|
||||||
'menu.models.modelfiles': '模型文件',
|
'menu.models.modelfiles': '模型文件',
|
||||||
'menu.accessControl': '访问控制',
|
'menu.accessControl': '访问控制',
|
||||||
'menu.accessControl.apikeys': 'API 密钥',
|
'menu.accessControl.apikeys': 'API 密钥',
|
||||||
|
|||||||
@@ -276,7 +276,9 @@ const VersionsForm: React.FC<AddModalProps> = ({
|
|||||||
key={name}
|
key={name}
|
||||||
defaultOpen
|
defaultOpen
|
||||||
styles={{
|
styles={{
|
||||||
body: collapseKey.has(name) ? { padding: 16 } : {},
|
body: collapseKey.has(name)
|
||||||
|
? { paddingInline: 16, paddingBlock: '16px 0' }
|
||||||
|
: {},
|
||||||
content: { paddingTop: 0 },
|
content: { paddingTop: 0 },
|
||||||
header: {
|
header: {
|
||||||
backgroundColor: 'unset'
|
backgroundColor: 'unset'
|
||||||
|
|||||||
@@ -11,8 +11,14 @@ import {
|
|||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Form } from 'antd';
|
import { Form } from 'antd';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import React, { forwardRef, useEffect, useImperativeHandle } from 'react';
|
import React, {
|
||||||
|
forwardRef,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useState
|
||||||
|
} from 'react';
|
||||||
import { ProviderType, ProviderValueMap } from '../config';
|
import { ProviderType, ProviderValueMap } from '../config';
|
||||||
|
import { FormContext } from '../config/form-context';
|
||||||
import {
|
import {
|
||||||
ClusterFormData as FormData,
|
ClusterFormData as FormData,
|
||||||
ClusterListItem as ListItem
|
ClusterListItem as ListItem
|
||||||
@@ -37,6 +43,7 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
const [k8sActiveKey, setK8sActiveKey] = React.useState<string[]>([
|
const [k8sActiveKey, setK8sActiveKey] = React.useState<string[]>([
|
||||||
'k8sOptions'
|
'k8sOptions'
|
||||||
]);
|
]);
|
||||||
|
const [submitAttempted, setSubmitAttempted] = useState(false);
|
||||||
const advanceConfigRef = React.useRef<any>(null);
|
const advanceConfigRef = React.useRef<any>(null);
|
||||||
const systemConfig = useAtomValue(systemConfigAtom);
|
const systemConfig = useAtomValue(systemConfigAtom);
|
||||||
|
|
||||||
@@ -171,7 +178,12 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
validateFields: async () => {
|
validateFields: async () => {
|
||||||
await form.validateFields();
|
try {
|
||||||
|
await form.validateFields();
|
||||||
|
} catch (e) {
|
||||||
|
setSubmitAttempted(true);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
const values = form.getFieldsValue(true);
|
const values = form.getFieldsValue(true);
|
||||||
|
|
||||||
const workerConfig = yaml2Json(
|
const workerConfig = yaml2Json(
|
||||||
@@ -187,103 +199,112 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const handleOnFinishFailed = () => {
|
||||||
|
setSubmitAttempted(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<FormContext.Provider value={{ submitAttempted }}>
|
||||||
name="clusterForm"
|
<Form
|
||||||
form={form}
|
name="clusterForm"
|
||||||
onFinish={handleOnFinish}
|
form={form}
|
||||||
preserve={false}
|
onFinish={handleOnFinish}
|
||||||
scrollToFirstError={true}
|
onFinishFailed={handleOnFinishFailed}
|
||||||
initialValues={currentData}
|
preserve={false}
|
||||||
>
|
scrollToFirstError={true}
|
||||||
<Form.Item<FormData>
|
initialValues={currentData}
|
||||||
name="name"
|
>
|
||||||
rules={[
|
<Form.Item<FormData>
|
||||||
{
|
name="name"
|
||||||
required: true,
|
rules={[
|
||||||
message: intl.formatMessage(
|
{
|
||||||
{ id: 'common.form.rule.input' },
|
required: true,
|
||||||
|
message: intl.formatMessage(
|
||||||
|
{ id: 'common.form.rule.input' },
|
||||||
|
{
|
||||||
|
name: intl.formatMessage({ id: 'common.table.name' })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CInput.Input
|
||||||
|
label={intl.formatMessage({ id: 'common.table.name' })}
|
||||||
|
required
|
||||||
|
trim={false}
|
||||||
|
></CInput.Input>
|
||||||
|
</Form.Item>
|
||||||
|
<PluginExtraFields name="CreateOrgScopeField" context={{ action }} />
|
||||||
|
{provider === ProviderValueMap.DigitalOcean && (
|
||||||
|
<CloudProvider
|
||||||
|
provider={provider}
|
||||||
|
action={action}
|
||||||
|
credentialID={currentData?.credential_id}
|
||||||
|
credentialList={credentialList}
|
||||||
|
></CloudProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form.Item<FormData>
|
||||||
|
name="description"
|
||||||
|
rules={[{ required: false }]}
|
||||||
|
style={{ marginBottom: 8 }}
|
||||||
|
>
|
||||||
|
<SealTextArea
|
||||||
|
scaleSize
|
||||||
|
label={intl.formatMessage({ id: 'common.table.description' })}
|
||||||
|
></SealTextArea>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{provider === ProviderValueMap.Kubernetes && (
|
||||||
|
<CollapsePanel
|
||||||
|
accordion={false}
|
||||||
|
activeKey={k8sActiveKey}
|
||||||
|
onChange={(keys) =>
|
||||||
|
setK8sActiveKey(Array.isArray(keys) ? keys : [keys])
|
||||||
|
}
|
||||||
|
items={[
|
||||||
{
|
{
|
||||||
name: intl.formatMessage({ id: 'common.table.name' })
|
key: 'k8sOptions',
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'clusters.k8sOptions.title'
|
||||||
|
}),
|
||||||
|
forceRender: true,
|
||||||
|
children: (
|
||||||
|
<K8sPodSpec
|
||||||
|
key={currentData?.id ?? 'new'}
|
||||||
|
action={action}
|
||||||
|
initialGpuInstanceOptions={
|
||||||
|
currentData?.k8s_options?.gpuInstanceOptions
|
||||||
|
}
|
||||||
|
></K8sPodSpec>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
]}
|
||||||
}
|
></CollapsePanel>
|
||||||
]}
|
)}
|
||||||
>
|
|
||||||
<CInput.Input
|
|
||||||
label={intl.formatMessage({ id: 'common.table.name' })}
|
|
||||||
required
|
|
||||||
trim={false}
|
|
||||||
></CInput.Input>
|
|
||||||
</Form.Item>
|
|
||||||
<PluginExtraFields name="CreateOrgScopeField" context={{ action }} />
|
|
||||||
{provider === ProviderValueMap.DigitalOcean && (
|
|
||||||
<CloudProvider
|
|
||||||
provider={provider}
|
|
||||||
action={action}
|
|
||||||
credentialID={currentData?.credential_id}
|
|
||||||
credentialList={credentialList}
|
|
||||||
></CloudProvider>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form.Item<FormData>
|
|
||||||
name="description"
|
|
||||||
rules={[{ required: false }]}
|
|
||||||
style={{ marginBottom: 8 }}
|
|
||||||
>
|
|
||||||
<SealTextArea
|
|
||||||
scaleSize
|
|
||||||
label={intl.formatMessage({ id: 'common.table.description' })}
|
|
||||||
></SealTextArea>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
{provider === ProviderValueMap.Kubernetes && (
|
|
||||||
<CollapsePanel
|
<CollapsePanel
|
||||||
accordion={false}
|
accordion={false}
|
||||||
activeKey={k8sActiveKey}
|
activeKey={activeKey}
|
||||||
onChange={(keys) =>
|
onChange={handleOnCollapseChange}
|
||||||
setK8sActiveKey(Array.isArray(keys) ? keys : [keys])
|
|
||||||
}
|
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
key: 'k8sOptions',
|
key: 'advanceConfig',
|
||||||
label: intl.formatMessage({ id: 'clusters.k8sOptions.title' }),
|
label: intl.formatMessage({ id: 'resources.form.advanced' }),
|
||||||
forceRender: true,
|
forceRender: true,
|
||||||
children: (
|
children: (
|
||||||
<K8sPodSpec
|
<AdvanceConfig
|
||||||
key={currentData?.id ?? 'new'}
|
|
||||||
action={action}
|
action={action}
|
||||||
initialGpuInstanceOptions={
|
provider={provider}
|
||||||
currentData?.k8s_options?.gpuInstanceOptions
|
currentData={currentData}
|
||||||
}
|
ref={advanceConfigRef}
|
||||||
></K8sPodSpec>
|
></AdvanceConfig>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
></CollapsePanel>
|
></CollapsePanel>
|
||||||
)}
|
</Form>
|
||||||
|
</FormContext.Provider>
|
||||||
<CollapsePanel
|
|
||||||
accordion={false}
|
|
||||||
activeKey={activeKey}
|
|
||||||
onChange={handleOnCollapseChange}
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
key: 'advanceConfig',
|
|
||||||
label: intl.formatMessage({ id: 'resources.form.advanced' }),
|
|
||||||
forceRender: true,
|
|
||||||
children: (
|
|
||||||
<AdvanceConfig
|
|
||||||
action={action}
|
|
||||||
provider={provider}
|
|
||||||
currentData={currentData}
|
|
||||||
ref={advanceConfigRef}
|
|
||||||
></AdvanceConfig>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
></CollapsePanel>
|
|
||||||
</Form>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { Input as CInput } from '@gpustack/core-ui';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Divider } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { ImageCredential } from '../config/types';
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface CredentialItemProps {
|
||||||
|
item: ImageCredential;
|
||||||
|
validated: boolean;
|
||||||
|
index: number;
|
||||||
|
onChange: (partial: Partial<ImageCredential>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CredentialItem: React.FC<CredentialItemProps> = ({
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
validated,
|
||||||
|
onChange
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const registryEmpty = !item.registry?.trim();
|
||||||
|
const registryStatus =
|
||||||
|
validated && registryEmpty ? ('error' as const) : ('success' as const);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
{index !== 0 && (
|
||||||
|
<Divider style={{ marginBlock: 8 }} variant="dashed"></Divider>
|
||||||
|
)}
|
||||||
|
<CInput.Input
|
||||||
|
required
|
||||||
|
status={registryStatus}
|
||||||
|
value={item.registry}
|
||||||
|
onChange={(e) => onChange({ registry: e.target.value })}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'clusters.imageCredentials.registry'
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<div className="row">
|
||||||
|
<CInput.Input
|
||||||
|
value={item.username}
|
||||||
|
status="success"
|
||||||
|
onChange={(e) => onChange({ username: e.target.value })}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'clusters.imageCredentials.username'
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<CInput.Password
|
||||||
|
status="success"
|
||||||
|
value={item.password}
|
||||||
|
onChange={(e) => onChange({ password: e.target.value })}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'clusters.imageCredentials.password'
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CredentialItem;
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { MetadataList, useAppUtils } from '@gpustack/core-ui';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Form } from 'antd';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useFormContext } from '../config/form-context';
|
||||||
|
import { ImageCredential as ImageCredentialType } from '../config/types';
|
||||||
|
import imgCredentialStyle from '../styles/img-credential.less';
|
||||||
|
import CredentialItem from './credential-item';
|
||||||
|
|
||||||
|
const FIELD_PATH = ['k8s_options', 'imageCredentials'];
|
||||||
|
|
||||||
|
const ImageCredential: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { getRuleMessage } = useAppUtils();
|
||||||
|
const { submitAttempted } = useFormContext();
|
||||||
|
const validated = !!submitAttempted;
|
||||||
|
const form = Form.useFormInstance();
|
||||||
|
const credentials: ImageCredentialType[] =
|
||||||
|
Form.useWatch(FIELD_PATH, form) || [];
|
||||||
|
|
||||||
|
const updateList = (list: ImageCredentialType[]) => {
|
||||||
|
form.setFieldValue(FIELD_PATH, list);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (validated) {
|
||||||
|
form.validateFields([FIELD_PATH]).catch(() => {});
|
||||||
|
}
|
||||||
|
}, [credentials, validated]);
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
updateList([...credentials, { registry: '', username: '', password: '' }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (index: number) => {
|
||||||
|
const next = [...credentials];
|
||||||
|
next.splice(index, 1);
|
||||||
|
updateList(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (
|
||||||
|
index: number,
|
||||||
|
partial: Partial<ImageCredentialType>
|
||||||
|
) => {
|
||||||
|
const next = [...credentials];
|
||||||
|
next[index] = { ...next[index], ...partial };
|
||||||
|
updateList(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={imgCredentialStyle.container}>
|
||||||
|
<Form.Item
|
||||||
|
name={FIELD_PATH}
|
||||||
|
style={{ marginTop: 24 }}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
validator: async (_, value: ImageCredentialType[]) => {
|
||||||
|
if (!value?.length) return;
|
||||||
|
const hasMissingRegistry = value.some(
|
||||||
|
(item) => !item?.registry?.trim()
|
||||||
|
);
|
||||||
|
if (hasMissingRegistry) {
|
||||||
|
throw new Error(
|
||||||
|
getRuleMessage('input', 'clusters.imageCredentials.registry')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<MetadataList
|
||||||
|
label={intl.formatMessage({ id: 'clusters.imageCredentials.title' })}
|
||||||
|
btnText={intl.formatMessage({ id: 'clusters.imageCredentials.add' })}
|
||||||
|
dataList={credentials}
|
||||||
|
onAdd={handleAdd}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
styles={{
|
||||||
|
delBtn: {
|
||||||
|
marginTop: 44
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(item: ImageCredentialType, index: number) => (
|
||||||
|
<CredentialItem
|
||||||
|
item={item}
|
||||||
|
index={index}
|
||||||
|
key={index}
|
||||||
|
validated={validated}
|
||||||
|
onChange={(partial) => handleChange(index, partial)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</MetadataList>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImageCredential;
|
||||||
@@ -1,20 +1,11 @@
|
|||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import {
|
import { Input as CInput, LabelSelector, SwitchCard } from '@gpustack/core-ui';
|
||||||
MinusOutlined,
|
|
||||||
PlusOutlined,
|
|
||||||
QuestionCircleOutlined
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import {
|
|
||||||
Input as CInput,
|
|
||||||
LabelSelector,
|
|
||||||
SwitchCard,
|
|
||||||
useAppUtils
|
|
||||||
} from '@gpustack/core-ui';
|
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Form, Tooltip } from 'antd';
|
import { Form } from 'antd';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { GpuInstanceOptions } from '../config/types';
|
import { GpuInstanceOptions } from '../config/types';
|
||||||
|
import ImageCredential from './image-credential';
|
||||||
import K8SVolumeMount from './k8s-volume-mount';
|
import K8SVolumeMount from './k8s-volume-mount';
|
||||||
|
|
||||||
const Title = styled.div`
|
const Title = styled.div`
|
||||||
@@ -32,151 +23,21 @@ const SectionWrap = styled.div`
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ImageCredentialsForm: React.FC = () => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const { getRuleMessage } = useAppUtils();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionWrap>
|
|
||||||
<Form.List name={['k8s_options', 'imageCredentials']}>
|
|
||||||
{(fields, { add, remove }) => (
|
|
||||||
<>
|
|
||||||
<Title>
|
|
||||||
<div className="flex-center gap-8">
|
|
||||||
<span>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: 'clusters.imageCredentials.title'
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
onClick={() =>
|
|
||||||
add({ registry: '', username: '', password: '' })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<PlusOutlined />{' '}
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: 'clusters.imageCredentials.add'
|
|
||||||
})}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Title>
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
||||||
{fields.map(({ key, name }) => (
|
|
||||||
<div
|
|
||||||
key={key}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
gap: 8,
|
|
||||||
padding: 12,
|
|
||||||
border: '1px solid var(--ant-color-split)',
|
|
||||||
borderRadius: 'var(--ant-border-radius-lg)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: 12
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
name={[name, 'registry']}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: getRuleMessage(
|
|
||||||
'input',
|
|
||||||
'clusters.imageCredentials.registry'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
style={{ marginBottom: 0 }}
|
|
||||||
>
|
|
||||||
<CInput.Input
|
|
||||||
required
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: 'clusters.imageCredentials.registry'
|
|
||||||
})}
|
|
||||||
></CInput.Input>
|
|
||||||
</Form.Item>
|
|
||||||
<div style={{ display: 'flex', gap: 12 }}>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<Form.Item
|
|
||||||
name={[name, 'username']}
|
|
||||||
style={{ marginBottom: 0 }}
|
|
||||||
>
|
|
||||||
<CInput.Input
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: 'clusters.imageCredentials.username'
|
|
||||||
})}
|
|
||||||
></CInput.Input>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<Form.Item
|
|
||||||
name={[name, 'password']}
|
|
||||||
style={{ marginBottom: 0 }}
|
|
||||||
>
|
|
||||||
<CInput.Password
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: 'clusters.imageCredentials.password'
|
|
||||||
})}
|
|
||||||
></CInput.Password>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
shape="circle"
|
|
||||||
style={{ marginTop: 8 }}
|
|
||||||
onClick={() => remove(name)}
|
|
||||||
>
|
|
||||||
<MinusOutlined />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Form.List>
|
|
||||||
</SectionWrap>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const NodeSelectorForm: React.FC = () => {
|
const NodeSelectorForm: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionWrap>
|
<SectionWrap>
|
||||||
<Title>
|
|
||||||
<span className="flex-center gap-4">
|
|
||||||
<span>
|
|
||||||
{intl.formatMessage({ id: 'clusters.nodeSelector.title' })}
|
|
||||||
</span>
|
|
||||||
<Tooltip
|
|
||||||
title={intl.formatMessage({ id: 'clusters.nodeSelector.tip' })}
|
|
||||||
>
|
|
||||||
<QuestionCircleOutlined
|
|
||||||
style={{ color: 'var(--ant-color-text-secondary)' }}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
</Title>
|
|
||||||
<Form.Item name={['k8s_options', 'nodeSelector']}>
|
<Form.Item name={['k8s_options', 'nodeSelector']}>
|
||||||
<LabelSelector
|
<LabelSelector
|
||||||
label={intl.formatMessage({ id: 'clusters.nodeSelector.title' })}
|
label={intl.formatMessage({ id: 'clusters.nodeSelector.title' })}
|
||||||
|
description={intl.formatMessage({ id: 'clusters.nodeSelector.tip' })}
|
||||||
></LabelSelector>
|
></LabelSelector>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</SectionWrap>
|
</SectionWrap>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render namespace. Kept as the first field of the section so the most
|
|
||||||
// fundamental K8s deployment knob is set before the rest.
|
|
||||||
const NamespaceForm: React.FC = () => {
|
const NamespaceForm: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
@@ -197,8 +58,6 @@ const NamespaceForm: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Operator-image override. A plain string knob that used to ride along inside
|
|
||||||
// worker_config; it now lives directly on k8s_options.
|
|
||||||
const OperatorImageForm: React.FC = () => {
|
const OperatorImageForm: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
@@ -273,7 +132,7 @@ const K8sPodSpec: React.FC<{
|
|||||||
<GpuInstanceOptionsForm initialValue={initialGpuInstanceOptions} />
|
<GpuInstanceOptionsForm initialValue={initialGpuInstanceOptions} />
|
||||||
<NamespaceForm />
|
<NamespaceForm />
|
||||||
<K8SVolumeMount action={action}></K8SVolumeMount>
|
<K8SVolumeMount action={action}></K8SVolumeMount>
|
||||||
<ImageCredentialsForm />
|
<ImageCredential />
|
||||||
<NodeSelectorForm />
|
<NodeSelectorForm />
|
||||||
<OperatorImageForm />
|
<OperatorImageForm />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -169,7 +169,9 @@ const VolumeMountsForm: React.FC<{ action: PageActionType }> = ({ action }) => {
|
|||||||
open={collapseKey.has(name)}
|
open={collapseKey.has(name)}
|
||||||
onToggle={(open: boolean) => onToggle(open, name)}
|
onToggle={(open: boolean) => onToggle(open, name)}
|
||||||
styles={{
|
styles={{
|
||||||
body: collapseKey.has(name) ? { padding: 16 } : {},
|
body: collapseKey.has(name)
|
||||||
|
? { paddingBlock: '16px 0', paddingInline: 16 }
|
||||||
|
: {},
|
||||||
content: { paddingTop: 0 },
|
content: { paddingTop: 0 },
|
||||||
header: {
|
header: {
|
||||||
backgroundColor: 'unset'
|
backgroundColor: 'unset'
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
import { ClusterListItem } from './types';
|
||||||
|
|
||||||
|
// for cluster form
|
||||||
|
interface FormContextProps {
|
||||||
|
currentData?: ClusterListItem;
|
||||||
|
submitAttempted?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormContext = createContext<FormContextProps>({});
|
||||||
|
|
||||||
|
export const useFormContext = () => useContext(FormContext);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.container {
|
||||||
|
:global(.item-container) {
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
:global(.btn) {
|
||||||
|
margin-top: 16px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ const Basic = ({ action }: { action: string }) => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item<FormData> name="displayName">
|
<Form.Item<FormData> name="displayName">
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
|
trim={false}
|
||||||
label={intl.formatMessage({ id: 'common.table.displayName' })}
|
label={intl.formatMessage({ id: 'common.table.displayName' })}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ModelFileFormData as FormData } from '@/pages/resources/config/types';
|
import { ModelFileFormData as FormData } from '@/pages/resources/config/types';
|
||||||
import {
|
import {
|
||||||
CheckboxField,
|
|
||||||
Input as CInput,
|
Input as CInput,
|
||||||
Cascader as SealCascader,
|
Cascader as SealCascader,
|
||||||
Select as SealSelect,
|
Select as SealSelect,
|
||||||
@@ -204,6 +203,13 @@ const TargetForm: React.FC<TargetFormProps> = forwardRef((props, ref) => {
|
|||||||
root: 'cascader-popup-wrapper gpu-selector'
|
root: 'cascader-popup-wrapper gpu-selector'
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
styles={{
|
||||||
|
popup: {
|
||||||
|
listItem: {
|
||||||
|
maxWidth: '100%'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
maxTagCount={1}
|
maxTagCount={1}
|
||||||
label={intl.formatMessage({ id: 'resources.worker' })}
|
label={intl.formatMessage({ id: 'resources.worker' })}
|
||||||
options={workerOptions}
|
options={workerOptions}
|
||||||
@@ -241,20 +247,6 @@ const TargetForm: React.FC<TargetFormProps> = forwardRef((props, ref) => {
|
|||||||
></CInput.Input>
|
></CInput.Input>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
{source === modelSourceMap.local_path_value && (
|
|
||||||
<Form.Item<FormData>
|
|
||||||
name="is_lora"
|
|
||||||
key="is_lora"
|
|
||||||
valuePropName="checked"
|
|
||||||
noStyle
|
|
||||||
>
|
|
||||||
<CheckboxField
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: 'resources.modelfiles.form.isLora'
|
|
||||||
})}
|
|
||||||
></CheckboxField>
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user