* -------------------------------------------------------------------------
* This file is part of the MindStudio project.
* Copyright (c) 2025 Huawei Technologies Co.,Ltd.
*
* MindStudio is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
* -------------------------------------------------------------------------
*/
import _ from 'lodash';
import { runInAction } from 'mobx';
import { customConsole as console } from '@insight/lib/utils';
import { LocalStorageKey, localStorageService } from '@insight/lib';
import connector from '@/connection';
import { store } from '@/store';
import { ProjectAction } from '@/utils/enum';
import {
DataSource,
FileOrDirectory,
GLOBAL_HOST,
ImportTreeInfo,
LayerType,
Project,
ProjectDirectory,
} from '@/centralServer/websocket/defs';
import { addDataPath } from '@/centralServer/server';
import {
clearProjects,
deleteDataPath,
deleteProject,
getHistoryProject,
resetTimeline,
updateProjectName as requestUpdateProjectName,
} from '@/utils/Request';
import i18n from '@insight/lib/i18n';
import { message as Message } from 'antd';
import { isProjectNameExisted, updateDataSourceName } from '@/utils/Resource';
import { sendReset, sendUpdateProjectName } from '@/connection/sendNotification';
import { cancelBaselineData, updateProjectNameHandler } from '@/utils/Compare';
import { updateRankMapByProjectName } from '@/utils/Rank';
import { closeLoading, openLoading } from '@/utils/useLoading';
import { DEFAULT_ACTIVE_DATASOURCE } from '@/entity/session';
export interface UpdateProjectParam {
projectAction: ProjectAction;
projectName: string;
projectPath: string[];
selectedFileType: LayerType;
selectedFilePath: string;
selectedRankId?: string;
children: FileOrDirectory[];
hasConflict: boolean;
}
export const transformFile = (tree: ImportTreeInfo | undefined, depth: number): FileOrDirectory[] | undefined => {
if (tree === undefined || depth >= 5) { return undefined; }
let result;
switch (tree.type) {
case 'CLUSTER':
result = [{
type: tree.type,
name: tree.fileDir,
path: tree.filePath,
children: tree.children.map((child) => transformFile(child, depth + 1))
.flat().filter((item) => item !== undefined) as FileOrDirectory[],
}];
break;
case 'PROJECT':
case 'HOST':
result = tree.children.map((child) => transformFile(child, depth))
.flat().filter((item) => item !== undefined) as FileOrDirectory[];
break;
case 'RANK':
case 'COMPUTE':
case 'IPYNB':
result = [{
type: tree.type,
name: tree.fileDir,
path: tree.filePath,
rankId: tree.rankId,
children: tree.children.map((child) => transformFile(child, depth + 1))
.flat().filter((item) => item !== undefined) as FileOrDirectory[],
}];
break;
default:
break;
}
return result;
};
const transformProject = (data: ProjectDirectory): Project => {
return {
projectName: data.projectName,
projectPath: [data.children?.[0].filePath ?? ''],
children: data.children?.map((child) => transformFile(child, 0))?.flat()?.filter((item) => item !== undefined) as FileOrDirectory[],
};
};
export const loadHistoryProject = async(): Promise<void> => {
const session = store.sessionStore.activeSession;
const sources: DataSource[] = [];
const result = await getHistoryProject() as {projectDirectoryList: ProjectDirectory[]};
const projectDirectoryList = result?.projectDirectoryList ?? [];
projectDirectoryList.forEach(item => {
const source: DataSource = { ...GLOBAL_HOST, ...transformProject(item) };
sources.push(source);
});
runInAction(() => {
session.dataSources = sources;
});
};
export async function handleProjectAction({ action, project, isConflict, selectedFileType, selectedFilePath, selectedRankId }:
{action: ProjectAction;project: Project;isConflict: boolean;selectedFileType?: LayerType;selectedFilePath?: string;selectedRankId?: string}): Promise<void> {
const session = store.sessionStore.activeSession;
runInAction(async() => {
const { activeDataSource, dataSources } = session;
const newProject = { ...project };
if (newProject.projectName === activeDataSource.projectName && arraysValueEqual(newProject.projectPath, activeDataSource.projectPath)) {
return;
}
openLoading();
if (action === ProjectAction.SWITCH_PROJECT) {
const firstFilePath = selectedFilePath ?? dataSources.find(data => data.projectName === newProject.projectName)?.projectPath[0];
const firstFileType: LayerType = selectedFileType ?? 'PROJECT';
if (firstFilePath !== undefined) {
newProject.selectedFileType = firstFileType;
newProject.selectedFilePath = firstFilePath;
newProject.selectedRankId = selectedRankId;
}
session.toBeActivedProject = newProject;
}
try {
const res = await addDataPath(newProject, action, isConflict, session);
if (!res) {
closeLoading();
return;
}
} catch (e) {
closeLoading();
return;
}
session.toBeActivedProject = undefined;
const path = newProject.projectPath[0].includes(newProject.projectName) ? newProject.projectName : newProject.projectPath[0];
localStorageService.setItem(LocalStorageKey.LAST_FILE_PATH, path);
});
cancelBaselineData();
}
function arraysValueEqual<T>(a: T[], b: T[]): boolean {
return a.length === b.length && a.every((value) => b.includes(value));
}
export const updateProject = ({ projectAction, projectName, children, hasConflict, projectPath, selectedFileType, selectedFilePath, selectedRankId }:
UpdateProjectParam): void => {
const session = store.sessionStore.activeSession;
const dataSource: DataSource = { ...GLOBAL_HOST, projectName, projectPath, children };
runInAction(() => {
if (projectAction === ProjectAction.ADD_FILE) {
session.dataSources = getMergedDataSources(session.dataSources, dataSource, hasConflict);
if (hasConflict || children.length > 1) {
const firstFile = getProjectFirstFile(dataSource);
session.activeDataSource = {
...dataSource,
selectedFileType: firstFile?.type ?? 'UNKNOWN',
selectedFilePath: firstFile?.path ?? '',
selectedRankId: firstFile?.rankId,
};
}
if (session.activeDataSource.projectName !== dataSource.projectName || session.activeDataSource.selectedFilePath !== '') {
session.activeDataSource = { ...dataSource, selectedFileType, selectedFilePath, selectedRankId };
return;
}
}
if (projectAction === ProjectAction.SWITCH_PROJECT) {
session.activeDataSource = { ...dataSource, selectedFileType, selectedFilePath, selectedRankId };
}
});
};
function getMergedDataSources(oldDataSources: DataSource[], dataSource: DataSource, hasConflict: boolean): DataSource[] {
const dataSources = JSON.parse(JSON.stringify(oldDataSources)) as DataSource[];
const projectIndex = dataSources.findIndex((item) =>
item.remote === dataSource.remote && item.port === dataSource.port && item.projectName === dataSource.projectName);
if (projectIndex === -1) {
dataSources.push(dataSource);
} else {
if (hasConflict) {
dataSources[projectIndex].children = dataSource.children;
} else {
if (!hasFileOverlap(dataSources[projectIndex], dataSource)) {
const merged = mergeProjects(dataSources[projectIndex], dataSource);
dataSources[projectIndex].children = merged.children;
}
}
}
return dataSources;
}
export const removeProject = (projectIndex: number): void => {
const session = store.sessionStore.activeSession;
runInAction(async() => {
try {
const dataSource = session.dataSources[projectIndex];
if (session.activeDataSource.projectName === dataSource.projectName) {
connector.send({ event: 'remote/remove', body: { dataSource } });
await resetTimeline();
session.reset(true);
session.activeDataSource = DEFAULT_ACTIVE_DATASOURCE;
}
await deleteProject(dataSource);
session.deleteDataSource(projectIndex);
} catch {
console.error('remove error');
}
closeLoading();
});
};
export const removeProjects = async (projectNameList: React.Key[] = []): Promise<void> => {
const session = store.sessionStore.activeSession;
runInAction(async() => {
try {
await clearProjects(projectNameList);
session.dataSources = session.dataSources.filter(dataSource => !projectNameList.includes(dataSource.projectName));
if (projectNameList.length === 0 || projectNameList.includes(session.activeDataSource.projectName)) {
sendReset();
await resetTimeline();
session.reset(true);
session.activeDataSource = DEFAULT_ACTIVE_DATASOURCE;
}
} catch {
console.error('remove error');
}
closeLoading();
});
};
export const removeDataPath = (projectIndex: number, dataPath: string): void => {
const session = store.sessionStore.activeSession;
runInAction(async() => {
try {
const dataSource = session.dataSources[projectIndex];
const childrenNum = calculateRemovableChildren(dataSource);
if (childrenNum === 1) {
removeProject(projectIndex);
return;
}
const singleDataPath = dataPath;
if (session.activeDataSource.projectName === dataSource.projectName) {
connector.send({ event: 'remote/removeSingle', body: { dataSource, singleDataPath } });
}
await deleteDataPath({ ...dataSource, dataPath: [singleDataPath] });
const nextDataSource = session.deleteDataPath(projectIndex, singleDataPath);
if (session.activeDataSource.projectName === dataSource.projectName && session.activeDataSource.selectedFilePath === singleDataPath) {
const firstFile = getProjectFirstFile(nextDataSource);
session.activeDataSource = {
...dataSource,
selectedFileType: firstFile?.type ?? 'UNKNOWN',
selectedFilePath: firstFile?.path ?? '',
selectedRankId: firstFile?.rankId,
};
}
} catch {
console.error('removeSingle error');
}
closeLoading();
});
};
export const updateProjectName = async (oldProjectName: string, newProjectName: string): Promise<boolean> => {
try {
const existed = isProjectNameExisted(newProjectName);
if (existed) {
Message.warning(i18n.t('Duplicate Project', { ns: 'framework' }));
return false;
}
updateRankMapByProjectName(oldProjectName, newProjectName);
await requestUpdateProjectName(oldProjectName, newProjectName);
sendUpdateProjectName(oldProjectName, newProjectName);
updateDataSourceName(oldProjectName, newProjectName);
updateProjectNameHandler(oldProjectName, newProjectName);
return true;
} catch {
Message.warning(i18n.t('Update Project Name Failed', { ns: 'framework' }));
return false;
}
};
export const getProjectFirstFile = (project: Project): FileOrDirectory | undefined => {
const getFirstFile = (files: FileOrDirectory[], depth: number): FileOrDirectory | undefined => {
if (files.length === 0) { return undefined; }
if (depth === 5) {
console.error('over 5 layers of file,deleting fail!');
return undefined;
}
if (files[0].children.length === 0) {
return files[0];
} else {
return getFirstFile(files[0].children, depth + 1);
}
};
return getFirstFile(project.children, 0);
};
export const hasFileOverlap = (a: Project, b: Project): boolean => {
const pathSet = new Set<string>();
recursiveSearchEveryProjectFileThen(a, (file) => {
pathSet.add(file.path);
});
return recursiveSearchSomeProjectFileThen(b, (file) => pathSet.has(file.path));
};
const createNodeMap = (nodes: FileOrDirectory[]): Map<string, FileOrDirectory> => {
const map: Map<string, FileOrDirectory> = new Map();
const addToMap = (node: FileOrDirectory, depth: number): void => {
if (depth >= 5) { return; }
map.set(node.path, node);
if (node.children) {
node.children.forEach((child) => addToMap(child, depth + 1));
}
};
nodes.forEach((child) => addToMap(child, 0));
return map;
};
const mergeNodes = (nodeA: FileOrDirectory | undefined, nodeB: FileOrDirectory): FileOrDirectory => {
if (!nodeA) { return _.cloneDeep(nodeB); }
if (!nodeB) { return _.cloneDeep(nodeA); }
const merged = _.cloneDeep(nodeA) as FileOrDirectory;
merged.children = [];
const allChildrenMap: Map<string, FileOrDirectory> = new Map();
if (nodeA.children) {
nodeA.children.forEach(child => {
allChildrenMap.set(child.path, child);
});
}
if (nodeB.children) {
nodeB.children.forEach(child => {
if (allChildrenMap.has(child.path)) {
allChildrenMap.set(child.path, mergeNodes(allChildrenMap.get(child.path), child));
} else {
allChildrenMap.set(child.path, child);
}
});
}
merged.children = Object.values(allChildrenMap).map(child => {
return _.cloneDeep(child);
});
return merged;
};
function findTopLevelPaths(paths: Set<string>): string[] {
if (!paths || paths.size === 0) { return []; }
const uniquePaths = [...paths];
const sep = uniquePaths[0].includes('\\') ? '\\' : '/';
uniquePaths.sort((a, b) => a.split(sep).length - b.split(sep).length);
const result: string[] = [];
for (const currentPath of uniquePaths) {
if (result.some((pathString: string) => currentPath.startsWith(pathString))) {
continue;
}
result.push(currentPath);
}
return result;
}
export const mergeProjects = (projectA: Project, projectB: Project): Project => {
const allRootPaths = [...new Set([...projectA.projectPath, ...projectB.projectPath])];
const buildRootsFromProject = (project: Project): FileOrDirectory[] => {
return project.children;
};
const rootsA = buildRootsFromProject(projectA);
const rootsB = buildRootsFromProject(projectB);
const mapA = createNodeMap(rootsA);
const mapB = createNodeMap(rootsB);
const mergedRoots: FileOrDirectory[] = [];
const allPaths = new Set([...mapA.keys(), ...mapB.keys()]);
findTopLevelPaths(allPaths).forEach(path => {
const nodeA = mapA.get(path);
const nodeB = mapB.get(path);
if (nodeA && nodeB) {
mergedRoots.push(mergeNodes(nodeA, nodeB));
} else if (nodeA) {
mergedRoots.push(_.cloneDeep(nodeA));
} else if (nodeB) {
mergedRoots.push(_.cloneDeep(nodeB));
} else { }
});
return {
projectName: `${projectA.projectName}+${projectB.projectName}`,
projectPath: allRootPaths,
children: mergedRoots,
};
};
export const deleteProjectDataPath = (project: Project, dataPath: string): void => {
recursiveSearchSomeProjectFileThen(project, (file) => file.path === dataPath, (__, foundIdx, files, parentInfo) => {
if (files.length === 1 && parentInfo.parent !== null && parentInfo.grandParent !== null) {
parentInfo.grandParent.children.splice(parentInfo.parentIndex, 1);
} else {
files.splice(foundIdx, 1);
}
});
};
function calculateRemovableChildren(project: Project): number {
let result = 0;
recursiveSearchEveryProjectFileThen(project, (file) => {
if (file.type !== 'PROJECT' && file.type !== 'CLUSTER') {
result++;
}
});
return result;
}
interface ParentInfo {
parent: FileOrDirectory | null;
parentIndex: number;
grandParent: FileOrDirectory | null;
}
const recursiveSearchSomeProjectFileThen = (project: Project,
searcher: (file: FileOrDirectory) => boolean,
action?: (file: FileOrDirectory, index: number, foundFiles: FileOrDirectory[], parentInfo: ParentInfo) => void): boolean => {
const searchFilesThen = (files: FileOrDirectory[], parentInfo: ParentInfo, depth: number): boolean => {
if (depth >= 5) {
console.error('over 5 layers of file,deleting fail!');
return false;
}
if (files.length === 0) { return false; }
const foundIdx: number = files.findIndex((file: FileOrDirectory): boolean => searcher(file));
if (foundIdx !== -1) {
action?.(files[foundIdx], foundIdx, files, parentInfo);
return true;
}
for (let i = 0; i < files.length; i++) {
if (searchFilesThen(files[i].children, {
parent: files[i],
parentIndex: i,
grandParent: parentInfo.parent,
}, depth + 1)) {
return true;
}
}
return false;
};
return searchFilesThen(project.children, { parent: project as unknown as FileOrDirectory, parentIndex: -1, grandParent: null }, 0);
};
const recursiveSearchEveryProjectFileThen = (project: Project, action?: (file: FileOrDirectory, index: number) => void): void => {
const searchFilesThen = (files: FileOrDirectory[], depth: number): void => {
if (depth >= 5) {
console.error('over 5 layers of file,deleting fail!');
return;
}
if (files.length === 0) { return; }
files.forEach((file, index) => {
action?.(file, index);
});
for (let i = 0; i < files.length; i++) {
searchFilesThen(files[i].children, depth + 1);
}
};
searchFilesThen(project.children, 0);
};