diff --git a/taier-ui/src/components/dataSync/index.tsx b/taier-ui/src/components/dataSync/index.tsx index 1907e6de1a..261fb4924b 100644 --- a/taier-ui/src/components/dataSync/index.tsx +++ b/taier-ui/src/components/dataSync/index.tsx @@ -295,7 +295,16 @@ function DataSync({ current }: molecule.model.IEditor) { }; const handleSaveTab = () => { - saveTask(); + saveTask() + .then((res) => res?.data?.id) + .then((id) => { + if (id !== undefined) { + molecule.editor.updateTab({ + id: current!.tab!.id, + status: undefined, + }); + } + }); }; // 获取当前任务的数据 diff --git a/taier-ui/src/components/folderPicker/index.tsx b/taier-ui/src/components/folderPicker/index.tsx index 108ca533db..0e70ecc1f1 100644 --- a/taier-ui/src/components/folderPicker/index.tsx +++ b/taier-ui/src/components/folderPicker/index.tsx @@ -26,7 +26,7 @@ import molecule from '@dtinsight/molecule'; import resourceManagerTree from '@/services/resourceManagerService'; import functionManagerService from '@/services/functionManagerService'; import type { TreeSelectProps } from 'antd/lib/tree-select'; -import { loadTreeNode } from '@/utils/extensions'; +import { catalogueService } from '@/services'; interface FolderPickerProps extends CustomTreeSelectProps { dataType: CATELOGUE_TYPE; @@ -35,9 +35,12 @@ interface FolderPickerProps extends CustomTreeSelectProps { export default function FolderPicker(props: FolderPickerProps) { const [flag, rerender] = useState(false); const loadDataAsync: TreeSelectProps['loadData'] = async (treeNode) => { - const currentData = treeNode.props.dataRef.data; + const currentData = treeNode.props.dataRef; if (!currentData.children?.length) { - await loadTreeNode(currentData, props.dataType); + await catalogueService.loadTreeNode( + { id: currentData?.data?.id, catalogueType: currentData?.data?.catalogueType }, + props.dataType, + ); rerender((f) => !f); } }; diff --git a/taier-ui/src/components/task/editFolder.tsx b/taier-ui/src/components/task/editFolder.tsx index a72a91cff4..edfd4769a0 100644 --- a/taier-ui/src/components/task/editFolder.tsx +++ b/taier-ui/src/components/task/editFolder.tsx @@ -99,10 +99,11 @@ export default connect( rules={[ { max: 64, - message: '任务名称不得超过20个字符!', + message: '目录名称不得超过64个字符!', }, { required: true, + message: '文件夹名称不能为空', }, ]} > diff --git a/taier-ui/src/constant/index.ts b/taier-ui/src/constant/index.ts index 8381d41fc4..d1d0c69fa1 100644 --- a/taier-ui/src/constant/index.ts +++ b/taier-ui/src/constant/index.ts @@ -23,6 +23,10 @@ import type { ISubMenuProps } from '@dtinsight/molecule/esm/components'; * ID 集合 */ export enum ID_COLLECTIONS { + /** + * 创建任务 + */ + TASK_CREATE_ID = 'task.create', /** * 任务运行按钮 */ diff --git a/taier-ui/src/extensions/catalogue/index.ts b/taier-ui/src/extensions/catalogue/index.ts index ac02961e4c..aad7cd04ed 100644 --- a/taier-ui/src/extensions/catalogue/index.ts +++ b/taier-ui/src/extensions/catalogue/index.ts @@ -16,68 +16,9 @@ * limitations under the License. */ -import { CATELOGUE_TYPE } from '@/constant'; -import functionManagerService from '@/services/functionManagerService'; -import resourceManagerService from '@/services/resourceManagerService'; -import { transformCatalogueToTree, loadTreeNode, getCatalogueViaNode } from '@/utils/extensions'; -import molecule from '@dtinsight/molecule'; import type { UniqueId } from '@dtinsight/molecule/esm/common/types'; import type { IExtension } from '@dtinsight/molecule/esm/model'; -import { getRootFolderViaSource } from '@/utils'; - -/** - * 获取根目录 - */ -export function getCatalogueTree() { - getCatalogueViaNode({ id: 0 }).then(async (res) => { - if (!res) return; - const { children } = res; - if (!children) return; - - const taskData = getRootFolderViaSource(children, CATELOGUE_TYPE.TASK); - const resourceData = getRootFolderViaSource(children, CATELOGUE_TYPE.RESOURCE); - const funcData = getRootFolderViaSource(children, CATELOGUE_TYPE.FUNCTION); - - // 资源根目录 - if (resourceData) { - const resourceRoot = resourceData; - const resourceNode = transformCatalogueToTree( - resourceRoot, - CATELOGUE_TYPE.RESOURCE, - true, - )!; - resourceManagerService.add(resourceNode); - } - - // 函数根目录 - if (funcData) { - const funcRoot = funcData; - const functionNode = transformCatalogueToTree(funcRoot, CATELOGUE_TYPE.FUNCTION, true)!; - functionManagerService.add(functionNode); - - // sql 节点必存在 catalogueType,对所有的节点都求一遍子树 - const SqlNodes = funcRoot?.children?.filter((child: any) => child.catalogueType) || []; - SqlNodes.forEach((sqlNode) => { - loadTreeNode(sqlNode, CATELOGUE_TYPE.FUNCTION); - }); - } - - // 任务开发根目录 - if (taskData) { - const taskRootFolder = taskData?.children?.[0]; - if (taskRootFolder) { - const taskNode = transformCatalogueToTree( - taskRootFolder, - CATELOGUE_TYPE.TASK, - true, - )!; - - molecule.folderTree.add(taskNode); - loadTreeNode(taskRootFolder, CATELOGUE_TYPE.TASK); - } - } - }); -} +import { catalogueService } from '@/services'; /** * This is for getting the root catalogues including the resouces and tasks and functions @@ -86,7 +27,7 @@ export default class CatalogueExtension implements IExtension { id: UniqueId = 'Catalogue'; name: string = 'Catalogue'; activate(): void { - getCatalogueTree(); + catalogueService.loadRootFolder(); } dispose(): void { throw new Error('Method not implemented.'); diff --git a/taier-ui/src/extensions/folderTree/index.tsx b/taier-ui/src/extensions/folderTree/index.tsx index b18d294432..0bc1beb509 100644 --- a/taier-ui/src/extensions/folderTree/index.tsx +++ b/taier-ui/src/extensions/folderTree/index.tsx @@ -25,44 +25,17 @@ import type { IFormFieldProps } from '@/components/task/open'; import Open from '@/components/task/open'; import EditFolder from '@/components/task/editFolder'; import DataSync from '@/components/dataSync'; -import { - transformCatalogueToTree, - loadTreeNode, - getCatalogueViaNode, - fileIcon, - getParentNode, -} from '@/utils/extensions'; +import { fileIcon, getParentNode } from '@/utils/extensions'; import api from '@/api'; import type { UniqueId } from '@dtinsight/molecule/esm/common/types'; -import { - CATELOGUE_TYPE, - MENU_TYPE_ENUM, - TASK_TYPE_ENUM, - CREATE_MODEL_TYPE, - ID_COLLECTIONS, -} from '@/constant'; -import type { CatalogueDataProps, IOfflineTaskProps } from '@/interface'; +import { CATELOGUE_TYPE, TASK_TYPE_ENUM, CREATE_MODEL_TYPE, ID_COLLECTIONS } from '@/constant'; +import type { IOfflineTaskProps } from '@/interface'; import { IComputeType } from '@/interface'; import { mappingTaskTypeToLanguage } from '@/utils/enums'; import StreamCollection from '@/components/streamCollection'; -import { editorActionBarService } from '@/services'; +import { breadcrumbService, catalogueService, editorActionBarService } from '@/services'; import { prettierJSONstring } from '@/utils'; - -/** - * Update task tree node - * @param data the back-end data - */ -function updateTree(data: Partial) { - return getCatalogueViaNode({ - id: data.parentId, - catalogueType: MENU_TYPE_ENUM.TASK_DEV, - }).then((treeData) => { - const nextNode = transformCatalogueToTree(treeData, CATELOGUE_TYPE.TASK); - if (nextNode) { - molecule.folderTree.update(nextNode); - } - }); -} +import notification from '@/components/notification'; /** * 实时采集和FlinkSql任务的computeType返回0 @@ -83,7 +56,7 @@ function openCreateTab(id?: string) { const onSubmit = (values: IFormFieldProps) => { const { syncModel } = values; return new Promise((resolve) => { - const params = { + const params: Record = { ...values, computeType: getComputeType(values.taskType), parentId: values.nodePid, @@ -101,10 +74,16 @@ function openCreateTab(id?: string) { if (!groupId) return; molecule.editor.closeTab(tabId, groupId); molecule.explorer.forceUpdate(); - // open this brand-new task - updateTree(data).then(() => { - openTaskInTab(data.id); - }); + + const parentNode = molecule.folderTree.get(`${params.parentId}-folder`); + if (parentNode) { + catalogueService + .loadTreeNode(parentNode.data, CATELOGUE_TYPE.TASK) + .then(() => { + // open this brand-new task + openTaskInTab(data.id); + }); + } } }) .finally(() => { @@ -121,6 +100,16 @@ function openCreateTab(id?: string) { modified: false, name: localize('create task', '新建任务'), icon: 'file-add', + breadcrumb: [ + { + id: catalogueService.getRootFolder(CATELOGUE_TYPE.TASK)!.id, + name: catalogueService.getRootFolder(CATELOGUE_TYPE.TASK)!.name, + }, + { + id: tabId, + name: localize('create task', '新建任务'), + }, + ], data: { // always create task under the root node or create task from context menu nodePid: id || folderTree.data?.[0].id, @@ -157,7 +146,7 @@ function init() { } // reload the parentNode - loadTreeNode(parentNode.data, CATELOGUE_TYPE.TASK).then(() => { + catalogueService.loadTreeNode(parentNode.data, CATELOGUE_TYPE.TASK).then(() => { // TODO: don't need it after fix the issue https://github.com/DTStack/molecule/issues/724 if (molecule.folderTree.getState().folderTree?.current?.id !== undefined) { document @@ -183,9 +172,14 @@ function initContextMenu() { }); molecule.folderTree.onRightClick((treeNode, menu) => { if (!treeNode.isLeaf) { + // remove rename action + const idx = menu.findIndex( + (m) => m.id === molecule.builtin.getConstants().RENAME_COMMAND_ID, + ); + menu.splice(idx, 1); // insert these menus into folder context - menu.splice(0, 1, { id: 'task.create', name: '新建任务' }); - menu.splice(3, 0, { + menu.splice(0, 1, { id: ID_COLLECTIONS.TASK_CREATE_ID, name: '新建任务' }); + menu.splice(2, 0, { id: ID_COLLECTIONS.FOLDERTREE_CONTEXT_EDIT, name: '编辑', }); @@ -204,7 +198,7 @@ function initContextMenu() { } function createTask() { - molecule.folderTree.onCreate((type, id) => { + molecule.folderTree.onCreate(async (type, id) => { if (!id && !molecule.folderTree.getState().folderTree?.data?.length) { message.error('请先配置集群并进行绑定!'); return; @@ -231,84 +225,38 @@ function createTask() { } function editTreeNodeName() { - function renameFile(file: any) { - const { data, name } = file; - if (!name) { - updateTree({ - parentId: data.parentId, - }); - return; - } - api.saveOfflineJobData({ - ...data, - name, - // 标识位,false 表示只修改了当前任务文件属性,不涉及任务内容属性 - preSave: false, - }).then((res: any) => { - if (res.code === 1) { - updateTree({ - parentId: data.parentId, - }); - molecule.explorer.forceUpdate(); - } - }); - } - - // 创建文件夹 - function createFolder(file: IFolderTreeNodeProps) { + molecule.folderTree.onUpdateFileName((file) => { const { name, data: { parentId }, id, } = file; - if (!name) { + if (!name || name.length > 64) { + notification.error({ key: 'create', message: '目录名称不得超过64个字符且不为空!' }); + if (molecule.folderTree.get(id)) { + molecule.folderTree.remove(id); + } return; } const [nodePid] = parentId.split('-'); api.addOfflineCatalogue({ nodeName: name, nodePid, - }).then((res: any) => { + }).then((res) => { if (res.code === 1) { - updateTree({ parentId: nodePid }); + const parentNode = molecule.folderTree.get(parentId); + catalogueService.loadTreeNode( + { + id: parentNode?.data.id, + catalogueType: parentNode?.data.catalogueType, + }, + CATELOGUE_TYPE.TASK, + ); molecule.explorer.forceUpdate(); } else { molecule.folderTree.remove(id); } }); - } - - function renameFolder(file: any) { - const { - name, - data: { id, parentId }, - } = file; - if (!name) { - updateTree({ parentId }); - return; - } - api.editOfflineCatalogue({ - type: 'folder', - engineCatalogueType: 0, - id, - nodeName: name, - nodePid: parentId, - }).then((res: any) => { - if (res.code === 1) { - updateTree({ parentId: id }); - molecule.explorer.forceUpdate(); - } - }); - } - molecule.folderTree.onUpdateFileName((file) => { - const { fileType, id } = file; - if (fileType === 'File') { - renameFile(file); - } else if (`${id}`.startsWith(ID_COLLECTIONS.CREATE_FOLDER_PREFIX)) { - createFolder(file); - } else { - renameFolder(file); - } }); } @@ -331,7 +279,7 @@ export function openTaskInTab( return; } - const { id: fileId, location } = file; + const { id: fileId } = file; api.getOfflineTaskByID({ id: fileId }).then((res) => { const { success, data } = res as { success: boolean; data: IOfflineTaskProps }; if (success) { @@ -348,11 +296,7 @@ export function openTaskInTab( language: mappingTaskTypeToLanguage(data.taskType), }, icon: fileIcon(data.taskType, CATELOGUE_TYPE.TASK), - breadcrumb: - location?.split('/')?.map((item: string) => ({ - id: item, - name: item, - })) || [], + breadcrumb: breadcrumbService.getBreadcrumb(fileId), }; molecule.editor.open(tabData); break; @@ -370,11 +314,7 @@ export function openTaskInTab( taskDesc: data.taskDesc, }, icon: fileIcon(data.taskType, CATELOGUE_TYPE.TASK), - breadcrumb: - location?.split('/')?.map((item: string) => ({ - id: item, - name: item, - })) || [], + breadcrumb: breadcrumbService.getBreadcrumb(fileId), }; // 向导模式渲染数据同步任务,脚本模式渲染编辑器 @@ -399,11 +339,7 @@ export function openTaskInTab( value: prettierJSONstring(data.sqlText), }, icon: fileIcon(data.taskType, CATELOGUE_TYPE.TASK), - breadcrumb: - location?.split('/')?.map((item: string) => ({ - id: item, - name: item, - })) || [], + breadcrumb: breadcrumbService.getBreadcrumb(fileId), }; if (data.createModel === CREATE_MODEL_TYPE.GUIDE) { tabData.renderPane = () => ; @@ -425,11 +361,7 @@ export function openTaskInTab( language: mappingTaskTypeToLanguage(data.taskType), }, icon: fileIcon(data.taskType, CATELOGUE_TYPE.TASK), - breadcrumb: - location?.split('/')?.map((item: string) => ({ - id: item, - name: item, - })) || [], + breadcrumb: breadcrumbService.getBreadcrumb(fileId), }; molecule.editor.open(tabData); break; @@ -494,7 +426,7 @@ function onRemove() { function contextMenu() { molecule.folderTree.onContextMenu((menu, treeNode) => { switch (menu.id) { - case 'task.create': { + case ID_COLLECTIONS.TASK_CREATE_ID: { openCreateTab(treeNode!.data.id); break; } @@ -505,32 +437,43 @@ function contextMenu() { ? `${ID_COLLECTIONS.EDIT_TASK_PREFIX}_${new Date().getTime()}` : `${ID_COLLECTIONS.EDIT_FOLDER_PREFIX}_${new Date().getTime()}`; - const afterSubmit = (params: any, values: any) => { - // 更新旧结点所在的文件夹 - updateTree({ - parentId: treeNode!.data.parentId, - }); - - // 如果文件的位置发生了移动,则还需要更新新结点所在的文件夹 - if (treeNode!.data.parentId !== params.nodePid) { - updateTree({ - parentId: params.nodePid, - }); - } - - // 确保 editor 的 tab 的 id 和 tree 的 id 保持一致 - // 同步去更新 tab 的 name - const isOpened = molecule.editor.isOpened(treeNode!.id); - if (isOpened) { - molecule.editor.updateTab({ - id: treeNode!.id, - name: values.name, - }); - } + const afterSubmit = (params: Record) => { + // 等待更新的文件夹目录 + const pendingUpdateFolderId = new Set([ + // 当前节点变更之前所在的文件夹 + treeNode!.data.parentId, + // 当前节点变更后所在的文件夹 + params.nodePid, + ]); + + Promise.all( + Array.from(pendingUpdateFolderId).map((id) => { + const folderNode = molecule.folderTree.get(`${id}-folder`); + if (folderNode) { + return catalogueService.loadTreeNode( + { + id: folderNode.data?.id, + catalogueType: folderNode.data?.catalogueType, + }, + CATELOGUE_TYPE.TASK, + ); + } + return Promise.resolve(); + }), + ).then(() => { + const isOpened = molecule.editor.isOpened(params.id.toString()); + if (isOpened) { + molecule.editor.updateTab({ + id: params.id.toString(), + name: params.name as string, + breadcrumb: breadcrumbService.getBreadcrumb(params.id), + }); + } - // 关闭当前编辑的 tab - const groupId = molecule.editor.getGroupIdByTab(tabId)!; - molecule.editor.closeTab(tabId, groupId); + // 关闭当前编辑的 tab + const groupId = molecule.editor.getGroupIdByTab(tabId)!; + molecule.editor.closeTab(tabId, groupId); + }); }; const onSubmit = (values: any) => { @@ -544,10 +487,10 @@ function contextMenu() { preSave: false, }; api.addOfflineTask(params) - .then((res: any) => { + .then((res) => { if (res.code === 1) { message.success('编辑成功'); - afterSubmit(params, values); + afterSubmit(params); } }) .finally(() => { @@ -564,9 +507,10 @@ function contextMenu() { ...values, }; api.editOfflineCatalogue(params) - .then((res: any) => { + .then((res) => { if (res.code === 1) { - afterSubmit(params, values); + message.success('编辑成功'); + afterSubmit(params); } }) .finally(() => { @@ -575,6 +519,18 @@ function contextMenu() { }); }; + const breadcrumb = [ + { + id: catalogueService.getRootFolder(CATELOGUE_TYPE.TASK)!.id, + name: catalogueService.getRootFolder(CATELOGUE_TYPE.TASK)!.name, + }, + { + id: tabId, + name: isFile + ? localize('update task', '编辑任务') + : localize('update folder', '编辑文件夹'), + }, + ]; const tabData: molecule.model.IEditorTab = { id: tabId, data: isFile @@ -591,11 +547,7 @@ function contextMenu() { dt_nodeName: treeNode?.name, }, icon: 'edit', - breadcrumb: - treeNode?.location?.split('/')?.map((item: string) => ({ - id: item, - name: item, - })) || [], + breadcrumb, name: isFile ? localize('update task', '编辑任务') : localize('update folder', '编辑文件夹'), @@ -634,9 +586,11 @@ function contextMenu() { // 文件夹树异步加载 function onLoadTree() { molecule.folderTree.onLoadData((treeNode, callback) => { - loadTreeNode(treeNode.data, CATELOGUE_TYPE.TASK).then((res) => { + catalogueService.loadTreeNode(treeNode.data, CATELOGUE_TYPE.TASK).then((res) => { if (res) { callback(res); + } else { + callback(treeNode); } }); }); @@ -663,7 +617,7 @@ export default class FolderTreeExtension implements IExtension { contextMenu(); // 删除事件 onRemove(); - // 重命名 + // 新建文件夹 editTreeNodeName(); } } diff --git a/taier-ui/src/pages/console/resource.tsx b/taier-ui/src/pages/console/resource.tsx index c21595dd5b..b7b093bed7 100644 --- a/taier-ui/src/pages/console/resource.tsx +++ b/taier-ui/src/pages/console/resource.tsx @@ -27,7 +27,7 @@ import Resource from './resourceView'; import BindTenant from './bindTenant'; import type { IClusterProps } from '@/components/bindCommModal'; import type { ITableProps } from './bindTenant'; -import { getCatalogueTree } from '@/extensions/catalogue'; +import { catalogueService } from '@/services'; import './resource.scss'; const FormItem = Form.Item; @@ -125,7 +125,7 @@ export default () => { // 刷新租户列表 bindTenantRef.current?.getTenant(); // 刷新目录树 - getCatalogueTree(); + catalogueService.loadRootFolder(); // 刷新资源管理 getClusterList(); } diff --git a/taier-ui/src/pages/function/index.tsx b/taier-ui/src/pages/function/index.tsx index 15f574dca3..ce6042ee31 100644 --- a/taier-ui/src/pages/function/index.tsx +++ b/taier-ui/src/pages/function/index.tsx @@ -18,11 +18,12 @@ import React, { useState } from 'react'; import type { IMenuItemProps, ITreeNodeItemProps } from '@dtinsight/molecule/esm/components'; +import { catalogueService } from '@/services'; import { ActionBar } from '@dtinsight/molecule/esm/components'; import { Content, Header } from '@dtinsight/molecule/esm/workbench/sidebar'; import { connect } from '@dtinsight/molecule/esm/react'; import functionManagerService from '../../services/functionManagerService'; -import type { IFolderTree, IFolderTreeNodeProps } from '@dtinsight/molecule/esm/model'; +import type { IFolderTree } from '@dtinsight/molecule/esm/model'; import { FolderTree } from '@dtinsight/molecule/esm/workbench/sidebar/explore'; import { Modal } from 'antd'; import ajax from '../../api'; @@ -35,7 +36,6 @@ import { MENU_TYPE_ENUM, TASK_TYPE_ENUM, } from '@/constant'; -import { loadTreeNode } from '@/utils/extensions'; import { TreeViewUtil } from '@dtinsight/molecule/esm/common/treeUtil'; import type { UniqueId } from '@dtinsight/molecule/esm/common/types'; import { DetailInfoModal } from '@/components/detailInfo'; @@ -87,11 +87,7 @@ const FunctionManagerView = ({ headerToolBar, panel, entry }: IFunctionViewProps const [expandKeys, setExpandKeys] = useState([]); const updateNodePid = async (node: ITreeNodeItemProps) => { - loadTreeNode(node.data, CATELOGUE_TYPE.FUNCTION); - }; - - const loadData = async (treeNode: IFolderTreeNodeProps) => { - updateNodePid(treeNode); + catalogueService.loadTreeNode(node.data, CATELOGUE_TYPE.FUNCTION); }; const handleSelect = (file?: ITreeNodeItemProps) => { @@ -358,7 +354,7 @@ const FunctionManagerView = ({ headerToolBar, panel, entry }: IFunctionViewProps onRightClick={handleRightClick} draggable={false} onSelectFile={handleSelect} - onLoadData={loadData} + onLoadData={updateNodePid} onClickContextMenu={handleContextMenu} panel={panel} entry={entry} diff --git a/taier-ui/src/pages/resource/index.tsx b/taier-ui/src/pages/resource/index.tsx index 62ed5b0905..f947a1e471 100644 --- a/taier-ui/src/pages/resource/index.tsx +++ b/taier-ui/src/pages/resource/index.tsx @@ -26,10 +26,10 @@ import type { ITreeNodeItemProps, } from '@dtinsight/molecule/esm/components'; import { ActionBar } from '@dtinsight/molecule/esm/components'; -import type { IFolderTree, IFolderTreeNodeProps } from '@dtinsight/molecule/esm/model'; +import type { IFolderTree } from '@dtinsight/molecule/esm/model'; import { FileTypes } from '@dtinsight/molecule/esm/model'; import { connect } from '@dtinsight/molecule/esm/react'; -import { loadTreeNode } from '@/utils/extensions'; +import { catalogueService } from '@/services'; import { CATELOGUE_TYPE, ID_COLLECTIONS, RESOURCE_ACTIONS } from '@/constant'; import resourceManagerTree from '../../services/resourceManagerService'; import type { IFormFieldProps } from './resModal'; @@ -71,7 +71,7 @@ export default ({ panel, headerToolBar, entry }: IResourceViewProps & IFolderTre >(undefined); const updateNodePid = async (node: ITreeNodeItemProps) => { - loadTreeNode(node.data, CATELOGUE_TYPE.RESOURCE); + catalogueService.loadTreeNode(node.data, CATELOGUE_TYPE.RESOURCE); }; const handleUpload = () => { @@ -229,10 +229,6 @@ export default ({ panel, headerToolBar, entry }: IResourceViewProps & IFolderTre } }; - const loadData = async (treeNode: IFolderTreeNodeProps) => { - loadTreeNode(treeNode.data, CATELOGUE_TYPE.RESOURCE); - }; - const handleSelect = (file?: ITreeNodeItemProps) => { resourceManagerTree.setActive(file?.id); if (file) { @@ -348,7 +344,7 @@ export default ({ panel, headerToolBar, entry }: IResourceViewProps & IFolderTre onRightClick={handleRightClick} draggable={false} onSelectFile={handleSelect} - onLoadData={loadData} + onLoadData={updateNodePid} onClickContextMenu={handleContextMenu} entry={entry} panel={panel} @@ -375,7 +371,7 @@ export default ({ panel, headerToolBar, entry }: IResourceViewProps & IFolderTre dataType={CATELOGUE_TYPE.RESOURCE} isModalShow={folderVisible} toggleCreateFolder={handleCloseFolderModal} - treeData={resourceManagerTree.getState().folderTree?.data?.[0]?.children?.[0].data} + treeData={catalogueService.getRootFolder(CATELOGUE_TYPE.RESOURCE)?.data} defaultData={folderData} addOfflineCatalogue={handleAddCatalogue} editOfflineCatalogue={handleEditCatalogue} diff --git a/taier-ui/src/pages/resource/resModal.tsx b/taier-ui/src/pages/resource/resModal.tsx index 460d1b9421..4e9b9ec6d0 100644 --- a/taier-ui/src/pages/resource/resModal.tsx +++ b/taier-ui/src/pages/resource/resModal.tsx @@ -23,14 +23,8 @@ import { CATELOGUE_TYPE, formItemLayout, RESOURCE_TYPE } from '@/constant'; import type { CatalogueDataProps } from '@/interface'; import { IComputeType } from '@/interface'; import type { RcFile } from 'antd/lib/upload'; -import resourceManagerTree from '@/services/resourceManagerService'; import { resourceNameMapping } from '@/utils/enums'; - -export function getContainer(id: string) { - const container = document.createElement('div'); - document.getElementById(id)?.appendChild(container); - return container; -} +import { catalogueService } from '@/services'; const FormItem = Form.Item; const { Option } = Select; @@ -128,7 +122,7 @@ export default function ResModal({ }); }; - const treeData = resourceManagerTree.getState().folderTree?.data?.[0]?.children?.[0]; + const treeData = catalogueService.getRootFolder(CATELOGUE_TYPE.RESOURCE); if (treeData) { loop([treeData.data]); } @@ -274,7 +268,7 @@ export default function ResModal({ }, ]} initialValue={ - resourceManagerTree.getState().folderTree?.data?.[0]?.children?.[0]?.id + catalogueService.getRootFolder(CATELOGUE_TYPE.RESOURCE)?.data?.id } > @@ -309,9 +303,7 @@ export default function ResModal({ validator: checkNotDir, }, ]} - initialValue={ - resourceManagerTree.getState().folderTree?.data?.[0]?.children?.[0]?.id - } + initialValue={catalogueService.getRootFolder(CATELOGUE_TYPE.RESOURCE)?.data?.id} > diff --git a/taier-ui/src/services/breadcrumbService.ts b/taier-ui/src/services/breadcrumbService.ts new file mode 100644 index 0000000000..4e0b260747 --- /dev/null +++ b/taier-ui/src/services/breadcrumbService.ts @@ -0,0 +1,52 @@ +import { CATELOGUE_TYPE } from '@/constant'; +import { TreeViewUtil } from '@dtinsight/molecule/esm/common/treeUtil'; +import type molecule from '@dtinsight/molecule'; +import type { UniqueId } from '@dtinsight/molecule/esm/common/types'; +import type { IBreadcrumbItemProps } from '@dtinsight/molecule/esm/components'; +import { catalogueService } from '.'; + +interface IBreadcrumbService { + /** + * 根据目录树 id 获取父节点并生成面包屑 + */ + getBreadcrumb: (id: UniqueId) => IBreadcrumbItemProps[]; +} + +export default class BreadcrumbService implements IBreadcrumbService { + private hashTree: TreeViewUtil | null = null; + constructor() { + catalogueService.onUpdate(() => { + this.updateTree(); + }); + } + + private updateTree = () => { + const root = catalogueService.getRootFolder(CATELOGUE_TYPE.TASK); + if (root) { + this.hashTree = new TreeViewUtil(root); + } + }; + + public getBreadcrumb = (id: UniqueId) => { + if (this.hashTree) { + const stack = [this.hashTree.getHashMap(id)]; + const res: IBreadcrumbItemProps[] = []; + while (stack.length) { + const hash = stack.pop(); + if (hash) { + res.push({ + id: hash.node.id, + name: hash.node.name, + }); + + if (hash.parent) { + stack.push(this.hashTree.getHashMap(hash.parent)); + } + } + } + + return res.reverse(); + } + return []; + }; +} diff --git a/taier-ui/src/services/catalogueService.ts b/taier-ui/src/services/catalogueService.ts new file mode 100644 index 0000000000..c631a6acb6 --- /dev/null +++ b/taier-ui/src/services/catalogueService.ts @@ -0,0 +1,279 @@ +import api from '@/api'; +import { CATELOGUE_TYPE, MENU_TYPE_ENUM } from '@/constant'; +import type { CatalogueDataProps } from '@/interface'; +import { getTenantId, getUserId } from '@/utils'; +import { fileIcon } from '@/utils/extensions'; +import molecule from '@dtinsight/molecule'; +import { GlobalEvent } from '@dtinsight/molecule/esm/common/event'; +import { FileTypes, TreeNodeModel } from '@dtinsight/molecule/esm/model'; +import { singleton } from 'tsyringe'; +import functionManagerService from './functionManagerService'; +import resourceManagerService from './resourceManagerService'; + +interface ICatalogueService { + /** + * 加载目录树的根目录 + */ + loadRootFolder: () => void; + /** + * 获取目录树的根目录 + */ + getRootFolder: (source: CATELOGUE_TYPE) => molecule.model.IFolderTreeNodeProps | undefined; + /** + * 获取目录树的子节点 + * @param node 更新当前 Node 节点的子节点,不改变 Node 节点 + */ + loadTreeNode: ( + node: Partial>, + source: CATELOGUE_TYPE, + ) => void; + /** + * 目录树更新监听事件 + */ + onUpdate: (callback: () => void) => void; +} + +export enum CatalogueEventKind { + onUpdate = 'catalogue.update', +} + +@singleton() +export default class CatalogueService extends GlobalEvent implements ICatalogueService { + /** + * 异步加载目录树节点 + */ + private getCatalogueViaNode = async ( + node: Partial>, + ): Promise => { + if (!node) throw new Error('[getCatalogueViaNode]: failed to get catelogue'); + const res = await api.getOfflineCatalogue({ + isGetFile: true, + nodePid: node.id, + catalogueType: node.catalogueType, + userId: getUserId(), + tenantId: getTenantId(), + }); + if (res.code === 1) { + return res.data; + } + return undefined; + }; + + private getRootFolderViaSource = (data: CatalogueDataProps[], source: CATELOGUE_TYPE) => { + switch (source) { + case CATELOGUE_TYPE.TASK: { + return data.find((item) => item.catalogueType === MENU_TYPE_ENUM.TASK); + } + + case CATELOGUE_TYPE.RESOURCE: { + return data.find((item) => item.catalogueType === MENU_TYPE_ENUM.RESOURCE); + } + + case CATELOGUE_TYPE.FUNCTION: { + return data.find((item) => item.catalogueType === MENU_TYPE_ENUM.FUNCTION); + } + default: + return undefined; + } + }; + + /** + * Only transform current catalogue to treeNodeModel, ignore children + */ + private transformCatalogueToTree = ( + catalogue: CatalogueDataProps | undefined, + source: CATELOGUE_TYPE, + ) => { + if (!catalogue) return; + const folderType = ['folder', 'catalogue']; + switch (source) { + case CATELOGUE_TYPE.RESOURCE: { + const fileType = folderType.includes(catalogue.type) + ? FileTypes.Folder + : FileTypes.File; + + return new TreeNodeModel({ + // prevent same id between folder and file + id: fileType === FileTypes.File ? catalogue.id : `${catalogue.id}-folder`, + name: catalogue.name, + fileType, + icon: fileIcon(catalogue.resourceType, source), + isLeaf: fileType === FileTypes.File, + data: catalogue, + children: [], + }); + } + + case CATELOGUE_TYPE.FUNCTION: { + const { id, type, name } = catalogue; + const fileType = folderType.includes(type) ? FileTypes.Folder : FileTypes.File; + // Because of the same id in different levels, so we should set another uniq id for each tree node + return new TreeNodeModel({ + id: `${id}-${folderType.includes(type) ? 'folder' : 'file'}`, + name, + location: name, + fileType, + isLeaf: fileType === FileTypes.File, + data: catalogue, + icon: fileIcon(catalogue.taskType, source), + children: [], + }); + } + + case CATELOGUE_TYPE.TASK: { + const fileType = folderType.includes(catalogue.type) + ? FileTypes.Folder + : FileTypes.File; + + // prevent same id between folder and file + const id = fileType === FileTypes.File ? catalogue.id : `${catalogue.id}-folder`; + return new TreeNodeModel({ + id, + name: catalogue.name, + location: catalogue.name, + fileType, + icon: fileIcon(catalogue.taskType, source), + isLeaf: fileType === FileTypes.File, + data: catalogue, + children: molecule.folderTree.get(id)?.children || [], + }); + } + + default: + return undefined; + } + }; + + private getServiceBySource = (source: CATELOGUE_TYPE) => { + switch (source) { + case CATELOGUE_TYPE.TASK: + return molecule.folderTree; + case CATELOGUE_TYPE.FUNCTION: + return functionManagerService; + case CATELOGUE_TYPE.RESOURCE: + return resourceManagerService; + default: + return null; + } + }; + + /** + * @param node 更新当前 Node 节点的子节点,不改变 Node 节点 + */ + public loadTreeNode = async ( + node: Partial>, + source: CATELOGUE_TYPE, + ) => { + const data = await this.getCatalogueViaNode(node); + if (data) { + const childrenNodes = ( + ( + data?.children?.map((child) => this.transformCatalogueToTree(child, source)) || + [] + ).filter(Boolean) + ); + const service = this.getServiceBySource(source); + const treeNode = service?.get(`${node.id!}-folder`); + if (treeNode) { + service?.update({ + ...treeNode, + children: childrenNodes, + }); + + this.emit(CatalogueEventKind.onUpdate); + return service?.get(treeNode.id); + } + } + }; + + public getRootFolder = (source: CATELOGUE_TYPE) => { + const service = this.getServiceBySource(source); + if (source === CATELOGUE_TYPE.TASK) { + return service?.getState().folderTree?.data?.[0]; + } + return service?.getState().folderTree?.data?.[0]?.children?.[0]; + }; + + public loadRootFolder = () => { + this.getCatalogueViaNode({ id: 0 }).then((res) => { + if (!res || !res.children) { + return; + } + const { children } = res; + + const taskData = this.getRootFolderViaSource(children, CATELOGUE_TYPE.TASK); + const resourceData = this.getRootFolderViaSource(children, CATELOGUE_TYPE.RESOURCE); + const funcData = this.getRootFolderViaSource(children, CATELOGUE_TYPE.FUNCTION); + + // 更新资源目录树 + if (resourceData) { + const resourceRoot = resourceData; + const resourceNode = this.transformCatalogueToTree( + resourceRoot, + CATELOGUE_TYPE.RESOURCE, + )!; + const childrenNodes = + resourceRoot.children?.map( + (child) => this.transformCatalogueToTree(child, CATELOGUE_TYPE.RESOURCE)!, + ) || []; + + // set a root folder + resourceNode.fileType = FileTypes.RootFolder; + // put children to root folder + resourceNode.children = childrenNodes; + resourceManagerService.add(resourceNode); + } + + // 更新函数目录树 + if (funcData) { + const funcRoot = funcData; + const functionNode = this.transformCatalogueToTree( + funcRoot, + CATELOGUE_TYPE.FUNCTION, + )!; + const childrenNodes = + funcRoot.children + // there is a system function in the children node of root folder, we'd better to filter it + ?.filter((child) => child.name !== '系统函数') + .map( + (child) => + this.transformCatalogueToTree(child, CATELOGUE_TYPE.FUNCTION)!, + ) || []; + + // set a root folder + functionNode.fileType = FileTypes.RootFolder; + // put children to root folder + functionNode.children = childrenNodes; + functionManagerService.add(functionNode); + + // // sql 节点必存在 catalogueType,对所有的节点都求一遍子树 + // const SqlNodes = + // funcRoot?.children?.filter((child: any) => child.catalogueType) || []; + // SqlNodes.forEach((sqlNode) => { + // loadTreeNode(sqlNode, CATELOGUE_TYPE.FUNCTION); + // }); + } + + // 任务开发根目录 + if (taskData) { + const taskRootFolder = taskData?.children?.[0]; + if (taskRootFolder) { + const taskNode = this.transformCatalogueToTree( + taskRootFolder, + CATELOGUE_TYPE.TASK, + )!; + + taskNode.fileType = FileTypes.RootFolder; + molecule.folderTree.add(taskNode); + + // 获取当前根目录的下级目录,确保打开 Explorer 有数据展示 + this.loadTreeNode(taskRootFolder, CATELOGUE_TYPE.TASK); + } + } + }); + }; + + public onUpdate = (callback: () => void) => { + this.subscribe(CatalogueEventKind.onUpdate, callback); + }; +} diff --git a/taier-ui/src/services/index.ts b/taier-ui/src/services/index.ts index a070d63e11..9a36255b10 100644 --- a/taier-ui/src/services/index.ts +++ b/taier-ui/src/services/index.ts @@ -1,8 +1,12 @@ import { container } from 'tsyringe'; import EditorActionBarService from './editorActionBarService'; import ExecuteService from './executeService'; +import CatalogueService from './catalogueService'; +import BreadcrumbService from './breadcrumbService'; const editorActionBarService = container.resolve(EditorActionBarService); const executeService = container.resolve(ExecuteService); +const catalogueService = container.resolve(CatalogueService); +const breadcrumbService = container.resolve(BreadcrumbService); -export { editorActionBarService, executeService }; +export { editorActionBarService, catalogueService, executeService, breadcrumbService }; diff --git a/taier-ui/src/utils/extensions.tsx b/taier-ui/src/utils/extensions.tsx index 960b6523d6..4643b6eafe 100644 --- a/taier-ui/src/utils/extensions.tsx +++ b/taier-ui/src/utils/extensions.tsx @@ -17,9 +17,7 @@ */ import molecule from '@dtinsight/molecule/esm'; -import { message } from 'antd'; import type { IFolderTreeNodeProps } from '@dtinsight/molecule/esm/model'; -import { FileTypes, TreeNodeModel } from '@dtinsight/molecule/esm/model'; import { FlinkSQLIcon, SyntaxIcon, @@ -28,9 +26,6 @@ import { ResourceIcon, DataCollectionIcon, } from '@/components/icon'; -import api from '@/api'; -import functionManagerService from '@/services/functionManagerService'; -import resourceManagerTree from '@/services/resourceManagerService'; import type { RESOURCE_TYPE } from '@/constant'; import { ID_COLLECTIONS } from '@/constant'; import { CATELOGUE_TYPE, TASK_TYPE_ENUM } from '@/constant'; @@ -38,18 +33,11 @@ import type { CatalogueDataProps, IOfflineTaskProps } from '@/interface'; import { executeService } from '@/services'; import taskResultService, { createLog } from '@/services/taskResultService'; import Result from '@/components/task/result'; -import { filterSql, getTenantId, getUserId } from '.'; +import { filterSql } from '.'; import stream from '@/api/stream'; import { TreeViewUtil } from '@dtinsight/molecule/esm/common/treeUtil'; import { transformTabDataToParams } from './saveTask'; -export function resetEditorGroup() { - molecule.editor.updateActions([ - { id: ID_COLLECTIONS.TASK_RUN_ID, disabled: true }, - { id: ID_COLLECTIONS.TASK_STOP_ID, disabled: true }, - ]); -} - /** * 根据不同任务渲染不同的图标 */ @@ -83,174 +71,6 @@ export function fileIcon( } } -/** - * 异步加载树结点 - * @param node - * @returns - */ -export async function getCatalogueViaNode( - node: Partial, -): Promise { - if (!node) throw new Error('[getCatalogueViaNode]: failed to get catelogue'); - const res = await api.getOfflineCatalogue({ - isGetFile: true, - nodePid: node.id, - catalogueType: node.catalogueType, - userId: getUserId(), - tenantId: getTenantId(), - }); - if (res.code === 1) { - return res.data; - } - return undefined; -} - -/** - * Transform the catalogue data from back-end to the tree structure - * @param catalogue - * @param source - * @returns - */ -export function transformCatalogueToTree( - catalogue: CatalogueDataProps | undefined, - source: CATELOGUE_TYPE, - isRootFolder: boolean = false, -): TreeNodeModel | undefined { - const folderType = ['folder', 'catalogue']; - if (!catalogue) return; - switch (source) { - case CATELOGUE_TYPE.RESOURCE: { - const children = (catalogue.children || []) - .map((child) => transformCatalogueToTree(child, source)) - .filter(Boolean) as TreeNodeModel[]; - - const catalogueType = folderType.includes(catalogue.type) - ? FileTypes.Folder - : FileTypes.File; - - const fileType = isRootFolder ? FileTypes.RootFolder : catalogueType; - - return new TreeNodeModel({ - // prevent same id between folder and file - id: catalogue.id, - name: catalogue.name, - location: catalogue.name, - fileType, - icon: fileIcon(catalogue.resourceType, source), - isLeaf: fileType === FileTypes.File, - data: catalogue, - children, - }); - } - case CATELOGUE_TYPE.TASK: { - const children = (catalogue.children || []) - .map((child) => transformCatalogueToTree(child, source)) - .filter(Boolean) as TreeNodeModel[]; - - const catalogueType = folderType.includes(catalogue.type) - ? FileTypes.Folder - : FileTypes.File; - - const fileType = isRootFolder ? FileTypes.RootFolder : catalogueType; - - // If the node already stored in folderTree, then use it - const prevNode = molecule.folderTree.get( - fileType === FileTypes.File ? catalogue.id : `${catalogue.id}-folder`, - ); - - // file always generate the new one - if (prevNode && fileType !== FileTypes.File) { - return new TreeNodeModel({ - id: prevNode.id, - name: prevNode.name, - location: prevNode.location, - fileType: prevNode.fileType, - icon: prevNode.icon, - isLeaf: prevNode.isLeaf, - data: catalogue, - children: children.map((cNode) => { - // change the locations to like 「root/abc」 so that render breadcrumbs correctly - // eslint-disable-next-line no-param-reassign - cNode.location = `${prevNode.location}/${cNode.location}`; - return cNode; - }), - }); - } - - return new TreeNodeModel({ - // prevent same id between folder and file - id: fileType === FileTypes.File ? catalogue.id : `${catalogue.id}-folder`, - name: catalogue.name, - location: catalogue.name, - fileType, - icon: fileIcon(catalogue.taskType, source), - isLeaf: fileType === FileTypes.File, - data: catalogue, - children, - }); - } - case CATELOGUE_TYPE.FUNCTION: { - const { id, type, name } = catalogue; - const children = (catalogue.children || []) - // there is a system function in the children node of root folder, we'd better to filter it - .filter((child) => !isRootFolder || child.name !== '系统函数') - .map((child) => transformCatalogueToTree(child, source)) - .filter(Boolean) as TreeNodeModel[]; - - const catalogueType = folderType.includes(type) ? FileTypes.Folder : FileTypes.File; - - const fileType = isRootFolder ? FileTypes.RootFolder : catalogueType; - - // Because of the same id in different levels, so we should set another uniq id for each tree node - return new TreeNodeModel({ - id: `${id}-${folderType.includes(type) ? 'folder' : 'file'}`, - name, - location: name, - fileType, - isLeaf: fileType === FileTypes.File, - data: catalogue, - icon: fileIcon(catalogue.taskType, source), - children, - }); - } - - default: - return undefined; - } -} - -/** - * Get the children data in node and save it into Service - * @param node - * @param source - */ -export async function loadTreeNode( - node: Partial, - source: CATELOGUE_TYPE, -): Promise { - const data = await getCatalogueViaNode(node); - const nextNode = transformCatalogueToTree(data, source); - if (!nextNode) { - message.error('load tree node failed'); - return null; - } - switch (source) { - case 'task': { - molecule.folderTree.update(nextNode); - break; - } - case 'resource': - resourceManagerTree.update(nextNode); - break; - case 'function': - functionManagerService.update(nextNode); - break; - default: - break; - } - return nextNode; -} - /** * 运行任务 */