Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/x-date-pickers": "^7.29.4",
"@nosferatu500/react-sortable-tree": "^3.0.6",
"@mui/x-tree-view": "^8.12.0",
"ag-grid-community": "~34.2.0",
"ag-grid-react": "~34.2.0",
"clsx": "^2.1.1",
Expand Down
4 changes: 0 additions & 4 deletions rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@ export default {
external: [
'react',
'react-dom',
'react-dnd',
'prop-types',
'react-dnd-html5-backend',
'@nosferatu500/react-dnd-scrollzone',
'react-virtualized',
'@mui/material',
'@mui/icons-material',
'@mui/styles',
Expand Down
145 changes: 29 additions & 116 deletions src/charts/ScenarioManagerTreeList/ScenarioManagerTreeList.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Cosmo Tech.
// Licensed under the MIT license.
import '@nosferatu500/react-sortable-tree/style.css';
import React, { useEffect, useMemo, useState, useRef, useReducer } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import {
UnfoldMore as UnfoldMoreIcon,
Expand All @@ -13,16 +12,9 @@ import { ScenarioUtils } from '@cosmotech/core';
import { SearchBar } from '../../inputs';
import { FadingTooltip } from '../../misc';
import { ScenarioSortableTree } from './components';
import { DEFAULT_LABELS } from './labels';
import useStyles from './style';

const initNodesDict = (scenarios, defaultExpanded) => {
const nodesDict = {};
scenarios.forEach((scenario) => {
nodesDict[scenario.id] = defaultExpanded;
});
return nodesDict;
};

const filterMatchesName = (scenario, searchStr) => scenario.name.toLowerCase().indexOf(searchStr.toLowerCase()) !== -1;
const filterMatchesValidationStatus = (labels, scenario, searchStr) => {
if (!scenario.validationStatus) return false;
Expand All @@ -41,47 +33,13 @@ const filterMatchesTag = (scenario, searchString) =>
const filterMatchesOwner = (scenario, searchString) =>
scenario.ownerName?.toLowerCase().includes(searchString.toLowerCase());

const DEFAULT_LABELS = {
status: 'Run status:',
runTemplateLabel: 'Run type:',
successful: 'Successful',
running: 'Running',
dataingestioninprogress: 'Transferring results',
failed: 'Failed',
created: 'Created',
delete: 'Delete this scenario',
edit: 'Edit scenario name',
scenarioRename: {
title: 'Scenario name',
errors: {
emptyScenarioName: 'Scenario name cannot be empty',
forbiddenCharsInScenarioName:
'Scenario name has to start with a letter, and can only contain ' +
'letters, digits, spaces, underscores, hyphens and dots.',
},
},
deleteDialog: {
description:
'This operation is irreversible. Dataset(s) will not be removed, but the scenario parameters will be lost. ' +
'If this scenario has children, they will be moved to a new parent. ' +
'The new parent will be the parent of the deleted scenario.',
cancel: 'Cancel',
confirm: 'Confirm',
},
dataset: 'Dataset:',
noDataset: 'None',
datasetNotFound: 'Not Found',
searchField: 'Filter',
toolbar: {
expandAll: 'Expand all',
expandTree: 'Expand tree',
collapseAll: 'Collapse all',
},
validationStatus: {
rejected: 'Rejected',
validated: 'Validated',
},
};
const doesScenarioMatchFilter = (labels, scenario, searchStr) =>
filterMatchesName(scenario, searchStr) ||
filterMatchesValidationStatus(labels, scenario, searchStr) ||
filterMatchesRunStatus(labels, scenario, searchStr) ||
filterMatchesDescription(scenario, searchStr) ||
filterMatchesTag(scenario, searchStr) ||
filterMatchesOwner(scenario, searchStr);

export const ScenarioManagerTreeList = (props) => {
const classes = useStyles();
Expand All @@ -92,42 +50,24 @@ export const ScenarioManagerTreeList = (props) => {
deleteScenario,
onScenarioRename,
checkScenarioNameValue = () => null,
userId,
buildSearchInfo,
buildDatasetInfo,
labels: tmpLabels,
buildScenarioNameToDelete,
showDeleteIcon,
canUserDeleteScenario,
canUserRenameScenario = () => true,
canUpdateScenario,
onScenarioUpdate = () => null,
} = props;

const labels = useMemo(() => ({ ...DEFAULT_LABELS, ...tmpLabels }), [tmpLabels]);

if (buildSearchInfo) {
console.warn(
'"buildSearchInfo" prop is deprecated in ScenarioManagerTreeList. Please consider removing this prop.'
);
}
if (showDeleteIcon != null) {
console.warn(
'"showDeleteIcon" prop is deprecated in ScenarioManagerTreeList. Please use "canUserDeleteScenario" instead.'
);
}
const allScenarioIds = useMemo(() => scenarios.map((scenario) => scenario.id), [scenarios]);

const [searchText, setSearchText] = useState('');
const nodesExpandedChildren = useRef(initNodesDict(scenarios, true));
const nodesExpandedDetails = useRef(initNodesDict(scenarios, false));
const [treeExpandedNodes, setTreeExpandedNodes] = useState(allScenarioIds);
const [detailExpandedNodes, setDetailExpandedNodes] = useState([]);

const formatScenariosToScenariosTree = (scenariosToFormat) => {
const scenarioTree = scenariosToFormat.map((scenario) => {
const displayDeleteIcon =
canUserDeleteScenario != null
? canUserDeleteScenario(scenario)
: showDeleteIcon !== false && scenario.ownerId === userId;

const scenarioList = scenariosToFormat.map((scenario) => {
labels.dataset = buildDatasetInfo(scenario.datasetList);

return {
Expand All @@ -137,7 +77,7 @@ export const ScenarioManagerTreeList = (props) => {
scenarioNodeProps: {
datasets,
scenario,
showDeleteIcon: displayDeleteIcon,
showDeleteIcon: canUserDeleteScenario(scenario),
onScenarioRedirect,
deleteScenario,
checkScenarioNameValue,
Expand All @@ -150,7 +90,7 @@ export const ScenarioManagerTreeList = (props) => {
},
};
});
return ScenarioUtils.getScenarioTree(scenarioTree, (scenA, scenB) => scenA.name.localeCompare(scenB.name));
return ScenarioUtils.getScenarioTree(scenarioList, (scenA, scenB) => scenA.name.localeCompare(scenB.name));
};

const scenarioTreeFull = useMemo(
Expand All @@ -167,22 +107,17 @@ export const ScenarioManagerTreeList = (props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scenarios, searchText, labels]);

const [refreshTick, forceRefresh] = useReducer((x) => x + 1, 0);

const collapseAll = () => {
nodesExpandedChildren.current = initNodesDict(scenarios, false);
nodesExpandedDetails.current = initNodesDict(scenarios, false);
forceRefresh();
setTreeExpandedNodes([]);
setDetailExpandedNodes([]);
};
const expandTree = () => {
nodesExpandedChildren.current = initNodesDict(scenarios, true);
nodesExpandedDetails.current = initNodesDict(scenarios, false);
forceRefresh();
setTreeExpandedNodes(allScenarioIds);
setDetailExpandedNodes([]);
};
const expandAll = () => {
nodesExpandedChildren.current = initNodesDict(scenarios, true);
nodesExpandedDetails.current = initNodesDict(scenarios, true);
forceRefresh();
setTreeExpandedNodes(allScenarioIds);
setDetailExpandedNodes(allScenarioIds);
};

const filterScenarios = (searchStr) => {
Expand All @@ -192,15 +127,7 @@ export const ScenarioManagerTreeList = (props) => {
return;
}
// Otherwise, filter scenarios based on their name
const filtered = scenarios.filter(
(scenario) =>
filterMatchesName(scenario, searchStr) ||
filterMatchesValidationStatus(labels, scenario, searchStr) ||
filterMatchesRunStatus(labels, scenario, searchStr) ||
filterMatchesDescription(scenario, searchStr) ||
filterMatchesTag(scenario, searchStr) ||
filterMatchesOwner(scenario, searchStr)
);
const filtered = scenarios.filter((scenario) => doesScenarioMatchFilter(labels, scenario, searchStr));
// Format list and set as tree data
setScenariosTree(formatScenariosToScenariosTree(filtered));
};
Expand Down Expand Up @@ -233,15 +160,16 @@ export const ScenarioManagerTreeList = (props) => {
</div>
</div>
<div data-cy="scenario-manager-view" className={classes.treesContainer}>
{scenariosTree.map((scenarioTree) => {
{scenariosTree.map((rootScenario) => {
return (
<ScenarioSortableTree
key={scenarioTree.id}
key={rootScenario.id}
classes={classes}
nodesExpandedChildrenRef={nodesExpandedChildren}
nodesExpandedDetailsRef={nodesExpandedDetails}
scenarioTree={scenarioTree}
refreshTick={refreshTick}
treeExpandedNodes={treeExpandedNodes}
setTreeExpandedNodes={setTreeExpandedNodes}
detailExpandedNodes={detailExpandedNodes}
setDetailExpandedNodes={setDetailExpandedNodes}
scenarioTree={rootScenario}
/>
);
})}
Expand Down Expand Up @@ -279,25 +207,10 @@ ScenarioManagerTreeList.propTypes = {
* Function bound to handle a scenario movement (moving a scenario = changing its parent)
*/
moveScenario: PropTypes.func.isRequired,
/**
* Current user id
*/
userId: PropTypes.string.isRequired,
/**
* DEPRECATED: Function building scenario search label
*/
buildSearchInfo: PropTypes.func,
/**
* Function building scenario dataset label
*/
buildDatasetInfo: PropTypes.func.isRequired,
/**
* DEPRECATED: this prop is deprecated, use 'canUserDeleteScenario' instead
* Boolean value to define whether scenario delete buttons must be shown in ScenarioNode elements:
* - false: delete buttons are always hidden
* - true: delete buttons are shown if the user id matches the scenario owner id
*/
showDeleteIcon: PropTypes.bool,
/**
* Function returning whether the current user can delete a given scenario. This function receives as parameter
* the scenario data and must return a boolean.
Expand Down
Loading