From c1d552ccaf06a4802ea2fbe6663a903d27607dbc Mon Sep 17 00:00:00 2001 From: Rouni Date: Fri, 15 Mar 2024 15:13:42 +0800 Subject: [PATCH 1/3] repo-sync-2024-03-15T15:13:21+0800 --- .github/workflows/ci.yml | 2 +- apps/platform/config/routes.ts | 5 + .../src/components/datatable-preview.tsx | 5 + .../custom-render/case-when-render/index.tsx | 1 + .../config-item-render/custom-render/utils.ts | 1 + .../defalt-col-selection-template.tsx | 5 + .../multi-field-select-modal.tsx | 2 + .../default-feature-selection/select-tree.tsx | 1 + .../quick-config-drawer.tsx | 2 +- .../quick-config-psi.tsx | 72 ++- .../src/modules/dag-log/dag-log.service.ts | 42 +- apps/platform/src/modules/dag-log/index.less | 1 + .../src/modules/dag-modal-manager/index.ts | 14 + .../modal-manger-protocol.ts | 4 +- .../modules/dag-model-submission/index.less | 71 +++ .../src/modules/dag-model-submission/index.ts | 2 + .../pipeline-title-view.tsx | 11 + .../preview-submit-node/index.less | 45 ++ .../preview-submit-node/index.tsx | 42 ++ .../submission-drawer.tsx | 447 ++++++++++++++++++ .../submission-service.ts | 103 ++++ .../useFormValidateOnly.tsx | 21 + apps/platform/src/modules/dag-submit/dag.ts | 17 + .../modules/dag-submit/graph-data-service.ts | 7 + .../modules/dag-submit/graph-hook-service.ts | 107 +++++ .../dag-submit/graph-request-service.ts | 101 ++++ .../src/modules/dag-submit/graph-service.ts | 249 ++++++++++ .../platform/src/modules/dag-submit/graph.tsx | 72 +++ .../src/modules/dag-submit/index.less | 166 +++++++ .../src/modules/dag-submit/toolbutton.tsx | 234 +++++++++ apps/platform/src/modules/dag-submit/util.ts | 267 +++++++++++ .../data-manager/data-manager.service.ts | 7 + .../data-manager/data-manager.view.tsx | 132 ++++-- .../add-http-data/http-data-add.view.tsx | 422 +++++++++++++++++ .../data-table-add/add-http-data/index.less | 93 ++++ .../data-table-auth/data-tabel-auth.view.tsx | 2 + .../project-auth-config/index.tsx | 3 + .../data-table-info/data-table-info.view.tsx | 16 +- .../data-table-tree/datatable-tree.view.tsx | 1 + .../src/modules/layout/dag-layout/index.tsx | 194 +++++--- .../project-edit.service.tsx | 5 + .../layout/model-submission-layout/index.less | 101 ++++ .../layout/model-submission-layout/index.tsx | 145 ++++++ .../main-dag/graph-request-service.tsx | 16 +- .../main-dag/model-submission-entry.tsx | 82 ++++ .../src/modules/model-manager/actions.tsx | 11 + .../src/modules/model-manager/index.less | 31 ++ .../src/modules/model-manager/index.tsx | 352 ++++++++++++++ .../model-manager/model-detail/index.less | 20 + .../model-detail/model-detail.view.tsx | 78 +++ .../model-manager/model-info/index.less | 51 ++ .../model-info/model-info.view.tsx | 122 +++++ .../model-manager/model-release/common.tsx | 219 +++++++++ .../model-manager/model-release/index.less | 142 ++++++ .../model-release/model-release.service.ts | 92 ++++ .../model-release/model-release.view.tsx | 409 ++++++++++++++++ .../modules/model-manager/model-service.ts | 192 ++++++++ .../src/modules/model-manager/types.ts | 27 ++ .../p2p-project-list/components/common.tsx | 2 +- apps/platform/src/modules/pipeline/index.less | 2 +- .../src/modules/pipeline/pipeline-service.ts | 1 + .../templates/pipeline-template-psi-guide.ts | 8 +- .../templates/pipeline-template-psi.ts | 18 +- .../templates/pipeline-template-risk-guide.ts | 8 +- .../templates/pipeline-template-risk.ts | 8 +- .../result-details/graph-data-service.ts | 12 +- .../modules/result-details/graph-manager.ts | 2 +- .../src/modules/result-details/graph.tsx | 31 +- apps/platform/src/pages/model-submission.tsx | 7 + .../src/services/secretpad/AuthController.ts | 3 - .../secretpad/FeatureDatasourceController.ts | 54 +++ .../src/services/secretpad/IndexController.ts | 10 + .../secretpad/ModelExportController.ts | 54 +++ .../secretpad/ModelManagementController.ts | 132 ++++++ apps/platform/src/services/secretpad/index.ts | 10 +- .../src/services/secretpad/typings.d.ts | 259 +++++++++- packages/dag/src/actions/render.ts | 8 + packages/dag/src/types/index.ts | 3 + 78 files changed, 5566 insertions(+), 150 deletions(-) create mode 100644 apps/platform/src/modules/dag-model-submission/index.less create mode 100644 apps/platform/src/modules/dag-model-submission/index.ts create mode 100644 apps/platform/src/modules/dag-model-submission/pipeline-title-view.tsx create mode 100644 apps/platform/src/modules/dag-model-submission/preview-submit-node/index.less create mode 100644 apps/platform/src/modules/dag-model-submission/preview-submit-node/index.tsx create mode 100644 apps/platform/src/modules/dag-model-submission/submission-drawer.tsx create mode 100644 apps/platform/src/modules/dag-model-submission/submission-service.ts create mode 100644 apps/platform/src/modules/dag-model-submission/useFormValidateOnly.tsx create mode 100644 apps/platform/src/modules/dag-submit/dag.ts create mode 100644 apps/platform/src/modules/dag-submit/graph-data-service.ts create mode 100644 apps/platform/src/modules/dag-submit/graph-hook-service.ts create mode 100644 apps/platform/src/modules/dag-submit/graph-request-service.ts create mode 100644 apps/platform/src/modules/dag-submit/graph-service.ts create mode 100644 apps/platform/src/modules/dag-submit/graph.tsx create mode 100644 apps/platform/src/modules/dag-submit/index.less create mode 100644 apps/platform/src/modules/dag-submit/toolbutton.tsx create mode 100644 apps/platform/src/modules/dag-submit/util.ts create mode 100644 apps/platform/src/modules/data-table-add/add-http-data/http-data-add.view.tsx create mode 100644 apps/platform/src/modules/data-table-add/add-http-data/index.less create mode 100644 apps/platform/src/modules/layout/model-submission-layout/index.less create mode 100644 apps/platform/src/modules/layout/model-submission-layout/index.tsx create mode 100644 apps/platform/src/modules/main-dag/model-submission-entry.tsx create mode 100644 apps/platform/src/modules/model-manager/actions.tsx create mode 100644 apps/platform/src/modules/model-manager/index.less create mode 100644 apps/platform/src/modules/model-manager/index.tsx create mode 100644 apps/platform/src/modules/model-manager/model-detail/index.less create mode 100644 apps/platform/src/modules/model-manager/model-detail/model-detail.view.tsx create mode 100644 apps/platform/src/modules/model-manager/model-info/index.less create mode 100644 apps/platform/src/modules/model-manager/model-info/model-info.view.tsx create mode 100644 apps/platform/src/modules/model-manager/model-release/common.tsx create mode 100644 apps/platform/src/modules/model-manager/model-release/index.less create mode 100644 apps/platform/src/modules/model-manager/model-release/model-release.service.ts create mode 100644 apps/platform/src/modules/model-manager/model-release/model-release.view.tsx create mode 100644 apps/platform/src/modules/model-manager/model-service.ts create mode 100644 apps/platform/src/modules/model-manager/types.ts create mode 100644 apps/platform/src/pages/model-submission.tsx create mode 100644 apps/platform/src/services/secretpad/FeatureDatasourceController.ts create mode 100644 apps/platform/src/services/secretpad/ModelExportController.ts create mode 100644 apps/platform/src/services/secretpad/ModelManagementController.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e378a3..a760f8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: - name: Use pnpm uses: pnpm/action-setup@v2 with: - version: ^8.8.0 + version: ^8.8 run_install: false - name: Get pnpm store directory diff --git a/apps/platform/config/routes.ts b/apps/platform/config/routes.ts index d4fab03..06e7060 100644 --- a/apps/platform/config/routes.ts +++ b/apps/platform/config/routes.ts @@ -16,6 +16,11 @@ export const routes = [ component: 'record', wrappers: ['@/wrappers/p2p-center-auth', '@/wrappers/component-wrapper'], }, + { + path: '/model-submission', + component: 'model-submission', + wrappers: ['@/wrappers/p2p-center-auth', '@/wrappers/component-wrapper'], + }, { path: '/node', component: 'new-node', diff --git a/apps/platform/src/components/datatable-preview.tsx b/apps/platform/src/components/datatable-preview.tsx index d1e2400..179ec4c 100644 --- a/apps/platform/src/components/datatable-preview.tsx +++ b/apps/platform/src/components/datatable-preview.tsx @@ -48,6 +48,11 @@ export const DatatablePreview = (props: DatatablePreviewInterface) => { nodeId, datatableId, projectId: projectId as string, + /** + * 需求: 数据管理列表不仅有csv数据,还有http数据,画布上只会用到 csv 类型 + * 所以除数据管理模块,其他用到的这个接口都需要加一个type: CSV 类型用来区分数据源类型,用于服务端做代码兼容 + */ + type: 'CSV', }); setLoading(false); if (data) { diff --git a/apps/platform/src/modules/component-config/config-item-render/custom-render/case-when-render/index.tsx b/apps/platform/src/modules/component-config/config-item-render/custom-render/case-when-render/index.tsx index 5bdb486..edf3c27 100644 --- a/apps/platform/src/modules/component-config/config-item-render/custom-render/case-when-render/index.tsx +++ b/apps/platform/src/modules/component-config/config-item-render/custom-render/case-when-render/index.tsx @@ -226,6 +226,7 @@ export const CaseWhenRender = (prop: { node: AtomicConfigNode }) => { datatableId, nodeId, projectId, + type: 'CSV', }); if (!data) return; const { configs } = data; diff --git a/apps/platform/src/modules/component-config/config-item-render/custom-render/utils.ts b/apps/platform/src/modules/component-config/config-item-render/custom-render/utils.ts index c349e53..e5f23a9 100644 --- a/apps/platform/src/modules/component-config/config-item-render/custom-render/utils.ts +++ b/apps/platform/src/modules/component-config/config-item-render/custom-render/utils.ts @@ -54,6 +54,7 @@ export const useCols = ( datatableId, nodeId, projectId, + type: 'CSV', }); if (!data) return; const { configs } = data; diff --git a/apps/platform/src/modules/component-config/config-item-render/defalt-col-selection-template.tsx b/apps/platform/src/modules/component-config/config-item-render/defalt-col-selection-template.tsx index 727fce7..b0dcdee 100644 --- a/apps/platform/src/modules/component-config/config-item-render/defalt-col-selection-template.tsx +++ b/apps/platform/src/modules/component-config/config-item-render/defalt-col-selection-template.tsx @@ -143,6 +143,11 @@ export const DefaultColSelection: React.FC> = (config) => { datatableId, nodeId, projectId, + /** + * 需求: 数据管理列表不仅有csv数据,还有http数据,画布上只会用到 csv 类型 + * 所以除数据管理模块,其他用到的这个接口都需要加一个type: CSV 类型用来区分数据源类型,用于服务端做代码兼容 + */ + type: 'CSV', }); if (!data) return; const { configs } = data; diff --git a/apps/platform/src/modules/component-config/config-item-render/default-feature-selection/multi-field-select-modal.tsx b/apps/platform/src/modules/component-config/config-item-render/default-feature-selection/multi-field-select-modal.tsx index add6335..6f2e650 100644 --- a/apps/platform/src/modules/component-config/config-item-render/default-feature-selection/multi-field-select-modal.tsx +++ b/apps/platform/src/modules/component-config/config-item-render/default-feature-selection/multi-field-select-modal.tsx @@ -43,6 +43,7 @@ export const MultiFieldSelectModal = (props: IProps) => { nodeId: selectedTable.nodeId, datatableId: selectedTable.datatableId, projectId, + type: 'CSV', }); if (!data) return; setSelectedFields(data.configs); @@ -64,6 +65,7 @@ export const MultiFieldSelectModal = (props: IProps) => { nodeId: table.nodeId, datatableId: table.datatableId, projectId, + type: 'CSV', }); if (!data) return; configs.push(data.configs); diff --git a/apps/platform/src/modules/component-config/config-item-render/default-feature-selection/select-tree.tsx b/apps/platform/src/modules/component-config/config-item-render/default-feature-selection/select-tree.tsx index ca915d7..5ce1a84 100644 --- a/apps/platform/src/modules/component-config/config-item-render/default-feature-selection/select-tree.tsx +++ b/apps/platform/src/modules/component-config/config-item-render/default-feature-selection/select-tree.tsx @@ -112,6 +112,7 @@ export const SelectTree = ({ datatableId, nodeId, projectId, + type: 'CSV', }); if (!data) return; const { configs } = data; diff --git a/apps/platform/src/modules/component-config/template-quick-config/quick-config-drawer.tsx b/apps/platform/src/modules/component-config/template-quick-config/quick-config-drawer.tsx index c8061d3..d0fcfa3 100644 --- a/apps/platform/src/modules/component-config/template-quick-config/quick-config-drawer.tsx +++ b/apps/platform/src/modules/component-config/template-quick-config/quick-config-drawer.tsx @@ -73,7 +73,7 @@ export const QuickConfigModal = () => { }} > {type === PipelineTemplateType.RISK && } - {type === PipelineTemplateType.PSI && } + {type === PipelineTemplateType.PSI && } {type === PipelineTemplateType.PSI_TEE && } {type === PipelineTemplateType.TEE && } diff --git a/apps/platform/src/modules/component-config/template-quick-config/quick-config-psi.tsx b/apps/platform/src/modules/component-config/template-quick-config/quick-config-psi.tsx index f825a8c..84dc56f 100644 --- a/apps/platform/src/modules/component-config/template-quick-config/quick-config-psi.tsx +++ b/apps/platform/src/modules/component-config/template-quick-config/quick-config-psi.tsx @@ -5,6 +5,7 @@ import { parse } from 'query-string'; import type { Dispatch, SetStateAction } from 'react'; import { useState, useEffect } from 'react'; import { useLocation } from 'umi'; +import { MultiTableFeatureSelection } from '../config-item-render/default-feature-selection/table-feature-selection'; import { getProject, @@ -23,10 +24,11 @@ type QuickConfigPSIComponentProps = { })[]; form: FormInstance; + type?: 'MPC' | 'TEE'; }; export const QuickConfigPSIComponent = (props: QuickConfigPSIComponentProps) => { - const { tables, tableList, form } = props; + const { tables, tableList, form, type = 'TEE' } = props; const { s: selectedReceiver } = Form.useWatch('dataTableReceiver', form) || {}; const { s: selectedSender } = Form.useWatch('dataTableSender', form) || {}; @@ -37,6 +39,15 @@ export const QuickConfigPSIComponent = (props: QuickConfigPSIComponentProps) => const { search } = useLocation(); const { projectId } = parse(search) as { projectId: string }; + const [selectedTableInfo, setSelectedTableInfo] = useState< + { + datatableId: string; + datatableName: string; + nodeId: string; + nodeName: string; + }[] + >([]); + const getCols = async ( selectedTable: string, callback: Dispatch>, @@ -47,6 +58,7 @@ export const QuickConfigPSIComponent = (props: QuickConfigPSIComponentProps) => projectId, nodeId: table.nodeId, datatableId: table.datatableId, + type: 'CSV', }); if (!tableConfig) return; const { configs } = tableConfig; @@ -61,6 +73,19 @@ export const QuickConfigPSIComponent = (props: QuickConfigPSIComponentProps) => } }; + useEffect(() => { + const tableSelected = tableList.filter( + (d) => d.datatableId === selectedReceiver || d.datatableId === selectedSender, + ) as { + datatableId: string; + datatableName: string; + nodeId: string; + nodeName: string; + }[]; + + if (tableSelected.length > 1) setSelectedTableInfo(tableSelected); + }, [selectedReceiver, selectedSender, projectId]); + useEffect(() => { getCols(selectedReceiver, setReceiverCols); }, [selectedReceiver, projectId]); @@ -253,11 +278,45 @@ export const QuickConfigPSIComponent = (props: QuickConfigPSIComponentProps) => )) } + + {type === 'MPC' && ( + 选择特征} + required + messageVariables={{ msg: '请选择特征列' }} + rules={[ + { + required: true, + message: '${msg}', + validator: (_, val) => { + if (!val || val.length === 0) + return Promise.reject(new Error('${msg}')); + + return Promise.resolve(); + }, + }, + ]} + getValueProps={(value) => { + return { value: value?.ss }; + }} + getValueFromEvent={(value) => { + return { ss: value }; + }} + > + + + )} ); }; -export const QuickConfigPSI = () => { +export const QuickConfigPSI = (props: { type?: 'MPC' | 'TEE' }) => { + const { type = 'TEE' } = props; const form = Form.useFormInstance(); const [tables, setTables] = useState< { datatableId: string; nodeName: string; datatableName: string }[] @@ -310,5 +369,12 @@ export const QuickConfigPSI = () => { getTables(); }, [projectId]); - return ; + return ( + + ); }; diff --git a/apps/platform/src/modules/dag-log/dag-log.service.ts b/apps/platform/src/modules/dag-log/dag-log.service.ts index 30bb2f6..ea63eef 100644 --- a/apps/platform/src/modules/dag-log/dag-log.service.ts +++ b/apps/platform/src/modules/dag-log/dag-log.service.ts @@ -1,10 +1,9 @@ import type { GraphNode } from '@secretflow/dag'; - -import { componentConfigDrawer } from '@/modules/component-config/config-modal'; -import { DefaultModalManager } from '@/modules/dag-modal-manager'; -import { ModalWidth } from '@/modules/dag-modal-manager/modal-manger-protocol'; -import { resultDrawer } from '@/modules/dag-result/result-modal'; -import { RecordListDrawerItem } from '@/modules/pipeline-record-list/record-list-drawer-view'; +import { + DefaultModalManager, + ModalsEnum, + ModalsWidth, +} from '@/modules/dag-modal-manager'; import { getGraphNodeLogs } from '@/services/secretpad/GraphController'; import { getJobLog } from '@/services/secretpad/ProjectController'; import { Model, getModel } from '@/util/valtio-helper'; @@ -91,35 +90,42 @@ export class DagLogService extends Model { super(); this.modalManager.onModalsChanged((modals: any) => { if ( - modals[componentConfigDrawer.id].visible && - modals[RecordListDrawerItem.id]?.visible + modals[ModalsEnum.ComponentConfigDrawer].visible && + modals[ModalsEnum.RecordListDrawer]?.visible ) { return this.setLogRightAllConfigWidth({ componentConfigWidth: - ModalWidth[componentConfigDrawer.id] + ModalWidth[RecordListDrawerItem.id], + ModalsWidth[ModalsEnum.ComponentConfigDrawer] + + ModalsWidth[ModalsEnum.RecordListDrawer], }); } - if (modals[resultDrawer.id].visible) { + if (modals[ModalsEnum.ResultDrawer].visible) { return this.setLogRightAllConfigWidth({ - componentResultWidth: ModalWidth[resultDrawer.id], + componentResultWidth: ModalsWidth[ModalsEnum.ResultDrawer], }); } - if (modals[RecordListDrawerItem.id]?.visible) { + if (modals[ModalsEnum.RecordListDrawer]?.visible) { + return this.setLogRightAllConfigWidth({ + componentResultWidth: ModalsWidth[ModalsEnum.RecordListDrawer], + }); + } + + if (modals[ModalsEnum.ComponentConfigDrawer].visible) { return this.setLogRightAllConfigWidth({ - componentResultWidth: ModalWidth[RecordListDrawerItem.id], + componentConfigWidth: ModalsWidth[ModalsEnum.ComponentConfigDrawer], }); } - if (modals[componentConfigDrawer.id].visible) { + if (modals[ModalsEnum.ModelSubmissionDrawer].visible) { return this.setLogRightAllConfigWidth({ - componentConfigWidth: ModalWidth[componentConfigDrawer.id], + componentConfigWidth: ModalsWidth[ModalsEnum.ModelSubmissionDrawer], }); } if ( - !modals[componentConfigDrawer.id].visible && - !modals[resultDrawer.id].visible && - !modals[RecordListDrawerItem.id]?.visible + !modals[ModalsEnum.ComponentConfigDrawer].visible && + !modals[ModalsEnum.ResultDrawer].visible && + !modals[ModalsEnum.RecordListDrawer]?.visible ) { return this.setLogRightAllConfigWidth({ componentConfigWidth: 0 }); } diff --git a/apps/platform/src/modules/dag-log/index.less b/apps/platform/src/modules/dag-log/index.less index e43ffd1..2182671 100644 --- a/apps/platform/src/modules/dag-log/index.less +++ b/apps/platform/src/modules/dag-log/index.less @@ -9,6 +9,7 @@ .logDrawerRoot { position: absolute; + overflow: hidden; } .activeTab { diff --git a/apps/platform/src/modules/dag-modal-manager/index.ts b/apps/platform/src/modules/dag-modal-manager/index.ts index a9e909b..1ef07e4 100644 --- a/apps/platform/src/modules/dag-modal-manager/index.ts +++ b/apps/platform/src/modules/dag-modal-manager/index.ts @@ -56,3 +56,17 @@ export class DefaultModalManager extends Model implements ModalManager { } }; } + +export enum ModalsEnum { + ComponentConfigDrawer = 'component-config', + RecordListDrawer = 'RecordListDrawer', + ResultDrawer = 'component-result', + ModelSubmissionDrawer = 'ModelSubmissionDrawer', +} + +export const ModalsWidth = { + [ModalsEnum.ComponentConfigDrawer]: 300, + [ModalsEnum.RecordListDrawer]: 320, + [ModalsEnum.ResultDrawer]: 600, + [ModalsEnum.ModelSubmissionDrawer]: 560, +}; diff --git a/apps/platform/src/modules/dag-modal-manager/modal-manger-protocol.ts b/apps/platform/src/modules/dag-modal-manager/modal-manger-protocol.ts index 8250009..77d2344 100644 --- a/apps/platform/src/modules/dag-modal-manager/modal-manger-protocol.ts +++ b/apps/platform/src/modules/dag-modal-manager/modal-manger-protocol.ts @@ -1,6 +1,7 @@ import type { Emitter } from '@secretflow/utils'; import { componentConfigDrawer } from '@/modules/component-config/config-modal'; +import { ModelSubmissionDrawerItem } from '@/modules/dag-model-submission/submission-drawer'; import { resultDrawer } from '@/modules/dag-result/result-modal'; import { RecordListDrawerItem } from '@/modules/pipeline-record-list/record-list-drawer-view'; @@ -23,7 +24,8 @@ export interface ModalManager { } export const ModalWidth = { - [componentConfigDrawer.id]: 300, + [componentConfigDrawer?.id]: 300, [RecordListDrawerItem.id]: 320, [resultDrawer.id]: 600, + [ModelSubmissionDrawerItem.id]: 560, }; diff --git a/apps/platform/src/modules/dag-model-submission/index.less b/apps/platform/src/modules/dag-model-submission/index.less new file mode 100644 index 0000000..908a5b3 --- /dev/null +++ b/apps/platform/src/modules/dag-model-submission/index.less @@ -0,0 +1,71 @@ +.submissionDrawer { + position: absolute; + + &:focus { + outline: none; + } + + :global { + .ant-drawer-content-wrapper { + box-shadow: 0 1px 4px 0 rgb(0 0 0 / 15%); + } + + .ant-drawer-body { + padding: 16px; + } + + .ant-drawer-header { + padding: 13px 16px; + border-bottom-color: transparent; + } + + .ant-form-item { + margin-bottom: 16px; + } + } + + .title { + color: rgb(0 0 0 / 85%); + font-size: 14px; + } + + .extra { + color: rgb(0 0 0 / 88%); + font-size: 12px; + } + + .formLabel { + color: rgb(0 0 0 / 85%); + font-size: 14px; + font-weight: 500; + line-height: 22px; + } + + .itemBlock { + padding: 16px 12px; + border-radius: 6px; + background: rgb(0 10 26 / 2%); + } + + .itemRowLabel { + margin-bottom: 6px; + } + + .itemRow { + display: flex; + flex-wrap: wrap; + } + + .itemMargin { + margin-top: 16px; + } + + .canvas { + display: flex; + min-height: 168px; + align-items: center; + justify-content: center; + border-radius: 6px; + background: rgb(0 10 26 / 2%); + } +} diff --git a/apps/platform/src/modules/dag-model-submission/index.ts b/apps/platform/src/modules/dag-model-submission/index.ts new file mode 100644 index 0000000..e45954e --- /dev/null +++ b/apps/platform/src/modules/dag-model-submission/index.ts @@ -0,0 +1,2 @@ +export { PipelineTitleComponent } from './pipeline-title-view'; +export { SubmissionDrawer } from './submission-drawer'; diff --git a/apps/platform/src/modules/dag-model-submission/pipeline-title-view.tsx b/apps/platform/src/modules/dag-model-submission/pipeline-title-view.tsx new file mode 100644 index 0000000..a52fe33 --- /dev/null +++ b/apps/platform/src/modules/dag-model-submission/pipeline-title-view.tsx @@ -0,0 +1,11 @@ +import { NodeIndexOutlined } from '@ant-design/icons'; +import { history } from 'umi'; + +export const PipelineTitleComponent = () => { + const { pipelineName } = (history.location?.state as any) || {}; + return ( + <> + 「{pipelineName || 'title'}」提交模型 + + ); +}; diff --git a/apps/platform/src/modules/dag-model-submission/preview-submit-node/index.less b/apps/platform/src/modules/dag-model-submission/preview-submit-node/index.less new file mode 100644 index 0000000..3730f8d --- /dev/null +++ b/apps/platform/src/modules/dag-model-submission/preview-submit-node/index.less @@ -0,0 +1,45 @@ +.wrapper { + padding: 16px 0; +} + +.dagNode { + display: flex; + width: 100%; + height: 32px; + align-items: center; + border: 1px solid #c2c8d5; + border-radius: 4px; + border-left: 4px solid rgb(35 182 95 / 100%); + background-color: #fff; + box-shadow: 0 2px 5px 1px rgb(0 0 0 / 6%); + + .icon { + flex-shrink: 0; + margin-left: 8px; + } + + .label { + display: inline-block; + overflow: hidden; + width: 122px; + flex-shrink: 0; + margin-left: 8px; + color: #666; + cursor: default; + font-size: 12px; + font-weight: 500; + text-overflow: ellipsis; + white-space: nowrap; + } + + .status { + flex-shrink: 0; + } +} + +.line { + width: 1px; + height: 32px; + border-left: 1px solid #b4bdcf; + margin-left: 88px; +} diff --git a/apps/platform/src/modules/dag-model-submission/preview-submit-node/index.tsx b/apps/platform/src/modules/dag-model-submission/preview-submit-node/index.tsx new file mode 100644 index 0000000..6a715b0 --- /dev/null +++ b/apps/platform/src/modules/dag-model-submission/preview-submit-node/index.tsx @@ -0,0 +1,42 @@ +import { CheckCircleOutlined } from '@ant-design/icons'; +import classNames from 'classnames'; + +import { ComponentIcons } from '@/modules/component-tree/component-icon'; + +import styles from './index.less'; + +interface IProps { + nodes: { + id: string; + icon: string; + label: string; + nodeDef: { + domain: string; + }; + }[]; +} + +export const PreviewSubmitNode = (props: IProps) => { + const { nodes = [] } = props; + return ( +
1, + })} + > + {nodes.map((item, index: number) => ( +
+
+ + {ComponentIcons[item.nodeDef.domain as string] || + ComponentIcons['default']} + + {item.label} + +
+ {index < nodes.length - 1 &&
} +
+ ))} +
+ ); +}; diff --git a/apps/platform/src/modules/dag-model-submission/submission-drawer.tsx b/apps/platform/src/modules/dag-model-submission/submission-drawer.tsx new file mode 100644 index 0000000..e033a41 --- /dev/null +++ b/apps/platform/src/modules/dag-model-submission/submission-drawer.tsx @@ -0,0 +1,447 @@ +import { + Drawer, + Button, + Space, + Form, + Input, + Select, + Empty, + Spin, + message, + Tooltip, + Alert, +} from 'antd'; +import classnames from 'classnames'; +import { useEffect, useCallback, useMemo } from 'react'; +import { history } from 'umi'; +import { DefaultModalManager } from '@/modules/dag-modal-manager'; +import submissionLayoutStyle from '@/modules/layout/model-submission-layout/index.less'; +import { getModel, useModel } from '@/util/valtio-helper'; + +import styles from './index.less'; +import { PreviewSubmitNode } from './preview-submit-node'; +import { SubmissionDrawerService } from './submission-service'; +import { parse } from 'query-string'; +import { useFormValidateOnly } from './useFormValidateOnly'; +import { SubmitGraphService } from '@/modules/dag-submit/graph-service'; +import { DagLayoutMenu, DagLayoutView } from '@/modules/layout/dag-layout'; +import { QuestionCircleOutlined } from '@ant-design/icons'; + +const WIDTH = 560; + +export const SubmissionDrawer = () => { + const modalManager = useModel(DefaultModalManager); + + const submissionDrawerService = useModel(SubmissionDrawerService); + const submitGraphService = useModel(SubmitGraphService); + const dagLayoutView = useModel(DagLayoutView); + + const { + modelInfo, + addressNodeList, + getModelNodesAddress, + submitModel, + checkSubmitModelStatus, + isSubmitting, + showAlert, + currentSubmitParams, + } = submissionDrawerService; + + const [form] = Form.useForm(); + + const { visible } = modalManager.modals[ModelSubmissionDrawerItem.id]; + + const addressFormData = Form.useWatch('address', form); + + const { submittable } = useFormValidateOnly(form); + + const [messageApi, contextHolder] = message.useMessage(); + + const { projectId, dagId } = parse(window.location.search); + + const modelId = (modelInfo.modelNode[0]?.outputs || []).find( + (item: { type: string }) => item.type === 'model', + )?.id; + + // 同时有模型训练和模型预测算子,只展示模型预测算子 + const previewNodes = + modelInfo.predictNode.length !== 0 + ? [...modelInfo.preNodes, ...modelInfo.predictNode, ...modelInfo.postNodes] + : [ + ...modelInfo.preNodes, + ...modelInfo.modelNode, + ...modelInfo.predictNode, + ...modelInfo.postNodes, + ]; + + const onCloseDrawer = () => { + modalManager.closeModal(ModelSubmissionDrawerItem.id); + submitGraphService.resetSelectNodeIdsObj(); + submitGraphService.clearGraphSelection(); + }; + + const onClose = () => { + onCloseDrawer(); + submissionDrawerService.isSubmitting = false; + }; + + const closeDrawer = () => { + onCloseDrawer(); + if (isSubmitting) { + message.warning(`「${currentSubmitParams.name}」模型提交中,请耐心等待`); + } + }; + + const addressOptions = addressNodeList.map((item) => ({ + value: item.nodeId, + label: item.nodeName, + })); + + const getDataSourcePath = (key: number) => { + if (!addressFormData) return; + const formItem = addressFormData[key]; + return addressNodeList.find((item) => item.nodeId === formItem.nodeName) + ?.dataSourcePath; + }; + + const nodeOptionsFilter = addressOptions.map((item) => { + if ( + (addressFormData || []).some( + (v: { nodeName: string }) => v.nodeName === item.value, + ) + ) { + return { + ...item, + disabled: true, + }; + } else { + return { + ...item, + disabled: false, + }; + } + }); + + const getAddress = useCallback( + async (modelId: string) => { + await getModelNodesAddress(modelId); + }, + [modelId], + ); + + useEffect(() => { + const initFormList = addressNodeList.map(() => ({})); + if (initFormList.length === 0) { + form.setFieldValue('address', [{}, {}]); + } else { + form.setFieldValue('address', initFormList); + } + }, [addressNodeList]); + + useEffect(() => { + if (!visible) return; + if (isSubmitting) { + form.setFieldsValue(currentSubmitParams); + } else { + form.resetFields(); + if (modelId) { + getAddress(modelId); + } else { + submissionDrawerService.clearAddressNodeList(); + } + } + }, [visible, modelId, isSubmitting]); + + const timestamp = useMemo(() => { + return Date.now(); + }, [modelId, visible]); + + const goToModelManager = () => { + history.push({ + pathname: '/dag', + search: window.location.search, + }); + dagLayoutView.setInitActiveMenu(DagLayoutMenu.MODELMANAGER); + dagLayoutView.setModelManagerShow(); + }; + + const handlePathChange = (value: string) => { + // 当前只有一个文件地址路径,所以节点文件必须相同 + const newFormAddress = addressFormData.map((item: { nodeName: string }) => ({ + nodeName: item.nodeName, + path: value, + })); + form.setFieldsValue({ + address: newFormAddress, + }); + }; + + const handleSubmit = async () => { + const value = await form.validateFields(); + submissionDrawerService.currentSubmitParams = { + ...value, + timestamp, + }; + // 如果即有模型训练算子,也有模型预测算子,则modelComponent只提交模型预测算子.trainId提交模型训练算子id + const submitNodes = + modelInfo.predictNode.length > 0 + ? [...modelInfo.preNodes, ...modelInfo.predictNode, ...modelInfo.postNodes] + : [...modelInfo.preNodes, ...modelInfo.modelNode, ...modelInfo.postNodes]; + const params = { + projectId: projectId as string, + graphId: dagId as string, + graphNodeOutPutId: modelId, + modelName: value.name, + modelDesc: value.desc, + modelPartyConfig: value.address.map( + (item: { nodeName: string; path: string }) => ({ + modelParty: item.nodeName, + modelDataName: `${item.path}_${timestamp}`, + modelDataSource: addressNodeList.find((node) => node.nodeId === item.nodeName) + ?.dataSourcePath, + }), + ), + modelComponent: submitNodes.map((item) => ({ + graphNodeId: item.id, + domain: item.nodeDef.domain, + name: item.nodeDef.name, + version: item.nodeDef.version, + })), + trainId: modelInfo.modelNode[0]?.id, + }; + submissionDrawerService.isSubmitting = true; + const { status, data } = await submitModel(params); + if (status?.code === 0 && data?.jobId) { + await pollingStatus(data.jobId, value.name); + } else { + submissionDrawerService.isSubmitting = false; + messageApi.error(`「${value.name}」模型提交失败, ${status?.msg}`); + } + }; + + const pollingStatus = async (jobId: string, modelName: string) => { + const { status, data } = await checkSubmitModelStatus(jobId); + if (status?.code === 0 && data?.status !== 'SUCCEED' && data?.status !== 'FAILED') { + submissionDrawerService.isSubmitting = true; + submissionDrawerService.submitTimer = setTimeout(async () => { + await pollingStatus(jobId, modelName); + }, 3000); + } else { + if (submissionDrawerService.submitTimer) { + clearTimeout(submissionDrawerService.submitTimer); + submissionDrawerService.submitTimer = undefined; + } + if (status?.code === 0 && data?.status === 'SUCCEED') { + submissionDrawerService.isSubmitting = false; + onClose(); + messageApi.open({ + type: 'success', + content: ( + <> + {`「${modelName}」模型提交成功,请到`} + 模型管理 + 查看 + + ), + duration: 3, + }); + } else { + messageApi.error( + `「${modelName}」模型提交失败, ${data?.errMsg ? data?.errMsg : status?.msg}`, + ); + submissionDrawerService.isSubmitting = false; + } + } + }; + + return ( + <> + {contextHolder} + 提交模型} + extra={ + + + + } + open={visible} + onClose={onClose} + width={WIDTH} + closable={false} + mask={false} + getContainer={() => { + return document.querySelector(`.${submissionLayoutStyle.center}`) as Element; + }} + footer={ + + + + + } + > + {showAlert && ( + + )} +
+ + + + + + + + + + 存储地址 + + } + required + > + + {(fields) => { + return ( + +
+ {fields.map(({ key, name, ...restField }) => { + return ( +
+
+ {'存储节点' + (key + 1)} +
+
+ { + // const nodeName = addressFormData.filter( + // (i: any) => i?.nodeName === value, + // ); + // if (nodeName.length > 1) + // return Promise.reject('节点重复'); + // return Promise.resolve(); + // }, + // }, + ]} + > + handlePathChange(e.target.value)} + addonAfter={ + isSubmitting + ? currentSubmitParams.timestamp + : timestamp + } + /> + +
+
+ ); + })} +
+
+ ); + }} +
+
+ + 提交组件}> +
+ {previewNodes.length === 0 ? ( + + ) : ( + + )} +
+
+
+
+ + ); +}; + +export const ModelSubmissionDrawerItem = { + id: 'ModelSubmissionDrawer', + visible: false, +}; + +getModel(DefaultModalManager).registerModal(ModelSubmissionDrawerItem); diff --git a/apps/platform/src/modules/dag-model-submission/submission-service.ts b/apps/platform/src/modules/dag-model-submission/submission-service.ts new file mode 100644 index 0000000..9aec735 --- /dev/null +++ b/apps/platform/src/modules/dag-model-submission/submission-service.ts @@ -0,0 +1,103 @@ +import { SubmitGraphService } from '@/modules/dag-submit/graph-service'; +import { Model, getModel } from '@/util/valtio-helper'; +import { parse } from 'query-string'; +import API from '@/services/secretpad'; +import { LoginService } from '@/modules/login/login.service'; +import { Platform } from '@/components/platform-wrapper'; + +export interface ModelInfo { + modelNode: any[]; + preNodes: any[]; + predictNode: any[]; + postNodes: any[]; +} + +export class SubmissionDrawerService extends Model { + loginService = getModel(LoginService); + + modelInfo: ModelInfo = { + modelNode: [], + preNodes: [], + predictNode: [], + postNodes: [], + }; + + addressNodeList: API.ModelPartyPathResponse[] = []; + + currentSubmitParams = { + name: '', + desc: '', + address: [{}, {}], + timestamp: '', + }; + + loading = false; + + isSubmitting = false; + + showAlert = false; + + submitGraphService = getModel(SubmitGraphService); + + submitTimer: ReturnType | undefined = undefined; + + constructor() { + super(); + this.submitGraphService.onModelSubmitChanged(this.setModelInfoChanged.bind(this)); + } + + setModelInfoChanged = (modeInfoData: ModelInfo) => { + this.modelInfo = modeInfoData; + }; + + clearAddressNodeList = () => { + this.addressNodeList = []; + }; + + // 获取列表路径 + getModelNodesAddress = async (modelId: string) => { + const { projectId } = parse(window.location.search); + const graphNodeId = modelId.split('-').slice(0, 3).join('-'); + this.loading = true; + const { status, data } = await API.ModelExportController.modelPartyPath({ + projectId: projectId as string, + graphNodeOutPutId: modelId, + graphNodeId: graphNodeId, + }); + const userInfo = await this.loginService.getUserInfo(); + if (userInfo.platformType === Platform.AUTONOMY) { + const currentId = userInfo.ownerId; + this.showAlert = !(data || [])?.some((item) => item.nodeId === currentId); + } else { + this.showAlert = false; + } + this.loading = false; + if (status && status.code === 0 && data) { + this.addressNodeList = data; + } + }; + + /** 提交模型 */ + submitModel = async (params: API.ModelExportPackageRequest) => { + return API.ModelExportController.pack(params); + }; + + /** 查询模型提交状态 */ + checkSubmitModelStatus = (jobId: string) => { + const { projectId } = parse(window.location.search); + if (!projectId) return; + return API.ModelExportController.status({ + jobId: jobId, + projectId: projectId as string, + }); + }; + + /** 取消模型提交轮询 */ + cancelSubmitTimer = () => { + this.isSubmitting = false; + if (this.submitTimer) { + clearTimeout(this.submitTimer); + this.submitTimer = undefined; + } + }; +} diff --git a/apps/platform/src/modules/dag-model-submission/useFormValidateOnly.tsx b/apps/platform/src/modules/dag-model-submission/useFormValidateOnly.tsx new file mode 100644 index 0000000..f43f0b3 --- /dev/null +++ b/apps/platform/src/modules/dag-model-submission/useFormValidateOnly.tsx @@ -0,0 +1,21 @@ +import { Form } from 'antd'; +import { FormInstance } from 'antd/lib'; +import { useEffect, useState } from 'react'; + +export const useFormValidateOnly = (form: FormInstance) => { + const [submittable, setSubmittable] = useState(false); + const values = Form.useWatch([], form); + + useEffect(() => { + form.validateFields({ validateOnly: true }).then( + () => { + setSubmittable(true); + }, + () => { + setSubmittable(false); + }, + ); + }, [values]); + + return { submittable }; +}; diff --git a/apps/platform/src/modules/dag-submit/dag.ts b/apps/platform/src/modules/dag-submit/dag.ts new file mode 100644 index 0000000..555c120 --- /dev/null +++ b/apps/platform/src/modules/dag-submit/dag.ts @@ -0,0 +1,17 @@ +import { DAG } from '@secretflow/dag'; +import { ref } from 'valtio'; + +import { SubmitGraphDataService } from './graph-data-service'; +import { GraphHookService } from './graph-hook-service'; +import { GraphSubmitRequestService } from './graph-request-service'; + +class MainDag extends DAG { + dataService: SubmitGraphDataService = new SubmitGraphDataService(this); + requestService: GraphSubmitRequestService = new GraphSubmitRequestService(this); + hookService: GraphHookService = new GraphHookService(this); +} + +const mainDag = new MainDag(); + +// 防止mainDag被valtio代理 +export default ref(mainDag); diff --git a/apps/platform/src/modules/dag-submit/graph-data-service.ts b/apps/platform/src/modules/dag-submit/graph-data-service.ts new file mode 100644 index 0000000..52381a6 --- /dev/null +++ b/apps/platform/src/modules/dag-submit/graph-data-service.ts @@ -0,0 +1,7 @@ +import { DefaultDataService } from '@secretflow/dag'; + +export class SubmitGraphDataService extends DefaultDataService { + getNode(nodeId: string) { + return this.getNodes().find(({ id }) => id === nodeId); + } +} diff --git a/apps/platform/src/modules/dag-submit/graph-hook-service.ts b/apps/platform/src/modules/dag-submit/graph-hook-service.ts new file mode 100644 index 0000000..9061a71 --- /dev/null +++ b/apps/platform/src/modules/dag-submit/graph-hook-service.ts @@ -0,0 +1,107 @@ +import type { GraphNodeOutput, GraphPort } from '@secretflow/dag'; +import { DefaultHookService } from '@secretflow/dag'; +import { parse } from 'query-string'; + +import { DefaultComponentTreeService } from '@/modules/component-tree/component-tree-service'; +import { getModel } from '@/util/valtio-helper'; + +import type { ComputeMode } from '../component-tree/component-protocol'; + +const DISTDATA_TYPE = { + REPORT: 'sf.report', + READ_DATA: 'sf.read_data', +}; + +export class GraphHookService extends DefaultHookService { + componentService = getModel(DefaultComponentTreeService); + + async createResult(nodeId: string, codeName: string) { + const [domain, name] = codeName.split('/'); + const { mode } = parse(window.location.search); + const component = await this.componentService.getComponentConfig( + { + domain, + name, + }, + mode as ComputeMode, + ); + + if (component) { + const results: GraphNodeOutput[] = []; + const { outputs } = component; + outputs.forEach((output, index) => { + if ( + output.types && + output.types[0] && + [DISTDATA_TYPE.READ_DATA].includes(output.types[0]) + ) { + return; + } + + results.push({ + id: `${nodeId}-output-${index}`, + name: output.name, + type: output.types[0].split('.')[1], + }); + }); + + return results; + } + return []; + } + + async createPort(nodeId: string, codeName: string) { + const [domain, name] = codeName.split('/'); + const { mode } = parse(window.location.search); + + const component = await this.componentService.getComponentConfig( + { + domain, + name, + }, + mode as ComputeMode, + ); + + if (component) { + const ports: GraphPort[] = []; + const { inputs, outputs } = component; + inputs?.forEach((input, index) => { + ports.push({ + id: `${nodeId}-input-${index}`, + group: 'top', + type: input.types[0], + attrs: { + circle: { + magnet: false, + }, + }, + }); + }); + + outputs.forEach((output, index) => { + // ignore the report output + if ( + output.types && + output.types[0] && + [DISTDATA_TYPE.REPORT, DISTDATA_TYPE.READ_DATA].includes(output.types[0]) + ) { + return; + } + + ports.push({ + id: `${nodeId}-output-${index}`, + group: 'bottom', + type: output.types[0], + attrs: { + circle: { + magnet: false, + }, + }, + }); + }); + + return ports; + } + return []; + } +} diff --git a/apps/platform/src/modules/dag-submit/graph-request-service.ts b/apps/platform/src/modules/dag-submit/graph-request-service.ts new file mode 100644 index 0000000..ce92df1 --- /dev/null +++ b/apps/platform/src/modules/dag-submit/graph-request-service.ts @@ -0,0 +1,101 @@ +import type { GraphModel } from '@secretflow/dag'; +import { NodeStatus } from '@secretflow/dag'; +import { parse } from 'query-string'; + +import { GraphRequestService } from '@/modules/main-dag/graph-request-service'; +import { nodeStatus } from '@/modules/main-dag/util'; +import { getGraphDetail } from '@/services/secretpad/GraphController'; + +export class GraphSubmitRequestService extends GraphRequestService { + graphData = {}; + + async saveDag(dagId: string, model: GraphModel): Promise { + return; + } + + async queryStatus(dagId: string) { + return { + nodeStatus: [], + finished: true, + }; + } + // startRun: (dagId: string, componentIds: string[]) => Promise; + // stopRun: (dagId: string, componentId: string) => Promise; + // getMaxNodeIndex: (dagId: string) => Promise; + + async queryDag(dagId: string) { + this.graphData = { nodes: [], edges: [] }; + + if (!dagId) { + return this.graphData as GraphModel; + } + + // 获取 graph 数据 + const { data } = await getGraphDetail({ + projectId: getProjectId(), + graphId: dagId, + }); + + if (!data) { + return this.graphData as GraphModel; + } + + const { nodes, edges } = data; + const convertedNodes = nodes?.map((n) => { + const { graphNodeId, status, codeName, nodeDef = {}, ...options } = n; + // 强行断言:后端没有定义 UNFINISHED 状态 + let graphNodeStatus = nodeStatus[status || 'STAGING'] as unknown as NodeStatus; + if (graphNodeStatus === NodeStatus.default) { + graphNodeStatus = NodeStatus.unfinished; + } + const { domain } = nodeDef; + return { + ...options, + codeName, + id: graphNodeId, + status: graphNodeStatus, + // 初始化 只有成功的模型训练并且有输入边才可提交 + styles: { + isOpaque: + domain === 'ml.train' && + nodehasInputEdge(graphNodeId, edges) && + status === 'SUCCEED' + ? false + : true, + isHighlighted: false, + }, + nodeDef, + }; + }); + + const convertedEdges = + edges?.map((e) => { + const { edgeId, ...options } = e; + return { + id: edgeId, + ...options, + styles: { + isOpaque: true, + }, + }; + }) || []; + + const convertedData = { + nodes: convertedNodes, + edges: convertedEdges, + }; + this.graphData = convertedData; + return convertedData; + } +} + +const getProjectId = () => { + const { search } = window.location; + const { projectId } = parse(search); + return projectId as string; +}; + +const nodehasInputEdge = (nodeId?: string, edges: API.GraphEdge[] = []) => { + if (!nodeId) return false; + return edges.some((edge) => edge.target === nodeId); +}; diff --git a/apps/platform/src/modules/dag-submit/graph-service.ts b/apps/platform/src/modules/dag-submit/graph-service.ts new file mode 100644 index 0000000..b7f2ab0 --- /dev/null +++ b/apps/platform/src/modules/dag-submit/graph-service.ts @@ -0,0 +1,249 @@ +import type { Edge } from '@antv/x6'; +import type { GraphNode, Node } from '@secretflow/dag'; +import type { GraphEventHandlerProtocol } from '@secretflow/dag'; +import { ActionType } from '@secretflow/dag'; +import { Emitter } from '@secretflow/utils'; +import { message } from 'antd'; + +import { dagLogDrawer } from '@/modules/dag-log/log.drawer.layout'; +import { DefaultModalManager } from '@/modules/dag-modal-manager'; +import { ModelSubmissionDrawerItem } from '@/modules/dag-model-submission/submission-drawer'; +import type { ModelInfo } from '@/modules/dag-model-submission/submission-service'; +import { getModel, Model } from '@/util/valtio-helper'; +import { DefaultComponentTreeService } from '@/modules/component-tree/component-tree-service'; + +import mainDag from './dag'; +import { + highlightSelectionByIds, + isModel, + isPost, + isPre, + isPredict, + resetGraphStyles, + sortNodes, + updateGraphNodesStyles, + getModelSameBranchNodes, +} from './util'; + +export class SubmitGraphService extends Model implements GraphEventHandlerProtocol { + onModelSubmitChangedEmitter = new Emitter(); + onModelSubmitChanged = this.onModelSubmitChangedEmitter.on; + + graphManager = mainDag.graphManager; + + modalManager = getModel(DefaultModalManager); + componentService = getModel(DefaultComponentTreeService); + + selectNodeIdsObj: { + modelNodeIds: string[]; + preNodesIds: string[]; + nextNodesIds: string[]; + predictNodesIds: string[]; + } = { + modelNodeIds: [], + preNodesIds: [], + nextNodesIds: [], + predictNodesIds: [], + }; + + resetSelectNodeIdsObj = () => { + this.selectNodeIdsObj = { + modelNodeIds: [], + preNodesIds: [], + nextNodesIds: [], + predictNodesIds: [], + }; + }; + + onCenterNode = (nodeId: string) => { + mainDag.graphManager.executeAction(ActionType.centerNode, nodeId); + }; + + onNodeClick: ((node: Node) => void) | undefined = async (node) => { + const data = node.getData(); + const graph = mainDag.graphManager.getGraphInstance(); + if (!graph) return; + const { styles = {} } = data; + const { isOpaque = false } = styles; + if (isOpaque !== true) { + this.modalManager.openModal(dagLogDrawer.id, { + nodeData: data, + from: 'pipeline', + }); + this.autoSelectModel(node); + this.resetSelectionEdge(); + this.emitModelSubmitChanged(); + } + }; + + /** 自动选中模型算子 */ + autoSelectModel = (node: Node) => { + const graph = mainDag.graphManager.getGraphInstance(); + if (!graph) return; + if (isModel(node)) { + // 如果 node 是模型并且被选中。点击 清空选择,然后选择模型的上下游 + if (this.selectNodeIdsObj.modelNodeIds.includes(node.id)) { + this.clearGraphSelection(); + // this.modalManager.closeModal(ModelSubmissionDrawerItem.id); + highlightSelectionByIds(this.getSelectIds()); + this.modalManager.closeModal(dagLogDrawer.id); + return; + } + // 重置可选算子 + this.clearGraphSelection(); + this.onCenterNode(node.getData().id); + const sameBrachNodes = getModelSameBranchNodes(node); + if (sameBrachNodes) { + // TODO: 暂只需要打包训练算子,不需要打包预测算子 + const newSameBrachNodes = { + modelNode: sameBrachNodes?.modelNode, + preNodes: sameBrachNodes?.preNodes, + nextNodes: sameBrachNodes?.nextNodes, + }; + updateGraphNodesStyles(Object.values(newSameBrachNodes).flat(), { + isOpaque: false, + }); + this.selectNodeIdsObj = { + modelNodeIds: sameBrachNodes.modelNode.map((item) => item.id), + preNodesIds: sameBrachNodes.preNodes.map((item) => item.id), + nextNodesIds: sameBrachNodes.nextNodes.map((item) => item.id), + predictNodesIds: [], + }; + highlightSelectionByIds(this.getSelectIds()); + this.modalManager.openModal(ModelSubmissionDrawerItem.id); + } + } else if (isPredict(node)) { + if (this.selectNodeIdsObj.predictNodesIds.includes(node.id)) { + this.clearGraphSelection(); + highlightSelectionByIds(this.getSelectIds()); + this.modalManager.closeModal(dagLogDrawer.id); + return; + } + + // 重置可选算子 + this.clearGraphSelection(); + this.onCenterNode(node.getData().id); + const sameBrachNodes = getModelSameBranchNodes(node); + if (sameBrachNodes) { + updateGraphNodesStyles(Object.values(sameBrachNodes).flat(), { + isOpaque: false, + }); + this.selectNodeIdsObj = { + // modelNodeIds: sameBrachNodes.modelNode.map((item) => item.id), + preNodesIds: sameBrachNodes.preNodes.map((item) => item.id), + nextNodesIds: sameBrachNodes.nextNodes.map((item) => item.id), + predictNodesIds: sameBrachNodes.predictNode.map((item) => item.id), + modelNodeIds: [], + }; + highlightSelectionByIds(this.getSelectIds()); + this.modalManager.openModal(ModelSubmissionDrawerItem.id); + } + } else { + /** 点击的不是模型训练算子也不是模型预测算子, 则 获取当前选中的模型算子 判断当前点击的是 上游还是下游 */ + const modelNode = graph + .getNodes() + .find( + (item) => + item.id === this.selectNodeIdsObj.modelNodeIds[0] || + item.id === this.selectNodeIdsObj.predictNodesIds[0], + ); + if (!modelNode) return; + + /** node 是否是模型的前序节点算子 */ + if (graph.isPredecessor(modelNode, node)) { + message.warning('当前组件为模型的上游,不可取消选择,请先取消模型组件'); + } + + /** node 是否是模型的后序节点算子 可取消选中 */ + if (graph.isSuccessor(modelNode, node)) { + const currentNodeId = node.id; + // 已经选中,则取消选中,没有选中,则选中 + if (this.selectNodeIdsObj.nextNodesIds.includes(currentNodeId)) { + this.selectNodeIdsObj.nextNodesIds = + this.selectNodeIdsObj.nextNodesIds.filter((item) => item !== currentNodeId); + } else { + this.selectNodeIdsObj.nextNodesIds = [ + ...this.selectNodeIdsObj.nextNodesIds, + currentNodeId, + ]; + } + } + + /** 重新设置选中节点 */ + highlightSelectionByIds(this.getSelectIds()); + } + }; + + /** 获取画布选中节点id */ + getSelectIds = () => { + return Object.values(this.selectNodeIdsObj).flat(); + }; + + /** 根据 selectNodeIdsObj 获取画布节点 */ + getGraphNodesBySelectIds = () => { + const graph = mainDag.graphManager.getGraphInstance(); + if (!graph) return []; + const valuesIds = Object.values(this.selectNodeIdsObj).flat(); + return graph.getNodes().filter((item) => valuesIds.includes(item.id)); + }; + + /** 清空画布选中算子,重置画布为只允许选择模型训练和模型预测算子状态 */ + clearGraphSelection = () => { + this.resetSelectNodeIdsObj(); + resetGraphStyles(); + }; + + /** 重置边的状态 */ + resetSelectionEdge = () => { + const selectNodes = this.getGraphNodesBySelectIds(); + const getSelectIds = this.getSelectIds(); + const graph = mainDag.graphManager.getGraphInstance(); + if (!graph) return; + graph.getEdges().forEach((edge: Edge) => { + edge.setAttrByPath(['line', 'opacity'], '0.25'); + }); + selectNodes.forEach((item: Node) => { + const edges = graph.getOutgoingEdges(item) || []; + edges.forEach((edge: Edge) => { + if (getSelectIds.includes(edge.getData().target)) { + edge.setAttrByPath(['line', 'opacity'], '1'); + } + }); + }); + }; + + emitModelSubmitChanged = () => { + const graph = mainDag.graphManager.getGraphInstance(); + if (!graph) return; + const result: ModelInfo = { + modelNode: [], + preNodes: [], + postNodes: [], + predictNode: [], + }; + const currentSelectedNodes = this.getGraphNodesBySelectIds() as Node[]; + const newSelectNodes = sortNodes(currentSelectedNodes); + newSelectNodes.forEach((node) => { + const nodeData = node.getData(); + if (isModel(node)) { + result.modelNode.push(nodeData); + } else if (isPre(node)) { + result.preNodes.push(nodeData); + } else if (isPost(node)) { + result.postNodes.push(nodeData); + } else if (isPredict(node)) { + // 如果选择了预测算子,需要将预测算子上面的训练算子也传给服务端,这里算子是根据画布选中节点来处理的,所以这里需要使用 getModelSameBranchNodes 额外获取一下训练算子 + result.predictNode.push(nodeData); + const sameBrachNodes = getModelSameBranchNodes(node); + result.modelNode.push(sameBrachNodes?.modelNode[0].getData()); + } + }); + this.onModelSubmitChangedEmitter.fire(result); + }; + + onBlankClick() { + highlightSelectionByIds(this.getSelectIds()); + } +} + +mainDag.addGraphEvents(getModel(SubmitGraphService)); diff --git a/apps/platform/src/modules/dag-submit/graph.tsx b/apps/platform/src/modules/dag-submit/graph.tsx new file mode 100644 index 0000000..cd12bac --- /dev/null +++ b/apps/platform/src/modules/dag-submit/graph.tsx @@ -0,0 +1,72 @@ +import type { GraphManager } from '@secretflow/dag'; +import { splitPortId } from '@secretflow/dag'; + +import { DefaultModalManager } from '@/modules/dag-modal-manager'; +import { GraphComponents, GraphView } from '@/modules/main-dag/graph'; +import { createPortTooltip } from '@/modules/main-dag/util'; +import { getModel, useModel } from '@/util/valtio-helper'; + +import type { ComputeMode } from '../component-tree/component-protocol'; + +import mainDag from './dag'; +import { SubmitGraphService } from './graph-service'; + +export function SubmitGraphComponent() { + return ( + + ); +} + +export class SubmitGraphView extends GraphView { + graphManager: GraphManager = mainDag.graphManager; + modelManager = getModel(DefaultModalManager); + graphService = getModel(SubmitGraphService); + + onViewUnMount() { + mainDag.dispose(); + } + + initGraph(dagId: string, container: HTMLDivElement, mode: ComputeMode) { + if (container) { + const { clientWidth, clientHeight } = container; + mainDag.init( + dagId, + { + container: container, + width: clientWidth, + height: clientHeight, + onPortRendered: async ({ contentContainer, port, node }) => { + const { codeName } = node.getData(); + const [domain, name] = codeName.split('/'); + const { type, index } = splitPortId(port.id); + const component = await this.componentService.getComponentConfig( + { + domain, + name, + }, + mode, + ); + if (component) { + const interpretion = this.componentInterpreter.getComponentTranslationMap( + { + domain, + name, + version: component.version, + }, + mode, + ); + const ioType = type === 'input' ? 'inputs' : 'outputs'; + const des = component[ioType][index].desc; + createPortTooltip( + contentContainer, + (interpretion ? interpretion[des] : undefined) || des, + 'main-widget-tooltip', + ); + } + }, + }, + 'LITE', + ); + } + } +} diff --git a/apps/platform/src/modules/dag-submit/index.less b/apps/platform/src/modules/dag-submit/index.less new file mode 100644 index 0000000..3bdd789 --- /dev/null +++ b/apps/platform/src/modules/dag-submit/index.less @@ -0,0 +1,166 @@ +.container { + width: 100%; + height: 100%; +} + +.graph { + width: 100%; + height: 100%; +} + +.empty { + position: absolute; + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; +} + +.toolbutton { + display: flex; + border: 1px solid #e6e8eb; + border-radius: 6px; + + .search { + display: flex; + align-items: center; + border-right: 1px solid #e6e8eb; + + .searchselect { + overflow: hidden; + width: 0; + flex-shrink: 0; + transition: width 0.3s; + } + + button { + width: 36px; + flex-shrink: 0; + border: none; + } + + :global(.ant-select-selector) { + border: none !important; + } + } + + .btns { + display: flex; + align-items: center; + justify-content: center; + + button { + width: 36px; + height: 36px; + border: 1px solid #fff; + + &:nth-child(4) { + border-left: 1px solid #e6e8eb; + } + } + } +} + +.toolbar { + display: flex; + height: 100%; + align-items: center; + justify-content: center; + + button { + color: rgb(0 0 0 / 65%); + font-size: 12px; + font-weight: 400; + + span:nth-child(2) { + margin-inline-start: 2px; + } + + &.active { + color: #1664ff; + } + } +} + +.popoverContent { + width: 208px; + + :global(.ant-popover-inner) { + border-radius: 8px; + } + + .title { + display: flex; + align-items: center; + justify-content: space-between; + + .titleText { + width: 40px; + height: 22px; + color: #000; + font-size: 14px; + font-weight: 500; + letter-spacing: 0; + line-height: 22px; + } + } + + .descContent { + margin-top: -8px; + + .text { + width: 184px; + height: 36px; + color: rgb(0 0 0 / 65%); + font-size: 12px; + font-weight: 400; + } + + img { + width: 100%; + height: 144px; + background-color: #f8f8fb; + } + } +} + +.tooltip-title { + display: flex; + flex-direction: column; + align-items: center; + padding: 0 4px; +} + +.resultCard { + :global(.ant-card-body) { + display: flex; + width: 94px; + height: 30px; + align-items: center; + justify-content: center; + padding: 0; + border-radius: 3px; + background-color: #fff; + box-shadow: 0 2px 4px 0 rgb(0 0 0 / 5%); + color: rgb(0 0 0 / 100%); + cursor: pointer; + font-size: 12px; + } +} + +.selected { + :global(.ant-card-body) { + border: 1px solid rgb(0 104 250 / 100%); + background-color: rgb(74 135 255 / 12%); + } +} + +.statusIcon { + padding-inline-end: 8px; + padding-inline-start: 8px; +} + +.resultTypeIcon { + padding-right: 4px; +} diff --git a/apps/platform/src/modules/dag-submit/toolbutton.tsx b/apps/platform/src/modules/dag-submit/toolbutton.tsx new file mode 100644 index 0000000..e7503cd --- /dev/null +++ b/apps/platform/src/modules/dag-submit/toolbutton.tsx @@ -0,0 +1,234 @@ +import { + CloseOutlined, + OneToOneOutlined, + SearchOutlined, + PlusOutlined, + MinusOutlined, +} from '@ant-design/icons'; +import { splitNodeId, ActionType } from '@secretflow/dag'; +import type { MenuProps } from 'antd'; +import { Button, Select, Tooltip, Dropdown } from 'antd'; +import React from 'react'; + +import { ReactComponent as ZoomFitIcon } from '@/assets/zoom-fit.icon.svg'; +import { getModel, Model, useModel } from '@/util/valtio-helper'; + +import mainDag from './dag'; +import styles from './index.less'; + +const TOOLS = [ + { + type: ActionType.zoomIn, + icon: , + render: 'button', + }, + { + type: ActionType.zoomTo, + icon: , + render: 'dropdown', + }, + { + type: ActionType.zoomOut, + icon: , + render: 'button', + }, + { + type: ActionType.zoomToFit, + icon: , + render: 'button', + }, + { + type: ActionType.zoomToOrigin, + icon: , + render: 'button', + }, +]; + +const dropDownItems: MenuProps['items'] = [ + { + key: 1, + label: '50%', + }, + { + key: 2, + label: '75%', + }, + { + key: 3, + label: '100%', + }, + { + key: 4, + label: '125%', + }, + { + key: 5, + label: '150%', + }, +]; + +export const ToolbuttonComponent: React.FC = () => { + const viewInstance = useModel(ToolButtonView); + + const { searchMode, nodeList, searchText, setSearchText } = viewInstance; + + return ( +
+
+ + +
+
+ {TOOLS.map((tool) => { + if (tool.render === 'button') { + return ( + + + + ); + } + })} +
+
+ ); +}; + +export class ToolButtonView extends Model { + searchMode = false; + + showSearchPanel = false; + + nodeList: { label: string; value: string }[] = []; + + zoom = 1; + + searchText = ''; + + get zoomLabel() { + return `${Math.floor(this.zoom * 100)}%`; + } + + changeSearchMode = () => { + this.searchMode = !this.searchMode; + if (this.searchMode) { + this.getNodeList(); + } + }; + + setSearchText = (value: string) => { + this.searchText = value; + }; + + getNodeList = () => { + const graph = mainDag.graphManager.getGraphInstance(); + if (graph) { + const nodes = graph.getNodes(); + this.nodeList = nodes.map((node) => { + const { id, label } = node.getData(); + const { index } = splitNodeId(id); + return { + label: `${label} (${index})`, + value: id, + }; + }); + } + }; + + onSelectNode = (nodeId: string) => { + mainDag.graphManager.executeAction( + [ActionType.selectNode, ActionType.centerNode], + nodeId, + ); + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + run = (type: ActionType, args?: any) => { + if (type === ActionType.zoomTo) { + const key = parseInt(args, 10); + const zoom = 0.25 * (key + 1); + mainDag.graphManager.executeAction(type, zoom); + return; + } + mainDag.graphManager.executeAction(type, args); + }; + + getToolButtonLabel = (type: ActionType) => { + const action = mainDag.graphManager.getActionInfo(type); + if (action) { + const { label, hotKey } = action; + if (hotKey) { + return `${label} ${hotKey.text}`; + } + return label; + } + }; + + isToolButtonEnabled = (type: ActionType) => { + if (type == ActionType.zoomIn) { + return this.zoom < 1.5; + } else if (type === ActionType.zoomOut) { + return this.zoom > 0.51; + } + return true; + }; + + onGraphScale = (zoom: number) => { + this.zoom = zoom; + }; +} + +mainDag.EventHub.register(getModel(ToolButtonView)); diff --git a/apps/platform/src/modules/dag-submit/util.ts b/apps/platform/src/modules/dag-submit/util.ts new file mode 100644 index 0000000..e9e1ca6 --- /dev/null +++ b/apps/platform/src/modules/dag-submit/util.ts @@ -0,0 +1,267 @@ +import type { Node } from '@secretflow/dag'; + +import mainDag from './dag'; + +/** 判断是不是模型训练算子 */ +export const isModel = (node: Node) => { + const { nodeDef = {} } = node.getData(); + const { domain } = nodeDef; + return domain === 'ml.train'; +}; + +/** 判断是不是模型预测算子 */ +export const isPredict = (node: Node) => { + const { nodeDef = {} } = node.getData(); + const { domain } = nodeDef; + return domain === 'ml.predict'; +}; + +/** 判断是不是前处理算子 */ +export const isPre = (node: Node) => { + const { nodeDef = {} } = node.getData(); + const { domain } = nodeDef; + return domain === 'preprocessing'; +}; + +/** 判断是不是后处理算子 */ +export const isPost = (node: Node) => { + const { nodeDef = {} } = node.getData(); + const { domain } = nodeDef; + // TODO: 后处理算子类型(目前暂无) + return domain === 'postprocessing'; +}; + +/** 将模型算子进行前后排序 */ +export const sortNodes = (nodes: Node[]) => { + const graph = mainDag.graphManager.getGraphInstance(); + if (!graph) return nodes; + nodes.sort((a, b) => { + if (graph.isPredecessor(a, b)) { + return 1; + } else if (graph.isSuccessor(a, b)) { + return -1; + } else { + return 0; + } + }); + return nodes; +}; + +/** 根据 id 高亮节点 */ +export const highlightSelectionByIds = (ids: string[]) => { + const graph = mainDag.graphManager.getGraphInstance(); + if (!graph) return; + const graphNodeList = graph.getNodes(); + graphNodeList.forEach((node) => { + const nodeData = node.getData(); + const data = { + ...nodeData, + styles: { + ...nodeData.styles, + isHighlighted: ids.includes(nodeData.id), + }, + }; + node.setData(data); + }); +}; + +/** + * 重置画布展示样式 + * 只有成功的模型训练算子才可点击 + * */ +export const resetGraphStyles = () => { + const graph = mainDag.graphManager.getGraphInstance(); + if (!graph) return; + const graphNodeList = graph.getNodes(); + graphNodeList.forEach((node) => { + const nodeData = node.getData(); + const hasEdge = (graph.getIncomingEdges(node) || []).length !== 0; + const data = { + ...nodeData, + styles: { + isOpaque: + nodeData?.nodeDef?.domain === 'ml.train' && nodeData.status === 0 && hasEdge + ? false + : true, + isHighlighted: false, + }, + }; + node.setData(data); + }); +}; + +/** + * 更新画布节点样式 + */ +export const updateGraphNodesStyles = (nodes: Node[], options: Record) => { + nodes.forEach((node) => { + const nodeData = node.getData(); + const data = { + ...nodeData, + styles: { + ...nodeData.styles, + ...options, + isOpaque: false, + }, + }; + node.setData(data); + }); +}; + +/** + * 获取目标算子的前处理算子,并且是从 table 类型桩连接下来的算子 + * 状态是成功的算子 + * */ +export const getPreNodes = (node: Node) => { + const graph = mainDag.graphManager.getGraphInstance(); + if (!graph) return []; + const predecessors = graph.getPredecessors(node) || []; + const result: Node[] = []; + predecessors.forEach((n) => { + const { status, codeName } = n.getData(); + const omitNodeCodeName: string[] = [ + // 'preprocessing/substitution', + // 'preprocessing/vert_bin_substitution', + ]; + if ( + status === 0 && + isPre(n as Node) && + !omitNodeCodeName.includes(codeName) && + isTableAnchorOutputConnectModel(node, n as Node) + ) { + result.push(n as Node); + } + }); + return result; +}; + +/** + * 获取目标算子的后处理算子,并且是从 table 类型桩连接下来的后处理算子 + * 状态是成功的算子 + * */ +export const getPostNodes = (node: Node) => { + const graph = mainDag.graphManager.getGraphInstance(); + if (!graph) return []; + const successors = graph.getSuccessors(node) || []; + const result: Node[] = []; + successors.forEach((n) => { + const { status } = n.getData(); + if ( + status === 0 && + isPost(n as Node) && + isTableAnchorOutputConnectModel(n as Node, node) + ) { + result.push(n as Node); + } + }); + return result; +}; + +/** + * 判断 targetNode 是不是通过 table 类型的连接桩一直连到 模型训练算子 + * 存在一条路径可以连接到模型训练算子 则返回 true,否则返回 false。 + */ +export const isTableAnchorOutputConnectModel = ( + modelNode: Node, + targetNode: Node, + visitedNodes = new Set(), +): boolean => { + const graph = mainDag.graphManager.getGraphInstance(); + if (!graph) return false; + + // 如果目标节点已经被访问过,说明存在循环连接,直接返回 false + if (visitedNodes.has(targetNode.id)) { + return false; + } + visitedNodes.add(targetNode.id); + + // 获取目标节点的输出连接桩 + const { outputs } = targetNode.getData(); + const outputsTable = outputs.filter( + (item: { type: string }) => item.type === 'table', + ); + // 获取目标节点的输出边 + const allOutputEdges = graph.getOutgoingEdges(targetNode) || []; + + for (const edge of allOutputEdges) { + const anchor = edge.getData().sourceAnchor; + const isTableEdge = outputsTable.some( + (output: { id: string }) => output.id === anchor, + ); + if (isTableEdge) { + const nextNodeId = edge.getData().target; + const nextNode = graph.getNodes().find((node) => node.id === nextNodeId); + if (nextNode && nextNode.id === modelNode.id) { + return true; // 如果找到连接到模型节点的路径,返回 true + } else { + if (nextNode && graph.isPredecessor(modelNode, nextNode)) { + // 如果下一个节点是模型节点的前序节点,递归继续查找 + if (isTableAnchorOutputConnectModel(modelNode, nextNode, visitedNodes)) { + return true; + } + } + } + } + } + + return false; +}; + +/** 获取模型训练算子或者模型预测算子的上下游组件 */ +export const getModelSameBranchNodes = (node: Node) => { + const graph = mainDag.graphManager.getGraphInstance(); + if (!graph) return; + const result: { + modelNode: Node[]; + predictNode: Node[]; + preNodes: Node[]; + nextNodes: Node[]; + } = { + preNodes: [], + modelNode: [], + predictNode: [], + nextNodes: [], + }; + + // 模型训练算子 + if (isModel(node)) { + result.modelNode.push(node); + const preNodesResult = getPreNodes(node); + result.preNodes = [...result.preNodes, ...preNodesResult]; + + // 后面有模型预测算子,才能去获取后处理算子 + const successors = graph.getSuccessors(node) || []; + const predictNode = successors.find((n) => { + const { status } = n.getData(); + if (status === 0 && isPredict(n as Node)) return n; + }); + if (predictNode) { + result.predictNode.push(predictNode as Node); + // 获取后处理算子 + const postNodesResult = getPostNodes(predictNode); + result.nextNodes = [...result.nextNodes, ...postNodesResult]; + } + } else if (isPredict(node)) { + // 模型预测算子 + result.predictNode.push(node); + const predecessors = graph.getPredecessors(node) || []; + + // 前面有模型训练算子,才能去获取前处理算子 + const modelNode = predecessors.find((n) => { + const { status } = n.getData(); + if (status === 0 && isModel(n as Node)) return n; + }); + if (modelNode) { + result.modelNode.push(modelNode as Node); + // 获取前处理算子 + const preNodesResult = getPreNodes(modelNode as Node); + result.preNodes = [...result.preNodes, ...preNodesResult]; + } + + // 获取后处理算子 只有模型预测算子,才可以获取后处理算子 + const postNodesResult = getPostNodes(node); + result.nextNodes = [...result.nextNodes, ...postNodesResult]; + } + + return result; +}; diff --git a/apps/platform/src/modules/data-manager/data-manager.service.ts b/apps/platform/src/modules/data-manager/data-manager.service.ts index 37830f9..766d1c2 100644 --- a/apps/platform/src/modules/data-manager/data-manager.service.ts +++ b/apps/platform/src/modules/data-manager/data-manager.service.ts @@ -8,6 +8,7 @@ export class DataManagerService extends Model { pageSize: number, status: string, search: string, + typeFilters: string, ) { const result = await listDatatables({ nodeId, @@ -15,6 +16,7 @@ export class DataManagerService extends Model { pageSize, statusFilter: status, datatableNameFilter: search, + types: typeFilters, }); return result.data; } @@ -25,3 +27,8 @@ export enum UploadStatus { SUCCESS = 'SUCCESS', // 数据已成功加密上传 FAILED = 'FAILED', // 数据上传失败 } + +export enum DataSheetType { + 'CSV' = 'CSV', + 'HTTP' = 'HTTP', +} diff --git a/apps/platform/src/modules/data-manager/data-manager.view.tsx b/apps/platform/src/modules/data-manager/data-manager.view.tsx index 0444fc2..b1cad9b 100644 --- a/apps/platform/src/modules/data-manager/data-manager.view.tsx +++ b/apps/platform/src/modules/data-manager/data-manager.view.tsx @@ -1,6 +1,11 @@ -import { SearchOutlined, InfoCircleOutlined } from '@ant-design/icons'; -import type { RadioChangeEvent, TourProps } from 'antd'; -import { message, Tag } from 'antd'; +import { + SearchOutlined, + InfoCircleOutlined, + DownOutlined, + ReloadOutlined, +} from '@ant-design/icons'; +import type { MenuProps, RadioChangeEvent, TourProps } from 'antd'; +import { Dropdown, message, Tag } from 'antd'; import { Button, Radio, @@ -15,6 +20,7 @@ import { Empty, } from 'antd'; import type { MessageInstance } from 'antd/es/message/interface'; +import type { FilterValue } from 'antd/es/table/interface'; import { parse } from 'query-string'; import type { ChangeEvent } from 'react'; import React, { useEffect, useRef } from 'react'; @@ -22,6 +28,7 @@ import React, { useEffect, useRef } from 'react'; import { confirmDelete } from '@/components/comfirm-delete'; import { EdgeAuthWrapper } from '@/components/edge-wrapper-auth'; import { Platform, hasAccess } from '@/components/platform-wrapper'; +import { HttpDataAddDrawer } from '@/modules/data-table-add/add-http-data/http-data-add.view'; import { DataTableAddContent } from '@/modules/data-table-add/data-table-add.view'; import { DatatableInfoService } from '@/modules/data-table-info/component/data-table-auth/data-table-auth.service'; import { DataTableAuth } from '@/modules/data-table-info/data-table-auth-drawer'; @@ -34,12 +41,17 @@ import { NodeService } from '@/modules/node'; import { deleteDatatable, pushDatatableToTeeNode, + getDatatable, } from '@/services/secretpad/DatatableController'; import { getModel, Model, useModel } from '@/util/valtio-helper'; import { LoginService } from '../login/login.service'; -import { DataManagerService, UploadStatus } from './data-manager.service'; +import { + DataManagerService, + DataSheetType, + UploadStatus, +} from './data-manager.service'; import styles from './index.less'; const embeddedSheets = ['alice.csv', 'bob.csv']; @@ -79,11 +91,10 @@ export const DataManagerComponent: React.FC = () => { dataIndex: 'type', key: 'type', width: '10%', - // filters: [ - // { text: '表', value: 'table' }, - // { text: '模型', value: 'model' }, - // { text: '规则', value: 'rule' }, - // ], + filters: [ + { text: 'CSV', value: DataSheetType.CSV }, + { text: 'HTTP', value: DataSheetType.HTTP }, + ], }, { title: '已授权项目', @@ -130,23 +141,26 @@ export const DataManagerComponent: React.FC = () => { title: '状态', dataIndex: 'status', key: 'status', - width: '10%', - render: (status: string) => { - if (status == 'Available') { - return ( - - - 可用 - - ); - } else { - return ( - - - 不可用 - - ); - } + width: '14%', + render: (status: string, record: API.DatatableVO) => { + return ( + <> + {status === 'Available' ? ( + + ) : ( + + )} + + + ); }, }, { @@ -162,6 +176,7 @@ export const DataManagerComponent: React.FC = () => { dataIndex: 'pushToTeeStatus', width: '15%', render: (status: string, record: API.DatatableVO) => { + if (record.type === DataSheetType.HTTP) return '-'; if (!status || status === '') { return ( + + +
@@ -312,6 +341,10 @@ export const DataManagerComponent: React.FC = () => { : columns } loading={viewInstance.tableLoading} + onChange={(pagination, filters, sorter) => { + viewInstance.typeFilters = filters?.type as FilterValue; + viewInstance.getTableList(); + }} pagination={{ total: viewInstance.totalNum || 1, current: viewInstance.pageNumber, @@ -320,7 +353,7 @@ export const DataManagerComponent: React.FC = () => { onChange: (page, pageSize) => { viewInstance.pageNumber = page; viewInstance.pageSize = pageSize; - viewInstance.getTableList(); + // viewInstance.getTableList(); }, size: 'default', }} @@ -374,6 +407,16 @@ export const DataManagerComponent: React.FC = () => { }} /> )} + + {viewInstance.showHttpDataAddDrawer && ( + { + viewInstance.getTableList(); + viewInstance.showHttpDataAddDrawer = false; + }} + visible={viewInstance.showHttpDataAddDrawer} + /> + )} {contextHolder}
); @@ -392,6 +435,8 @@ export class DataManagerView extends Model { statusFilter = ''; + typeFilters: FilterValue | null = null; + search = ''; tableLoading = false; @@ -408,6 +453,8 @@ export class DataManagerView extends Model { showDatatableInfoDrawer = false; + showHttpDataAddDrawer = false; + currentNode: API.NodeVO = {}; tableListTimeOut: NodeJS.Timeout | undefined; @@ -448,6 +495,7 @@ export class DataManagerView extends Model { this.pageSize, this.statusFilter, this.search, + this.typeFilters, ); this.tableLoading = false; @@ -461,6 +509,10 @@ export class DataManagerView extends Model { this.showAddDataDrawer = true; } + addHttpData = () => { + this.showHttpDataAddDrawer = true; + }; + openDataInfo(tableInfo: API.DatatableVO) { this.tableInfo = tableInfo; this.showDatatableInfoDrawer = true; @@ -475,11 +527,13 @@ export class DataManagerView extends Model { datatableName: string, dataId: string, messageApi: MessageInstance, + type: string, ) => { if (!this.nodeService.currentNode?.nodeId) return; const { status } = await deleteDatatable({ nodeId: this.nodeService.currentNode?.nodeId, datatableId: dataId, + type, }); if (status && status.code !== 0) { messageApi.error(status.msg); @@ -529,4 +583,22 @@ export class DataManagerView extends Model { this.getTableList(); }, 300) as unknown as number; } + + refreshTableStatus = async (record: API.DatatableVO) => { + try { + const { status } = await getDatatable({ + datatableId: record.datatableId, + nodeId: this.currentNode.nodeId, + type: record.type, + }); + if (status?.code === 0) { + message.success('数据状态刷新成功'); + this.getTableList(); + } else { + message.error('数据状态刷新失败'); + } + } catch (error) { + message.error((error as Error).message); + } + }; } diff --git a/apps/platform/src/modules/data-table-add/add-http-data/http-data-add.view.tsx b/apps/platform/src/modules/data-table-add/add-http-data/http-data-add.view.tsx new file mode 100644 index 0000000..f1884a6 --- /dev/null +++ b/apps/platform/src/modules/data-table-add/add-http-data/http-data-add.view.tsx @@ -0,0 +1,422 @@ +import { + DeleteOutlined, + DownloadOutlined, + PlusOutlined, + UploadOutlined, +} from '@ant-design/icons'; +import { + Alert, + Button, + Checkbox, + Form, + Input, + Select, + Space, + Upload, + message, +} from 'antd'; +import { Drawer } from 'antd'; +import { useEffect, useRef } from 'react'; +import { CSVLink } from 'react-csv'; +import { createFeatureDatasource } from '@/services/secretpad/FeatureDatasourceController'; + +import { Model, useModel } from '@/util/valtio-helper'; + +import { analysisCsv } from '../component/upload-table/util'; + +import styles from './index.less'; +import { parse } from 'query-string'; +import { flushSync } from 'react-dom'; + +const downloadData = [ + { 特征名称: 'id1', 特征类型: 'string', 特征描述: '' }, + { 特征名称: 'x1', 特征类型: 'integer', 特征描述: '描述' }, + { 特征名称: 'x2', 特征类型: 'integer', 特征描述: '' }, + { 特征名称: 'x3', 特征类型: 'integer', 特征描述: '' }, + { 特征名称: 'x4', 特征类型: 'float', 特征描述: '' }, + { 特征名称: 'x5', 特征类型: 'float', 特征描述: '' }, + { 特征名称: 'x6', 特征类型: 'float', 特征描述: '' }, + { 特征名称: 'x7', 特征类型: 'float', 特征描述: '' }, + { 特征名称: 'x8', 特征类型: 'float', 特征描述: '' }, + { 特征名称: 'x9', 特征类型: 'float', 特征描述: '' }, + { 特征名称: 'x10', 特征类型: 'float', 特征描述: '' }, +]; + +export const HttpDataAddDrawer = ({ + visible, + onClose, +}: { + visible: boolean; + onClose: () => void; +}) => { + const httpDataAddService = useModel(HttpDataAddService); + const [form] = Form.useForm(); + const values = Form.useWatch([], form); + + useEffect(() => { + if (values?.address && values?.tableName && values?.features?.length !== 0) { + httpDataAddService.submitDisabled = false; + } else { + httpDataAddService.submitDisabled = true; + } + }, [values]); + + const csvRef = useRef<{ + link: HTMLLinkElement; + }>(null); + + const triggerDownload = () => { + if (csvRef.current) { + csvRef.current.link.click(); + } + }; + + const featureTypeOptions = [ + { value: 'int', label: 'integer' }, + { value: 'float', label: 'float' }, + { value: 'str', label: 'string' }, + ]; + + useEffect(() => { + if (visible) { + form.setFieldValue('features', [{}]); + } + }, [visible]); + + const handleClose = () => { + onClose(); + form.resetFields(); + httpDataAddService.loading = false; + httpDataAddService.featuresError = []; + }; + + const checkColCsvFormat = (ary: any) => { + if (ary.length !== 3) { + return false; + } + for (const item of ary) { + if (!['特征名称', '特征类型', '特征描述'].includes(item) && item) { + return false; + } + } + return true; + }; + + const handleColCsvUpload = async (file: File, fileList: File[]) => { + form.resetFields(['features']); + try { + const csvData = await analysisCsv(file); + const { + meta: { fields }, + data, + } = csvData; + const checkResult = checkColCsvFormat(fields); + if (!checkResult) { + message.error('请检查CSV格式'); + return; + } + const cols = data.map((info: any) => { + return { + featureName: info['特征名称'] || undefined, + featureType: + featureTypeOptions.find((i) => i.label === info['特征类型'])?.value || + undefined, + featureDescription: info['特征描述'], + }; + }); + const uniqueFeatures = cols.reduce((acc, cur) => { + acc[cur.featureName] = acc[cur.featureName] || cur; + return acc; + }, {} as Record); + const resultCols = Object.values(uniqueFeatures); + form.setFieldValue('features', resultCols); + const repeatLength = cols.length - resultCols.length; + if (repeatLength !== 0) { + message.success( + `上传了${cols.length}个字段, 有${repeatLength}个重复字段已自动去重`, + ); + } else { + message.success(`上传了${cols.length}个字段`); + } + setTimeout(() => { + validateForm(); + }); + } catch (e) { + console.log(e); + message.error('请检查CSV格式'); + form.setFieldValue('features', [{}]); + } + }; + + const validateForm = async (options = {}) => { + try { + const validateRes = await form.validateFields(options); + return validateRes; + } catch (e: any) { + const { errorFields } = e; + const featuresError = errorFields.filter((i: any) => i.name[0] === 'features'); + httpDataAddService.featuresError = featuresError; + throw e; + } + }; + + const handleOk = async () => { + const validateRes = await validateForm(); + httpDataAddService.featuresError = []; + const values = validateRes; + try { + httpDataAddService.loading = true; + const { status } = await httpDataAddService.addHttpData(values); + httpDataAddService.loading = false; + if (status && status.code === 0) { + message.success('添加成功'); + onClose(); + } else { + message.error(status?.msg || '添加失败'); + } + } catch (error) { + httpDataAddService.loading = false; + } + }; + + const handelFeatureChange = async () => { + await validateForm({ dirty: true }); + httpDataAddService.featuresError = []; + }; + + return ( + + + + + } + > +
数据表类型:http数据源
+ +
+ + + + + + + + + +
+
数据表结构
+
+ + + handleColCsvUpload(file, fileList) + } + customRequest={() => { + return; + }} + > + + +
+
+ + {httpDataAddService.featuresError.length > 0 && ( +
+ + + {`有${httpDataAddService.featuresError.length}个字段错误请检查`} + + + + (httpDataAddService.showFeatureErrorChecked = e.target.checked) + } + > + 仅看错误 + + +
+ } + type="error" + showIcon + /> + + )} + +
+
特征名称
+
类型
+
描述(可选)
+
操作
+
+ + + {(fields, { add, remove }) => ( +
+
+ +
+ {fields.reverse().map((field, index) => { + let display = 'flex'; + // 如果有错误,并且勾选了仅看错误项 + if ( + httpDataAddService.featuresError.length > 0 && + httpDataAddService.showFeatureErrorChecked + ) { + if ( + httpDataAddService.featuresError.find( + (i: any) => i.name[1] == fields.length - 1 - index, + ) + ) { + display = 'flex'; + } else { + display = 'none'; + } + } + return ( + + { + const values = form.getFieldValue('features'); + const features = values.filter( + (i: any) => value && i?.featureName === value, + ); + if (features.length > 1) return Promise.reject(); + return Promise.resolve(); + }, + message: '存在重复特征', + }, + ]} + > + + + + + +
+ { + remove(field.name); + setTimeout(() => { + validateForm(); + handelFeatureChange(); + }); + }} + /> +
+
+ ); + })} +
+ )} +
+
+
+ ); +}; + +export class HttpDataAddService extends Model { + loading = false; + + submitDisabled = true; + + featuresError = []; + + showFeatureErrorChecked = false; + + addHttpData = async (value: any) => { + const params = { + featureTableName: value.tableName, + nodeId: parse(window.location.search)?.nodeId, + type: 'HTTP', + desc: value.tableDesc, + url: value.address, + columns: value.features.map( + (item: { + featureName: string; + featureType: string; + featureDescription: string; + }) => ({ + colName: item.featureName, + colType: item.featureType, + colComment: item.featureDescription, + }), + ), + }; + return await createFeatureDatasource(params); + }; +} diff --git a/apps/platform/src/modules/data-table-add/add-http-data/index.less b/apps/platform/src/modules/data-table-add/add-http-data/index.less new file mode 100644 index 0000000..261b35a --- /dev/null +++ b/apps/platform/src/modules/data-table-add/add-http-data/index.less @@ -0,0 +1,93 @@ +.titleSheet { + margin-bottom: 16px; + color: rgb(0 0 0 / 88%); + font-size: 14px; + line-height: 22px; +} + +.dataSheetTitle { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; + + .title { + color: rgb(0 0 0 / 88%); + font-size: 14px; + font-weight: 500; + line-height: 22px; + } + + .options { + display: flex; + gap: 24px; + + button { + padding: 0; + } + } +} + +.tableHeader { + display: flex; + width: 100%; + height: 36px; + box-sizing: border-box; + padding: 8px 0; + margin-bottom: 8px; + background: rgb(0 0 0 / 2%); + box-shadow: inset 0 -1px 0 0 #e8e9ea; + color: rgb(0 0 0 / 85%); + font-size: 12px; + font-weight: 500; + line-height: 20px; + + .tableHeaderFeature { + width: 178px; + border-right: 1px solid rgb(0 10 26 / 7%); + margin-left: 32px; + } + + .tableHeaderType { + width: 230px; + box-sizing: border-box; + padding-left: 12px; + border-right: 1px solid rgb(0 10 26 / 7%); + } + + .tableHeaderDesc { + width: 205px; + box-sizing: border-box; + padding-left: 12px; + border-right: 1px solid rgb(0 10 26 / 7%); + } + + .tableHeaderBtn { + width: 40px; + box-sizing: border-box; + padding-left: 12px; + } +} + +.addBtn { + margin-bottom: 8px; +} + +.formFeatureName { + width: 202px !important; + margin-right: 16px; +} + +.formFeatureDesc { + width: 200px !important; + margin-right: 16px; +} + +.formFeatureDelete { + margin-bottom: 24px; + margin-left: -8px; + + &:hover { + color: #1677ff; + } +} diff --git a/apps/platform/src/modules/data-table-info/data-table-auth/data-tabel-auth.view.tsx b/apps/platform/src/modules/data-table-info/data-table-auth/data-tabel-auth.view.tsx index 5152d47..c5ef530 100644 --- a/apps/platform/src/modules/data-table-info/data-table-auth/data-tabel-auth.view.tsx +++ b/apps/platform/src/modules/data-table-info/data-table-auth/data-tabel-auth.view.tsx @@ -137,6 +137,7 @@ export class DataTableAuthModel extends Model { const response = await getDatatable({ datatableId: tableInfo.datatableId, nodeId: nodeId as string, + type: tableInfo.type, }); this.tableInfo = response.data || {}; this.projectAuthList = [...(this.tableInfo.authProjects || [])].reverse() || []; @@ -147,6 +148,7 @@ export class DataTableAuthModel extends Model { projectId: item.projectId, nodeId: this.nodeService.currentNode?.nodeId, datatableId: tableInfo.datatableId, + type: tableInfo.type, }); if (res.status?.code == 0) { diff --git a/apps/platform/src/modules/data-table-info/data-table-auth/project-auth-config/index.tsx b/apps/platform/src/modules/data-table-info/data-table-auth/project-auth-config/index.tsx index 9f21f27..1752b91 100644 --- a/apps/platform/src/modules/data-table-info/data-table-auth/project-auth-config/index.tsx +++ b/apps/platform/src/modules/data-table-info/data-table-auth/project-auth-config/index.tsx @@ -114,6 +114,7 @@ export const ProjectAuthConfigDrawer = ({ projectId: data?.projectId, nodeId: nodeId as string, datatableId: tableInfo.datatableId, + type: tableInfo.type, }); if (res?.status?.code === 0) { form.setFieldsValue({ @@ -135,6 +136,7 @@ export const ProjectAuthConfigDrawer = ({ nodeId: nodeId as string, datatableId: tableInfo.datatableId, configs: value.fields, + type: tableInfo.type, }); if (res.status?.code == 0) { message.success('授权成功'); @@ -151,6 +153,7 @@ export const ProjectAuthConfigDrawer = ({ nodeId: nodeId as string, datatableId: tableInfo.datatableId, configs: value.fields, + type: tableInfo.type, }); if (res.status?.code == 0) { message.success('授权修改成功'); diff --git a/apps/platform/src/modules/data-table-info/data-table-info.view.tsx b/apps/platform/src/modules/data-table-info/data-table-info.view.tsx index 69ff27a..53d94d0 100644 --- a/apps/platform/src/modules/data-table-info/data-table-info.view.tsx +++ b/apps/platform/src/modules/data-table-info/data-table-info.view.tsx @@ -3,11 +3,10 @@ import { Badge, Descriptions, message, Space, Tabs } from 'antd'; import { Drawer } from 'antd'; import type { TabsProps } from 'antd'; import React, { useEffect } from 'react'; - +import { DataSheetType } from '@/modules/data-manager/data-manager.service'; import { getDatatable } from '@/services/secretpad/DatatableController'; import { Model, useModel } from '@/util/valtio-helper'; -import { DataTableAuthComponent } from './component/data-table-auth/data-table-auth.view'; import { DataTableStructure } from './component/data-table-structure'; import styles from './index.less'; @@ -44,7 +43,6 @@ export const DataTableInfoDrawer: React.FC> = (props) => { // children: , // }, ]; - return ( > = (props) => { 「{tableInfo.datatableName}」 详情{' '} - - 可用 + {tableInfo.status === 'Available' ? ( + + ) : ( + + )} > = (props) => { {/* {tableInfo.datasourceId} */} 默认数据源 - 节点本地数据 + + {tableInfo.type === DataSheetType.CSV ? '节点本地数据' : 'HTTP数据'} + {tableInfo.relativeUri} @@ -110,6 +113,7 @@ export class DataTableInfoDrawerView extends Model { const response = await getDatatable({ datatableId: tableInfo.datatableId, nodeId: node.nodeId, + type: tableInfo.type, }); setTimeout(() => { diff --git a/apps/platform/src/modules/data-table-tree/datatable-tree.view.tsx b/apps/platform/src/modules/data-table-tree/datatable-tree.view.tsx index e646e34..88ebbf3 100644 --- a/apps/platform/src/modules/data-table-tree/datatable-tree.view.tsx +++ b/apps/platform/src/modules/data-table-tree/datatable-tree.view.tsx @@ -277,6 +277,7 @@ export class DatatableTreeView extends Model { nodeId, datatableId, projectId: projectId as string, + type: 'CSV', }); // 防止多个数据表预览出现冲突 diff --git a/apps/platform/src/modules/layout/dag-layout/index.tsx b/apps/platform/src/modules/layout/dag-layout/index.tsx index d5d28d9..2e7076f 100644 --- a/apps/platform/src/modules/layout/dag-layout/index.tsx +++ b/apps/platform/src/modules/layout/dag-layout/index.tsx @@ -1,6 +1,6 @@ import { ArrowLeftOutlined } from '@ant-design/icons'; import type { TabsProps } from 'antd'; -import { Divider, Tabs } from 'antd'; +import { Divider, Tabs, Space } from 'antd'; import classnames from 'classnames'; import { parse } from 'query-string'; import { useEffect } from 'react'; @@ -20,13 +20,15 @@ import { DagLogDrawer } from '@/modules/dag-log/log.drawer.layout'; import { DagLog } from '@/modules/dag-log/log.view'; import { DefaultModalManager } from '@/modules/dag-modal-manager'; import { ModalWidth } from '@/modules/dag-modal-manager/modal-manger-protocol'; -import { ResultDrawer } from '@/modules/dag-result/result-modal'; +import { ResultDrawer, resultDrawer } from '@/modules/dag-result/result-modal'; import { DatatableTreeComponent } from '@/modules/data-table-tree/datatable-tree.view'; import { LoginService } from '@/modules/login/login.service'; import { GraphComponents } from '@/modules/main-dag/graph'; +import { ModelSubmissionEntry } from '@/modules/main-dag/model-submission-entry'; import { RecordComponent } from '@/modules/main-dag/record'; import { ToolbarComponent } from '@/modules/main-dag/toolbar'; import { ToolbuttonComponent } from '@/modules/main-dag/toolbutton'; +import { ModelListComponent } from '@/modules/model-manager'; import { PipelineCreationComponent } from '@/modules/pipeline/pipeline-creation-view'; import { PipelineViewComponent } from '@/modules/pipeline/pipeline-view'; import { RecordListDrawerItem } from '@/modules/pipeline-record-list/record-list-drawer-view'; @@ -54,11 +56,17 @@ const tabItems: TabsProps['items'] = [ }, ]; +export enum DagLayoutMenu { + PROJECTDATA = 'project-data', + MODELTRAIN = 'model-train', + MODELMANAGER = 'model-manager', +} + export const DagLayout = () => { const viewInstance = useModel(DagLayoutView); const loginService = useModel(LoginService); - const { type = 'DAG' } = parse(window.location.search); + const { type = 'DAG', mode } = parse(window.location.search); const goBack = async () => { const userInfo = await loginService.getUserInfo(); @@ -75,14 +83,35 @@ export const DagLayout = () => { { key: 'DAG-项目数据', label: '项目数据', - callBack: () => viewInstance.setActiveKey('datatable'), + id: DagLayoutMenu.PROJECTDATA, + callBack: () => { + viewInstance.setDagShow(); + viewInstance.setActiveKey('datatable'); + }, isInit: false, + projectMode: ['MPC', 'TEE'], }, { key: 'DAG-模型训练', label: '模型训练', - callBack: () => viewInstance.setActiveKey('pipeline'), + id: DagLayoutMenu.MODELTRAIN, + callBack: () => { + viewInstance.setDagShow(); + viewInstance.setActiveKey('pipeline'); + }, isInit: true, + projectMode: ['MPC', 'TEE'], + }, + { + key: '模型管理', + label: '模型管理', + id: DagLayoutMenu.MODELMANAGER, + callBack: () => { + viewInstance.setModelManagerShow(); + viewInstance.setActiveKey('pipeline'); + }, + isInit: false, + projectMode: ['MPC'], }, ], PSI: [], @@ -90,10 +119,25 @@ export const DagLayout = () => { }; useEffect(() => { - viewInstance.setActiveMenu( - P2pMenuList[type as keyof typeof P2pMenuList]?.findIndex((item) => item.isInit), + const currentMenu = P2pMenuList[type as keyof typeof P2pMenuList]?.find( + (item) => item.isInit, ); - }, []); + viewInstance.setActiveMenu(currentMenu?.id || DagLayoutMenu.PROJECTDATA); + currentMenu?.callBack && currentMenu.callBack(); + }, [type, mode]); + + useEffect(() => { + if (viewInstance.initActiveMenu) { + const currentMenuList = P2pMenuList[type as keyof typeof P2pMenuList]; + const currentMenu = currentMenuList.find( + (item) => item.id === viewInstance.initActiveMenu, + ); + if (currentMenu) { + viewInstance.setActiveMenu(viewInstance.initActiveMenu); + currentMenu?.callBack && currentMenu.callBack(); + } + } + }, [viewInstance.initActiveMenu]); return (
@@ -106,76 +150,84 @@ export const DagLayout = () => { - +
- {P2pMenuList[type as keyof typeof P2pMenuList].map( - (item, index: number) => { + {P2pMenuList[type as keyof typeof P2pMenuList] + .filter((menu) => menu.projectMode.includes(mode as string)) + .map((item, index: number) => { return (
{ - viewInstance.setActiveMenu(index); + viewInstance.setActiveMenu(item.id); item?.callBack && item.callBack(); }} > {item.label}
); - }, - )} + })}
-
-
-
- viewInstance.setActiveKey(key)} - /> -
-
- -
-
-
viewInstance.toggleLeftPanel()} - /> - -
-
- -
- + {/*
*/} + {viewInstance.modelManagerShow && } + {/*
*/} + {viewInstance.dagShow && ( +
+
+
+ viewInstance.setActiveKey(key)} + /> +
+
+
-
-
-
viewInstance.toggleLeftPanel()} + /> + +
- +
+ +
+ + + {!isTeeProject() && } + +
+
+
+ +
+
+ +
-
+ )} @@ -195,6 +247,12 @@ export const DagLayout = () => { const RIGHT_DIST = 20; +/** 判断项目是不是TEE项目,TEE项目没有模型提交功能 */ +export const isTeeProject = () => { + const { mode } = parse(window.location.search); + return mode === 'TEE'; +}; + export class DagLayoutView extends Model { modalManager = getModel(DefaultModalManager); constructor() { @@ -233,16 +291,36 @@ export class DagLayoutView extends Model { activeKey = 'pipeline'; - activeMenu = 0; + activeMenu = ''; + + initActiveMenu: string | null = ''; + + modelManagerShow = false; + dagShow = true; setActiveKey = (key: string) => { this.activeKey = key; }; - setActiveMenu = (key: number) => { + setInitActiveMenu = (id: string) => { + this.initActiveMenu = id; + }; + + setActiveMenu = (key: string) => { this.activeMenu = key; }; + setModelManagerShow = () => { + this.modelManagerShow = true; + this.dagShow = false; + this.modalManager.closeAllModals(); + }; + + setDagShow = () => { + this.modelManagerShow = false; + this.dagShow = true; + }; + toggleLeftPanel() { this.leftPanelShow = !this.leftPanelShow; } diff --git a/apps/platform/src/modules/layout/header-project-list/project-edit.service.tsx b/apps/platform/src/modules/layout/header-project-list/project-edit.service.tsx index c9fbd23..2c2dda1 100644 --- a/apps/platform/src/modules/layout/header-project-list/project-edit.service.tsx +++ b/apps/platform/src/modules/layout/header-project-list/project-edit.service.tsx @@ -20,6 +20,7 @@ export class ProjectEditService extends Model { runAllToolTip: '', recordStoptaskDisabled: false, pipelineEditDisabled: false, + submitModelDisabled: false, }; changeCanEditTrue = () => { @@ -33,6 +34,7 @@ export class ProjectEditService extends Model { runAllToolTip: '', recordStoptaskDisabled: true, pipelineEditDisabled: true, + submitModelDisabled: true, }; }; @@ -47,6 +49,7 @@ export class ProjectEditService extends Model { runAllToolTip: '', recordStoptaskDisabled: false, pipelineEditDisabled: false, + submitModelDisabled: false, }; }; @@ -84,4 +87,6 @@ type CanEditType = { recordStoptaskDisabled: boolean; /** pipeline edit */ pipelineEditDisabled: boolean; + /** 画布提交模型 */ + submitModelDisabled: boolean; }; diff --git a/apps/platform/src/modules/layout/model-submission-layout/index.less b/apps/platform/src/modules/layout/model-submission-layout/index.less new file mode 100644 index 0000000..7638bc2 --- /dev/null +++ b/apps/platform/src/modules/layout/model-submission-layout/index.less @@ -0,0 +1,101 @@ +.wrap { + overflow: hidden; + width: 100%; + height: 100%; +} + +.header { + display: flex; + width: 100%; + height: 56px; + align-items: center; + padding-left: 16px; + border-bottom: 1px solid #eaebed; + + .back { + display: flex; + width: 24px; + height: 24px; + align-items: center; + justify-content: center; + border-radius: 50%; + cursor: pointer; + + &:hover { + background-color: #eee; + } + } + + .title { + color: #1d2129; + font-size: 20px; + font-weight: 500; + } + + .slot { + margin-left: 16px; + } +} + +.content { + display: flex; + width: 100%; + height: calc(100% - 56px); + box-sizing: border-box; + + .center { + position: relative; + width: 100%; + height: 100%; + + .header { + display: flex; + width: 100%; + height: 50px; + box-sizing: border-box; + align-items: center; + justify-content: space-between; + padding: 0 16px; + background-color: #f6f8fa; + + .right { + display: flex; + height: 100%; + align-items: center; + padding-left: 320px; + + :global(.ant-btn-link) { + color: rgb(0 0 0 / 88%); + font-size: 12px; + } + } + + .left { + display: flex; + align-items: center; + color: rgb(0 0 0 / 85%); + font-size: 14px; + } + } + + .graph { + width: 100%; + height: calc(100% - 42px); + } + + .graphContent { + width: calc(100% - 560px) !important; + } + + .toolbutton { + position: absolute; + right: 20px; + bottom: 36px; + } + } + + .alert { + height: 36px; + margin-left: 12px; + } +} diff --git a/apps/platform/src/modules/layout/model-submission-layout/index.tsx b/apps/platform/src/modules/layout/model-submission-layout/index.tsx new file mode 100644 index 0000000..61ac422 --- /dev/null +++ b/apps/platform/src/modules/layout/model-submission-layout/index.tsx @@ -0,0 +1,145 @@ +import { ArrowLeftOutlined } from '@ant-design/icons'; +import { Divider, Button, Alert, Tooltip } from 'antd'; +import classNames from 'classnames'; +import { history } from 'umi'; + +import { Platform } from '@/components/platform-wrapper'; +import { Log } from '@/modules/dag-log/log-viewer.view'; +import { DagLogDrawer } from '@/modules/dag-log/log.drawer.layout'; +import { DagLog } from '@/modules/dag-log/log.view'; +import { DefaultModalManager } from '@/modules/dag-modal-manager'; +import { ModalWidth } from '@/modules/dag-modal-manager/modal-manger-protocol'; +import { + PipelineTitleComponent, + SubmissionDrawer, +} from '@/modules/dag-model-submission'; +import { ModelSubmissionDrawerItem } from '@/modules/dag-model-submission/submission-drawer'; +import { SubmitGraphComponent } from '@/modules/dag-submit/graph'; +import { ToolbuttonComponent } from '@/modules/dag-submit/toolbutton'; +import { LoginService } from '@/modules/login/login.service'; +import { Model, useModel, getModel } from '@/util/valtio-helper'; + +import styles from './index.less'; +import { SubmissionDrawerService } from '@/modules/dag-model-submission/submission-service'; + +const RIGHT_DIST = 20; + +export const ModelSubmissionLayout = () => { + const viewInstance = useModel(ModelSubmissionView); + const loginService = useModel(LoginService); + + const goBack = async () => { + viewInstance.submissionDrawerService.cancelSubmitTimer(); + const userInfo = await loginService.getUserInfo(); + if (userInfo.platformType === Platform.AUTONOMY) { + history.push(`/edge?nodeId=${userInfo.ownerId}`); + } else { + history.push('/home?tab=project-management'); + } + }; + + return ( +
+
+ + + + + + + 项目空间 + +
+
+
+
+
+ + + 点击画布中的模型组件可提交模型,再次点击模型组件 + 后取消选择 + + } + closable + type="info" + showIcon + /> +
+
+ + + +
+
+
+ +
+
+ +
+
+
+ + + + + + +
+ ); +}; + +export class ModelSubmissionView extends Model { + modalManager = getModel(DefaultModalManager); + submissionDrawerService = getModel(SubmissionDrawerService); + + constructor() { + super(); + this.modalManager.onModalsChanged(() => { + const status = this.modalManager.modals; + if (status[ModelSubmissionDrawerItem.id].visible) { + return (this.rightModalSize = + RIGHT_DIST + ModalWidth[ModelSubmissionDrawerItem.id]); + } + if (!status[ModelSubmissionDrawerItem.id].visible) { + return (this.rightModalSize = RIGHT_DIST); + } + }); + } + + rightModalWidth = 320 - 42; + rightModalVisible = false; + + rightModalSize = RIGHT_DIST; + + openSubmissionDrawer = () => { + this.rightModalVisible = true; + this.modalManager.openModal(ModelSubmissionDrawerItem.id); + }; + + closeSubmissionDrawer() { + this.modalManager.closeModal(ModelSubmissionDrawerItem.id); + this.rightModalVisible = false; + } +} diff --git a/apps/platform/src/modules/main-dag/graph-request-service.tsx b/apps/platform/src/modules/main-dag/graph-request-service.tsx index 1f2baaa..07d577f 100644 --- a/apps/platform/src/modules/main-dag/graph-request-service.tsx +++ b/apps/platform/src/modules/main-dag/graph-request-service.tsx @@ -1,6 +1,7 @@ import type { GraphModel, GraphNode } from '@secretflow/dag'; import { NodeStatus } from '@secretflow/dag'; import { DefaultRequestService } from '@secretflow/dag'; +import { Emitter } from '@secretflow/utils'; import { message, Image as AntdImage } from 'antd'; import { parse } from 'query-string'; @@ -69,6 +70,12 @@ export class GraphRequestService extends DefaultRequestService { componentConfigRegistry = getModel(ComponentConfigRegistry); componentConfigService = getModel(DefaultComponentConfigService); + onNodeStatusChangedEmitter = new Emitter(); + onNodeStatusChanged = this.onNodeStatusChangedEmitter.on; + + onNodeChangedEmitter = new Emitter(); + onNodeChanged = this.onNodeChangedEmitter.on; + async queryStatus(dagId: string) { const { data } = await listGraphNodeStatus({ projectId: getProjectId(), @@ -87,6 +94,11 @@ export class GraphRequestService extends DefaultRequestService { if (isAllNodeStatusSuccess) { this.logDagSuccess(); } + // 将状态传过去,用于判断模型提交按钮能否置灰 + this.onNodeStatusChangedEmitter.fire({ + nodes, + finished, + }); return { nodeStatus: @@ -181,9 +193,11 @@ export class GraphRequestService extends DefaultRequestService { edges: convertedEdges as IGraphEdgeType[], }; + // 将节点数据传过去,用于判断模型提交按钮能否置灰 + this.onNodeChangedEmitter.fire(convertedNodes as IGraphNodeType[]); + this.graphData = convertedData; this.graphUpdated = false; - return convertedData; } diff --git a/apps/platform/src/modules/main-dag/model-submission-entry.tsx b/apps/platform/src/modules/main-dag/model-submission-entry.tsx new file mode 100644 index 0000000..3327faa --- /dev/null +++ b/apps/platform/src/modules/main-dag/model-submission-entry.tsx @@ -0,0 +1,82 @@ +import { Button, Tooltip } from 'antd'; +import { useEffect } from 'react'; +import { history } from 'umi'; + +import { Model, useModel } from '@/util/valtio-helper'; + +import mainDag from './dag'; +import type { IGraphNodeType } from './graph.protocol'; +import styles from './index.less'; +import { Platform, hasAccess } from '@/components/platform-wrapper'; +import { ProjectEditService } from '@/modules/layout/header-project-list/project-edit.service'; + +export const ModelSubmissionEntry = () => { + const viewInstance = useModel(ModelSubmissionEntryView); + const projectEditService = useModel(ProjectEditService); + + const { disabled } = viewInstance; + + useEffect(() => { + const { nodes = [] } = viewInstance.statusObj; + const modelNodes = (viewInstance.nodes || []).filter( + (item) => item.nodeDef.domain === 'ml.train', + ); + viewInstance.disabled = !modelNodes.some( + (modelNode) => + nodes.find((n) => n.graphNodeId === modelNode.id)?.status === 'SUCCEED', + ); + }, [viewInstance.nodes, viewInstance.statusObj]); + + const isP2P = hasAccess({ type: [Platform.AUTONOMY] }); + + return ( +
+ + + +
+ ); +}; + +export class ModelSubmissionEntryView extends Model { + constructor() { + super(); + mainDag.requestService.onNodeStatusChanged(this.onNodeStatusChanged.bind(this)); + mainDag.requestService.onNodeChanged(this.onNodeChanged.bind(this)); + } + + disabled = true; + + nodes: IGraphNodeType[] = []; + + statusObj: API.GraphStatus = { nodes: [], finished: true }; + + onNodeStatusChanged = (statusObj: API.GraphStatus) => { + this.statusObj = statusObj; + }; + + onNodeChanged = (data: IGraphNodeType[]) => { + this.nodes = data; + }; +} diff --git a/apps/platform/src/modules/model-manager/actions.tsx b/apps/platform/src/modules/model-manager/actions.tsx new file mode 100644 index 0000000..4370e8d --- /dev/null +++ b/apps/platform/src/modules/model-manager/actions.tsx @@ -0,0 +1,11 @@ +import { ModelStatus } from './types'; + +export const ModelStatusSelectOptions = [ + { value: '', label: '全部状态' }, + { value: ModelStatus.PENDING, label: '待发布' }, + { value: ModelStatus.PUBLISHING, label: '发布中' }, + { value: ModelStatus.PUBLISHED, label: '已发布' }, + { value: ModelStatus.FAILED, label: '发布失败' }, + { value: ModelStatus.OFFLINE, label: '已下线' }, + { value: ModelStatus.DISCARDED, label: '已废弃' }, +]; diff --git a/apps/platform/src/modules/model-manager/index.less b/apps/platform/src/modules/model-manager/index.less new file mode 100644 index 0000000..c7e15a4 --- /dev/null +++ b/apps/platform/src/modules/model-manager/index.less @@ -0,0 +1,31 @@ +.modelListContainer { + height: calc(100% - 89px); + padding: 16px 32px; + background: #f7f8fa; + + .content { + overflow: auto; + width: 100%; + height: 100%; + // height: 730px; + // height: 100%; + border-radius: 8px; + background: #fff; + } + + .header { + display: flex; + justify-content: space-between; + padding: 16px 24px; + } + + .table { + padding: 0 24px; + } +} + +.optionsBtns { + :global(.ant-btn) { + padding: 0 !important; + } +} diff --git a/apps/platform/src/modules/model-manager/index.tsx b/apps/platform/src/modules/model-manager/index.tsx new file mode 100644 index 0000000..9b56686 --- /dev/null +++ b/apps/platform/src/modules/model-manager/index.tsx @@ -0,0 +1,352 @@ +import { SearchOutlined } from '@ant-design/icons'; +import { + Space, + Input, + Select, + Button, + Table, + Badge, + Typography, + Popconfirm, + BadgeProps, +} from 'antd'; +import { parse } from 'query-string'; +import { useEffect } from 'react'; +import { useLocation } from 'umi'; + +import { formatTimestamp } from '@/modules/dag-result/utils'; +import { Model, useModel } from '@/util/valtio-helper'; + +import { ModelReleaseInfoModal } from './model-info/model-info.view'; +import { ModelReleaseModal } from './model-release/model-release.view'; +import { ModelService } from './model-service'; +import { ModelStatus } from './types'; +import { ModelStatusSelectOptions } from './actions'; + +import styles from './index.less'; +import { Platform, hasAccess } from '@/components/platform-wrapper'; +import { ModelDetailModal } from './model-detail/model-detail.view'; +import { LoginService } from '@/modules/login/login.service'; + +const statusMap = { + [ModelStatus.PENDING]: { status: 'warning', text: '待发布' }, + [ModelStatus.PUBLISHED]: { status: 'success', text: '已发布' }, + [ModelStatus.OFFLINE]: { status: 'default', text: '已下线' }, + [ModelStatus.DISCARDED]: { status: 'default', text: '已废弃' }, + [ModelStatus.FAILED]: { status: 'error', text: '发布失败' }, + [ModelStatus.PUBLISHING]: { status: 'processing', text: '发布中' }, +}; + +const showReleaseBtn = [ModelStatus.PENDING, ModelStatus.OFFLINE, ModelStatus.FAILED]; +const showDiscardBtn = [ModelStatus.PENDING, ModelStatus.OFFLINE, ModelStatus.FAILED]; +const showOfflineBtn = [ModelStatus.PUBLISHED, ModelStatus.PUBLISHING]; +const showDeleteBtn = [ModelStatus.DISCARDED]; +const showServiceBtn = [ModelStatus.PUBLISHED, ModelStatus.OFFLINE]; +const showNotBtn: ModelStatus[] = []; + +const { Link } = Typography; + +export const ModelListComponent = () => { + const viewInstance = useModel(ModelListView); + const modelService = useModel(ModelService); + const loginService = useModel(LoginService); + + const { search } = useLocation(); + const { projectId } = parse(search) as { projectId: string }; + const isP2p = hasAccess({ type: [Platform.AUTONOMY] }); + + const isOwner = (ownerId: string) => { + return ownerId === loginService.userInfo?.ownerId; + }; + + useEffect(() => { + if (projectId) { + modelService.getModelList(); + modelService.getListProject(projectId, isP2p); + } + }, [projectId]); + + const renderButtons = (record: API.ModelPackVO) => { + return ( + + {showReleaseBtn.includes(record.modelStats as ModelStatus) && ( + + )} + {showDiscardBtn.includes(record.modelStats as ModelStatus) && ( + { + await modelService.discard(record.modelId!); + await modelService.getModelList(); + }} + okText="废弃" + cancelText="取消" + > + + + )} + {showOfflineBtn.includes(record.modelStats as ModelStatus) && ( + { + await modelService.setOffline(record.servingId!); + await modelService.getModelList(); + }} + okText="下线" + cancelText="取消" + > + + + )} + {showDeleteBtn.includes(record.modelStats as ModelStatus) && ( + { + await modelService.delete(record.modelId!); + await modelService.getModelList(); + }} + okText="删除" + cancelText="取消" + okButtonProps={{ + danger: true, + ghost: true, + }} + > + + + )} + {showServiceBtn.includes(record.modelStats as ModelStatus) && ( + + )} + {showNotBtn.includes(record.modelStats as ModelStatus) && ( + + )} + + ); + }; + + const columns = [ + { + title: '模型名称', + dataIndex: 'modelName', + key: 'modelName', + ellipsis: true, + render: (text: string, record: API.ModelPackVO) => { + return ( + { + viewInstance.setModelReleaseInfo(record); + viewInstance.setModelDetailVisible(true); + }} + > + {text} + + ); + }, + }, + { + title: '模型ID', + dataIndex: 'modelId', + key: 'modelId', + ellipsis: true, + }, + { + title: '模型描述', + dataIndex: 'modelDesc', + key: 'modelDesc', + ellipsis: true, + render: (text: string) => {text}, + }, + { + title: '发布状态', + dataIndex: 'modelStats', + key: 'modelStats', + ellipsis: true, + render: (text: string, record: API.ModelPackVO) => ( + + ), + }, + { + title: '提交时间', + dataIndex: 'gmtCreate', + key: 'gmtCreate', + sorter: true, + ellipsis: true, + render: (gmtCreate: string) => ( + + {formatTimestamp(gmtCreate as string)} + + ), + }, + { + title: '操作', + key: 'actions', + render: (_: string, record: API.ModelPackVO) => { + return renderButtons(record); + }, + }, + ]; + + return ( +
+
+
+ + modelService.searchModel(e)} + style={{ width: 200 }} + suffix={ + + } + /> + + handleFeatureOnlineChange(fieldKey, value, record)} + /> + ); + }, + }, + ]; + + return ( + + ); +}; diff --git a/apps/platform/src/modules/model-manager/model-release/index.less b/apps/platform/src/modules/model-manager/model-release/index.less new file mode 100644 index 0000000..3049c9b --- /dev/null +++ b/apps/platform/src/modules/model-manager/model-release/index.less @@ -0,0 +1,142 @@ +.toggleBtnDisabled { + color: #d9d9d9; + opacity: 0.4; +} + +.featuresTitle { + margin-bottom: 8px; + color: rgb(0 0 0 / 88%); + font-size: 14px; + font-weight: 500; + line-height: 22px; +} + +.emptyFeature { + display: flex; + height: 158px; + align-items: center; + justify-content: center; + background-color: rgb(0 0 0 / 2%); +} + +.tableHeader { + display: flex; + width: 100%; + height: 36px; + box-sizing: border-box; + padding: 8px 0; + background: rgb(0 0 0 / 2%); + box-shadow: inset 0 -1px 0 0 #e8e9ea; + color: rgb(0 0 0 / 85%); + font-size: 12px; + font-weight: 500; + line-height: 20px; + + .tableHeaderNode { + width: 178px; + border-right: 1px solid rgb(0 10 26 / 7%); + margin-left: 32px; + } + + .tableHeaderService { + width: 247px; + box-sizing: border-box; + padding-left: 12px; + border-right: 1px solid rgb(0 10 26 / 7%); + } + + .tableHeaderStatus { + width: 90px; + box-sizing: border-box; + padding-left: 12px; + border-right: 1px solid rgb(0 10 26 / 7%); + } + + .tableHeaderOptions { + padding-left: 12px; + } +} + +.featuresList { + width: 100%; + padding: 2px 0; + box-shadow: inset 0 -1px 0 0 #e8e9ea; + + .featuresListContent { + display: flex; + gap: 8px; + + :global(.ant-form-item) { + margin-bottom: 0; + } + } +} + +.formToggle { + width: 16px; + margin-left: 8px; +} + +.formNode { + width: 168px !important; + margin-right: 16px; +} + +.formService { + width: 222px !important; + margin-right: 16px; +} + +.tableHeaderError { + margin-top: -6px; + margin-left: 16px; +} + +// ------- common.tsx +.intoHeader { + margin-left: 24px; + color: rgb(0 0 0 / 85%); + font-size: 12px; + font-weight: 500; + line-height: 20px; +} + +.onlineHeader { + display: flex; + align-items: center; + + .features { + margin-right: 12px; + color: rgb(0 0 0 / 85%); + font-size: 12px; + font-weight: 500; + line-height: 20px; + } + + .checkBoxLabel { + color: rgb(0 0 0 / 85%); + font-size: 12px; + font-weight: 500; + line-height: 22px; + } +} + +.featureOnline { + width: 222px !important; +} + +.featureOnlineError { + :global(.ant-select-selector) { + border: 1px solid red !important; + } +} + +.mockStyle { + display: flex; + align-items: center; + gap: 8px; + + :global(.ant-tag) { + scale: 0.9; + } +} diff --git a/apps/platform/src/modules/model-manager/model-release/model-release.service.ts b/apps/platform/src/modules/model-manager/model-release/model-release.service.ts new file mode 100644 index 0000000..7bbc5a0 --- /dev/null +++ b/apps/platform/src/modules/model-manager/model-release/model-release.service.ts @@ -0,0 +1,92 @@ +import API from '@/services/secretpad'; +import { Model } from '@/util/valtio-helper'; +import { parse } from 'query-string'; +import { ModelStatus } from '../types'; + +export class ModelReleaseService extends Model { + loading = false; + + // 预测节点 + // 根据模型获取节点 + predictionNodes: Array = []; + + // 入模特征 + // 根据节点获取入模特征 + intoFeatures: Record = {}; + + // 特征服务表 + // 根据节点获取特征服务表 + featureServicesSheets: { label: string; value: string }[][] = []; + + // 在线特征 key: node-sheetId + // 根据特征服务表获取在线特征 + onlineFeatures: Record = {}; + + // 获取预测节点列表 + modelList: API.ModelPackVO[] = []; + + getCanSubmitModelList = async () => { + const { projectId } = parse(window.location.search); + if (!projectId) return []; + const info = await API.ModelManagementController.modelPackPage({ + projectId: projectId as string, + page: 1, + size: 1000, + }); + if (info.status && info.status.code === 0 && info.data) { + const canSubmitModelStatus = [ + ModelStatus.PENDING, + ModelStatus.OFFLINE, + ModelStatus.FAILED, + ]; + this.modelList = + info.data.modelPacks?.filter((item) => + canSubmitModelStatus.includes(item.modelStats as ModelStatus), + ) || []; + } + }; + + getPredictionNodes = async (modelId: string) => { + const { projectId } = parse(window.location.search); + if (!projectId) return []; + this.loading = true; + const { status, data } = await API.ModelManagementController.modelPackDetail({ + modelId, + projectId: projectId as string, + }); + this.loading = false; + if (status && status.code === 0 && data) { + this.predictionNodes = data?.parties || []; + (data?.parties || []).forEach((item) => { + if (!item.nodeId) return; + this.intoFeatures[item.nodeId] = item?.columns || []; + }); + } else { + this.predictionNodes = []; + } + }; + + getFeatureServices = async (nodeId: string, key: number) => { + const { projectId } = parse(window.location.search); + if (!projectId) return []; + const { status, data } = + await API.FeatureDatasourceController.projectFeatureTableList({ + nodeId: nodeId, + projectId: projectId as string, + }); + if (status && status.code === 0 && data) { + /** 设置特征服务 */ + this.featureServicesSheets[key] = data.map((item) => ({ + label: item.featureTableName!, + value: item.featureTableId!, + })) as { label: string; value: string }[]; + + /** 设置节点的特征服务下面的在线特征 */ + data.forEach((item) => { + const id = `${nodeId}_${item.featureTableId}`; + const newColumns = (item?.columns || []).map((feature) => feature.colName!); + this.onlineFeatures[id] = newColumns; + }); + } + }; +} diff --git a/apps/platform/src/modules/model-manager/model-release/model-release.view.tsx b/apps/platform/src/modules/model-manager/model-release/model-release.view.tsx new file mode 100644 index 0000000..12f3b29 --- /dev/null +++ b/apps/platform/src/modules/model-manager/model-release/model-release.view.tsx @@ -0,0 +1,409 @@ +import { Form, Drawer, Button, Space, Select, Empty, Row, Spin, Tag } from 'antd'; +import { useEffect } from 'react'; +import { useState } from 'react'; + +import { useModel } from '@/util/valtio-helper'; + +import { ModelService } from '../model-service'; + +import type { FeaturesItem } from './common'; +import { FeatureTable, MatchTag, ToggleButton } from './common'; +import { ModelReleaseService } from './model-release.service'; +import { Platform, hasAccess } from '@/components/platform-wrapper'; +import { LoginService } from '@/modules/login/login.service'; +import { parse } from 'query-string'; + +import styles from './index.less'; + +type ModelReleaseModalType = { + visible: boolean; + close: () => void; + modelId?: string; + data?: Record; + onOk?: () => void; +}; + +export const ModelReleaseModal = (props: ModelReleaseModalType) => { + const { visible, modelId, close } = props; + const { projectId } = parse(window.location.search); + const [form] = Form.useForm(); + const modelSelect = Form.useWatch('model', form); + const featuresValue = Form.useWatch('features', form); + const submitDisabled = (featuresValue || []).every( + (item: { status: string }) => item.status === 'success', + ); + const modelService = useModel(ModelService); + const modelReleaseService = useModel(ModelReleaseService); + const loginService = useModel(LoginService); + const isP2p = hasAccess({ type: [Platform.AUTONOMY] }); + + const [featuresItemsObj, setFeatureItemsObj] = useState< + Record + >({}); + + const modelListFilter = isP2p + ? modelReleaseService.modelList.filter( + (item) => item.ownerId === loginService.userInfo?.ownerId, + ) + : modelReleaseService.modelList; + + const modelOptions = modelListFilter.map((item) => ({ + value: item.modelId, + label: item.modelName, + })); + + const nodeOptions = modelReleaseService.predictionNodes.map((item) => ({ + value: item.nodeId, + label: item.nodeName, + })); + + const nodeOptionsFilter = nodeOptions.map((item) => { + if ((featuresValue || []).some((v) => v.node === item.value)) { + return { + ...item, + disabled: true, + }; + } else { + return { + ...item, + disabled: false, + }; + } + }); + + const mockOptions = [ + { + label: ( +
+ mock服务 + Mock +
+ ), + value: 'mock', + key: 'custom-mock', + }, + ]; + + const getPredictionNodes = async (id: string) => { + if (!id) return; + modelReleaseService.loading = true; + await modelReleaseService.getPredictionNodes(id); + modelReleaseService.loading = false; + // 设置初始状态 + const initFormFeature = { + toggle: false, + toggleDisabled: true, + node: undefined, + featureService: undefined, + status: 'default', + matchDisabled: true, + }; + const InitFormlist = modelReleaseService.predictionNodes.map(() => initFormFeature); + form.setFieldValue( + 'features', + InitFormlist.length !== 0 ? InitFormlist : [initFormFeature], + ); + setFeatureItemsObj({}); + }; + + useEffect(() => { + if (!visible) return; + if (modelId) { + form.setFieldValue('model', modelId); + } + if (modelSelect) { + getPredictionNodes(modelSelect); + } + }, [visible, modelSelect]); + + useEffect(() => { + if (!visible) return; + // 获取模型列表 + const getCanSubmitModels = async () => { + await modelReleaseService.getCanSubmitModelList(); + }; + getCanSubmitModels(); + }, [visible]); + + const handleClose = () => { + modelService.submitLoading = false; + close(); + }; + + const handleOk = () => { + form.validateFields().then(async (value) => { + if (!projectId) return; + const partyConfigs = value.features.map( + (item: { node: string; featureService: string }, index: number) => { + const features = (featuresItemsObj?.[index] || []).map((feature) => ({ + offlineName: feature.into, + onlineName: feature.online, + })); + return { + nodeId: item.node, + featureTableId: item.featureService, + isMock: item.featureService === 'mock', + features, + }; + }, + ); + const paramas = { + modelId: value.model, + projectId: projectId as string, + partyConfigs: partyConfigs, + }; + await modelService.publish(paramas); + close(); + await modelService.getModelList(); + }); + }; + + // 预测节点 + const handleNodeChange = async (value: string, key: number) => { + await modelReleaseService.getFeatureServices(value, key); + const featuresItem = (modelReleaseService.intoFeatures[value] || []).map( + (item: string) => ({ into: item, online: undefined }), + ); + setFeatureItemsObj((prev) => ({ ...prev, [key]: featuresItem })); + form.setFieldsValue({ + features: { + [key]: { + featureService: undefined, + status: 'default', + toggle: false, + toggleDisabled: true, + matchDisabled: true, + }, + }, + }); + }; + + // 特征服务 + const handleFeatureServiceChange = async (value: string, key: number) => { + // 如果是 mock, 自动匹配成功 + if (value === 'mock') { + const newFeatureItem = (featuresItemsObj[key] || []).map((item) => ({ + into: item.into, + online: item.into, + })); + form.setFieldsValue({ + features: { + [key]: { + toggle: false, + toggleDisabled: false, + matchDisabled: true, + status: 'success', + }, + }, + }); + setFeatureItemsObj((prev: Record) => ({ + ...prev, + [key]: newFeatureItem, + })); + } else { + // 修改,进行同名匹配 + handleMatch(key, value); + } + }; + + // 同名匹配 + const handleMatch = (key: number, featureService: string) => { + const nodeId = featuresValue[key].node; + const id = `${nodeId}_${featureService}`; + const arr = modelReleaseService.onlineFeatures[id] || []; + const featuresItem = featuresItemsObj[key] || []; + + // 同名设置,不同名设置undefined + const errorLength: number[] = []; + const newFeatureItem = featuresItem.map((item: FeaturesItem, index: number) => { + if (arr.includes(item.into)) { + return { + into: item.into, + online: item.into, + }; + } else { + errorLength.push(index); + return { + into: item.into, + online: undefined, + }; + } + }); + // 在这一步应该计算出匹配成功,匹配失败,以及能不能点击匹配,以及自动展开 + form.setFieldsValue({ + features: { + [key]: { + toggle: errorLength.length === 0 ? false : true, + toggleDisabled: false, + matchDisabled: false, + status: errorLength.length === 0 ? 'success' : 'error', + }, + }, + }); + setFeatureItemsObj((prev: Record) => ({ + ...prev, + [key]: newFeatureItem, + })); + }; + + // 设置在线特征的select options + const getOnlineFeaturesOptions = (key: number) => { + const nodeId = featuresValue?.[key]?.node; + if (!nodeId) return; + const featureServiceId = featuresValue[key].featureService; + const id = `${nodeId}_${featureServiceId}`; + const arr = (modelReleaseService.onlineFeatures[id] || []).map((item) => ({ + label: item, + value: item, + })); + return arr; + }; + + return ( + + + + + } + > +
+ + handleNodeChange(value, field.key)} + allowClear + /> + + +