| @@ -198,6 +198,17 @@ export default [ | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| name: '代码配置', | |||
| path: 'codeConfig', | |||
| routes: [ | |||
| { | |||
| name: '代码配置', | |||
| path: '', | |||
| component: './CodeConfig/List', | |||
| }, | |||
| ], | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| @@ -10,6 +10,12 @@ export enum CommonTabKeys { | |||
| Public = 'Public', // 公开 | |||
| } | |||
| // 公开还是私有 | |||
| export enum AvailableRange { | |||
| Public = 1, // 公开 | |||
| Private = 0, // 私有 | |||
| } | |||
| // 实验状态 | |||
| export enum ExperimentStatus { | |||
| Running = 'Running', // 运行中 | |||
| @@ -0,0 +1,47 @@ | |||
| .code-config-list { | |||
| display: flex; | |||
| flex: 1; | |||
| flex-direction: column; | |||
| height: 100%; | |||
| height: 100%; | |||
| padding: 20px 0; | |||
| background: white; | |||
| box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09); | |||
| &__header { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| height: 32px; | |||
| margin-bottom: 30px; | |||
| padding: 0 30px; | |||
| color: @text-color; | |||
| font-size: 15px; | |||
| } | |||
| &__content { | |||
| display: flex; | |||
| flex: 1; | |||
| flex-wrap: wrap; | |||
| gap: 20px; | |||
| align-content: flex-start; | |||
| width: 100%; | |||
| margin-bottom: 30px; | |||
| padding: 0 30px; | |||
| overflow-y: auto; | |||
| } | |||
| &__empty { | |||
| display: flex; | |||
| flex: 1; | |||
| align-items: center; | |||
| justify-content: center; | |||
| } | |||
| :global { | |||
| .ant-pagination { | |||
| margin-right: 30px; | |||
| text-align: right; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,173 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { deleteCodeConfigReq, getCodeConfigListReq } from '@/services/codeConfig'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { to } from '@/utils/promise'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { App, Button, Empty, Input, Pagination, PaginationProps } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import AddCodeConfigModal, { OperationType } from '../components/AddCodeConfigModal'; | |||
| import CodeConfigItem from '../components/CodeConfigItem'; | |||
| import styles from './index.less'; | |||
| // 代码配置数据 | |||
| export type CodeConfigData = { | |||
| id: number; | |||
| code_repo_name: string; | |||
| code_repo_vis: number; | |||
| git_url: string; | |||
| git_branch: string; | |||
| git_user_name: string; | |||
| git_password: string; | |||
| ssh_key: string; | |||
| verify_mode: number; | |||
| create_by: string; | |||
| create_time: string; | |||
| update_by: string; | |||
| update_time: string; | |||
| }; | |||
| export type ResourceListRef = { | |||
| reset: () => void; | |||
| }; | |||
| function CodeConfigList() { | |||
| const [dataList, setDataList] = useState<CodeConfigData[]>([]); | |||
| const [total, setTotal] = useState(0); | |||
| const [pagination, setPagination] = useState<PaginationProps>({ | |||
| current: 1, | |||
| pageSize: 20, | |||
| }); | |||
| const [searchText, setSearchText] = useState<string | undefined>(undefined); | |||
| const [inputText, setInputText] = useState<string | undefined>(undefined); | |||
| const { message } = App.useApp(); | |||
| useEffect(() => { | |||
| getDataList(); | |||
| }, [pagination, searchText]); | |||
| // 获取数据请求 | |||
| const getDataList = async () => { | |||
| const params = { | |||
| page: pagination.current! - 1, | |||
| size: pagination.pageSize, | |||
| code_repo_name: searchText !== '' ? searchText : undefined, | |||
| }; | |||
| const [res] = await to(getCodeConfigListReq(params)); | |||
| if (res && res.data && res.data.content) { | |||
| setDataList(res.data.content); | |||
| setTotal(res.data.totalElements); | |||
| } | |||
| }; | |||
| // 删除请求 | |||
| const deleteRecord = async (id: number) => { | |||
| const [res] = await to(deleteCodeConfigReq(id)); | |||
| if (res) { | |||
| getDataList(); | |||
| message.success('删除成功'); | |||
| } | |||
| }; | |||
| // 搜索 | |||
| const handleSearch = (value: string) => { | |||
| setSearchText(value); | |||
| }; | |||
| // 删除 | |||
| const handleRemove = (record: CodeConfigData) => { | |||
| modalConfirm({ | |||
| title: '确定删除这个代码配置吗?', | |||
| onOk: () => { | |||
| deleteRecord(record.id); | |||
| }, | |||
| }); | |||
| }; | |||
| // 修改 | |||
| const handleClick = (record: CodeConfigData) => { | |||
| const { close } = openAntdModal(AddCodeConfigModal, { | |||
| opType: OperationType.Update, | |||
| codeConfigData: record, | |||
| onOk: () => { | |||
| getDataList(); | |||
| close(); | |||
| }, | |||
| }); | |||
| }; | |||
| // 新建 | |||
| const createCodeConfig = () => { | |||
| const { close } = openAntdModal(AddCodeConfigModal, { | |||
| opType: OperationType.Create, | |||
| onOk: () => { | |||
| getDataList(); | |||
| close(); | |||
| }, | |||
| }); | |||
| }; | |||
| // 分页切换 | |||
| const handlePageChange: PaginationProps['onChange'] = (page, pageSize) => { | |||
| setPagination({ | |||
| current: page, | |||
| pageSize: pageSize, | |||
| }); | |||
| }; | |||
| return ( | |||
| <div className={styles['code-config-list']}> | |||
| <div className={styles['code-config-list__header']}> | |||
| <span>数据总数:{total}个</span> | |||
| <div> | |||
| <Input.Search | |||
| placeholder="按代码仓库名称筛选" | |||
| allowClear | |||
| onSearch={handleSearch} | |||
| style={{ | |||
| width: 300, | |||
| }} | |||
| onChange={(e) => setInputText(e.target.value)} | |||
| value={inputText} | |||
| /> | |||
| <Button | |||
| type="default" | |||
| style={{ marginLeft: '20px' }} | |||
| onClick={createCodeConfig} | |||
| icon={<KFIcon type="icon-xinjian2" />} | |||
| > | |||
| 新建代码配置 | |||
| </Button> | |||
| </div> | |||
| </div> | |||
| {dataList?.length !== 0 ? ( | |||
| <> | |||
| <div className={styles['code-config-list__content']}> | |||
| {dataList?.map((item) => ( | |||
| <CodeConfigItem | |||
| item={item} | |||
| key={item.id} | |||
| onRemove={handleRemove} | |||
| onClick={handleClick} | |||
| /> | |||
| ))} | |||
| </div> | |||
| <Pagination | |||
| total={total} | |||
| showSizeChanger | |||
| defaultPageSize={20} | |||
| pageSizeOptions={[20, 40, 60, 80, 100]} | |||
| showQuickJumper | |||
| onChange={handlePageChange} | |||
| {...pagination} | |||
| /> | |||
| </> | |||
| ) : ( | |||
| <div className={styles['code-config-list__empty']}> | |||
| <Empty></Empty> | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| export default CodeConfigList; | |||
| @@ -0,0 +1,244 @@ | |||
| import KFModal from '@/components/KFModal'; | |||
| import { AvailableRange } from '@/enums'; | |||
| import { type CodeConfigData } from '@/pages/CodeConfig/List'; | |||
| import { addCodeConfigReq, updateCodeConfigReq } from '@/services/codeConfig'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Form, Input, Radio, message, type ModalProps } from 'antd'; | |||
| import { omit } from 'lodash'; | |||
| export enum VerifyMode { | |||
| Password = 0, // 用户名密码 | |||
| SSH = 1, // SSH Key | |||
| } | |||
| export enum OperationType { | |||
| Create = 0, // 新建 | |||
| Update = 1, // 更新 | |||
| } | |||
| type FormData = Partial<CodeConfigData>; | |||
| interface AddCodeConfigModalProps extends Omit<ModalProps, 'onOk'> { | |||
| opType: OperationType; | |||
| codeConfigData?: CodeConfigData; | |||
| onOk: () => void; | |||
| } | |||
| function AddCodeConfigModal({ opType, codeConfigData, onOk, ...rest }: AddCodeConfigModalProps) { | |||
| // 上传请求 | |||
| const createCodeConfig = async (formData: FormData) => { | |||
| const params: FormData & { id?: number } = { | |||
| ...formData, | |||
| }; | |||
| // 清除多余的信息 | |||
| if (formData.code_repo_vis === AvailableRange.Public) { | |||
| omit(params, ['verify_mode', 'git_user_name', 'git_password', 'ssh_key']); | |||
| } | |||
| if (formData.verify_mode === VerifyMode.Password) { | |||
| omit(params, ['ssh_key']); | |||
| } else if (formData.verify_mode === VerifyMode.SSH) { | |||
| omit(params, ['git_user_name', 'git_password']); | |||
| } | |||
| if (opType === OperationType.Update) { | |||
| params.id = codeConfigData?.id; | |||
| } | |||
| const request = opType === OperationType.Create ? addCodeConfigReq : updateCodeConfigReq; | |||
| const [res] = await to(request(params)); | |||
| if (res) { | |||
| message.success(opType === OperationType.Create ? '创建成功' : '修改成功'); | |||
| onOk?.(); | |||
| } | |||
| }; | |||
| // 提交 | |||
| const onFinish = (formData: FormData) => { | |||
| createCodeConfig(formData); | |||
| }; | |||
| // 设置初始值 | |||
| const initialValues: FormData = codeConfigData ?? { | |||
| code_repo_vis: AvailableRange.Public, | |||
| verify_mode: VerifyMode.Password, | |||
| }; | |||
| if (initialValues.verify_mode === undefined || initialValues.verify_mode === null) { | |||
| initialValues.verify_mode = VerifyMode.Password; | |||
| } | |||
| return ( | |||
| <KFModal | |||
| {...rest} | |||
| title="新建代码配置" | |||
| image={require('@/assets/img/create-experiment.png')} | |||
| width={825} | |||
| okButtonProps={{ | |||
| htmlType: 'submit', | |||
| form: 'form', | |||
| }} | |||
| destroyOnClose | |||
| > | |||
| <Form | |||
| name="form" | |||
| layout="vertical" | |||
| onFinish={onFinish} | |||
| initialValues={initialValues} | |||
| autoComplete="off" | |||
| > | |||
| {/* 禁止 Chrome 自动填充 */} | |||
| {/* <Input type="text" style={{ display: 'none' }} /> | |||
| <Input type="password" style={{ display: 'none' }} /> */} | |||
| <Form.Item | |||
| label="代码仓库名称" | |||
| name="code_repo_name" | |||
| required | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入代码仓库名称', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入代码仓库名称" showCount allowClear maxLength={64} /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="代码仓库可见性" | |||
| name="code_repo_vis" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请选择代码仓库可见性', | |||
| }, | |||
| ]} | |||
| > | |||
| <Radio.Group> | |||
| <Radio value={AvailableRange.Public}>公开</Radio> | |||
| <Radio value={AvailableRange.Private}>私有</Radio> | |||
| </Radio.Group> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="Git 地址" | |||
| name="git_url" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入 Git 地址', | |||
| }, | |||
| { | |||
| type: 'url', | |||
| message: '请输入正确的 Git 地址', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入 Git 地址" showCount allowClear maxLength={256} /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="代码分支/Tag" | |||
| name="git_branch" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入代码分支/Tag', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入代码分支/Tag" showCount allowClear maxLength={64} /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| noStyle | |||
| shouldUpdate={(prevValues, currentValues) => | |||
| prevValues?.code_repo_vis !== currentValues?.code_repo_vis | |||
| } | |||
| > | |||
| {({ getFieldValue }) => { | |||
| return getFieldValue('code_repo_vis') === AvailableRange.Private ? ( | |||
| <> | |||
| <Form.Item | |||
| label="验证方式" | |||
| name="verify_mode" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请选择验证方式', | |||
| }, | |||
| ]} | |||
| > | |||
| <Radio.Group> | |||
| <Radio value={VerifyMode.Password}>用户名/密码</Radio> | |||
| <Radio value={VerifyMode.SSH}>SSH Key</Radio> | |||
| </Radio.Group> | |||
| </Form.Item> | |||
| <Form.Item | |||
| noStyle | |||
| shouldUpdate={(prevValues, currentValues) => | |||
| prevValues?.verify_mode !== currentValues?.verify_mode | |||
| } | |||
| > | |||
| {({ getFieldValue }) => { | |||
| return getFieldValue('verify_mode') === VerifyMode.Password ? ( | |||
| <> | |||
| <Form.Item | |||
| label="Git 用户名" | |||
| name="git_user_name" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入 Git 用户名', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input | |||
| placeholder="请输入 Git 用户名" | |||
| autoComplete="off" | |||
| showCount | |||
| allowClear | |||
| maxLength={64} | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="Git 密码" | |||
| name="git_password" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入 Git 密码', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.Password | |||
| autoComplete="new-password" | |||
| placeholder="请输入 Git 密码" | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| </> | |||
| ) : ( | |||
| <Form.Item | |||
| label="SSH Key" | |||
| name="ssh_key" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入 SSH Key', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| placeholder="请输入 SSH Key" | |||
| showCount | |||
| maxLength={1024} | |||
| autoSize={{ minRows: 3, maxRows: 6 }} | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| ); | |||
| }} | |||
| </Form.Item> | |||
| </> | |||
| ) : null; | |||
| }} | |||
| </Form.Item> | |||
| </Form> | |||
| </KFModal> | |||
| ); | |||
| } | |||
| export default AddCodeConfigModal; | |||
| @@ -0,0 +1,68 @@ | |||
| .code-config-item { | |||
| position: relative; | |||
| width: calc(25% - 15px); | |||
| padding: 20px; | |||
| background: white; | |||
| border: 1px solid #eaeaea; | |||
| border-radius: 4px; | |||
| cursor: pointer; | |||
| @media screen and (max-width: 1860px) { | |||
| & { | |||
| width: calc(33.33% - 13.33px); | |||
| } | |||
| } | |||
| &__name { | |||
| position: relative; | |||
| display: inline-block; | |||
| height: 24px; | |||
| margin: 0 10px 0 0 !important; | |||
| color: @text-color; | |||
| font-size: 16px; | |||
| } | |||
| &__url { | |||
| margin-bottom: 10px; | |||
| color: @text-color-secondary; | |||
| font-size: 14px; | |||
| } | |||
| &__description { | |||
| height: 44px; | |||
| margin-bottom: 20px; | |||
| color: @text-color-secondary; | |||
| font-size: 14px; | |||
| .multiLine(2); | |||
| } | |||
| &__time { | |||
| display: flex; | |||
| flex: 0 1 content; | |||
| align-items: center; | |||
| width: 100%; | |||
| color: #808080; | |||
| font-size: 13px; | |||
| } | |||
| &:hover { | |||
| border-color: @primary-color; | |||
| box-shadow: 0px 0px 6px 1px rgba(0, 0, 0, 0.1); | |||
| .resource-item__name { | |||
| color: @primary-color; | |||
| } | |||
| } | |||
| } | |||
| .resource-item__name { | |||
| &::after { | |||
| position: absolute; | |||
| top: 14px; | |||
| left: 0; | |||
| width: 100%; | |||
| height: 6px; | |||
| background: linear-gradient(to right, rgba(22, 100, 255, 0.3) 0, rgba(22, 100, 255, 0) 100%); | |||
| content: ''; | |||
| } | |||
| } | |||
| @@ -0,0 +1,51 @@ | |||
| import clock from '@/assets/img/clock.png'; | |||
| import creatByImg from '@/assets/img/creatBy.png'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { type CodeConfigData } from '@/pages/CodeConfig/List'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { Button, Flex, Typography } from 'antd'; | |||
| import styles from './index.less'; | |||
| type CodeConfigItemProps = { | |||
| item: CodeConfigData; | |||
| onClick: (item: CodeConfigData) => void; | |||
| onRemove: (item: CodeConfigData) => void; | |||
| }; | |||
| function CodeConfigItem({ item, onClick, onRemove }: CodeConfigItemProps) { | |||
| return ( | |||
| <div className={styles['code-config-item']} onClick={() => onClick(item)}> | |||
| <Flex justify="space-between" align="center" style={{ marginBottom: '20px', height: '32px' }}> | |||
| <Typography.Paragraph | |||
| className={styles['code-config-item__name']} | |||
| ellipsis={{ tooltip: item.code_repo_name }} | |||
| > | |||
| {item.code_repo_name} | |||
| </Typography.Paragraph> | |||
| <Button | |||
| type="text" | |||
| shape="circle" | |||
| onClick={(e) => { | |||
| e.stopPropagation(); | |||
| onRemove(item); | |||
| }} | |||
| > | |||
| <KFIcon type="icon-shanchu" font={17} /> | |||
| </Button> | |||
| </Flex> | |||
| <div className={styles['code-config-item__description']}>{item.git_url}</div> | |||
| <Flex justify="space-between"> | |||
| <div className={styles['code-config-item__time']}> | |||
| <img style={{ width: '17px', marginRight: '6px' }} src={creatByImg} alt="" /> | |||
| <span>{item.create_by}</span> | |||
| </div> | |||
| <div className={styles['code-config-item__time']}> | |||
| <img style={{ width: '12px', marginRight: '5px' }} src={clock} alt="" /> | |||
| <span>最近更新: {formatDate(item.update_time, 'YYYY-MM-DD')}</span> | |||
| </div> | |||
| </Flex> | |||
| </div> | |||
| ); | |||
| } | |||
| export default CodeConfigItem; | |||
| @@ -9,7 +9,7 @@ import { App, Button, Input, Pagination, PaginationProps } from 'antd'; | |||
| import { Ref, forwardRef, useEffect, useImperativeHandle, useState } from 'react'; | |||
| import { CategoryData, ResourceData, ResourceType, resourceConfig } from '../../config'; | |||
| import AddDatasetModal from '../AddDatasetModal'; | |||
| import ResourceItem from '../Resourcetem'; | |||
| import ResourceItem from '../ResourceItem'; | |||
| import styles from './index.less'; | |||
| export type ResourceListRef = { | |||
| @@ -161,7 +161,7 @@ function ResourceList( | |||
| <span>数据总数:{total}个</span> | |||
| <div> | |||
| <Input.Search | |||
| placeholder="按数据名称筛选" | |||
| placeholder={`按${config.name}名称筛选`} | |||
| allowClear | |||
| onSearch={handleSearch} | |||
| style={{ | |||
| @@ -166,7 +166,7 @@ function EditorList() { | |||
| key: 'name', | |||
| width: '30%', | |||
| render: (text, record) => | |||
| record.url ? ( | |||
| record.url && record.status === DevEditorStatus.Running ? ( | |||
| <a className="kf-table-row-link" onClick={(e) => gotoEditorPage(e, record)}> | |||
| {text} | |||
| </a> | |||
| @@ -0,0 +1,39 @@ | |||
| import { request } from '@umijs/max'; | |||
| // 分页查询代码配置 | |||
| export function getCodeConfigListReq(params) { | |||
| return request(`/api/mmp/codeConfig`, { | |||
| method: 'GET', | |||
| params, | |||
| }); | |||
| } | |||
| // 新增代码配置 | |||
| export function addCodeConfigReq(data) { | |||
| return request(`/api/mmp/codeConfig`, { | |||
| method: 'POST', | |||
| data, | |||
| }); | |||
| } | |||
| // 更新代码配置 | |||
| export function updateCodeConfigReq(data) { | |||
| return request(`/api/mmp/codeConfig`, { | |||
| method: 'PUT', | |||
| data, | |||
| }); | |||
| } | |||
| // 删除代码配置 | |||
| export function deleteCodeConfigReq(id) { | |||
| return request(`/api/mmp/codeConfig/${id}`, { | |||
| method: 'DELETE', | |||
| }); | |||
| } | |||
| // 查询代码配置详情 | |||
| export function getCodeConfigDetailReq(id) { | |||
| return request(`/api/mmp/codeConfig/${id}`, { | |||
| method: 'GET' | |||
| }); | |||
| } | |||