| @@ -271,7 +271,18 @@ export default [ | |||
| { | |||
| name: '镜像详情', | |||
| path: 'info/:id', | |||
| component: './Mirror/Info', | |||
| routes: [ | |||
| { | |||
| name: '镜像详情', | |||
| path: '', | |||
| component: './Mirror/Info', | |||
| }, | |||
| { | |||
| name: '新增镜像版本', | |||
| path: 'add-version', | |||
| component: './Mirror/Create', | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| name: '创建镜像', | |||
| @@ -44,7 +44,7 @@ const mirrorRadioItems: KFRadioItem[] = [ | |||
| function MirrorCreate() { | |||
| const navigate = useNavigate(); | |||
| const [form] = Form.useForm(); | |||
| const [nameDisabled, setNameDisabled] = useState(false); | |||
| const [isAddVersion, setIsAddVersion] = useState(false); // 是制作镜像还是新增镜像版本 | |||
| const { message } = App.useApp(); | |||
| const uploadProps: UploadProps = { | |||
| @@ -60,7 +60,7 @@ function MirrorCreate() { | |||
| const name = SessionStorage.getItem(SessionStorage.mirrorNameKey); | |||
| if (name) { | |||
| form.setFieldValue('name', name); | |||
| setNameDisabled(true); | |||
| setIsAddVersion(true); | |||
| } | |||
| return () => { | |||
| SessionStorage.removeItem(SessionStorage.mirrorNameKey); | |||
| @@ -120,7 +120,7 @@ function MirrorCreate() { | |||
| return ( | |||
| <div className={styles['mirror-create']}> | |||
| <PageTitle title="创建镜像"></PageTitle> | |||
| <PageTitle title={!isAddVersion ? '创建镜像' : '新增镜像版本'}></PageTitle> | |||
| <div className={styles['mirror-create__content']}> | |||
| <div> | |||
| <Form | |||
| @@ -142,7 +142,7 @@ function MirrorCreate() { | |||
| <Row gutter={10}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="镜像名称及Tag" | |||
| label="镜像名称和版本" | |||
| name="name" | |||
| rules={[ | |||
| { | |||
| @@ -158,7 +158,7 @@ function MirrorCreate() { | |||
| <Input | |||
| placeholder="请输入镜像名称" | |||
| maxLength={64} | |||
| disabled={nameDisabled} | |||
| disabled={isAddVersion} | |||
| showCount | |||
| allowClear | |||
| /> | |||
| @@ -174,22 +174,22 @@ function MirrorCreate() { | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入镜像Tag', | |||
| message: '请输入镜像版本', | |||
| }, | |||
| { | |||
| pattern: /^[a-zA-Z0-9._-]+$/, | |||
| message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)', | |||
| message: '镜像版本只支持字母、数字、点(.)、下划线(_)、中横线(-)', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入镜像Tag" maxLength={64} showCount allowClear /> | |||
| <Input placeholder="请输入镜像版本" maxLength={64} showCount allowClear /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={10}> | |||
| <Col span={20}> | |||
| <Form.Item | |||
| label="镜像描述" | |||
| label={isAddVersion ? '镜像版本描述' : '镜像描述'} | |||
| name="description" | |||
| rules={[ | |||
| { | |||
| @@ -303,7 +303,7 @@ function MirrorCreate() { | |||
| <Form.Item wrapperCol={{ offset: 0, span: 16 }}> | |||
| <Button type="primary" htmlType="submit"> | |||
| 创建镜像 | |||
| 确定 | |||
| </Button> | |||
| <Button | |||
| type="default" | |||
| @@ -155,7 +155,7 @@ function MirrorInfo() { | |||
| }; | |||
| const createMirrorVersion = () => { | |||
| navigate(`/dataset/mirror/create`); | |||
| navigate(`add-version`); | |||
| SessionStorage.setItem(SessionStorage.mirrorNameKey, mirrorInfo.name || ''); | |||
| setCacheState({ | |||
| pagination, | |||
| @@ -167,27 +167,34 @@ function MirrorInfo() { | |||
| title: '镜像版本', | |||
| dataIndex: 'tag_name', | |||
| key: 'tag_name', | |||
| width: '25%', | |||
| width: '20%', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '镜像地址', | |||
| dataIndex: 'url', | |||
| key: 'url', | |||
| render: tableCellRender(), | |||
| width: '20%', | |||
| render: tableCellRender('auto', TableCellValueType.Text, { copyable: true }), | |||
| }, | |||
| { | |||
| title: '版本描述', | |||
| dataIndex: 'description', | |||
| key: 'description', | |||
| render: tableCellRender(true), | |||
| }, | |||
| { | |||
| title: '状态', | |||
| dataIndex: 'status', | |||
| key: 'status', | |||
| width: 150, | |||
| width: 100, | |||
| render: MirrorStatusCell, | |||
| }, | |||
| { | |||
| title: '镜像大小', | |||
| dataIndex: 'file_size', | |||
| key: 'file_size', | |||
| width: 150, | |||
| width: 120, | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| @@ -200,7 +207,7 @@ function MirrorInfo() { | |||
| { | |||
| title: '操作', | |||
| dataIndex: 'operation', | |||
| width: 150, | |||
| width: 120, | |||
| key: 'operation', | |||
| hidden: isPublic, | |||
| render: (_: any, record: MirrorVersionData) => ( | |||
| @@ -128,7 +128,7 @@ function MirrorList() { | |||
| // 查看详情 | |||
| const toDetail = (record: MirrorData) => { | |||
| navigate(`/dataset/mirror/info/${record.id}`); | |||
| navigate(`info/${record.id}`); | |||
| setCacheState({ | |||
| activeTab, | |||
| pagination, | |||
| @@ -149,7 +149,7 @@ function MirrorList() { | |||
| // 创建镜像 | |||
| const createMirror = () => { | |||
| navigate(`/dataset/mirror/create`); | |||
| navigate(`create`); | |||
| SessionStorage.setItem(SessionStorage.mirrorNameKey, ''); | |||
| setCacheState({ | |||
| activeTab, | |||
| @@ -262,7 +262,7 @@ function MirrorList() { | |||
| onClick={createMirror} | |||
| icon={<KFIcon type="icon-xinjian2" />} | |||
| > | |||
| 制作镜像 | |||
| 创建镜像 | |||
| </Button> | |||
| )} | |||
| <Button | |||
| @@ -25,6 +25,7 @@ export type TableCellValueOptions<T> = { | |||
| dateFormat?: string; // 类型为 Date 时有效 | |||
| onClick?: (record: T, e: React.MouseEvent) => void; // 类型为 Link 时有效 | |||
| format?: (value: any | undefined | null, record: T, index: number) => string | undefined | null; // 类型为 Custom 时有效 | |||
| copyable?: boolean; // 省略时是否可以复制 | |||
| }; | |||
| type TableCellFormatter = (value: any | undefined | null) => string | undefined | null; | |||
| @@ -93,17 +94,17 @@ function tableCellRender<T>( | |||
| } | |||
| if (ellipsis === 'auto' && text) { | |||
| return renderCell(type, text, 'auto', record, options?.onClick); | |||
| return renderCell(type, text, 'auto', record, options); | |||
| } else if (ellipsis && text) { | |||
| const tooltipProps = typeof ellipsis === 'object' ? ellipsis : {}; | |||
| const { overlayStyle, ...rest } = tooltipProps; | |||
| return ( | |||
| <Tooltip {...rest} overlayStyle={{ maxWidth: 400, ...overlayStyle }} title={text}> | |||
| {renderCell(type, text, true, record, options?.onClick)} | |||
| {renderCell(type, text, true, record, options)} | |||
| </Tooltip> | |||
| ); | |||
| } else { | |||
| return renderCell(type, text, false, record, options?.onClick); | |||
| return renderCell(type, text, false, record, options); | |||
| } | |||
| }; | |||
| } | |||
| @@ -113,31 +114,38 @@ function renderCell<T>( | |||
| text: any | undefined | null, | |||
| ellipsis: boolean | 'auto', | |||
| record: T, | |||
| onClick?: (record: T, e: React.MouseEvent) => void, | |||
| options?: TableCellValueOptions<T>, | |||
| ) { | |||
| return type === TableCellValueType.Link | |||
| ? renderLink(text, ellipsis, record, onClick) | |||
| : renderText(text, ellipsis); | |||
| ? renderLink(text, ellipsis, record, options) | |||
| : renderText(text, ellipsis, options); | |||
| } | |||
| function renderLink<T>( | |||
| text: any | undefined | null, | |||
| ellipsis: boolean | 'auto', | |||
| record: T, | |||
| onClick?: (record: T, e: React.MouseEvent) => void, | |||
| options?: TableCellValueOptions<T>, | |||
| ) { | |||
| const { onClick } = options ?? {}; | |||
| return ( | |||
| <a className="kf-table-row-link" onClick={(e) => onClick?.(record, e)}> | |||
| {renderText(text, ellipsis)} | |||
| {renderText(text, ellipsis, options)} | |||
| </a> | |||
| ); | |||
| } | |||
| function renderText(text: any | undefined | null, ellipsis: boolean | 'auto') { | |||
| function renderText<T>( | |||
| text: any | undefined | null, | |||
| ellipsis: boolean | 'auto', | |||
| options?: TableCellValueOptions<T>, | |||
| ) { | |||
| const { copyable } = options ?? {}; | |||
| if (ellipsis === 'auto') { | |||
| return ( | |||
| <Typography.Paragraph | |||
| style={{ marginBottom: 0 }} | |||
| copyable={copyable} | |||
| ellipsis={{ | |||
| tooltip: { | |||
| title: text, | |||