| @@ -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: '创建镜像', | |||
| @@ -67,6 +67,7 @@ | |||
| "@types/crypto-js": "^4.2.2", | |||
| "@umijs/route-utils": "^4.0.1", | |||
| "antd": "~5.21.4", | |||
| "caniuse-lite": "~1.0.30001707", | |||
| "classnames": "^2.3.2", | |||
| "crypto-js": "^4.2.0", | |||
| "echarts": "^5.5.0", | |||
| @@ -4,16 +4,6 @@ | |||
| font-display: swap; | |||
| } | |||
| @font-face { | |||
| font-family: 'TaoBaoMaiCaiTi'; | |||
| src: url('./TaoBaoMaiCaiTi-Regular.woff2') format('woff2'), /* 最优先使用 woff2 */ | |||
| url('./TaoBaoMaiCaiTi-Regular.woff') format('woff'), /* 兼容性较好的 woff */ | |||
| url('./TaoBaoMaiCaiTi-Regular.ttf') format('truetype'), /* ttf 作为备选 */ | |||
| url('./TaoBaoMaiCaiTi-Regular.otf') format('opentype'); /* otf 作为最后选项 */ | |||
| font-display: swap; /* 优化页面加载时的字体显示 */ | |||
| } | |||
| @font-face { | |||
| font-family: 'DingTalk-JinBuTi'; | |||
| src: url('./DingTalk-JinBuTi.woff2') format('woff2'), /* 最优先使用 woff2 */ | |||
| @@ -2,6 +2,7 @@ import { AvailableRange } from '@/enums'; | |||
| import { type CodeConfigData } from '@/pages/CodeConfig/List'; | |||
| import { Flex, Typography } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useState } from 'react'; | |||
| import styles from './index.less'; | |||
| type CodeConfigItemProps = { | |||
| @@ -10,6 +11,7 @@ type CodeConfigItemProps = { | |||
| }; | |||
| function CodeConfigItem({ item, onClick }: CodeConfigItemProps) { | |||
| const [isEllipsis, setIsEllipsis] = useState(false); | |||
| return ( | |||
| <div className={styles['code-config-item']} onClick={() => onClick?.(item)}> | |||
| <Flex justify="space-between" align="center" style={{ marginBottom: '15px' }}> | |||
| @@ -32,11 +34,20 @@ function CodeConfigItem({ item, onClick }: CodeConfigItemProps) { | |||
| </Flex> | |||
| <Typography.Paragraph | |||
| className={styles['code-config-item__url']} | |||
| ellipsis={{ rows: 2, tooltip: item.git_url }} | |||
| ellipsis={{ | |||
| rows: 2, | |||
| tooltip: isEllipsis ? item.git_url : false, // 仅当省略时显示 tooltip | |||
| onEllipsis: (ellipsis) => setIsEllipsis(ellipsis), | |||
| }} | |||
| > | |||
| {item.git_url} | |||
| </Typography.Paragraph> | |||
| <div className={styles['code-config-item__branch']}>{item.git_branch}</div> | |||
| <Typography.Paragraph | |||
| className={styles['code-config-item__branch']} | |||
| ellipsis={{ tooltip: item.git_branch }} | |||
| > | |||
| {item.git_branch} | |||
| </Typography.Paragraph> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -48,12 +48,9 @@ function IframePage({ type, className, style }: IframePageProps) { | |||
| useEffect(() => { | |||
| const requestIframeUrl = async () => { | |||
| setLoading(true); | |||
| const [res] = await to(getRequestAPI(type)()); | |||
| if (res && res.data) { | |||
| setIframeUrl(res.data); | |||
| } else { | |||
| setLoading(false); | |||
| } | |||
| }; | |||
| @@ -6,7 +6,7 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import ResourceSelectorModal, { | |||
| ResourceSelectorResponse, | |||
| type ResourceSelectorResponse, | |||
| ResourceSelectorType, | |||
| selectorTypeConfig, | |||
| } from '@/components/ResourceSelectorModal'; | |||
| @@ -28,7 +28,6 @@ function AutoMLInstance() { | |||
| const [autoMLInfo, setAutoMLInfo] = useState<AutoMLData | undefined>(undefined); | |||
| const [instanceInfo, setInstanceInfo] = useState<AutoMLInstanceData | undefined>(undefined); | |||
| const params = useParams(); | |||
| // const autoMLId = safeInvoke(Number)(params.autoMLId); | |||
| const instanceId = safeInvoke(Number)(params.id); | |||
| const evtSourceRef = useRef<EventSource | null>(null); | |||
| @@ -187,6 +186,7 @@ function AutoMLInstance() { | |||
| icon: <KFIcon type="icon-Trialliebiao" />, | |||
| children: ( | |||
| <ExperimentHistory | |||
| calcMetrics={autoMLInfo?.scoring_functions} | |||
| fileUrl={instanceInfo?.run_history_path} | |||
| isClassification={autoMLInfo?.task_type === AutoMLTaskType.Classification} | |||
| /> | |||
| @@ -8,8 +8,9 @@ import TrialStatusCell from '../TrialStatusCell'; | |||
| import styles from './index.less'; | |||
| type ExperimentHistoryProps = { | |||
| fileUrl?: string; | |||
| isClassification: boolean; | |||
| calcMetrics?: string; // 计算指标 | |||
| fileUrl?: string; // 文件url | |||
| isClassification: boolean; // 是否是分类 | |||
| }; | |||
| type TableData = { | |||
| @@ -22,7 +23,7 @@ type TableData = { | |||
| althorithm?: string; | |||
| }; | |||
| function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps) { | |||
| function ExperimentHistory({ calcMetrics, fileUrl, isClassification }: ExperimentHistoryProps) { | |||
| const [tableData, setTableData] = useState<TableData[]>([]); | |||
| useEffect(() => { | |||
| // 获取实验运行历史记录 | |||
| @@ -33,7 +34,7 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps | |||
| const list: TableData[] = data.map((item) => { | |||
| return { | |||
| id: item[0]?.[0], | |||
| accuracy: item[1]?.[5]?.accuracy, | |||
| accuracy: calcMetrics ? item[1]?.[5]?.[calcMetrics] : undefined, | |||
| duration: item[1]?.[5]?.duration, | |||
| train_loss: item[1]?.[5]?.train_loss, | |||
| status: item[1]?.[2]?.['__enum__']?.split('.')?.[1], | |||
| @@ -64,12 +65,6 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps | |||
| width: 80, | |||
| render: tableCellRender(false), | |||
| }, | |||
| { | |||
| title: '准确率', | |||
| dataIndex: 'accuracy', | |||
| key: 'accuracy', | |||
| render: tableCellRender(true), | |||
| }, | |||
| { | |||
| title: '耗时', | |||
| dataIndex: 'duration', | |||
| @@ -103,6 +98,15 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps | |||
| }, | |||
| ]; | |||
| if (calcMetrics) { | |||
| columns.splice(0, 0, { | |||
| title: `指标:${calcMetrics}`, | |||
| dataIndex: 'accuracy', | |||
| key: 'accuracy', | |||
| render: tableCellRender(true), | |||
| }); | |||
| } | |||
| return ( | |||
| <div className={styles['experiment-history']}> | |||
| <div className={styles['experiment-history__content']}> | |||
| @@ -107,7 +107,7 @@ function ExperimentInstanceComponent({ | |||
| }; | |||
| if (!experimentInsList || experimentInsList.length === 0) { | |||
| return <div style={{ textAlign: 'center' }}>暂无实验实例</div>; | |||
| return <div style={{ textAlign: 'center' }}>暂无数据</div>; | |||
| } | |||
| return ( | |||
| @@ -188,6 +188,7 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| if (expanded) { | |||
| setExpandedRowKeys([record.id]); | |||
| getExperimentInsList(record.id, 0); | |||
| refreshExperimentList(); | |||
| } else { | |||
| setExpandedRowKeys([]); | |||
| } | |||
| @@ -70,7 +70,12 @@ function CodeConfigItem({ item, onClick, onEdit, onRemove }: CodeConfigItemProps | |||
| > | |||
| {item.git_url} | |||
| </Typography.Paragraph> | |||
| <div className={styles['code-config-item__branch']}>{item.git_branch}</div> | |||
| <Typography.Paragraph | |||
| className={styles['code-config-item__branch']} | |||
| ellipsis={{ tooltip: item.git_branch }} | |||
| > | |||
| {item.git_branch} | |||
| </Typography.Paragraph> | |||
| </div> | |||
| <Flex justify="space-between"> | |||
| <div className={styles['code-config-item__user']}> | |||
| @@ -159,27 +159,42 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr | |||
| showSearch | |||
| /> | |||
| </Form.Item> | |||
| {/* <Form.Item label="集群版本" name="available_cluster"> | |||
| <Select allowClear placeholder="请选择集群版本" options={clusterOptions} /> | |||
| </Form.Item> */} | |||
| <Form.Item | |||
| label="数据集简介" | |||
| label="数据集描述" | |||
| name="description" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入数据集简介', | |||
| message: '请输入数据集描述', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| placeholder="请输入数据集简介" | |||
| placeholder="请输入数据集描述" | |||
| showCount | |||
| maxLength={200} | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="版本描述" | |||
| name="version_desc" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入版本描述', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| placeholder="请输入版本描述" | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| maxLength={200} | |||
| showCount | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="可见性" | |||
| name="is_public" | |||
| @@ -143,23 +143,41 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="模型简介" | |||
| label="模型描述" | |||
| name="description" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入模型简介', | |||
| message: '请输入模型描述', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| placeholder="请输入模型简介" | |||
| placeholder="请输入模型描述" | |||
| maxLength={200} | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| showCount | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="版本描述" | |||
| name="version_desc" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入版本描述', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| placeholder="请输入版本描述" | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| maxLength={200} | |||
| showCount | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="可见性" | |||
| name="is_public" | |||
| @@ -21,7 +21,7 @@ const getDatasetDatas = (data: DatasetData): BasicInfoData[] => [ | |||
| value: data.name, | |||
| }, | |||
| { | |||
| label: '版本', | |||
| label: '数据集版本', | |||
| value: data.version, | |||
| }, | |||
| { | |||
| @@ -64,7 +64,7 @@ const getModelDatas = (data: ModelData): BasicInfoData[] => [ | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '版本', | |||
| label: '模型版本', | |||
| value: data.version, | |||
| ellipsis: true, | |||
| }, | |||
| @@ -51,8 +51,8 @@ function CreateMirrorModal({ envId, onOk, ...rest }: CreateMirrorModalProps) { | |||
| message: '请输入镜像名称', | |||
| }, | |||
| { | |||
| pattern: /^[a-z0-9/_-]*$/, | |||
| message: '只支持小写字母、数字、下划线(_)、中横线(-)、斜杠(/)', | |||
| pattern: /^[a-z0-9/._-]*$/, | |||
| message: '只支持小写字母、数字、点(.)、下划线(_)、中横线(-)、斜杠(/)', | |||
| }, | |||
| ]} | |||
| > | |||
| @@ -179,11 +179,12 @@ function ExperimentText() { | |||
| if (!statusNode) { | |||
| return; | |||
| } | |||
| const { finishedAt, startedAt, phase, id } = statusNode; | |||
| const { finishedAt, startedAt, phase, id, message } = statusNode; | |||
| workflowNode.experimentStartTime = startedAt; | |||
| workflowNode.experimentEndTime = finishedAt; | |||
| workflowNode.experimentStatus = phase; | |||
| workflowNode.workflowId = id; | |||
| workflowNode.message = message; | |||
| workflowNode.img = phase | |||
| ? `${workflowNode.imgName}-${phase}.png` | |||
| : `${workflowNode.imgName}.png`; | |||
| @@ -3,7 +3,7 @@ import editExperimentIcon from '@/assets/img/edit-experiment.png'; | |||
| import KFModal from '@/components/KFModal'; | |||
| import { type PipelineGlobalParam } from '@/types'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Button, Form, Input, Radio, Select, type FormRule } from 'antd'; | |||
| import { Button, Form, Input, Radio, Select, Typography, type FormRule } from 'antd'; | |||
| import { useState } from 'react'; | |||
| import styles from './index.less'; | |||
| @@ -63,13 +63,14 @@ export const getParamRules = (paramType: number, required: boolean = false): For | |||
| }; | |||
| // 根据参数设置 label | |||
| export const getParamType = (param: PipelineGlobalParam): string => { | |||
| export const getParamLabel = (param: PipelineGlobalParam): React.ReactNode => { | |||
| const paramTypes: Readonly<Record<number, string>> = { | |||
| 1: '字符串', | |||
| 2: '整型', | |||
| 3: '布尔类型', | |||
| }; | |||
| return param.param_name + `(${paramTypes[param.param_type]})`; | |||
| const label = param.param_name + `(${paramTypes[param.param_type]})`; | |||
| return <Typography.Text ellipsis={{ tooltip: label }}>{label}</Typography.Text>; | |||
| }; | |||
| function AddExperimentModal({ | |||
| @@ -99,8 +100,8 @@ function AddExperimentModal({ | |||
| }; | |||
| const paramLayout = { | |||
| labelCol: { span: 8 }, | |||
| wrapperCol: { span: 16 }, | |||
| labelCol: { span: 6 }, | |||
| wrapperCol: { span: 18 }, | |||
| }; | |||
| // 除了流水线选择发生变化 | |||
| @@ -157,7 +158,6 @@ function AddExperimentModal({ | |||
| form={form} | |||
| {...layout} | |||
| labelAlign="left" | |||
| labelWrap | |||
| > | |||
| <Form.Item | |||
| label="实验名称" | |||
| @@ -215,9 +215,9 @@ function AddExperimentModal({ | |||
| {...restField} | |||
| {...paramLayout} | |||
| key={key} | |||
| label={getParamType(globalParam[name])} | |||
| label={getParamLabel(globalParam[name])} | |||
| name={[name, 'param_value']} | |||
| rules={getParamRules(globalParam[name]['param_type'])} | |||
| rules={getParamRules(globalParam[name]['param_type'], true)} | |||
| > | |||
| {getParamComponent( | |||
| globalParam[name]['param_type'], | |||
| @@ -13,7 +13,6 @@ | |||
| } | |||
| &__tabs { | |||
| height: calc(100% - 169px); | |||
| :global { | |||
| .ant-tabs-nav { | |||
| padding-left: 24px; | |||
| @@ -35,7 +34,7 @@ | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 15px; | |||
| padding-left: 24px; | |||
| padding: 0 24px; | |||
| color: @text-color; | |||
| font-size: 15px; | |||
| } | |||
| @@ -3,7 +3,7 @@ import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import { PipelineNodeModelSerialize } from '@/types'; | |||
| import { elapsedTime, formatDate } from '@/utils/date'; | |||
| import { CloseOutlined, DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; | |||
| import { Drawer, Tabs } from 'antd'; | |||
| import { Drawer, Tabs, Typography } from 'antd'; | |||
| import { useMemo } from 'react'; | |||
| import ExperimentParameter from '../ExperimentParameter'; | |||
| import ExperimentResult from '../ExperimentResult'; | |||
| @@ -129,6 +129,14 @@ const ExperimentDrawer = ({ | |||
| '--' | |||
| )} | |||
| </div> | |||
| {instanceNodeData.message && ( | |||
| <div className={styles['experiment-drawer__info']}> | |||
| <div style={{ flex: 'none' }}>消息:</div> | |||
| <Typography.Text ellipsis={{ tooltip: instanceNodeData.message }}> | |||
| {instanceNodeData.message ?? '--'} | |||
| </Typography.Text> | |||
| </div> | |||
| )} | |||
| <div className={styles['experiment-drawer__info']}> | |||
| 启动时间:{formatDate(instanceNodeStartTime)} | |||
| </div> | |||
| @@ -137,7 +145,14 @@ const ExperimentDrawer = ({ | |||
| {elapsedTime(instanceNodeStartTime, instanceNodeEndTime)} | |||
| </div> | |||
| </div> | |||
| <Tabs defaultActiveKey="1" items={items} className={styles['experiment-drawer__tabs']} /> | |||
| <Tabs | |||
| defaultActiveKey="1" | |||
| items={items} | |||
| className={styles['experiment-drawer__tabs']} | |||
| style={{ | |||
| height: instanceNodeData.message ? 'calc(100% - 169px - 39px)' : 'calc(100% - 169px)', | |||
| }} | |||
| /> | |||
| </Drawer> | |||
| ); | |||
| }; | |||
| @@ -1,31 +1,14 @@ | |||
| .params_container { | |||
| max-height: 230px; | |||
| padding: 15px 15px 0; | |||
| .params-container { | |||
| max-height: calc(100vh - 300px); | |||
| padding: 24px 24px 0; | |||
| overflow-y: auto; | |||
| border: 1px solid #e6e6e6; | |||
| border-radius: 8px; | |||
| &_line { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 15px; | |||
| &_label { | |||
| width: 180px; | |||
| color: @text-color; | |||
| font-size: 15px; | |||
| } | |||
| &_value { | |||
| flex: 1; | |||
| width: 100px; | |||
| margin-left: 15px; | |||
| padding: 10px 20px; | |||
| color: @text-color; | |||
| font-size: @font-size; | |||
| line-height: 20px; | |||
| background: #f6f6f6; | |||
| border: 1px solid #e0e0e1; | |||
| border-radius: 4px; | |||
| } | |||
| .params-empty { | |||
| :global { | |||
| .kf-empty__image { | |||
| width: 300px; | |||
| } | |||
| } | |||
| } | |||
| @@ -4,9 +4,11 @@ | |||
| * @Description: 查看实验使用的参数 | |||
| */ | |||
| import parameterImg from '@/assets/img/modal-parameter.png'; | |||
| import KFEmpty, { EmptyType } from '@/components/KFEmpty'; | |||
| import KFModal from '@/components/KFModal'; | |||
| import { type PipelineGlobalParam } from '@/types'; | |||
| import { getParamType } from '../AddExperimentModal'; | |||
| import { Form } from 'antd'; | |||
| import { getParamComponent, getParamLabel } from '../AddExperimentModal'; | |||
| import styles from './index.less'; | |||
| type ParamsModalProps = { | |||
| @@ -26,14 +28,44 @@ function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) { | |||
| cancelButtonProps={{ style: { display: 'none' } }} | |||
| width={825} | |||
| > | |||
| <div className={styles.params_container}> | |||
| {globalParam?.map((item) => ( | |||
| <div key={item.param_name} className={styles.params_container_line}> | |||
| <span className={styles.params_container_line_label}>{getParamType(item)}</span> | |||
| <span className={styles.params_container_line_value}>{item.param_value}</span> | |||
| </div> | |||
| ))} | |||
| </div> | |||
| {Array.isArray(globalParam) && globalParam.length > 0 ? ( | |||
| <div className={styles['params-container']}> | |||
| <Form | |||
| name="view_params_form" | |||
| labelCol={{ span: 6 }} | |||
| wrapperCol={{ span: 18 }} | |||
| initialValues={{ global_param: globalParam }} | |||
| labelAlign="left" | |||
| disabled | |||
| > | |||
| <Form.List name="global_param"> | |||
| {(fields) => | |||
| fields.map(({ key, name, ...restField }) => ( | |||
| <Form.Item | |||
| {...restField} | |||
| key={key} | |||
| name={[name, 'param_value']} | |||
| label={getParamLabel(globalParam[name])} | |||
| > | |||
| {getParamComponent( | |||
| globalParam[name]['param_type'], | |||
| globalParam[name]['is_sensitive'], | |||
| )} | |||
| </Form.Item> | |||
| )) | |||
| } | |||
| </Form.List> | |||
| </Form> | |||
| </div> | |||
| ) : ( | |||
| <KFEmpty | |||
| className={styles['params-empty']} | |||
| type={EmptyType.NoData} | |||
| title="暂无数据" | |||
| content="该流水线没有设置全局参数" | |||
| hasFooter={false} | |||
| /> | |||
| )} | |||
| </KFModal> | |||
| ); | |||
| } | |||
| @@ -206,6 +206,7 @@ function Experiment() { | |||
| setExpandedRowKeys(null); | |||
| } else { | |||
| getQueryByExperiment(record.id, 0); | |||
| refreshExperimentList(); | |||
| } | |||
| }; | |||
| @@ -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); | |||
| @@ -70,32 +70,36 @@ function MirrorCreate() { | |||
| // 创建公网、本地镜像 | |||
| const createPublicMirror = async (formData: FormData) => { | |||
| const upload_type = formData['upload_type']; | |||
| let params; | |||
| if (upload_type === CommonTabKeys.Public) { | |||
| params = { | |||
| const params = { | |||
| ...omit(formData, ['upload_type']), | |||
| upload_type: 0, | |||
| image_type: 0, | |||
| }; | |||
| const [res] = await to(createMirrorReq(params)); | |||
| if (res) { | |||
| message.success('创建成功'); | |||
| navigate(-1); | |||
| } | |||
| } else { | |||
| const fileList = formData['fileList'] ?? []; | |||
| if (validateUploadFiles(fileList)) { | |||
| const file = fileList[0]; | |||
| params = { | |||
| const params = { | |||
| ...omit(formData, ['fileList', 'upload_type']), | |||
| path: file.response.data.url, | |||
| file_size: file.response.data.fileSize, | |||
| upload_type: 1, | |||
| image_type: 0, | |||
| }; | |||
| const [res] = await to(createMirrorReq(params)); | |||
| if (res) { | |||
| message.success('创建成功'); | |||
| navigate(-1); | |||
| } | |||
| } | |||
| } | |||
| const [res] = await to(createMirrorReq(params)); | |||
| if (res) { | |||
| message.success('创建成功'); | |||
| navigate(-1); | |||
| } | |||
| }; | |||
| // 提交 | |||
| @@ -118,14 +122,16 @@ function MirrorCreate() { | |||
| return true; | |||
| }; | |||
| const descTitle = isAddVersion ? '版本描述' : '镜像描述'; | |||
| return ( | |||
| <div className={styles['mirror-create']}> | |||
| <PageTitle title="创建镜像"></PageTitle> | |||
| <PageTitle title={!isAddVersion ? '创建镜像' : '新增镜像版本'}></PageTitle> | |||
| <div className={styles['mirror-create__content']}> | |||
| <div> | |||
| <Form | |||
| name="mirror-create" | |||
| labelCol={{ flex: '130px' }} | |||
| labelCol={{ flex: '135px' }} | |||
| wrapperCol={{ flex: 1 }} | |||
| labelAlign="left" | |||
| form={form} | |||
| @@ -142,7 +148,7 @@ function MirrorCreate() { | |||
| <Row gutter={10}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="镜像名称及Tag" | |||
| label="镜像名称和版本" | |||
| name="name" | |||
| rules={[ | |||
| { | |||
| @@ -150,15 +156,15 @@ function MirrorCreate() { | |||
| message: '请输入镜像名称', | |||
| }, | |||
| { | |||
| pattern: /^[a-z0-9/_-]*$/, | |||
| message: '只支持小写字母、数字、下划线(_)、中横线(-)、斜杠(/)', | |||
| pattern: /^[a-z0-9/._-]*$/, | |||
| message: '只支持小写字母、数字、点(.)、下划线(_)、中横线(-)、斜杠(/)', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input | |||
| placeholder="请输入镜像名称" | |||
| maxLength={64} | |||
| disabled={nameDisabled} | |||
| disabled={isAddVersion} | |||
| showCount | |||
| allowClear | |||
| /> | |||
| @@ -174,33 +180,33 @@ 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={descTitle} | |||
| name="description" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入镜像描述', | |||
| message: `请输入${descTitle}`, | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| placeholder="请输入镜像描述,最长128字符" | |||
| placeholder={`请输入${descTitle}`} | |||
| maxLength={128} | |||
| showCount | |||
| allowClear | |||
| @@ -303,7 +309,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, | |||
| @@ -174,20 +174,27 @@ function MirrorInfo() { | |||
| title: '镜像地址', | |||
| dataIndex: 'url', | |||
| key: 'url', | |||
| render: tableCellRender(), | |||
| width: '25%', | |||
| 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 | |||
| @@ -18,6 +18,7 @@ | |||
| &__table { | |||
| flex: 1; | |||
| min-height: 0; | |||
| margin-top: 24px; | |||
| } | |||
| } | |||
| @@ -133,17 +133,18 @@ function ServiceInfo() { | |||
| if (res) { | |||
| message.success('删除成功'); | |||
| // 如果是一页的唯一数据,删除时,请求第一页的数据 | |||
| // 否则直接刷新这一页的数据 | |||
| // 避免回到第一页 | |||
| if (tableData.length > 1) { | |||
| // 否则直接刷新这一页的数据,避免回到第一页 | |||
| if (tableData.length === 1) { | |||
| setPagination((prev) => ({ | |||
| ...prev, | |||
| current: 1, | |||
| })); | |||
| } else { | |||
| getServiceInfo(); | |||
| getServiceVersions(); | |||
| setPagination((prev) => ({ | |||
| ...prev, | |||
| })); | |||
| } | |||
| getServiceInfo(); | |||
| } | |||
| }; | |||
| @@ -432,7 +433,7 @@ function ServiceInfo() { | |||
| onClick={() => createServiceVersion(ServiceOperationType.Create)} | |||
| icon={<KFIcon type="icon-xinjian2" />} | |||
| > | |||
| 新增版本 | |||
| 新增服务版本 | |||
| </Button> | |||
| <Button style={{ marginRight: '15px' }} type="default" onClick={handleVersionCompare}> | |||
| 版本对比 | |||
| @@ -136,31 +136,35 @@ const GlobalParamsDrawer = forwardRef( | |||
| cur.global_param?.[name]?.param_type | |||
| } | |||
| > | |||
| {({ getFieldValue }) => ( | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'param_value']} | |||
| label="值" | |||
| rules={getParamRules( | |||
| getFieldValue(['global_param', name, 'param_type']), | |||
| true, | |||
| )} | |||
| > | |||
| {getParamComponent(getFieldValue(['global_param', name, 'param_type']))} | |||
| </Form.Item> | |||
| )} | |||
| </Form.Item> | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'is_sensitive']} | |||
| label="脱敏显示" | |||
| rules={[{ required: true, message: '请选择' }]} | |||
| tooltip="展示关联的流水线的参数,脱敏的参数以xxxx展示" | |||
| > | |||
| <Radio.Group> | |||
| <Radio value={1}>是</Radio> | |||
| <Radio value={0}>否</Radio> | |||
| </Radio.Group> | |||
| {({ getFieldValue }) => { | |||
| const type = getFieldValue(['global_param', name, 'param_type']); | |||
| return ( | |||
| <> | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'param_value']} | |||
| label="值" | |||
| rules={getParamRules(type, true)} | |||
| > | |||
| {getParamComponent(type)} | |||
| </Form.Item> | |||
| {type !== 3 && ( | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'is_sensitive']} | |||
| label="脱敏显示" | |||
| rules={[{ required: true, message: '请选择' }]} | |||
| tooltip="展示关联的流水线的参数,脱敏的参数以xxxx展示" | |||
| > | |||
| <Radio.Group> | |||
| <Radio value={1}>是</Radio> | |||
| <Radio value={0}>否</Radio> | |||
| </Radio.Group> | |||
| </Form.Item> | |||
| )} | |||
| </> | |||
| ); | |||
| }} | |||
| </Form.Item> | |||
| <Tooltip title="删除参数"> | |||
| <Button | |||
| @@ -216,6 +216,11 @@ function PointsDetail() { | |||
| label: '进行中', | |||
| color: themes['primaryColor'], | |||
| }, | |||
| { | |||
| value: 0, | |||
| label: '准备中', | |||
| color: themes['pendingColor'], | |||
| }, | |||
| ]), | |||
| }, | |||
| ]; | |||
| @@ -1,12 +1,18 @@ | |||
| .total-statistics { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| height: 140px; | |||
| padding: 0 16px; | |||
| // 媒体查询 | |||
| @media screen and (max-width: 1600px) { | |||
| flex: 1 1 content; | |||
| } | |||
| &__icon { | |||
| width: 63px; | |||
| margin-right: 15px; | |||
| margin-right: 16px; | |||
| } | |||
| &__title { | |||
| @@ -8,7 +8,6 @@ | |||
| &__overview { | |||
| flex: 1; | |||
| gap: 15px; | |||
| margin-bottom: 16px; | |||
| padding: 20px 30px; | |||
| background-color: white; | |||
| border-radius: 4px; | |||
| @@ -22,7 +21,6 @@ | |||
| &__content { | |||
| display: flex; | |||
| flex-wrap: wrap; | |||
| gap: 15px; | |||
| align-items: center; | |||
| @@ -35,6 +33,11 @@ | |||
| rgba(22, 100, 255, 0.02) 58.35% | |||
| ); | |||
| border-radius: 4px; | |||
| // 媒体查询 | |||
| @media screen and (max-width: 1600px) { | |||
| flex: 1 1 content; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -44,6 +47,7 @@ | |||
| gap: 15px; | |||
| align-items: flex-start; | |||
| width: 100%; | |||
| margin-top: 16px; | |||
| } | |||
| &__user { | |||
| @@ -46,7 +46,7 @@ function Workspace() { | |||
| return ( | |||
| <div className={styles.workspace}> | |||
| <WorkspaceIntro></WorkspaceIntro> | |||
| <Flex gap={'0 15px'} wrap> | |||
| <Flex gap={'15px'} wrap> | |||
| <div className={styles['workspace__overview']}> | |||
| <div className={styles['workspace__overview__title']}>运行概览</div> | |||
| <div className={styles['workspace__overview__content']}> | |||
| @@ -59,7 +59,7 @@ function Workspace() { | |||
| <Divider | |||
| type="vertical" | |||
| dashed | |||
| style={{ color: '1px dashed rgba(96, 107, 122, 0.19)', height: 68 }} | |||
| style={{ color: '1px dashed rgba(96, 107, 122, 0.19)', height: 68, margin: 0 }} | |||
| /> | |||
| <TotalStatistics | |||
| icon={require('@/assets/img/workspace-experiment.png')} | |||
| @@ -71,6 +71,7 @@ export type PipelineNodeModel = { | |||
| component_label: string; | |||
| icon_path: string; | |||
| workflowId?: string; | |||
| message?: string; | |||
| }; | |||
| // 流水线节点模型数据 | |||
| @@ -41,7 +41,10 @@ export const elapsedTime = (begin?: string | null, end?: string | null): string | |||
| if (hours !== 0) { | |||
| return `${hours}小时${minutes}分`; | |||
| } | |||
| return `${minutes}分${seconds}秒`; | |||
| if (minutes !== 0) { | |||
| return `${minutes}分${seconds}秒`; | |||
| } | |||
| return `${seconds}秒`; | |||
| }; | |||
| /** | |||
| @@ -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, | |||
| @@ -31,6 +31,9 @@ public class ImageVersion implements Serializable { | |||
| @ApiModelProperty(value = "镜像版本") | |||
| private String version; | |||
| @ApiModelProperty(value = "镜像版本描述") | |||
| private String description; | |||
| @ApiModelProperty(value = "镜像推送地址") | |||
| private String url; | |||
| @@ -18,4 +18,12 @@ public interface RayDao { | |||
| int save(@Param("ray") Ray ray); | |||
| int edit(@Param("ray") Ray ray); | |||
| List<Ray> queryByDatasetId(@Param("datasetId") String datasetId); | |||
| List<Ray> queryByModelId(@Param("modelId") String modelId); | |||
| List<Ray> queryByImageId(@Param("imageId") String imageId); | |||
| List<Ray> queryByCodeConfig(@Param("codeConfig") String codeConfig); | |||
| } | |||
| @@ -199,9 +199,9 @@ public class AutoMlServiceImpl implements AutoMlService { | |||
| String outputPath = minioEndpoint + "/" + output2.get("path").replace("{{workflow.name}}", (String) metadata.get("name")) + "/"; | |||
| autoMlIns.setModelPath(outputPath + "save_model.joblib"); | |||
| if (Constant.AutoMl_Classification.equals(autoMl.getTaskType())) { | |||
| autoMlIns.setImgPath(outputPath + "Auto-sklearn_accuracy_over_time.png" + "," + outputPath + "Train_Confusion_Matrix.png" + "," + outputPath + "Test_Confusion_Matrix.png"); | |||
| autoMlIns.setImgPath(outputPath + "Auto-sklearn_metric_over_time.png" + "," + outputPath + "Train_Confusion_Matrix.png" + "," + outputPath + "Test_Confusion_Matrix.png"); | |||
| } else { | |||
| autoMlIns.setImgPath(outputPath + "Auto-sklearn_accuracy_over_time.png" + "," + outputPath + "regression.png"); | |||
| autoMlIns.setImgPath(outputPath + "Auto-sklearn_metric_over_time.png" + "," + outputPath + "regression.png"); | |||
| } | |||
| autoMlIns.setResultPath(outputPath + "result.txt"); | |||
| String seed = autoMl.getSeed() != null ? String.valueOf(autoMl.getSeed()) : "1"; | |||
| @@ -1,5 +1,6 @@ | |||
| package com.ruoyi.platform.service.impl; | |||
| import com.alibaba.fastjson2.JSON; | |||
| import com.ruoyi.common.security.utils.SecurityUtils; | |||
| import com.ruoyi.platform.constant.Constant; | |||
| import com.ruoyi.platform.domain.AssetWorkflow; | |||
| @@ -13,9 +14,12 @@ import org.springframework.data.domain.Page; | |||
| import org.springframework.data.domain.PageImpl; | |||
| import org.springframework.data.domain.PageRequest; | |||
| import org.springframework.stereotype.Service; | |||
| import com.ruoyi.platform.domain.Ray; | |||
| import com.ruoyi.platform.mapper.RayDao; | |||
| import javax.annotation.Resource; | |||
| import java.util.Date; | |||
| import java.util.HashMap; | |||
| import java.util.List; | |||
| import java.util.stream.Collectors; | |||
| @@ -26,6 +30,8 @@ public class CodeConfigServiceImpl implements CodeConfigService { | |||
| private CodeConfigDao codeConfigDao; | |||
| @Resource | |||
| private AssetWorkflowDao assetWorkflowDao; | |||
| @Resource | |||
| private RayDao rayDao; | |||
| @Override | |||
| public Page<CodeConfig> queryByPage(CodeConfig codeConfig, PageRequest pageRequest) { | |||
| @@ -88,6 +94,14 @@ public class CodeConfigServiceImpl implements CodeConfigService { | |||
| throw new Exception("该代码配置被流水线:" + workflows + "使用,不能删除,请先删除流水线。"); | |||
| } | |||
| HashMap<String, String> map = new HashMap<>(); | |||
| map.put("code_path", codeConfig.getGitUrl()); | |||
| List<Ray> rayList = rayDao.queryByCodeConfig(JSON.toJSONString(map)); | |||
| if (rayList != null && !rayList.isEmpty()) { | |||
| String rays = String.join(",", rayList.stream().map(Ray::getName).collect(Collectors.toSet())); | |||
| throw new Exception("该代码配置被超参数自动寻优:" + rays + "使用,不能删除,请先删除超参数自动寻优。"); | |||
| } | |||
| LoginUser loginUser = SecurityUtils.getLoginUser(); | |||
| String username = loginUser.getUsername(); | |||
| String createBy = codeConfig.getCreateBy(); | |||
| @@ -1,16 +1,11 @@ | |||
| package com.ruoyi.platform.service.impl; | |||
| import com.alibaba.fastjson2.JSON; | |||
| import com.alibaba.fastjson2.util.DateUtils; | |||
| import com.ruoyi.common.security.utils.SecurityUtils; | |||
| import com.ruoyi.platform.constant.Constant; | |||
| import com.ruoyi.platform.domain.AssetWorkflow; | |||
| import com.ruoyi.platform.domain.DevEnvironment; | |||
| import com.ruoyi.platform.domain.Image; | |||
| import com.ruoyi.platform.domain.ImageVersion; | |||
| import com.ruoyi.platform.mapper.AssetWorkflowDao; | |||
| import com.ruoyi.platform.mapper.DevEnvironmentDao; | |||
| import com.ruoyi.platform.mapper.ImageDao; | |||
| import com.ruoyi.platform.mapper.ImageVersionDao; | |||
| import com.ruoyi.platform.domain.*; | |||
| import com.ruoyi.platform.mapper.*; | |||
| import com.ruoyi.platform.service.ImageService; | |||
| import com.ruoyi.platform.service.ImageVersionService; | |||
| import com.ruoyi.platform.service.MinioService; | |||
| @@ -53,10 +48,10 @@ public class ImageServiceImpl implements ImageService { | |||
| private ImageVersionDao imageVersionDao; | |||
| @Resource | |||
| private DevEnvironmentDao devEnvironmentDao; | |||
| @Resource | |||
| private AssetWorkflowDao assetWorkflowDao; | |||
| @Resource | |||
| private RayDao rayDao; | |||
| @Resource | |||
| private ImageVersionService imageVersionService; | |||
| @@ -182,6 +177,14 @@ public class ImageServiceImpl implements ImageService { | |||
| throw new Exception("该镜像被流水线:" + workflows + "使用,不能删除,请先删除流水线。"); | |||
| } | |||
| HashMap<String, String> map = new HashMap<>(); | |||
| map.put("id", String.valueOf(id)); | |||
| List<Ray> rayList = rayDao.queryByImageId(JSON.toJSONString(map)); | |||
| if (rayList != null && !rayList.isEmpty()) { | |||
| String rays = String.join(",", rayList.stream().map(Ray::getName).collect(Collectors.toSet())); | |||
| throw new Exception("该镜像被超参数自动寻优:" + rays + "使用,不能删除,请先删除超参数自动寻优。"); | |||
| } | |||
| //判断权限,只有admin和创建者本身可以删除该数据集 | |||
| LoginUser loginUser = SecurityUtils.getLoginUser(); | |||
| String username = loginUser.getUsername(); | |||
| @@ -246,6 +249,7 @@ public class ImageServiceImpl implements ImageService { | |||
| imageVersion.setVersion(imageVo.getVersion()); | |||
| imageVersion.setTagName(imageVo.getTagName()); | |||
| imageVersion.setFileSize(imageVo.getFileSize()); | |||
| imageVersion.setDescription(imageVo.getDescription()); | |||
| imageVersion.setStatus("building"); | |||
| ImageVersion imageVersionInsert = this.imageVersionService.insert(imageVersion); | |||
| if (imageVersionInsert == null) { | |||
| @@ -1,23 +1,25 @@ | |||
| package com.ruoyi.platform.service.impl; | |||
| import com.alibaba.fastjson2.JSON; | |||
| import com.ruoyi.common.security.utils.SecurityUtils; | |||
| import com.ruoyi.platform.constant.Constant; | |||
| import com.ruoyi.platform.domain.AssetWorkflow; | |||
| import com.ruoyi.platform.domain.ImageVersion; | |||
| import com.ruoyi.platform.domain.Ray; | |||
| import com.ruoyi.platform.mapper.AssetWorkflowDao; | |||
| import com.ruoyi.platform.mapper.ImageDao; | |||
| import com.ruoyi.platform.mapper.ImageVersionDao; | |||
| import com.ruoyi.platform.domain.ModelsVersion; | |||
| import com.ruoyi.platform.mapper.RayDao; | |||
| import com.ruoyi.platform.service.ImageVersionService; | |||
| import com.ruoyi.system.api.model.LoginUser; | |||
| import org.apache.commons.lang3.StringUtils; | |||
| import org.springframework.stereotype.Service; | |||
| import org.springframework.data.domain.Page; | |||
| import org.springframework.data.domain.PageImpl; | |||
| import org.springframework.data.domain.PageRequest; | |||
| import org.springframework.stereotype.Service; | |||
| import javax.annotation.Resource; | |||
| import java.util.Date; | |||
| import java.util.HashMap; | |||
| import java.util.List; | |||
| import java.util.stream.Collectors; | |||
| @@ -31,9 +33,10 @@ import java.util.stream.Collectors; | |||
| public class ImageVersionServiceImpl implements ImageVersionService { | |||
| @Resource | |||
| private ImageVersionDao imageVersionDao; | |||
| @Resource | |||
| private AssetWorkflowDao assetWorkflowDao; | |||
| @Resource | |||
| private RayDao rayDao; | |||
| /** | |||
| * 通过ID查询单条数据 | |||
| @@ -50,7 +53,7 @@ public class ImageVersionServiceImpl implements ImageVersionService { | |||
| * 分页查询 | |||
| * | |||
| * @param imageVersion 筛选条件 | |||
| * @param pageRequest 分页对象 | |||
| * @param pageRequest 分页对象 | |||
| * @return 查询结果 | |||
| */ | |||
| @Override | |||
| @@ -60,14 +63,14 @@ public class ImageVersionServiceImpl implements ImageVersionService { | |||
| } | |||
| @Override | |||
| public List<ImageVersion> queryByImageId(Integer imageId){ | |||
| public List<ImageVersion> queryByImageId(Integer imageId) { | |||
| return imageVersionDao.queryByImageId(imageId); | |||
| } | |||
| @Override | |||
| public String removeById(Integer id) throws Exception { | |||
| ImageVersion imageVersion = this.imageVersionDao.queryById(id); | |||
| if (imageVersion == null){ | |||
| if (imageVersion == null) { | |||
| return "该版本下模型文件信息不存在"; | |||
| } | |||
| @@ -77,16 +80,24 @@ public class ImageVersionServiceImpl implements ImageVersionService { | |||
| throw new Exception("该镜像版本被流水线:" + workflows + "使用,不能删除,请先删除流水线。"); | |||
| } | |||
| HashMap<String, String> map = new HashMap<>(); | |||
| map.put("version", String.valueOf(id)); | |||
| List<Ray> rayList = rayDao.queryByImageId(JSON.toJSONString(map)); | |||
| if (rayList != null && !rayList.isEmpty()) { | |||
| String rays = String.join(",", rayList.stream().map(Ray::getName).collect(Collectors.toSet())); | |||
| throw new Exception("该镜像版本被超参数自动寻优:" + rays + "使用,不能删除,请先删除超参数自动寻优。"); | |||
| } | |||
| //判断权限,只有admin和创建者本身可以删除该数据集 | |||
| LoginUser loginUser = SecurityUtils.getLoginUser(); | |||
| String username = loginUser.getUsername(); | |||
| String createdBy = imageVersion.getCreateBy(); | |||
| if (!(StringUtils.equals(username,"admin") || StringUtils.equals(username,createdBy))){ | |||
| if (!(StringUtils.equals(username, "admin") || StringUtils.equals(username, createdBy))) { | |||
| return "无权限删除该版本下模型信息"; | |||
| } | |||
| imageVersion.setState(0); | |||
| return this.imageVersionDao.update(imageVersion)>0?"删除成功":"删除失败"; | |||
| return this.imageVersionDao.update(imageVersion) > 0 ? "删除成功" : "删除失败"; | |||
| } | |||
| @@ -117,7 +128,7 @@ public class ImageVersionServiceImpl implements ImageVersionService { | |||
| @Override | |||
| public ImageVersion update(ImageVersion imageVersion) { | |||
| int currentState = imageVersion.getState(); | |||
| if(currentState == 0){ | |||
| if (currentState == 0) { | |||
| throw new RuntimeException("镜像版本已被删除,无法更新。"); | |||
| } | |||
| LoginUser loginUser = SecurityUtils.getLoginUser(); | |||
| @@ -9,10 +9,7 @@ import com.ruoyi.platform.annotations.CheckDuplicate; | |||
| import com.ruoyi.platform.constant.Constant; | |||
| import com.ruoyi.platform.domain.*; | |||
| import com.ruoyi.platform.domain.dependencydomain.TrainTaskDepency; | |||
| import com.ruoyi.platform.mapper.AssetWorkflowDao; | |||
| import com.ruoyi.platform.mapper.ModelDependency1Dao; | |||
| import com.ruoyi.platform.mapper.ModelsDao; | |||
| import com.ruoyi.platform.mapper.ModelsVersionDao; | |||
| import com.ruoyi.platform.mapper.*; | |||
| import com.ruoyi.platform.service.*; | |||
| import com.ruoyi.platform.utils.*; | |||
| import com.ruoyi.platform.vo.*; | |||
| @@ -67,7 +64,8 @@ public class ModelsServiceImpl implements ModelsService { | |||
| private ModelsDao modelsDao; | |||
| @Resource | |||
| private ModelsVersionDao modelsVersionDao; | |||
| @Resource | |||
| private RayDao rayDao; | |||
| @Resource | |||
| private ModelsVersionService modelsVersionService; | |||
| @@ -634,7 +632,6 @@ public class ModelsServiceImpl implements ModelsService { | |||
| "</code></pre>"); | |||
| modelMetaVo.setIdentifier(repositoryName); | |||
| modelMetaVo.setOwner(owner); | |||
| modelMetaVo.setVersionDesc(modelMetaVo.getDescription()); | |||
| modelMetaVo.setRelativePaths(relatePath); | |||
| File folder = new File(modelPath); | |||
| long folderSize = FileUtil.getFolderSize(folder); | |||
| @@ -1154,6 +1151,14 @@ public class ModelsServiceImpl implements ModelsService { | |||
| throw new Exception("该模型被流水线:" + workflows + "使用,不能删除,请先删除流水线。"); | |||
| } | |||
| HashMap<String, String> queryMap = new HashMap<>(); | |||
| queryMap.put("id", String.valueOf(repoId)); | |||
| List<Ray> rayList = rayDao.queryByModelId(JSON.toJSONString(queryMap)); | |||
| if (rayList != null && !rayList.isEmpty()) { | |||
| String rays = String.join(",", rayList.stream().map(Ray::getName).collect(Collectors.toSet())); | |||
| throw new Exception("该模型被超参数自动寻优:" + rays + "使用,不能删除,请先删除超参数自动寻优。"); | |||
| } | |||
| String token = gitService.checkoutToken(); | |||
| gitService.deleteProject(token, owner, identifier); | |||
| //删除模型依赖 | |||
| @@ -1177,6 +1182,15 @@ public class ModelsServiceImpl implements ModelsService { | |||
| throw new Exception("该模型版本被流水线:" + workflows + "使用,不能删除,请先删除流水线。"); | |||
| } | |||
| HashMap<String, String> queryMap = new HashMap<>(); | |||
| queryMap.put("id", String.valueOf(repoId)); | |||
| queryMap.put("version", version); | |||
| List<Ray> rayList = rayDao.queryByModelId(JSON.toJSONString(queryMap)); | |||
| if (rayList != null && !rayList.isEmpty()) { | |||
| String rays = String.join(",", rayList.stream().map(Ray::getName).collect(Collectors.toSet())); | |||
| throw new Exception("该数据集版本被超参数自动寻优:" + rays + "使用,不能删除,请先删除超参数自动寻优。"); | |||
| } | |||
| String token = gitService.checkoutToken(); | |||
| String rootPath = Paths.get(localPath + "/" + relativePath).getParent().toString(); | |||
| gitService.deleteBranch(token, owner, identifier, version, rootPath); | |||
| @@ -4,12 +4,10 @@ import com.alibaba.fastjson2.JSON; | |||
| import com.ruoyi.common.core.utils.DateUtils; | |||
| import com.ruoyi.common.security.utils.SecurityUtils; | |||
| import com.ruoyi.platform.constant.Constant; | |||
| import com.ruoyi.platform.domain.AssetWorkflow; | |||
| import com.ruoyi.platform.domain.AutoMl; | |||
| import com.ruoyi.platform.domain.Dataset; | |||
| import com.ruoyi.platform.domain.DatasetTempStorage; | |||
| import com.ruoyi.platform.domain.*; | |||
| import com.ruoyi.platform.mapper.AssetWorkflowDao; | |||
| import com.ruoyi.platform.mapper.AutoMlDao; | |||
| import com.ruoyi.platform.mapper.RayDao; | |||
| import com.ruoyi.platform.service.DatasetTempStorageService; | |||
| import com.ruoyi.platform.service.GitService; | |||
| import com.ruoyi.platform.service.NewDatasetService; | |||
| @@ -56,6 +54,8 @@ public class NewDatasetServiceImpl implements NewDatasetService { | |||
| private AssetWorkflowDao assetWorkflowDao; | |||
| @Resource | |||
| private AutoMlDao autoMlDao; | |||
| @Resource | |||
| private RayDao rayDao; | |||
| @Value("${spring.redis.host}") | |||
| private String redisHost; | |||
| @@ -146,7 +146,6 @@ public class NewDatasetServiceImpl implements NewDatasetService { | |||
| // 拼接生产的元数据后写入yaml文件 | |||
| datasetVo.setCreateBy(String.valueOf(StringUtils.isNotEmpty((String) userInfo.get("nickname")) ? userInfo.get("nickname") : userInfo.get("login"))); | |||
| datasetVo.setUpdateTime(DateUtils.getTime()); | |||
| datasetVo.setVersionDesc(datasetVo.getDescription()); | |||
| datasetVo.setUsage("<pre><code>" + | |||
| "# 克隆数据集配置文件与存储参数到本地\n" + | |||
| "git clone -b " + branchName + " " + projectUrl + "\n" + | |||
| @@ -421,6 +420,12 @@ public class NewDatasetServiceImpl implements NewDatasetService { | |||
| throw new Exception("该数据集被自动机器学习:" + autoMls + "使用,不能删除,请先删除自动机器学习。"); | |||
| } | |||
| List<Ray> rayList = rayDao.queryByDatasetId(JSON.toJSONString(map)); | |||
| if (rayList != null && !rayList.isEmpty()) { | |||
| String rays = String.join(",", rayList.stream().map(Ray::getName).collect(Collectors.toSet())); | |||
| throw new Exception("该数据集被超参数自动寻优:" + rays + "使用,不能删除,请先删除超参数自动寻优。"); | |||
| } | |||
| String token = gitService.checkoutToken(); | |||
| gitService.deleteProject(token, owner, repo); | |||
| @@ -446,6 +451,12 @@ public class NewDatasetServiceImpl implements NewDatasetService { | |||
| throw new Exception("该数据集版本被自动机器学习:" + autoMls + "使用,不能删除,请先删除自动机器学习。"); | |||
| } | |||
| List<Ray> rayList = rayDao.queryByDatasetId(JSON.toJSONString(map)); | |||
| if (rayList != null && !rayList.isEmpty()) { | |||
| String rays = String.join(",", rayList.stream().map(Ray::getName).collect(Collectors.toSet())); | |||
| throw new Exception("该数据集版本被超参数自动寻优:" + rays + "使用,不能删除,请先删除超参数自动寻优。"); | |||
| } | |||
| String token = gitService.checkoutToken(); | |||
| String rootPath = Paths.get(localPathlocal + "/" + relativePath).getParent().toString(); | |||
| gitService.deleteBranch(token, owner, repo, version, rootPath); | |||
| @@ -33,8 +33,8 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { | |||
| ComputingResource computingResource = computingResourceDao.queryById(computingResourceId); | |||
| LoginUser loginUser = SecurityUtils.getLoginUser(); | |||
| if (loginUser.getSysUser().getCredit() == 0) { | |||
| throw new Exception("当前积分为零"); | |||
| if (loginUser.getSysUser().getCredit() <= 0) { | |||
| throw new Exception("当前积分不足"); | |||
| } | |||
| if (Constant.Computing_Resource_GPU.equals(computingResource.getComputingResource())) { | |||
| if (resourceOccupyDao.haveResource(computingResource.getResourceId(), computingResource.getGpuNums() * replicas)) { | |||
| @@ -323,7 +323,7 @@ public class ServiceServiceImpl implements ServiceService { | |||
| // 记录开始扣积分 | |||
| if (reRun) { | |||
| resourceOccupyService.startDeduce(serviceVersion.getComputingResourceId(), serviceVersion.getReplicas(), Constant.TaskType_Service, serviceVersion.getServiceId(), serviceVersion.getId(), null, service.getServiceName(), null, null); | |||
| } else if (Constant.Running.equals(serviceVersionById.getRunState())) { | |||
| } else if (Constant.Running.equals(serviceVersionById.getRunState()) || Constant.Pending.equals(serviceVersionById.getRunState())) { | |||
| resourceOccupyService.update(Constant.TaskType_Service, serviceVersion.getServiceId(), serviceVersion.getId(), serviceVersion.getComputingResourceId(), serviceVersion.getReplicas()); | |||
| } | |||
| return "修改成功"; | |||
| @@ -126,6 +126,13 @@ public class WorkflowServiceImpl implements WorkflowService { | |||
| this.workflowDao.update(workflow); | |||
| assetWorkflowDao.deleteByWorkFlowId(workflow.getId()); | |||
| saveAssetWorkFlow(workflow); | |||
| if (StringUtils.isNotEmpty(workflow.getGlobalParam())) { | |||
| List<Experiment> experimentList = experimentService.queryByWorkflowId(workflow.getId()); | |||
| for (Experiment experiment : experimentList) { | |||
| experiment.setGlobalParam(workflow.getGlobalParam()); | |||
| experimentService.update(experiment); | |||
| } | |||
| } | |||
| return this.queryById(workflow.getId()); | |||
| } | |||
| @@ -129,8 +129,8 @@ | |||
| <!--新增所有列--> | |||
| <insert id="insert" keyProperty="id" useGeneratedKeys="true"> | |||
| insert into image_version(image_id, version, url, tag_name, file_size, status, create_by, create_time, update_by, update_time, state) | |||
| values (#{imageVersion.imageId}, #{imageVersion.version}, #{imageVersion.url}, #{imageVersion.tagName}, #{imageVersion.fileSize}, #{imageVersion.status}, #{imageVersion.createBy}, #{imageVersion.createTime}, #{imageVersion.updateBy}, #{imageVersion.updateTime}, #{imageVersion.state}) | |||
| insert into image_version(image_id, version, description, url, tag_name, file_size, status, create_by, create_time, update_by, update_time, state) | |||
| values (#{imageVersion.imageId}, #{imageVersion.version}, #{imageVersion.description}, #{imageVersion.url}, #{imageVersion.tagName}, #{imageVersion.fileSize}, #{imageVersion.status}, #{imageVersion.createBy}, #{imageVersion.createTime}, #{imageVersion.updateBy}, #{imageVersion.updateTime}, #{imageVersion.state}) | |||
| </insert> | |||
| @@ -109,6 +109,34 @@ | |||
| where id = #{id} | |||
| </select> | |||
| <select id="queryByDatasetId" resultType="com.ruoyi.platform.domain.Ray"> | |||
| select * | |||
| from ray | |||
| where JSON_CONTAINS(dataset, #{datasetId}) | |||
| and state = 1 | |||
| </select> | |||
| <select id="queryByModelId" resultType="com.ruoyi.platform.domain.Ray"> | |||
| select * | |||
| from ray | |||
| where JSON_CONTAINS(model, #{modelId}) | |||
| and state = 1 | |||
| </select> | |||
| <select id="queryByImageId" resultType="com.ruoyi.platform.domain.Ray"> | |||
| select * | |||
| from ray | |||
| where JSON_CONTAINS(image, #{imageId}) | |||
| and state = 1 | |||
| </select> | |||
| <select id="queryByCodeConfig" resultType="com.ruoyi.platform.domain.Ray"> | |||
| select * | |||
| from ray | |||
| where JSON_CONTAINS(code_config, #{codeConfig}) | |||
| and state = 1 | |||
| </select> | |||
| <sql id="common_condition"> | |||
| <where> | |||
| state = 1 | |||