| @@ -16,7 +16,7 @@ const config: StorybookConfig = { | |||||
| name: '@storybook/react-webpack5', | name: '@storybook/react-webpack5', | ||||
| options: {}, | options: {}, | ||||
| }, | }, | ||||
| staticDirs: ['../public'], | |||||
| staticDirs: ['../static'], | |||||
| docs: { | docs: { | ||||
| defaultName: 'Documentation', | defaultName: 'Documentation', | ||||
| }, | }, | ||||
| @@ -166,7 +166,7 @@ | |||||
| }, | }, | ||||
| "msw": { | "msw": { | ||||
| "workerDirectory": [ | "workerDirectory": [ | ||||
| "public" | |||||
| "static" | |||||
| ] | ] | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,21 +0,0 @@ | |||||
| import { Typography } from 'antd'; | |||||
| import styles from './index.less'; | |||||
| type DisabledInputProps = { | |||||
| value?: any; | |||||
| valuePropName?: string; | |||||
| }; | |||||
| /** | |||||
| * 模拟禁用的输入框,但是完全显示内容 | |||||
| */ | |||||
| function DisabledInput({ value, valuePropName }: DisabledInputProps) { | |||||
| const data = valuePropName ? value[valuePropName] : value; | |||||
| return ( | |||||
| <div className={styles['disabled-input']}> | |||||
| <Typography.Text ellipsis={{ tooltip: data }}>{data}</Typography.Text> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default DisabledInput; | |||||
| @@ -1,4 +1,5 @@ | |||||
| .disabled-input { | |||||
| .form-info { | |||||
| min-height: 32px; | |||||
| padding: 4px 11px; | padding: 4px 11px; | ||||
| color: @text-disabled-color; | color: @text-disabled-color; | ||||
| font-size: @font-size-input; | font-size: @font-size-input; | ||||
| @@ -6,4 +7,14 @@ | |||||
| border: 1px solid #d9d9d9; | border: 1px solid #d9d9d9; | ||||
| border-radius: 6px; | border-radius: 6px; | ||||
| cursor: not-allowed; | cursor: not-allowed; | ||||
| .ant-typography { | |||||
| margin: 0 !important; | |||||
| } | |||||
| } | |||||
| .form-info--multiline { | |||||
| .ant-typography { | |||||
| white-space: pre-wrap; | |||||
| } | |||||
| } | } | ||||
| @@ -0,0 +1,41 @@ | |||||
| import { Typography } from 'antd'; | |||||
| import classNames from 'classnames'; | |||||
| import './index.less'; | |||||
| type FormInfoProps = { | |||||
| /** 自定义类名 */ | |||||
| value?: any; | |||||
| /** 如果 `value` 是对象时,取对象的哪个属性作为值 */ | |||||
| valuePropName?: string; | |||||
| /** 是否是多行 */ | |||||
| multiline?: boolean; | |||||
| /** 自定义类名 */ | |||||
| className?: string; | |||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | |||||
| }; | |||||
| /** | |||||
| * 模拟禁用的输入框,但是内容超长时,hover 时显示所有内容 | |||||
| */ | |||||
| function FormInfo({ value, valuePropName, className, style, multiline = false }: FormInfoProps) { | |||||
| const data = value && typeof value === 'object' && valuePropName ? value[valuePropName] : value; | |||||
| return ( | |||||
| <div | |||||
| className={classNames( | |||||
| 'form-info', | |||||
| { | |||||
| 'form-info--multiline': multiline, | |||||
| }, | |||||
| className, | |||||
| )} | |||||
| style={style} | |||||
| > | |||||
| <Typography.Paragraph ellipsis={multiline ? false : { tooltip: data }}> | |||||
| {data} | |||||
| </Typography.Paragraph> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default FormInfo; | |||||
| @@ -107,7 +107,7 @@ function ExperimentInstanceComponent({ | |||||
| }; | }; | ||||
| if (!experimentInsList || experimentInsList.length === 0) { | if (!experimentInsList || experimentInsList.length === 0) { | ||||
| return null; | |||||
| return <div style={{ textAlign: 'center' }}>暂无实验实例</div>; | |||||
| } | } | ||||
| return ( | return ( | ||||
| @@ -20,7 +20,7 @@ import TensorBoardStatusCell from '../TensorBoardStatus'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| type ExperimentInstanceProps = { | type ExperimentInstanceProps = { | ||||
| experimentInList?: ExperimentInstance[]; | |||||
| experimentInsList?: ExperimentInstance[]; | |||||
| experimentInsTotal: number; | experimentInsTotal: number; | ||||
| onClickInstance?: (instance: ExperimentInstance) => void; | onClickInstance?: (instance: ExperimentInstance) => void; | ||||
| onClickTensorBoard?: (instance: ExperimentInstance) => void; | onClickTensorBoard?: (instance: ExperimentInstance) => void; | ||||
| @@ -30,7 +30,7 @@ type ExperimentInstanceProps = { | |||||
| }; | }; | ||||
| function ExperimentInstanceComponent({ | function ExperimentInstanceComponent({ | ||||
| experimentInList, | |||||
| experimentInsList, | |||||
| experimentInsTotal, | experimentInsTotal, | ||||
| onClickInstance, | onClickInstance, | ||||
| onClickTensorBoard, | onClickTensorBoard, | ||||
| @@ -40,8 +40,8 @@ function ExperimentInstanceComponent({ | |||||
| }: ExperimentInstanceProps) { | }: ExperimentInstanceProps) { | ||||
| const { message } = App.useApp(); | const { message } = App.useApp(); | ||||
| const allIntanceIds = useMemo(() => { | const allIntanceIds = useMemo(() => { | ||||
| return experimentInList?.map((item) => item.id) || []; | |||||
| }, [experimentInList]); | |||||
| return experimentInsList?.map((item) => item.id) || []; | |||||
| }, [experimentInsList]); | |||||
| const [ | const [ | ||||
| selectedIns, | selectedIns, | ||||
| setSelectedIns, | setSelectedIns, | ||||
| @@ -57,7 +57,7 @@ function ExperimentInstanceComponent({ | |||||
| if (allIntanceIds.length === 0) { | if (allIntanceIds.length === 0) { | ||||
| setSelectedIns([]); | setSelectedIns([]); | ||||
| } | } | ||||
| }, [experimentInList]); | |||||
| }, [experimentInsList]); | |||||
| // 删除实验实例确认 | // 删除实验实例确认 | ||||
| const handleRemove = (instance: ExperimentInstance) => { | const handleRemove = (instance: ExperimentInstance) => { | ||||
| @@ -118,8 +118,8 @@ function ExperimentInstanceComponent({ | |||||
| } | } | ||||
| }; | }; | ||||
| if (!experimentInList || experimentInList.length === 0) { | |||||
| return null; | |||||
| if (!experimentInsList || experimentInsList.length === 0) { | |||||
| return <div style={{ textAlign: 'center' }}>暂无数据</div>; | |||||
| } | } | ||||
| return ( | return ( | ||||
| @@ -152,7 +152,7 @@ function ExperimentInstanceComponent({ | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| {experimentInList.map((item, index) => ( | |||||
| {experimentInsList.map((item, index) => ( | |||||
| <div | <div | ||||
| key={item.id} | key={item.id} | ||||
| className={classNames(styles.tableExpandBox, styles.tableExpandBoxContent)} | className={classNames(styles.tableExpandBox, styles.tableExpandBoxContent)} | ||||
| @@ -244,7 +244,7 @@ function ExperimentInstanceComponent({ | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| ))} | ))} | ||||
| {experimentInsTotal > experimentInList.length ? ( | |||||
| {experimentInsTotal > experimentInsList.length ? ( | |||||
| <div className={styles.loadMoreBox}> | <div className={styles.loadMoreBox}> | ||||
| <Button type="link" onClick={onLoadMore}> | <Button type="link" onClick={onLoadMore}> | ||||
| 更多 | 更多 | ||||
| @@ -1,11 +1,10 @@ | |||||
| import ParameterInput from '@/components/ParameterInput'; | |||||
| import FormInfo from '@/components/FormInfo'; | |||||
| import ParameterSelect from '@/components/ParameterSelect'; | import ParameterSelect from '@/components/ParameterSelect'; | ||||
| import SubAreaTitle from '@/components/SubAreaTitle'; | import SubAreaTitle from '@/components/SubAreaTitle'; | ||||
| import { useComputingResource } from '@/hooks/resource'; | import { useComputingResource } from '@/hooks/resource'; | ||||
| import { PipelineNodeModelSerialize } from '@/types'; | import { PipelineNodeModelSerialize } from '@/types'; | ||||
| import { Form, Input, Select } from 'antd'; | |||||
| import { Form, Select } from 'antd'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| const { TextArea } = Input; | |||||
| type ExperimentParameterProps = { | type ExperimentParameterProps = { | ||||
| nodeData: PipelineNodeModelSerialize; | nodeData: PipelineNodeModelSerialize; | ||||
| @@ -64,7 +63,7 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { | |||||
| }, | }, | ||||
| ]} | ]} | ||||
| > | > | ||||
| <Input disabled /> | |||||
| <FormInfo /> | |||||
| </Form.Item> | </Form.Item> | ||||
| <Form.Item | <Form.Item | ||||
| label="任务ID" | label="任务ID" | ||||
| @@ -76,7 +75,7 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { | |||||
| }, | }, | ||||
| ]} | ]} | ||||
| > | > | ||||
| <Input disabled /> | |||||
| <FormInfo /> | |||||
| </Form.Item> | </Form.Item> | ||||
| <div className={styles['experiment-parameter__title']}> | <div className={styles['experiment-parameter__title']}> | ||||
| <SubAreaTitle | <SubAreaTitle | ||||
| @@ -94,14 +93,14 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { | |||||
| }, | }, | ||||
| ]} | ]} | ||||
| > | > | ||||
| <Input disabled /> | |||||
| <FormInfo /> | |||||
| </Form.Item> | </Form.Item> | ||||
| <Form.Item label="工作目录" name="working_directory"> | <Form.Item label="工作目录" name="working_directory"> | ||||
| <Input disabled /> | |||||
| <FormInfo /> | |||||
| </Form.Item> | </Form.Item> | ||||
| <Form.Item label="启动命令" name="command"> | <Form.Item label="启动命令" name="command"> | ||||
| <TextArea disabled /> | |||||
| <FormInfo multiline /> | |||||
| </Form.Item> | </Form.Item> | ||||
| <Form.Item | <Form.Item | ||||
| label="资源规格" | label="资源规格" | ||||
| @@ -123,14 +122,14 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { | |||||
| /> | /> | ||||
| </Form.Item> | </Form.Item> | ||||
| <Form.Item label="挂载路径" name="mount_path"> | <Form.Item label="挂载路径" name="mount_path"> | ||||
| <Input disabled /> | |||||
| <FormInfo /> | |||||
| </Form.Item> | </Form.Item> | ||||
| <Form.Item label="环境变量" name="env_variables"> | <Form.Item label="环境变量" name="env_variables"> | ||||
| <TextArea disabled /> | |||||
| <FormInfo multiline /> | |||||
| </Form.Item> | </Form.Item> | ||||
| {controlStrategyList.map((item) => ( | {controlStrategyList.map((item) => ( | ||||
| <Form.Item key={item.key} name={['control_strategy', item.key]} label={item.value.label}> | <Form.Item key={item.key} name={['control_strategy', item.key]} label={item.value.label}> | ||||
| <ParameterInput disabled /> | |||||
| <FormInfo valuePropName="showValue" /> | |||||
| </Form.Item> | </Form.Item> | ||||
| ))} | ))} | ||||
| <div className={styles['experiment-parameter__title']}> | <div className={styles['experiment-parameter__title']}> | ||||
| @@ -149,7 +148,7 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { | |||||
| {item.value.type === 'select' ? ( | {item.value.type === 'select' ? ( | ||||
| <ParameterSelect disabled /> | <ParameterSelect disabled /> | ||||
| ) : ( | ) : ( | ||||
| <ParameterInput disabled /> | |||||
| <FormInfo valuePropName="showValue" /> | |||||
| )} | )} | ||||
| </Form.Item> | </Form.Item> | ||||
| ))} | ))} | ||||
| @@ -166,7 +165,7 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { | |||||
| label={item.value.label + '(' + item.key + ')'} | label={item.value.label + '(' + item.key + ')'} | ||||
| rules={[{ required: item.value.require ? true : false }]} | rules={[{ required: item.value.require ? true : false }]} | ||||
| > | > | ||||
| <ParameterInput disabled /> | |||||
| <FormInfo valuePropName="showValue" /> | |||||
| </Form.Item> | </Form.Item> | ||||
| ))} | ))} | ||||
| </Form> | </Form> | ||||
| @@ -537,7 +537,7 @@ function Experiment() { | |||||
| expandable={{ | expandable={{ | ||||
| expandedRowRender: (record) => ( | expandedRowRender: (record) => ( | ||||
| <ExperimentInstance | <ExperimentInstance | ||||
| experimentInList={experimentInList} | |||||
| experimentInsList={experimentInList} | |||||
| experimentInsTotal={experimentInsTotal} | experimentInsTotal={experimentInsTotal} | ||||
| onClickInstance={(item) => gotoInstanceInfo(item, record)} | onClickInstance={(item) => gotoInstanceInfo(item, record)} | ||||
| onClickTensorBoard={handleTensorboard} | onClickTensorBoard={handleTensorboard} | ||||
| @@ -71,6 +71,7 @@ export const Primary: Story = { | |||||
| /** 通过 `openAntdModal` 函数打开 */ | /** 通过 `openAntdModal` 函数打开 */ | ||||
| export const OpenByFunction: Story = { | export const OpenByFunction: Story = { | ||||
| name: '通过函数的方式打开', | |||||
| render: function Render(args) { | render: function Render(args) { | ||||
| const handleClick = () => { | const handleClick = () => { | ||||
| const { close } = openAntdModal(CodeSelectorModal, { | const { close } = openAntdModal(CodeSelectorModal, { | ||||
| @@ -0,0 +1,124 @@ | |||||
| import FormInfo from '@/components/FormInfo'; | |||||
| import type { Meta, StoryObj } from '@storybook/react'; | |||||
| import { Form, Input, Select, Typography } from 'antd'; | |||||
| // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export | |||||
| const meta = { | |||||
| title: 'Components/FormInfo', | |||||
| component: FormInfo, | |||||
| parameters: { | |||||
| // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout | |||||
| // layout: 'centered', | |||||
| }, | |||||
| // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs | |||||
| tags: ['autodocs'], | |||||
| // More on argTypes: https://storybook.js.org/docs/api/argtypes | |||||
| argTypes: { | |||||
| // backgroundColor: { control: 'color' }, | |||||
| }, | |||||
| // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args | |||||
| // args: { onClick: fn() }, | |||||
| } satisfies Meta<typeof FormInfo>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args | |||||
| export const InForm: Story = { | |||||
| render: () => { | |||||
| return ( | |||||
| <Form | |||||
| name="form" | |||||
| style={{ width: 300 }} | |||||
| labelCol={{ flex: '100px' }} | |||||
| initialValues={{ | |||||
| text: '文本', | |||||
| large_text: | |||||
| '超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本', | |||||
| multiline_text: `多行文本\n超长文本超长文本超长文本超长文本\n 多行文本`, | |||||
| object_text: { | |||||
| value: 1, | |||||
| showValue: '对象文本', | |||||
| }, | |||||
| input_text: | |||||
| '超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本', | |||||
| antd_select: 1, | |||||
| select_text: 1, | |||||
| select_large_text: 1, | |||||
| }} | |||||
| > | |||||
| <Form.Item label="文本" name="text"> | |||||
| <FormInfo /> | |||||
| </Form.Item> | |||||
| <Form.Item label="超长文本" name="large_text"> | |||||
| <FormInfo /> | |||||
| </Form.Item> | |||||
| <Form.Item label="多行文本" name="multiline_text"> | |||||
| <FormInfo multiline /> | |||||
| </Form.Item> | |||||
| <Form.Item label="对象" name="object_text"> | |||||
| <FormInfo valuePropName="showValue" /> | |||||
| </Form.Item> | |||||
| <Form.Item label="无内容" name="empty_text"> | |||||
| <FormInfo /> | |||||
| </Form.Item> | |||||
| <Form.Item label="Input" name="input_text"> | |||||
| <Input disabled /> | |||||
| </Form.Item> | |||||
| <Form.Item label="Select" name="antd_select"> | |||||
| <Select | |||||
| disabled | |||||
| options={[ | |||||
| { | |||||
| label: | |||||
| '超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本', | |||||
| value: 1, | |||||
| }, | |||||
| ]} | |||||
| /> | |||||
| </Form.Item> | |||||
| <Form.Item label="Select" name="select_text"> | |||||
| <Select | |||||
| labelRender={(props) => { | |||||
| return ( | |||||
| <div style={{ width: '100%', lineHeight: 'normal' }}> | |||||
| <Typography.Text ellipsis={{ tooltip: props.label }} style={{ margin: 0 }}> | |||||
| {props.label} | |||||
| </Typography.Text> | |||||
| </div> | |||||
| ); | |||||
| }} | |||||
| disabled | |||||
| options={[ | |||||
| { | |||||
| label: '选择文本', | |||||
| value: 1, | |||||
| }, | |||||
| ]} | |||||
| /> | |||||
| </Form.Item> | |||||
| <Form.Item label="Long Select" name="select_large_text"> | |||||
| <Select | |||||
| labelRender={(props) => { | |||||
| return ( | |||||
| <div style={{ width: '100%', lineHeight: 'normal' }}> | |||||
| <Typography.Text ellipsis={{ tooltip: props.label }} style={{ margin: 0 }}> | |||||
| {props.label} | |||||
| </Typography.Text> | |||||
| </div> | |||||
| ); | |||||
| }} | |||||
| disabled | |||||
| options={[ | |||||
| { | |||||
| label: | |||||
| '超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本', | |||||
| value: 1, | |||||
| }, | |||||
| ]} | |||||
| /> | |||||
| </Form.Item> | |||||
| </Form> | |||||
| ); | |||||
| }, | |||||
| }; | |||||
| @@ -3,7 +3,7 @@ import KFModal from '@/components/KFModal'; | |||||
| import { openAntdModal } from '@/utils/modal'; | import { openAntdModal } from '@/utils/modal'; | ||||
| import { useArgs } from '@storybook/preview-api'; | import { useArgs } from '@storybook/preview-api'; | ||||
| import type { Meta, StoryObj } from '@storybook/react'; | import type { Meta, StoryObj } from '@storybook/react'; | ||||
| import { fn } from '@storybook/test'; | |||||
| import { expect, fn, screen, userEvent, within } from '@storybook/test'; | |||||
| import { Button } from 'antd'; | import { Button } from 'antd'; | ||||
| // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export | ||||
| @@ -70,10 +70,17 @@ export const Primary: Story = { | |||||
| </> | </> | ||||
| ); | ); | ||||
| }, | }, | ||||
| play: async ({ canvasElement }) => { | |||||
| const canvas = within(canvasElement); | |||||
| await userEvent.click(canvas.getByRole('button', { name: /打开 KFModal/i })); | |||||
| const dialog = await screen.findByRole('dialog'); | |||||
| await expect(dialog).toBeInTheDocument(); | |||||
| }, | |||||
| }; | }; | ||||
| /** 推荐通过 `openAntdModal` 函数打开 */ | /** 推荐通过 `openAntdModal` 函数打开 */ | ||||
| export const OpenByFunction: Story = { | export const OpenByFunction: Story = { | ||||
| name: '通过函数的方式打开', | |||||
| render: function Render() { | render: function Render() { | ||||
| const handleClick = () => { | const handleClick = () => { | ||||
| const { close } = openAntdModal(KFModal, { | const { close } = openAntdModal(KFModal, { | ||||
| @@ -182,6 +182,7 @@ export const Mirror: Story = { | |||||
| /** 通过 `openAntdModal` 函数打开 */ | /** 通过 `openAntdModal` 函数打开 */ | ||||
| export const OpenByFunction: Story = { | export const OpenByFunction: Story = { | ||||
| name: '通过函数的方式打开', | |||||
| args: { | args: { | ||||
| type: ResourceSelectorType.Mirror, | type: ResourceSelectorType.Mirror, | ||||
| }, | }, | ||||
| @@ -148,7 +148,7 @@ function CodeConfigItem({ item, onClick }: CodeConfigItemProps) { | |||||
| ``` | ``` | ||||
| ### 一些建议 | |||||
| ### 一点建议 | |||||
| 如果你陷入嵌套地狱,比如 | 如果你陷入嵌套地狱,比如 | ||||