diff --git a/react-ui/config/config.ts b/react-ui/config/config.ts
index 3b681f86..759d7c56 100644
--- a/react-ui/config/config.ts
+++ b/react-ui/config/config.ts
@@ -76,7 +76,7 @@ export default defineConfig({
* @name layout 插件
* @doc https://umijs.org/docs/max/layout-menu
*/
- title: '智能软件开发平台',
+ title: '智能材料科研平台',
layout: {
locale: false,
...defaultSettings,
diff --git a/react-ui/config/defaultSettings.ts b/react-ui/config/defaultSettings.ts
index 97a26343..d1842286 100644
--- a/react-ui/config/defaultSettings.ts
+++ b/react-ui/config/defaultSettings.ts
@@ -16,7 +16,7 @@ const Settings: ProLayoutProps & {
fixSiderbar: false,
splitMenus: false,
colorWeak: false,
- title: '智能软件开发平台',
+ title: '智能材料科研平台',
pwa: true,
logo: '/assets/images/left-top-logo.png',
token: {
diff --git a/react-ui/config/routes.ts b/react-ui/config/routes.ts
index e89d5d60..960a3cb0 100644
--- a/react-ui/config/routes.ts
+++ b/react-ui/config/routes.ts
@@ -112,7 +112,7 @@ export default [
{
name: '开发环境',
path: '',
- component: './DevelopmentEnvironment/List',
+ component: './DevelopmentEnvironment/Editor',
},
{
name: '创建编辑器',
diff --git a/react-ui/mock/model.ts b/react-ui/mock/model.ts
index 02054802..af637db0 100644
--- a/react-ui/mock/model.ts
+++ b/react-ui/mock/model.ts
@@ -48,12 +48,33 @@ export default defineMock({
exp_ins_id: null,
version: 'v0.1.0',
ref_item: null,
- train_task: {},
- train_dataset: [],
- train_params: [],
- train_image: null,
- test_dataset: [],
- project_dependency: {},
+ train_task: {
+ name: '模型训练测试导出0529',
+ ins_id: 229,
+ task_id: 'model-train-5d76f002',
+ },
+ train_dataset: [
+ {
+ dataset_id: 20,
+ dataset_version: 'v0.1.0',
+ dataset_name: '手写体识别模型依赖测试训练数据集',
+ },
+ ],
+ train_params: ['256', '2'],
+ train_image:
+ '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim',
+ test_dataset: [
+ {
+ dataset_id: 20,
+ dataset_version: 'v0.1.0',
+ dataset_name: '手写体识别模型依赖测试训练数据集',
+ },
+ ],
+ project_dependency: {
+ url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git',
+ name: 'somun202304241505581',
+ branch: 'train_ci_test',
+ },
parent_models_map: [],
parent_models: [],
children_models: null,
@@ -80,12 +101,38 @@ export default defineMock({
exp_ins_id: null,
version: 'v0.3.0',
ref_item: null,
- train_task: {},
- train_dataset: [],
- train_params: [],
- train_image: null,
- test_dataset: [],
- project_dependency: {},
+ train_task: {
+ name: '模型训练测试导出0529',
+ ins_id: 229,
+ task_id: 'model-train-5d76f002',
+ },
+ train_dataset: [
+ {
+ dataset_id: 120,
+ dataset_version: 'v0.1.0',
+ dataset_name: '手写体识别模型依赖测试训练数据集',
+ },
+ {
+ dataset_id: 20,
+ dataset_version: 'v0.1.0',
+ dataset_name: '手写体识别模型依赖测试训练数据集',
+ },
+ ],
+ train_params: ['256', '2'],
+ train_image:
+ '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim',
+ test_dataset: [
+ {
+ dataset_id: 20,
+ dataset_version: 'v0.1.0',
+ dataset_name: '手写体识别模型依赖测试训练数据集',
+ },
+ ],
+ project_dependency: {
+ url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git',
+ name: 'somun202304241505581',
+ branch: 'train_ci_test',
+ },
parent_models_map: [],
parent_models: [],
children_models: [],
@@ -110,12 +157,33 @@ export default defineMock({
exp_ins_id: null,
version: 'v0.31.0',
ref_item: null,
- train_task: {},
- train_dataset: [],
- train_params: [],
- train_image: null,
- test_dataset: [],
- project_dependency: {},
+ train_task: {
+ name: '模型训练测试导出0529',
+ ins_id: 229,
+ task_id: 'model-train-5d76f002',
+ },
+ train_dataset: [
+ {
+ dataset_id: 20,
+ dataset_version: 'v0.1.0',
+ dataset_name: '手写体识别模型依赖测试训练数据集',
+ },
+ ],
+ train_params: ['256', '2'],
+ train_image:
+ '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim',
+ test_dataset: [
+ {
+ dataset_id: 20,
+ dataset_version: 'v0.1.0',
+ dataset_name: '手写体识别模型依赖测试训练数据集',
+ },
+ ],
+ project_dependency: {
+ url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git',
+ name: 'somun202304241505581',
+ branch: 'train_ci_test',
+ },
parent_models_map: [],
parent_models: [],
children_models: [],
@@ -140,12 +208,33 @@ export default defineMock({
exp_ins_id: null,
version: 'v0.4.0',
ref_item: null,
- train_task: {},
- train_dataset: [],
- train_params: [],
- train_image: null,
- test_dataset: [],
- project_dependency: {},
+ train_task: {
+ name: '模型训练测试导出0529',
+ ins_id: 229,
+ task_id: 'model-train-5d76f002',
+ },
+ train_dataset: [
+ {
+ dataset_id: 20,
+ dataset_version: 'v0.1.0',
+ dataset_name: '手写体识别模型依赖测试训练数据集',
+ },
+ ],
+ train_params: ['256', '2'],
+ train_image:
+ '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim',
+ test_dataset: [
+ {
+ dataset_id: 20,
+ dataset_version: 'v0.1.0',
+ dataset_name: '手写体识别模型依赖测试训练数据集',
+ },
+ ],
+ project_dependency: {
+ url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git',
+ name: 'somun202304241505581',
+ branch: 'train_ci_test',
+ },
parent_models_map: [],
parent_models: [],
children_models: [
@@ -154,12 +243,33 @@ export default defineMock({
exp_ins_id: null,
version: 'v0.6.0',
ref_item: null,
- train_task: {},
- train_dataset: [],
- train_params: [],
- train_image: null,
- test_dataset: [],
- project_dependency: {},
+ train_task: {
+ name: '模型训练测试导出0529',
+ ins_id: 229,
+ task_id: 'model-train-5d76f002',
+ },
+ train_dataset: [
+ {
+ dataset_id: 20,
+ dataset_version: 'v0.1.0',
+ dataset_name: '手写体识别模型依赖测试训练数据集',
+ },
+ ],
+ train_params: ['256', '2'],
+ train_image:
+ '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim',
+ test_dataset: [
+ {
+ dataset_id: 20,
+ dataset_version: 'v0.1.0',
+ dataset_name: '手写体识别模型依赖测试训练数据集',
+ },
+ ],
+ project_dependency: {
+ url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git',
+ name: 'somun202304241505581',
+ branch: 'train_ci_test',
+ },
parent_models_map: [],
parent_models: [],
children_models: [],
@@ -231,12 +341,33 @@ export default defineMock({
exp_ins_id: null,
version: 'v0.5.0',
ref_item: null,
- train_task: {},
- train_dataset: [],
- train_params: [],
- train_image: null,
- test_dataset: [],
- project_dependency: {},
+ train_task: {
+ name: '模型训练测试导出0529',
+ ins_id: 229,
+ task_id: 'model-train-5d76f002',
+ },
+ train_dataset: [
+ {
+ dataset_id: 20,
+ dataset_version: 'v0.1.0',
+ dataset_name: '手写体识别模型依赖测试训练数据集',
+ },
+ ],
+ train_params: ['256', '2'],
+ train_image:
+ '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim',
+ test_dataset: [
+ {
+ dataset_id: 20,
+ dataset_version: 'v0.1.0',
+ dataset_name: '手写体识别模型依赖测试训练数据集',
+ },
+ ],
+ project_dependency: {
+ url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git',
+ name: 'somun202304241505581',
+ branch: 'train_ci_test',
+ },
parent_models_map: [],
parent_models: [],
children_models: [
@@ -275,12 +406,33 @@ export default defineMock({
exp_ins_id: null,
version: 'v0.11.0',
ref_item: null,
- train_task: {},
- train_dataset: [],
- train_params: [],
- train_image: null,
- test_dataset: [],
- project_dependency: {},
+ train_task: {
+ name: '模型训练测试导出0529',
+ ins_id: 229,
+ task_id: 'model-train-5d76f002',
+ },
+ train_dataset: [
+ {
+ dataset_id: 20,
+ dataset_version: 'v0.1.0',
+ dataset_name: '手写体识别模型依赖测试训练数据集',
+ },
+ ],
+ train_params: ['256', '2'],
+ train_image:
+ '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim',
+ test_dataset: [
+ {
+ dataset_id: 20,
+ dataset_version: 'v0.1.0',
+ dataset_name: '手写体识别模型依赖测试训练数据集',
+ },
+ ],
+ project_dependency: {
+ url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git',
+ name: 'somun202304241505581',
+ branch: 'train_ci_test',
+ },
parent_models_map: [],
parent_models: [],
children_models: [],
diff --git a/react-ui/public/assets/images/left-top-logo-1.png b/react-ui/public/assets/images/left-top-logo-1.png
new file mode 100644
index 00000000..64bdaf2c
Binary files /dev/null and b/react-ui/public/assets/images/left-top-logo-1.png differ
diff --git a/react-ui/public/assets/images/left-top-logo.png b/react-ui/public/assets/images/left-top-logo.png
index 64bdaf2c..e2fbcfe5 100644
Binary files a/react-ui/public/assets/images/left-top-logo.png and b/react-ui/public/assets/images/left-top-logo.png differ
diff --git a/react-ui/src/assets/img/experiment-pending.png b/react-ui/src/assets/img/experiment-pending.png
new file mode 100644
index 00000000..ceefa027
Binary files /dev/null and b/react-ui/src/assets/img/experiment-pending.png differ
diff --git a/react-ui/src/assets/img/experiment-running.png b/react-ui/src/assets/img/experiment-running.png
new file mode 100644
index 00000000..d4121030
Binary files /dev/null and b/react-ui/src/assets/img/experiment-running.png differ
diff --git a/react-ui/src/assets/img/pipeline-warning.png b/react-ui/src/assets/img/pipeline-warning.png
new file mode 100644
index 00000000..67b8c65c
Binary files /dev/null and b/react-ui/src/assets/img/pipeline-warning.png differ
diff --git a/react-ui/src/components/KFIcon/index.tsx b/react-ui/src/components/KFIcon/index.tsx
index 65239957..e50dabec 100644
--- a/react-ui/src/components/KFIcon/index.tsx
+++ b/react-ui/src/components/KFIcon/index.tsx
@@ -1,7 +1,7 @@
/*
* @Author: 赵伟
* @Date: 2024-04-17 12:53:06
- * @Description:
+ * @Description: 封装 iconfont 组件
*/
import '@/iconfont/iconfont-menu.js';
import '@/iconfont/iconfont.js';
diff --git a/react-ui/src/components/KFSpin/index.less b/react-ui/src/components/KFSpin/index.less
new file mode 100644
index 00000000..931e7ea0
--- /dev/null
+++ b/react-ui/src/components/KFSpin/index.less
@@ -0,0 +1,19 @@
+.kf-spin {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background-color: rgba(255, 255, 255, 0.5);
+
+ &__label {
+ margin-top: 20px;
+ color: @text-color;
+ font-size: @font-size-content;
+ }
+}
diff --git a/react-ui/src/components/KFSpin/index.tsx b/react-ui/src/components/KFSpin/index.tsx
new file mode 100644
index 00000000..64d315a9
--- /dev/null
+++ b/react-ui/src/components/KFSpin/index.tsx
@@ -0,0 +1,13 @@
+import { Spin, SpinProps } from 'antd';
+import styles from './index.less';
+
+function KFSpin(props: SpinProps) {
+ return (
+
+ );
+}
+
+export default KFSpin;
diff --git a/react-ui/src/components/PageTitle/index.less b/react-ui/src/components/PageTitle/index.less
index d120009b..47907246 100644
--- a/react-ui/src/components/PageTitle/index.less
+++ b/react-ui/src/components/PageTitle/index.less
@@ -6,5 +6,5 @@
background-image: url(@/assets/img/page-title-bg.png);
background-repeat: no-repeat;
background-position: top center;
- background-size: 100%;
+ background-size: 100% 100%;
}
diff --git a/react-ui/src/components/ParameterInput/index.less b/react-ui/src/components/ParameterInput/index.less
index 2d4f0489..6426985e 100644
--- a/react-ui/src/components/ParameterInput/index.less
+++ b/react-ui/src/components/ParameterInput/index.less
@@ -62,3 +62,7 @@
font-size: 12px;
}
}
+
+.parameter-input.parameter-input--error {
+ border-color: @error-color;
+}
diff --git a/react-ui/src/components/ParameterInput/index.tsx b/react-ui/src/components/ParameterInput/index.tsx
index 0fc08551..8ce18830 100644
--- a/react-ui/src/components/ParameterInput/index.tsx
+++ b/react-ui/src/components/ParameterInput/index.tsx
@@ -1,18 +1,28 @@
import { CloseOutlined } from '@ant-design/icons';
-import { Input } from 'antd';
+import { Form, Input } from 'antd';
+import { RuleObject } from 'antd/es/form';
import classNames from 'classnames';
import './index.less';
-type ParameterInputData = {
- value?: any;
- showValue?: any;
- fromSelect?: boolean;
-} & Record;
+// 对象
+export type ParameterInputObject = {
+ value?: any; // 值
+ showValue?: any; // 显示值
+ fromSelect?: boolean; // 是否来自选择
+ activeTab?: string; // 选择镜像、数据集、模型时,保存当前激活的tab
+ expandedKeys?: string[]; // 选择镜像、数据集、模型时,保存展开的keys
+ checkedKeys?: string[]; // 选择镜像、数据集、模型时,保存选中的keys
+ [key: string]: any;
+};
-interface ParameterInputProps {
- value?: ParameterInputData;
- onChange?: (value: ParameterInputData) => void;
+// 值类型
+export type ParameterInputValue = ParameterInputObject | string;
+
+export interface ParameterInputProps {
+ value?: ParameterInputValue;
+ onChange?: (value?: ParameterInputValue) => void;
onClick?: () => void;
+ onRemove?: () => void;
canInput?: boolean;
textArea?: boolean;
placeholder?: string;
@@ -21,12 +31,14 @@ interface ParameterInputProps {
style?: React.CSSProperties;
size?: 'middle' | 'small' | 'large';
disabled?: boolean;
+ id?: string;
}
function ParameterInput({
value,
onChange,
onClick,
+ onRemove,
canInput = true,
textArea = false,
allowClear,
@@ -34,6 +46,7 @@ function ParameterInput({
style,
size = 'middle',
disabled = false,
+ id,
...rest
}: ParameterInputProps) {
const valueObj =
@@ -42,16 +55,34 @@ function ParameterInput({
valueObj.showValue = valueObj.value;
}
const isSelect = valueObj?.fromSelect;
- const InputComponent = textArea ? Input.TextArea : Input;
const placeholder = valueObj?.placeholder || rest?.placeholder;
+ const InputComponent = textArea ? Input.TextArea : Input;
+ const { status } = Form.Item.useStatus();
+
+ // 删除
+ const handleRemove = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ onChange?.({
+ ...valueObj,
+ value: undefined,
+ showValue: undefined,
+ fromSelect: false,
+ activeTab: undefined,
+ expandedKeys: [],
+ checkedKeys: [],
+ });
+ onRemove?.();
+ };
return (
<>
{(isSelect || !canInput) && !disabled ? (
{valueObj?.showValue}
{
- e.stopPropagation();
- onChange?.({
- ...valueObj,
- value: undefined,
- showValue: undefined,
- fromSelect: false,
- activeTab: undefined,
- expandedKeys: undefined,
- checkedKeys: undefined,
- });
- }}
+ onClick={handleRemove}
/>
) : (
@@ -83,6 +103,7 @@ function ParameterInput({
) : (
onChange?.({
...valueObj,
- fromSelect: false,
value: e.target.value,
showValue: e.target.value,
+ fromSelect: false,
})
}
/>
@@ -105,3 +126,12 @@ function ParameterInput({
}
export default ParameterInput;
+
+// 必填校验
+export const requiredValidator = (rule: RuleObject, value: any) => {
+ const trueValue = typeof value === 'object' ? value?.value : value;
+ if (!trueValue) {
+ return Promise.reject(rule.message || '必填项');
+ }
+ return Promise.resolve();
+};
diff --git a/react-ui/src/components/ResourceSelect/index.less b/react-ui/src/components/ResourceSelect/index.less
new file mode 100644
index 00000000..8a586c52
--- /dev/null
+++ b/react-ui/src/components/ResourceSelect/index.less
@@ -0,0 +1,11 @@
+.kf-resource-select {
+ position: relative;
+ display: flex;
+ align-items: center;
+
+ &__button {
+ position: absolute;
+ top: 0;
+ left: calc(100% + 10px);
+ }
+}
diff --git a/react-ui/src/components/ResourceSelect/index.tsx b/react-ui/src/components/ResourceSelect/index.tsx
new file mode 100644
index 00000000..96f5e96e
--- /dev/null
+++ b/react-ui/src/components/ResourceSelect/index.tsx
@@ -0,0 +1,104 @@
+import KFIcon from '@/components/KFIcon';
+import ResourceSelectorModal, {
+ ResourceSelectorResponse,
+ ResourceSelectorType,
+ selectorTypeConfig,
+} from '@/pages/Pipeline/components/ResourceSelectorModal';
+import { openAntdModal } from '@/utils/modal';
+import { Button } from 'antd';
+import { useState } from 'react';
+import ParameterInput, { type ParameterInputProps } from '../ParameterInput';
+import './index.less';
+
+export { requiredValidator, type ParameterInputObject } from '../ParameterInput';
+
+type ResourceSelectProps = {
+ type: ResourceSelectorType;
+} & ParameterInputProps;
+
+// 获取选择数据集、模型后面按钮 icon
+const getSelectBtnIcon = (type: ResourceSelectorType) => {
+ return ;
+};
+
+function ResourceSelect({ type, value, onChange, ...rest }: ResourceSelectProps) {
+ const [selectedResource, setSelectedResource] = useState(
+ undefined,
+ );
+
+ const selectResource = () => {
+ const resource = selectedResource;
+ const { close } = openAntdModal(ResourceSelectorModal, {
+ type,
+ defaultExpandedKeys: resource ? [resource.id] : [],
+ defaultCheckedKeys: resource ? [`${resource.id}-${resource.version}`] : [],
+ defaultActiveTab: resource?.activeTab,
+ onOk: (res) => {
+ setSelectedResource(res);
+ if (res) {
+ const { activeTab, id, name, version, path } = res;
+ if (type === ResourceSelectorType.Mirror) {
+ onChange?.({
+ value: path,
+ showValue: path,
+ fromSelect: true,
+ activeTab,
+ expandedKeys: [`${id}`],
+ checkedKeys: [`${id}-${version}`],
+ });
+ } else {
+ const jsonObj = {
+ id,
+ version,
+ path,
+ };
+ const jsonObjStr = JSON.stringify(jsonObj);
+ const showValue = `${name}:${version}`;
+ onChange?.({
+ value: jsonObjStr,
+ showValue,
+ fromSelect: true,
+ activeTab,
+ expandedKeys: [`${id}`],
+ checkedKeys: [`${id}-${version}`],
+ ...jsonObj,
+ });
+ }
+ } else {
+ onChange?.({
+ value: undefined,
+ showValue: undefined,
+ fromSelect: false,
+ activeTab: undefined,
+ expandedKeys: [],
+ checkedKeys: [],
+ });
+ }
+ close();
+ },
+ });
+ };
+
+ return (
+
+
setSelectedResource(undefined)}
+ onClick={selectResource}
+ >
+
+
+ );
+}
+
+export default ResourceSelect;
diff --git a/react-ui/src/hooks/index.ts b/react-ui/src/hooks/index.ts
index b34d5156..adf61e7d 100644
--- a/react-ui/src/hooks/index.ts
+++ b/react-ui/src/hooks/index.ts
@@ -32,6 +32,7 @@ export function useStateRef(initialValue: T) {
*/
export function useVisible(initialValue: boolean) {
const [visible, setVisible] = useState(initialValue);
+ const ref = useRef(initialValue);
const open = useCallback(() => {
setVisible(true);
@@ -41,7 +42,11 @@ export function useVisible(initialValue: boolean) {
setVisible(false);
}, []);
- return [visible, open, close] as const;
+ useEffect(() => {
+ ref.current = visible;
+ }, [visible]);
+
+ return [visible, open, close, ref] as const;
}
type Callback = (state: T) => void;
diff --git a/react-ui/src/iconfont/iconfont.js b/react-ui/src/iconfont/iconfont.js
index 1ec213e7..6d617cfb 100644
--- a/react-ui/src/iconfont/iconfont.js
+++ b/react-ui/src/iconfont/iconfont.js
@@ -1 +1 @@
-window._iconfont_svg_string_4511447='',function(t){var a=(a=document.getElementsByTagName("script"))[a.length-1],h=a.getAttribute("data-injectcss"),a=a.getAttribute("data-disable-injectsvg");if(!a){var l,v,z,i,o,m=function(a,h){h.parentNode.insertBefore(a,h)};if(h&&!t.__iconfont__svg__cssinject__){t.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}l=function(){var a,h=document.createElement("div");h.innerHTML=t._iconfont_svg_string_4511447,(h=h.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",h=h,(a=document.body).firstChild?m(h,a.firstChild):a.appendChild(h))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(l,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),l()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(z=l,i=t.document,o=!1,d(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,p())})}function p(){o||(o=!0,z())}function d(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(d,50)}p()}}(window);
\ No newline at end of file
+window._iconfont_svg_string_4511447='',function(t){var a=(a=document.getElementsByTagName("script"))[a.length-1],h=a.getAttribute("data-injectcss"),a=a.getAttribute("data-disable-injectsvg");if(!a){var l,v,z,i,o,m=function(a,h){h.parentNode.insertBefore(a,h)};if(h&&!t.__iconfont__svg__cssinject__){t.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}l=function(){var a,h=document.createElement("div");h.innerHTML=t._iconfont_svg_string_4511447,(h=h.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",h=h,(a=document.body).firstChild?m(h,a.firstChild):a.appendChild(h))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(l,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),l()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(z=l,i=t.document,o=!1,d(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,p())})}function p(){o||(o=!0,z())}function d(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(d,50)}p()}}(window);
\ No newline at end of file
diff --git a/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx b/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx
index 036fc12c..e59e9698 100644
--- a/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx
+++ b/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx
@@ -1,35 +1,32 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
- * @Description: 创建镜像
+ * @Description: 创建开发环境
*/
import KFIcon from '@/components/KFIcon';
import KFRadio, { type KFRadioItem } from '@/components/KFRadio';
import PageTitle from '@/components/PageTitle';
-import ParameterInput from '@/components/ParameterInput';
+import ResourceSelect, {
+ requiredValidator,
+ type ParameterInputObject,
+} from '@/components/ResourceSelect';
import SubAreaTitle from '@/components/SubAreaTitle';
import { useComputingResource } from '@/hooks/resource';
-import ResourceSelectorModal, {
- ResourceSelectorResponse,
- ResourceSelectorType,
- selectorTypeConfig,
-} from '@/pages/Pipeline/components/ResourceSelectorModal';
+import { ResourceSelectorType } from '@/pages/Pipeline/components/ResourceSelectorModal';
import { createEditorReq } from '@/services/developmentEnvironment';
-import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise';
import { useNavigate } from '@umijs/max';
import { App, Button, Col, Form, Input, Row, Select } from 'antd';
-import { pick } from 'lodash';
-import { useState } from 'react';
+import { omit, pick } from 'lodash';
import styles from './index.less';
type FormData = {
name: string;
computing_resource: string;
standard: string;
- image: string;
- model: ResourceSelectorResponse;
- dataset: ResourceSelectorResponse;
+ image: ParameterInputObject;
+ model: ParameterInputObject;
+ dataset: ParameterInputObject;
};
enum ComputingResourceType {
@@ -55,25 +52,20 @@ function EditorCreate() {
const [form] = Form.useForm();
const { message } = App.useApp();
const [resourceStandardList, filterResourceStandard] = useComputingResource();
- const [selectedModel, setSelectedModel] = useState(
- undefined,
- ); // 选择的模型,为了再次打开时恢复原来的选择
- const [selectedDataset, setSelectedDataset] = useState(
- undefined,
- ); // 选择的数据集,为了再次打开时恢复原来的选择
- const [selectedMirror, setSelectedMirror] = useState(
- undefined,
- ); // 选择的镜像,为了再次打开时恢复原来的选择
// 创建编辑器
const createEditor = async (formData: FormData) => {
- // const { model, dataset } = formData;
- // const params = {
- // ...formData,
- // model: JSON.stringify(omit(model, ['showValue'])),
- // dataset: JSON.stringify(dataset, ['showValue']),
- // };
- const [res] = await to(createEditorReq(formData));
+ // 根据后台要求,修改表单数据
+ const image = formData['image'];
+ const model = formData['model'];
+ const dataset = formData['dataset'];
+ const params = {
+ ...omit(formData, ['image', 'model', 'dataset']),
+ image: image.value,
+ model: pick(model, ['id', 'version', 'path', 'showValue']),
+ dataset: pick(dataset, ['id', 'version', 'path', 'showValue']),
+ };
+ const [res] = await to(createEditorReq(params));
if (res) {
message.success('创建成功');
navgite(-1);
@@ -89,61 +81,6 @@ function EditorCreate() {
const cancel = () => {
navgite(-1);
};
- // 获取选择数据集、模型后面按钮 icon
- const getSelectBtnIcon = (type: ResourceSelectorType) => {
- return ;
- };
-
- // 选择模型、镜像、数据集
- const selectResource = (name: string, type: ResourceSelectorType) => {
- let resource: ResourceSelectorResponse | undefined;
- switch (type) {
- case ResourceSelectorType.Model:
- resource = selectedModel;
- break;
- case ResourceSelectorType.Dataset:
- resource = selectedDataset;
- break;
- default:
- resource = selectedMirror;
- break;
- }
- const { close } = openAntdModal(ResourceSelectorModal, {
- type,
- defaultExpandedKeys: resource ? [resource.id] : [],
- defaultCheckedKeys: resource ? [`${resource.id}-${resource.version}`] : [],
- defaultActiveTab: resource?.activeTab,
- onOk: (res) => {
- if (res) {
- if (type === ResourceSelectorType.Mirror) {
- form.setFieldValue(name, res.path);
- setSelectedMirror(res);
- } else {
- const showValue = `${res.name}:${res.version}`;
- form.setFieldValue(name, {
- ...pick(res, ['id', 'version', 'path']),
- showValue,
- });
- if (type === ResourceSelectorType.Model) {
- setSelectedModel(res);
- } else if (type === ResourceSelectorType.Dataset) {
- setSelectedDataset(res);
- }
- }
- } else {
- if (type === ResourceSelectorType.Model) {
- setSelectedModel(undefined);
- } else if (type === ResourceSelectorType.Dataset) {
- setSelectedDataset(undefined);
- } else if (type === ResourceSelectorType.Mirror) {
- setSelectedMirror(undefined);
- }
- form.setFieldValue(name, '');
- }
- close();
- },
- });
- };
return (
@@ -230,64 +167,46 @@ function EditorCreate() {
- selectResource('image', ResourceSelectorType.Mirror)}
/>
-
-
-
- selectResource('model', ResourceSelectorType.Model)}
/>
-
-
-
@@ -296,29 +215,20 @@ function EditorCreate() {
name="dataset"
rules={[
{
- required: true,
+ validator: requiredValidator,
message: '请选择数据集',
},
]}
+ required
>
- selectResource('dataset', ResourceSelectorType.Dataset)}
/>
-
-
-
diff --git a/react-ui/src/pages/DevelopmentEnvironment/List/index.tsx b/react-ui/src/pages/DevelopmentEnvironment/List/index.tsx
index c7b22c6a..38a98e6a 100644
--- a/react-ui/src/pages/DevelopmentEnvironment/List/index.tsx
+++ b/react-ui/src/pages/DevelopmentEnvironment/List/index.tsx
@@ -1,7 +1,7 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
- * @Description: 开发环境
+ * @Description: 开发环境列表
*/
import CommonTableCell from '@/components/CommonTableCell';
import DateTableCell from '@/components/DateTableCell';
diff --git a/react-ui/src/pages/Experiment/Comparison/config.tsx b/react-ui/src/pages/Experiment/Comparison/config.tsx
new file mode 100644
index 00000000..c6c53971
--- /dev/null
+++ b/react-ui/src/pages/Experiment/Comparison/config.tsx
@@ -0,0 +1,17 @@
+export enum ComparisonType {
+ Train = 'Train', // 训练
+ Evaluate = 'Evaluate', // 评估
+}
+
+type ComparisonTypeInfo = {
+ title: string;
+};
+
+export const comparisonConfig: Record = {
+ [ComparisonType.Train]: {
+ title: '训练',
+ },
+ [ComparisonType.Evaluate]: {
+ title: '评估',
+ },
+};
diff --git a/react-ui/src/pages/Experiment/Comparison/index.less b/react-ui/src/pages/Experiment/Comparison/index.less
index a491c621..7a97a588 100644
--- a/react-ui/src/pages/Experiment/Comparison/index.less
+++ b/react-ui/src/pages/Experiment/Comparison/index.less
@@ -22,6 +22,12 @@
.ant-table-container {
border: none !important;
}
+ .ant-table-thead {
+ .ant-table-cell {
+ background-color: rgb(247, 247, 247);
+ border-color: #e8e8e8 !important;
+ }
+ }
.ant-table-tbody {
.ant-table-cell {
border-right: none !important;
diff --git a/react-ui/src/pages/Experiment/Comparison/index.tsx b/react-ui/src/pages/Experiment/Comparison/index.tsx
index 59cd3f8b..3aca2028 100644
--- a/react-ui/src/pages/Experiment/Comparison/index.tsx
+++ b/react-ui/src/pages/Experiment/Comparison/index.tsx
@@ -7,17 +7,13 @@ import {
import { to } from '@/utils/promise';
import tableCellRender, { arrayFormatter, dateFormatter } from '@/utils/table';
import { useSearchParams } from '@umijs/max';
-import { App, Button, Table, /*TablePaginationConfig,*/ TableProps } from 'antd';
+import { App, Button, Table, /* TablePaginationConfig,*/ TableProps, Tooltip } from 'antd';
import classNames from 'classnames';
import { useEffect, useMemo, useState } from 'react';
import ExperimentStatusCell from '../components/ExperimentStatusCell';
+import { ComparisonType, comparisonConfig } from './config';
import styles from './index.less';
-export enum ComparisonType {
- Train = 'Train', // 训练
- Evaluate = 'Evaluate', // 评估
-}
-
type TableData = {
experiment_ins_id: number;
run_id: string;
@@ -32,13 +28,15 @@ type TableData = {
function ExperimentComparison() {
const [searchParams] = useSearchParams();
- const comparisonType = searchParams.get('type');
+ const comparisonType = searchParams.get('type') as ComparisonType;
const experimentId = searchParams.get('id');
const [tableData, setTableData] = useState([]);
// const [cacheState, setCacheState] = useCacheState();
// const [total, setTotal] = useState(0);
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
+ const [loading, setLoading] = useState(false);
const { message } = App.useApp();
+ const config = useMemo(() => comparisonConfig[comparisonType], [comparisonType]);
// const [pagination, setPagination] = useState(
// cacheState?.pagination ?? {
// current: 1,
@@ -52,9 +50,11 @@ function ExperimentComparison() {
// 获取对比数据列表
const getComparisonData = async () => {
+ setLoading(true);
const request =
comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq;
const [res] = await to(request(experimentId));
+ setLoading(false);
if (res && res.data) {
// const { content = [], totalElements = 0 } = res.data;
setTableData(res.data);
@@ -91,6 +91,7 @@ function ExperimentComparison() {
// 选择行
const rowSelection: TableProps['rowSelection'] = {
type: 'checkbox',
+ fixed: 'left',
selectedRowKeys,
onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
@@ -108,7 +109,9 @@ function ExperimentComparison() {
title: '实例 ID',
dataIndex: 'experiment_ins_id',
key: 'experiment_ins_id',
- width: '20%',
+ width: 100,
+ fixed: 'left',
+ align: 'center',
render: tableCellRender(),
},
{
@@ -116,43 +119,61 @@ function ExperimentComparison() {
dataIndex: 'start_time',
key: 'start_time',
width: 180,
+ fixed: 'left',
+ align: 'center',
render: tableCellRender(false, dateFormatter),
},
{
title: '运行状态',
dataIndex: 'status',
key: 'status',
- width: '20%',
+ width: 100,
+ fixed: 'left',
+ align: 'center',
render: ExperimentStatusCell,
},
{
- title: '训练数据集',
+ title: `${config.title}数据集`,
dataIndex: 'dataset',
key: 'dataset',
- width: '20%',
+ width: 180,
+ fixed: 'left',
+ align: 'center',
render: tableCellRender(true, arrayFormatter()),
ellipsis: { showTitle: false },
},
],
},
{
- title: '训练参数',
+ title: `${config.title}参数`,
+ align: 'center',
children: first?.params_names.map((name) => ({
- title: name,
+ title: (
+
+ {name}
+
+ ),
dataIndex: ['params', name],
key: name,
- width: '20%',
+ width: 120,
+ align: 'center',
render: tableCellRender(true),
ellipsis: { showTitle: false },
})),
},
{
- title: '训练指标',
+ title: `${config.title}指标`,
+ align: 'center',
children: first?.metrics_names.map((name) => ({
- title: name,
+ title: (
+
+ {name}
+
+ ),
dataIndex: ['metrics', name],
key: name,
- width: '20%',
+ width: 120,
+ align: 'center',
render: tableCellRender(true),
ellipsis: { showTitle: false },
})),
@@ -177,9 +198,10 @@ function ExperimentComparison() {
dataSource={tableData}
columns={columns}
rowSelection={rowSelection}
- scroll={{ y: 'calc(100% - 55px)' }}
+ scroll={{ y: 'calc(100% - 55px)', x: '100%' }}
pagination={false}
bordered={true}
+ loading={loading}
// pagination={{
// ...pagination,
// total: total,
diff --git a/react-ui/src/pages/Experiment/Info/index.jsx b/react-ui/src/pages/Experiment/Info/index.jsx
index db217197..3c1eda2f 100644
--- a/react-ui/src/pages/Experiment/Info/index.jsx
+++ b/react-ui/src/pages/Experiment/Info/index.jsx
@@ -1,76 +1,158 @@
+import { ExperimentStatus } from '@/enums';
import { useStateRef, useVisible } from '@/hooks';
import { getExperimentIns } from '@/services/experiment/index.js';
import { getWorkflowById } from '@/services/pipeline/index.js';
import themes from '@/styles/theme.less';
import { fittingString } from '@/utils';
import { elapsedTime, formatDate } from '@/utils/date';
-import G6 from '@antv/g6';
+import { to } from '@/utils/promise';
+import G6, { Util } from '@antv/g6';
import { Button } from 'antd';
-import { useEffect, useRef } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import ParamsModal from '../components/ViewParamsModal';
import { experimentStatusInfo } from '../status';
import styles from './index.less';
-import Props from './props';
+import ExperimentDrawer from './props';
let graph = null;
function ExperimentText() {
- const [message, setMessage, messageRef] = useStateRef({});
- const propsRef = useRef();
- const navgite = useNavigate();
- const locationParams = useParams(); //新版本获取路由参数接口
- const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false);
+ const [experimentIns, setExperimentIns] = useState(undefined);
+ const [experimentNodeData, setExperimentNodeData, experimentNodeDataRef] = useStateRef(undefined);
const graphRef = useRef();
+ const timerRef = useRef();
+ const workflowRef = useRef();
+ const locationParams = useParams(); // 新版本获取路由参数接口
+ const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false);
+ const [propsDrawerOpen, openPropsDrawer, closePropsDrawer, propsDrawerOpenRef] =
+ useVisible(false);
+ const navigate = useNavigate();
+ const width = 110;
+ const height = 36;
- const getGraphData = (data) => {
- if (graph) {
- // 修改历史数据有蓝色边框的问题
- data.nodes.forEach((item) => {
- item.style.stroke = '#fff';
+ useEffect(() => {
+ initGraph();
+ getWorkflow();
+
+ const changeSize = () => {
+ if (!graph || graph.get('destroyed')) return;
+ if (!graphRef.current) return;
+ graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight);
+ graph.fitView();
+ };
+
+ window.addEventListener('resize', changeSize);
+ return () => {
+ window.removeEventListener('resize', changeSize);
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ }
+ };
+ }, []);
+
+ useEffect(() => {
+ propsDrawerOpenRef.current = propsDrawerOpen;
+ }, [propsDrawerOpen]);
+
+ // 获取流水线模版
+ const getWorkflow = async () => {
+ const [res] = await to(getWorkflowById(locationParams.workflowId));
+ if (res && res.data && res.data.dag) {
+ try {
+ const dag = JSON.parse(res.data.dag);
+ dag.nodes.forEach((item) => {
+ item.in_parameters = JSON.parse(item.in_parameters);
+ item.out_parameters = JSON.parse(item.out_parameters);
+ item.control_strategy = JSON.parse(item.control_strategy);
+ item.imgName = item.img.slice(0, item.img.length - 4);
+ });
+ workflowRef.current = dag;
+ getExperimentInstance(true);
+ } catch (error) {
+ // JSON.parse 错误
+ console.log(error);
+ }
+ }
+ };
+
+ // 获取实验实例
+ const getExperimentInstance = async (first) => {
+ const [res] = await to(getExperimentIns(locationParams.id));
+ if (res && res.data && workflowRef.current) {
+ setExperimentIns(res.data);
+ const { status, nodes_status } = res.data;
+ const workflowData = workflowRef.current;
+ const experimentStatusObjs = JSON.parse(nodes_status);
+ workflowData.nodes.forEach((item) => {
+ const experimentNode = experimentStatusObjs?.[item.id] ?? {};
+ const { finishedAt, startedAt, phase, id } = experimentNode;
+ item.experimentStartTime = startedAt;
+ item.experimentEndTime = finishedAt;
+ item.experimentStatus = phase;
+ item.workflowId = id;
+ item.img = phase ? `${item.imgName}-${phase}.png` : `${item.imgName}.png`;
});
+
+ // 更新打开的抽屉数据
+ if (propsDrawerOpenRef.current && experimentNodeDataRef.current) {
+ const currentId = experimentNodeDataRef.current.id;
+ const node = workflowData.nodes.find((item) => item.id === currentId);
+ if (node) {
+ setExperimentNodeData(node);
+ }
+ }
+
+ getGraphData(workflowData, first);
+
+ // 运行中或者等待中,每5秒获取一次实验实例
+ if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) {
+ timerRef.current = setTimeout(() => {
+ getExperimentInstance(false);
+ }, 5 * 1000);
+ }
+
+ if (first && status === ExperimentStatus.Pending) {
+ const node = workflowData.nodes[0];
+ if (node) {
+ setExperimentNodeData(node);
+ openPropsDrawer();
+ }
+ } else if (first && status === ExperimentStatus.Running) {
+ const node =
+ workflowData.nodes.find((item) => item.experimentStatus === ExperimentStatus.Running) ??
+ workflowData.nodes[0];
+ if (node) {
+ setExperimentNodeData(node);
+ openPropsDrawer();
+ }
+ }
+ }
+ };
+
+ // 根据数据,渲染图
+ const getGraphData = (data, first) => {
+ if (graph) {
+ const zoom = graph.getZoom();
+ // 在拉取新数据重新渲染页面之前先获取点(0, 0)在画布上的位置
+ const lastPoint = graph.getCanvasByPoint(0, 0);
graph.data(data);
graph.render();
+ if (first) {
+ graph.fitView();
+ } else {
+ graph.zoomTo(zoom);
+ // 获取重新渲染之后点(0, 0)在画布的位置
+ const newPoint = graph.getCanvasByPoint(0, 0);
+ // 移动画布相对位移;
+ graph.translate(lastPoint.x - newPoint.x, lastPoint.y - newPoint.y);
+ }
} else {
setTimeout(() => {
getGraphData(data);
}, 500);
}
};
- const getFirstWorkflow = (val) => {
- getWorkflowById(val).then((pipelineRes) => {
- if (graph && pipelineRes.data && pipelineRes.data.dag) {
- getExperimentIns(locationParams.id).then((experimentRes) => {
- if (experimentRes.code === 200) {
- setMessage(experimentRes.data);
- const experimentStatusObjs = JSON.parse(experimentRes.data.nodes_status);
- const newNodeList = JSON.parse(pipelineRes.data.dag).nodes.map((item) => {
- return {
- ...item,
- experimentEndTime: experimentStatusObjs?.[item.id]?.finishedAt,
- experimentStartTime: experimentStatusObjs?.[item.id]?.startedAt,
- experimentStatus: experimentStatusObjs?.[item.id]?.phase,
- component_id: experimentStatusObjs?.[item.id]?.id,
- img: experimentStatusObjs?.[item.id]?.phase
- ? item.img.slice(0, item.img.length - 4) +
- '-' +
- experimentStatusObjs[item.id].phase +
- '.png'
- : item.img,
- };
- });
- const newData = { ...JSON.parse(pipelineRes.data.dag), nodes: newNodeList };
- getGraphData(newData);
- }
- });
- }
- });
- };
-
- useEffect(() => {
- initGraph();
- getFirstWorkflow(locationParams.workflowId);
- }, []);
const initGraph = () => {
G6.registerNode(
@@ -116,6 +198,54 @@ function ExperimentText() {
draggable: true,
});
}
+ const hasRightImg =
+ cfg.experimentStatus === ExperimentStatus.Pending ||
+ cfg.experimentStatus === ExperimentStatus.Running;
+ if (hasRightImg) {
+ const image = group.addShape('image', {
+ attrs: {
+ x: -10,
+ y: -10,
+ width: 20,
+ height: 20,
+ img:
+ cfg.experimentStatus === ExperimentStatus.Pending
+ ? require('@/assets/img/experiment-pending.png')
+ : require('@/assets/img/experiment-running.png'),
+ cursor: 'pointer',
+ },
+ draggable: false,
+ capture: false,
+ });
+
+ if (cfg.experimentStatus === ExperimentStatus.Running) {
+ image.animate(
+ (ratio) => {
+ const toMatrix = Util.transform(
+ [1, 0, 0, 0, 1, 0, 0, 0, 1],
+ [
+ ['r', ratio * Math.PI * 2],
+ ['t', width / 2 - 14 + 10, -height / 2 - 6 + 10],
+ ],
+ );
+ return {
+ matrix: toMatrix,
+ };
+ },
+ {
+ repeat: true, // 动画重复
+ duration: 1000,
+ easing: 'easeLinear',
+ },
+ );
+ } else if (cfg.experimentStatus === ExperimentStatus.Pending) {
+ const toMatrix = Util.transform(
+ [1, 0, 0, 0, 1, 0, 0, 0, 1],
+ [['t', width / 2 - 14 + 10, -height / 2 - 6 + 10]],
+ );
+ image.setMatrix(toMatrix);
+ }
+ }
const bbox = group.getBBox();
const anchorPoints = this.getAnchorPoints(cfg);
anchorPoints.forEach((anchorPos, i) => {
@@ -139,12 +269,12 @@ function ExperimentText() {
// response the state changes and show/hide the link-point circles
setState(name, value, item) {
const group = item.getContainer();
- const shape = group.get('children')[0];
+ const shape = group.get('children')?.[0];
if (name === 'hover') {
if (value) {
- shape.attr('stroke', themes['primaryColor']);
+ shape?.attr('stroke', themes['primaryColor']);
} else {
- shape.attr('stroke', '#fff');
+ shape?.attr('stroke', 'transparent');
}
}
},
@@ -181,7 +311,7 @@ function ExperimentText() {
defaultNode: {
type: 'rect-node',
- size: [110, 36],
+ size: [width, height],
labelCfg: {
style: {
@@ -196,8 +326,14 @@ function ExperimentText() {
},
style: {
fill: '#fff',
- stroke: '#fff',
- radius: 10,
+ stroke: 'transparent',
+ cursor: 'pointer',
+ radius: 8,
+ shadowColor: 'rgba(75, 84, 137, 0.4)',
+ shadowBlur: 6,
+ shadowOffsetX: 0,
+ shadowOffsetY: 0,
+ overflow: 'hidden',
lineWidth: 0.5,
},
},
@@ -224,9 +360,28 @@ function ExperimentText() {
},
},
});
+
+ // 修改历史数据样式问题
+ graph.node((node) => {
+ return {
+ style: {
+ stroke: 'transparent',
+ radius: 8,
+ },
+ };
+ });
+
+ // 绑定事件
+ bindEvents();
+ };
+
+ // 绑定事件
+ const bindEvents = () => {
graph.on('node:click', (e) => {
if (e.target.get('name') !== 'anchor-point' && e.item) {
- propsRef.current.showDrawer(e, locationParams.id, messageRef.current);
+ const model = e.item.getModel();
+ setExperimentNodeData(model);
+ openPropsDrawer();
}
});
graph.on('node:mouseenter', (e) => {
@@ -235,22 +390,17 @@ function ExperimentText() {
graph.on('node:mouseleave', (e) => {
graph.setItemState(e.item, 'hover', false);
});
- window.onresize = () => {
- if (!graph || graph.get('destroyed')) return;
- if (!graphRef.current) return;
- graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight);
- graph.fitView();
- };
};
+
return (
- 启动时间:{formatDate(message.create_time)}
+ 启动时间:{formatDate(experimentIns?.create_time)}
执行时长:
- {elapsedTime(message.create_time, message.finish_time)}
+ {elapsedTime(experimentIns?.create_time, experimentIns?.finish_time)}
状态:
@@ -260,11 +410,11 @@ function ExperimentText() {
height: '8px',
borderRadius: '50%',
marginRight: '6px',
- backgroundColor: experimentStatusInfo[message.status]?.color,
+ backgroundColor: experimentStatusInfo[experimentIns?.status]?.color,
}}
>
-
- {experimentStatusInfo[message.status]?.label}
+
+ {experimentStatusInfo[experimentIns?.status]?.label}
-
+ {experimentNodeData ? (
+
+ ) : null}
);
diff --git a/react-ui/src/pages/Experiment/Info/index.less b/react-ui/src/pages/Experiment/Info/index.less
index f2f5510d..9004ce33 100644
--- a/react-ui/src/pages/Experiment/Info/index.less
+++ b/react-ui/src/pages/Experiment/Info/index.less
@@ -30,4 +30,10 @@
background-image: url(/assets/images/pipeline-canvas-back.png);
background-size: 100% 100%;
}
+
+ :global {
+ .ant-drawer-mask {
+ background: transparent !important;
+ }
+ }
}
diff --git a/react-ui/src/pages/Experiment/Info/props.less b/react-ui/src/pages/Experiment/Info/props.less
index b3294d55..1d5bdc34 100644
--- a/react-ui/src/pages/Experiment/Info/props.less
+++ b/react-ui/src/pages/Experiment/Info/props.less
@@ -14,7 +14,12 @@
border: 1px solid #e0eaff;
}
.ant-tabs-content-holder {
- overflow-y: auto;
+ .ant-tabs-content {
+ height: 100%;
+ .ant-tabs-tabpane {
+ height: 100%;
+ }
+ }
}
}
}
diff --git a/react-ui/src/pages/Experiment/Info/props.tsx b/react-ui/src/pages/Experiment/Info/props.tsx
index 48d72c0d..5940756a 100644
--- a/react-ui/src/pages/Experiment/Info/props.tsx
+++ b/react-ui/src/pages/Experiment/Info/props.tsx
@@ -1,11 +1,9 @@
-import { getNodeResult, getQueryByExperimentLog } from '@/services/experiment/index.js';
+import { ExperimentStatus } from '@/enums';
import { PipelineNodeModelSerialize } from '@/types';
import { elapsedTime, formatDate } from '@/utils/date';
-import { to } from '@/utils/promise';
import { DatabaseOutlined, ProfileOutlined } from '@ant-design/icons';
-import { Drawer, Form, Tabs } from 'antd';
-import dayjs from 'dayjs';
-import { forwardRef, useImperativeHandle, useState } from 'react';
+import { Drawer, Tabs } from 'antd';
+import { forwardRef, useImperativeHandle, useMemo } from 'react';
import ExperimentParameter from '../components/ExperimentParameter';
import ExperimentResult from '../components/ExperimentResult';
import LogList from '../components/LogList';
@@ -19,154 +17,130 @@ export type ExperimentLog = {
start_time?: string; // 日志开始时间
};
-const Props = forwardRef((_, ref) => {
- const [form] = Form.useForm();
- const [experimentNodeData, setExperimentNodeData] = useState(
- {} as PipelineNodeModelSerialize,
- );
- const [experimentResults, setExperimentResults] = useState([]);
- const [experimentLogList, setExperimentLogList] = useState([]);
+type ExperimentDrawerProps = {
+ open: boolean;
+ onClose: () => void;
+ instanceId?: number; // 实验实例 id
+ instanceName?: string; // 实验实例 name
+ instanceNamespace?: string; // 实验实例 namespace
+ instanceNodeData: PipelineNodeModelSerialize; // 节点数据,在定时刷新实验实例状态中不会变化
+ workflowId?: string; // 实验实例工作流 id
+ instanceNodeStatus?: ExperimentStatus; // 在定时刷新实验实例状态中,变化一两次
+ instanceNodeStartTime?: string; // 在定时刷新实验实例状态中,变化一两次
+ instanceNodeEndTime?: string; // 在定时刷新实验实例状态中,会经常变化
+};
- const items = [
- {
- key: '1',
- label: '日志详情',
- children: (
-
- ),
- icon: ,
- },
- {
- key: '2',
- label: '配置参数',
- icon: ,
- children: ,
- },
+const ExperimentDrawer = forwardRef(
+ (
{
- key: '3',
- label: '输出结果',
- children: ,
- icon: ,
- },
- ];
- const [open, setOpen] = useState(false);
- const onClose = () => {
- setOpen(false);
- };
-
- // 获取实验日志
- const getExperimentLog = async (params: any, start_time: number) => {
- const [res] = await to(getQueryByExperimentLog(params));
- if (res && res.data) {
- const { log_type, pods, log_detail } = res.data;
- if (log_type === 'normal') {
- const list = [
- {
- ...log_detail,
- log_type,
- },
- ];
- setExperimentLogList(list);
- } else if (log_type === 'resource') {
- const list = pods.map((v: string) => ({
- log_type,
- pod_name: v,
- log_content: '',
- start_time,
- }));
- setExperimentLogList(list);
- }
- }
- };
-
- // 获取实验结果
- const getExperimentResult = async (params: any) => {
- const [res] = await to(getNodeResult(params));
- if (res && res.data) {
- setExperimentResults(res.data);
- }
- };
+ open,
+ onClose,
+ instanceId,
+ instanceName,
+ instanceNamespace,
+ instanceNodeData,
+ workflowId,
+ instanceNodeStatus,
+ instanceNodeStartTime,
+ instanceNodeEndTime,
+ }: ExperimentDrawerProps,
+ ref,
+ ) => {
+ useImperativeHandle(ref, () => ({}));
- useImperativeHandle(ref, () => ({
- showDrawer(e: any, id: string, message: any) {
- setOpen(true);
+ // 如果性能有问题,可以进一步拆解
+ const items = useMemo(
+ () => [
+ {
+ key: '1',
+ label: '日志详情',
+ children: (
+
+ ),
+ icon: ,
+ },
+ {
+ key: '2',
+ label: '配置参数',
+ icon: ,
+ children: ,
+ },
+ {
+ key: '3',
+ label: '输出结果',
+ children: (
+
+ ),
+ icon: ,
+ },
+ ],
+ [
+ instanceNodeData,
+ instanceId,
+ instanceName,
+ instanceNamespace,
+ instanceNodeStatus,
+ workflowId,
+ instanceNodeStartTime,
+ ],
+ );
- // 获取实验参数
- const model = e.item.getModel();
- try {
- const nodeData = {
- ...model,
- in_parameters: JSON.parse(model.in_parameters),
- out_parameters: JSON.parse(model.out_parameters),
- control_strategy: JSON.parse(model.control_strategy),
- };
- setExperimentNodeData(nodeData);
- form.setFieldsValue(nodeData);
- } catch (error) {
- console.log(error);
- }
-
- // 获取实验日志和实验结果
- setExperimentLogList([]);
- setExperimentResults([]);
- // 如果已经运行到了
- if (e.item?.getModel()?.component_id) {
- const model = e.item.getModel();
- const start_time = dayjs(model.experimentStartTime).valueOf() * 1.0e6;
- const params = {
- task_id: model.id,
- component_id: model.component_id,
- name: message.argo_ins_name,
- namespace: message.argo_ins_ns,
- start_time: start_time,
- };
- getExperimentLog(params, start_time);
- getExperimentResult({ id, node_id: model.id });
- }
- },
- }));
- return (
-
-
-
- 任务名称:{experimentNodeData.label}
-
-
- 执行状态:
-
-
- {experimentStatusInfo[experimentNodeData.experimentStatus]?.label}
-
-
-
- 启动时间:{formatDate(experimentNodeData.experimentStartTime)}
-
-
- 耗时:
- {elapsedTime(
- experimentNodeData.experimentStartTime,
- experimentNodeData.experimentEndTime,
- )}
+ return (
+
+
+
+ 任务名称:{instanceNodeData.label}
+
+
+ 执行状态:
+ {instanceNodeStatus ? (
+ <>
+
+
+ {experimentStatusInfo[instanceNodeStatus]?.label}
+
+ >
+ ) : (
+ '--'
+ )}
+
+
+ 启动时间:{formatDate(instanceNodeStartTime)}
+
+
+ 耗时:
+ {elapsedTime(instanceNodeStartTime, instanceNodeEndTime)}
+
-
-
-
- );
-});
+
+
+ );
+ },
+);
-export default Props;
+export default ExperimentDrawer;
diff --git a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.less b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.less
new file mode 100644
index 00000000..650b3153
--- /dev/null
+++ b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.less
@@ -0,0 +1,69 @@
+.tableExpandBox {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ padding: 0 0 0 33px;
+ color: @text-color;
+ font-size: 15px;
+
+ & > div {
+ padding: 0 16px;
+ }
+
+ .index {
+ width: calc((100% + 32px + 33px) / 6.25);
+ }
+
+ .tensorBoard {
+ width: calc((100% + 32px + 33px) / 6.25);
+ }
+
+ .description {
+ display: flex;
+ flex: 1;
+ align-items: center;
+
+ .startTime {
+ .singleLine();
+ }
+ }
+
+ .status {
+ width: 200px;
+ }
+
+ .operation {
+ width: 334px;
+ }
+}
+
+.tableExpandBoxContent {
+ height: 45px;
+ background-color: #fff;
+ border: 1px solid #eaeaea;
+
+ & + & {
+ border-top: none;
+ }
+
+ .statusBox {
+ display: flex;
+ align-items: center;
+ width: 200px;
+
+ .statusIcon {
+ visibility: hidden;
+ transition: all 0.2s;
+ }
+ }
+ .statusBox:hover .statusIcon {
+ visibility: visible;
+ }
+}
+
+.loadMoreBox {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 16px auto 0;
+}
diff --git a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx
new file mode 100644
index 00000000..1fb3441e
--- /dev/null
+++ b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx
@@ -0,0 +1,178 @@
+import KFIcon from '@/components/KFIcon';
+import { ExperimentStatus } from '@/enums';
+import { experimentStatusInfo } from '@/pages/Experiment/status';
+import {
+ deleteQueryByExperimentInsId,
+ putQueryByExperimentInsId,
+} from '@/services/experiment/index.js';
+import themes from '@/styles/theme.less';
+import { type ExperimentInstance } from '@/types';
+import { elapsedTime, formatDate } from '@/utils/date';
+import { to } from '@/utils/promise';
+import { modalConfirm } from '@/utils/ui';
+import { DoubleRightOutlined } from '@ant-design/icons';
+import { App, Button, ConfigProvider, Tooltip } from 'antd';
+import classNames from 'classnames';
+import TensorBoardStatusCell from '../TensorBoardStatus';
+import styles from './index.less';
+
+type ExperimentInstanceProps = {
+ experimentInList?: ExperimentInstance[];
+ experimentInsTotal: number;
+ onClickInstance?: (instance: ExperimentInstance) => void;
+ onClickTensorBoard?: (instance: ExperimentInstance) => void;
+ onRemove?: () => void;
+ onTerminate?: (instance: ExperimentInstance) => void;
+ onLoadMore?: () => void;
+};
+
+function ExperimentInstanceComponent({
+ experimentInList,
+ experimentInsTotal,
+ onClickInstance,
+ onClickTensorBoard,
+ onRemove,
+ onTerminate,
+ onLoadMore,
+}: ExperimentInstanceProps) {
+ const { message } = App.useApp();
+
+ // 删除实验实例确认
+ const handleRemove = (instance: ExperimentInstance) => {
+ modalConfirm({
+ title: '确定删除该条实例吗?',
+ onOk: () => {
+ deleteExperimentInstance(instance.id);
+ },
+ });
+ };
+
+ // 删除实验实例
+ const deleteExperimentInstance = async (id: number) => {
+ const [res] = await to(deleteQueryByExperimentInsId(id));
+ if (res) {
+ message.success('删除成功');
+ onRemove?.();
+ }
+ };
+
+ // 终止实验实例
+ const terminateExperimentInstance = async (instance: ExperimentInstance) => {
+ const [res] = await to(putQueryByExperimentInsId(instance.id));
+ if (res) {
+ message.success('终止成功');
+ onTerminate?.(instance);
+ }
+ };
+
+ if (!experimentInList || experimentInList.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+
+ {experimentInList.map((item, index) => (
+
+
onClickInstance?.(item)}
+ >
+ {index + 1}
+
+
+ {item.nodes_result?.tensorboard_log ? (
+ onClickTensorBoard?.(item)}
+ >
+ ) : (
+ '--'
+ )}
+
+
+
{elapsedTime(item.create_time, item.finish_time)}
+
+
+ {formatDate(item.create_time)}
+
+
+
+
+

+
+ {experimentStatusInfo[item.status as ExperimentStatus]?.label}
+
+
+
+ }
+ onClick={() => terminateExperimentInstance(item)}
+ >
+ 终止
+
+
+ }
+ onClick={() => handleRemove(item)}
+ >
+ 删除
+
+
+
+
+ ))}
+ {experimentInsTotal > experimentInList.length ? (
+
+
+
+ ) : null}
+
+ );
+}
+
+export default ExperimentInstanceComponent;
diff --git a/react-ui/src/pages/Experiment/components/ExperimentParameter/index.less b/react-ui/src/pages/Experiment/components/ExperimentParameter/index.less
index 44b590ea..c5d9824e 100644
--- a/react-ui/src/pages/Experiment/components/ExperimentParameter/index.less
+++ b/react-ui/src/pages/Experiment/components/ExperimentParameter/index.less
@@ -1,5 +1,7 @@
.experiment-parameter {
+ height: 100%;
padding-top: 8px;
+ overflow-y: auto;
&__title {
display: flex;
diff --git a/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx
index 62be954f..eb516935 100644
--- a/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx
+++ b/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx
@@ -3,16 +3,15 @@ import ParameterSelect from '@/components/ParameterSelect';
import SubAreaTitle from '@/components/SubAreaTitle';
import { useComputingResource } from '@/hooks/resource';
import { PipelineNodeModelSerialize } from '@/types';
-import { Form, Input, Select, type FormProps } from 'antd';
+import { Form, Input, Select } from 'antd';
import styles from './index.less';
const { TextArea } = Input;
type ExperimentParameterProps = {
- form: FormProps['form'];
nodeData: PipelineNodeModelSerialize;
};
-function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) {
+function ExperimentParameter({ nodeData }: ExperimentParameterProps) {
const [resourceStandardList] = useComputingResource(); // 资源规模
// 控制策略
@@ -42,7 +41,7 @@ function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) {
wrapperCol={{
span: 24,
}}
- form={form}
+ initialValues={nodeData}
style={{
maxWidth: 600,
}}
diff --git a/react-ui/src/pages/Experiment/components/ExperimentResult/index.less b/react-ui/src/pages/Experiment/components/ExperimentResult/index.less
index 078fe4f2..78684d72 100644
--- a/react-ui/src/pages/Experiment/components/ExperimentResult/index.less
+++ b/react-ui/src/pages/Experiment/components/ExperimentResult/index.less
@@ -1,5 +1,7 @@
.experiment-result {
+ height: 100%;
padding: 8px;
+ overflow-y: auto;
color: @text-color;
font-size: 14px;
diff --git a/react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx
index e3cbd4da..feabcc3d 100644
--- a/react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx
+++ b/react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx
@@ -1,11 +1,15 @@
+import { getNodeResult } from '@/services/experiment/index.js';
import { downLoadZip } from '@/utils/downloadfile';
import { openAntdModal } from '@/utils/modal';
+import { to } from '@/utils/promise';
import { App, Button } from 'antd';
+import { useEffect, useState } from 'react';
import ExportModelModal from '../ExportModelModal';
import styles from './index.less';
type ExperimentResultProps = {
- results?: ExperimentResultData[] | null;
+ experimentInsId?: number; // 实验实例 id
+ pipelineNodeId?: string; // 流水线节点 id
};
type ExperimentResultData = {
@@ -18,8 +22,21 @@ type ExperimentResultData = {
}[];
};
-function ExperimentResult({ results }: ExperimentResultProps) {
+function ExperimentResult({ experimentInsId, pipelineNodeId }: ExperimentResultProps) {
const { message } = App.useApp();
+ const [experimentResults, setExperimentResults] = useState
([]);
+
+ useEffect(() => {
+ getExperimentResult({ id: `${experimentInsId}`, node_id: pipelineNodeId });
+ }, []);
+
+ // 获取实验结果
+ const getExperimentResult = async (params: any) => {
+ const [res] = await to(getNodeResult(params));
+ if (res && res.data) {
+ setExperimentResults(res.data);
+ }
+ };
// 下载
const download = (path: string) => {
@@ -40,9 +57,9 @@ function ExperimentResult({ results }: ExperimentResultProps) {
return (
- {results && results.length > 0 ? (
- results.map((item) => (
-
+ {experimentResults.length > 0 ? (
+ experimentResults.map((item) => (
+
{item.name}