Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
* PBENCH-115

Add flexible display of metadata values on row expansion in Overview page.
  • Loading branch information
MVarshini authored Aug 23, 2023
1 parent bbd948e commit 6bd64e8
Show file tree
Hide file tree
Showing 11 changed files with 514 additions and 59 deletions.
96 changes: 96 additions & 0 deletions dashboard/src/actions/metadataTreeActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import * as TYPES from "./types";

import store from "store/store";

const { getState } = store;

// Helper functions
const isChecked = (dataItem, checkedItems) =>
checkedItems && checkedItems.some((item) => item === dataItem.key);
const setChildNodes = (childNodes, isChecked) => {
childNodes.forEach(function iter(a) {
a.checkProps.checked = isChecked;
Array.isArray(a.children) && a.children.forEach(iter);
});
};
const getCheckedItemsKey = (ary) =>
ary.reduce(
(a, b) =>
a.concat(b.key, "children" in b ? getCheckedItemsKey(b.children) : []),
[]
);
const updateChildKeysList = (checked, checkedItems, childKeys) =>
checked
? [...checkedItems, ...childKeys]
: checkedItems.filter((x) => !childKeys.includes(x));

export const mapTree = (item) => {
const retVal = { ...item };
if (item.children) {
retVal.children = item.children.map((child) => mapTree(child));
item.checkProps.checked = null;
let seen = undefined;
for (const c of item.children) {
if (c.checkProps.checked == null) {
return retVal;
} else if (seen === undefined) {
seen = c.checkProps.checked;
} else if (seen !== c.checkProps.checked) return retVal;
}
item.checkProps.checked = seen;
} else {
const checkedItems = getState().overview.checkedItems;
item.checkProps.checked = isChecked(item, checkedItems);
}

return retVal;
};

export const onCheck =
(evt, treeViewItem, dataType) => async (dispatch, getState) => {
const checked = evt.target.checked;
const treeData = [...getState().overview.treeData];
let checkedItems = getState().overview.checkedItems;
const { options } = treeData.find((item) => item.title === dataType);
if ("children" in treeViewItem) {
const childNodes = treeViewItem.children;
const childKeys = getCheckedItemsKey(childNodes);

setChildNodes(childNodes, checked);
treeViewItem.checkProps.checked = checked;

checkedItems = updateChildKeysList(checked, checkedItems, childKeys);
} else if ("children" in options[0]) {
// if first child
const childNodes = options[0]["children"];
const node = childNodes.find((item) => item.key === treeViewItem.key);
node.checkProps.checked = checked;
// if only child of the parent, push the parent
if (childNodes.length === 1) {
options[0].checkProps.checked = checked;

checkedItems = updateChildKeysList(
checked,
checkedItems,
options[0].key
);
}
} else {
// leaf node
const node = options.find((item) => treeViewItem.key.includes(item.key));
node.checkProps.checked = checked;
}
if (checked) {
checkedItems = [...checkedItems, treeViewItem.key];
} else {
checkedItems = checkedItems.filter((item) => item !== treeViewItem.key);
}
dispatch({
type: TYPES.SET_TREE_DATA,
payload: treeData,
});
dispatch({
type: TYPES.SET_METADATA_CHECKED_KEYS,
payload: checkedItems,
});
};
169 changes: 166 additions & 3 deletions dashboard/src/actions/overviewActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export const getDatasets = () => async (dispatch, getState) => {
params.append("metadata", CONSTANTS.DATASET_UPLOADED);
params.append("metadata", CONSTANTS.SERVER_DELETION);
params.append("metadata", CONSTANTS.USER_FAVORITE);
params.append("metadata", "server");
params.append("metadata", "dataset");
params.append("metadata", "global");
params.append("metadata", "user");

params.append("mine", "true");
dispatch(setSelectedRuns([]));
Expand All @@ -38,7 +42,6 @@ export const getDatasets = () => async (dispatch, getState) => {
type: TYPES.USER_RUNS,
payload: data,
});

dispatch(initializeRuns());
}
}
Expand Down Expand Up @@ -70,7 +73,6 @@ const initializeRuns = () => (dispatch, getState) => {

clearEditableFields(item);
item[CONSTANTS.NAME_VALIDATED] = CONSTANTS.SUCCESS;

item[CONSTANTS.IS_ITEM_SEEN] = !!item?.metadata?.[CONSTANTS.DASHBOARD_SEEN];
item[CONSTANTS.IS_ITEM_FAVORITED] =
!!item?.metadata?.[CONSTANTS.USER_FAVORITE];
Expand Down Expand Up @@ -147,8 +149,16 @@ export const updateDataset =
);

for (const key in response.data.metadata) {
if (key in item.metadata)
if (key in item.metadata) {
item.metadata[key] = response.data.metadata[key];
if (checkNestedPath(key, item.metadata)) {
item.metadata = setValueFromPath(
key,
item.metadata,
response.data.metadata[key]
);
}
}
}
dispatch({
type: TYPES.USER_RUNS,
Expand Down Expand Up @@ -407,3 +417,156 @@ const clearEditableFields = (item) => {
item[CONSTANTS.IS_DIRTY_NAME] = false;
item[CONSTANTS.IS_DIRTY_SERVER_DELETE] = false;
};
export const setMetadataModal = (isOpen) => ({
type: TYPES.SET_METADATA_MODAL,
payload: isOpen,
});
/**
* Function to get keySummary and send to parse data
* @function
* @param {Function} dispatch - dispatch method of redux store
* @param {Function} getState - getstate method of redux store
* @return {Function} - dispatch the action and update the state
*/
export const getKeySummary = async (dispatch, getState) => {
try {
const endpoints = getState().apiEndpoint.endpoints;
const response = await API.get(uriTemplate(endpoints, "datasets_list"), {
params: { keysummary: true },
});
if (response.status === 200) {
if (response.data.keys) {
dispatch(parseKeySummaryforTree(response.data.keys));
}
}
} catch (error) {
dispatch(showToast(DANGER, ERROR_MSG));
}
};
/**
* Function to parse keySummary for the Tree View with checkboxes
* @function
* @param {Object} keySummary - dataset key summary
* @return {Function} - dispatch the action and update the state
*/
export const parseKeySummaryforTree = (keySummary) => (dispatch, getState) => {
const parsedData = [];

const checkedItems = [...getState().overview.checkedItems];

for (const [item, subitem] of Object.entries(keySummary)) {
const dataObj = { title: item, options: [] };

for (const [key, value] of Object.entries(subitem)) {
const aggregateKey = `${item}${CONSTANTS.KEYS_JOIN_BY}${key}`;
if (!isServerInternal(aggregateKey)) {
const isChecked = checkedItems.includes(aggregateKey);
const obj = constructTreeObj(aggregateKey, isChecked);
if (value) {
// has children
obj["children"] = constructChildTreeObj(
aggregateKey,
value,
checkedItems
);
}
dataObj.options.push(obj);
}
}
parsedData.push(dataObj);
}
dispatch({
type: TYPES.SET_METADATA_CHECKED_KEYS,
payload: checkedItems,
});
dispatch({
type: TYPES.SET_TREE_DATA,
payload: parsedData,
});
};

const constructChildTreeObj = (aggregateKey, entity, checkedItems) => {
const childObj = [];
for (const item in entity) {
if (!isServerInternal(`${aggregateKey}${CONSTANTS.KEYS_JOIN_BY}${item}`)) {
const newKey = `${aggregateKey}${CONSTANTS.KEYS_JOIN_BY}${item}`;
const isParentChecked = checkedItems.includes(aggregateKey);

const isChecked = isParentChecked || checkedItems.includes(newKey);
if (isParentChecked && !checkedItems.includes(newKey)) {
checkedItems.push(newKey);
}
const obj = constructTreeObj(newKey, isChecked);

if (entity[item]) {
obj["children"] = constructChildTreeObj(
newKey,
entity[item],
checkedItems
);
}
childObj.push(obj);
}
}
return childObj;
};

const constructTreeObj = (aggregateKey, isChecked) => ({
name: aggregateKey.split(CONSTANTS.KEYS_JOIN_BY).pop(),
key: aggregateKey,
id: aggregateKey,
checkProps: {
"aria-label": `${aggregateKey}-check`,
checked: isChecked,
},
});

const nonEssentialKeys = [
CONSTANTS.KEY_INDEX_REGEX,
CONSTANTS.KEY_OPERATIONS_REGEX,
CONSTANTS.KEY_TOOLS_REGEX,
CONSTANTS.KEY_ITERATIONS_REGEX,
CONSTANTS.KEY_TARBALL_PATH_REGEX,
];

const isServerInternal = (string) =>
nonEssentialKeys.some((e) => string.match(e));

/**
* Function to update metadata
* @function
* @param {String} path - nested key to update
* @param {Object} obj - nested object
* @param {String} obj - new value to be updated in the object
* @return {Object} - updated object with new value
*/

const setValueFromPath = (path, obj, value) => {
const [head, ...rest] = path.split(".");

return {
...obj,
[head]: rest.length
? setValueFromPath(rest.join("."), obj[head], value)
: value,
};
};

/**
* Function to check if the nested object has the given path of key
* @function
* @param {String} path - path of key
* @param {Object} obj - nested object
* @return {Boolean} - true/false if the object has/not the key
*/

const checkNestedPath = function (path, obj = {}) {
const args = path.split(".");
for (let i = 0; i < args.length; i++) {
if (!obj || !obj.hasOwnProperty(args[i])) {
return false;
}
obj = obj[args[i]];
}
return true;
};
3 changes: 3 additions & 0 deletions dashboard/src/actions/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ export const SET_LOADING_FLAG = "SET_LOADING_FLAG";
export const SELECTED_SAVED_RUNS = "SELECTED_SAVED_RUNS";
export const TOGGLE_RELAY_MODAL = "TOGGLE_RELAY_MODAL";
export const SET_RELAY_DATA = "SET_RELAY_DATA";
export const SET_METADATA_MODAL = "SET_METADATA_MODAL";
export const SET_TREE_DATA = "SET_TREE_DATA";
export const SET_METADATA_CHECKED_KEYS = "SET_METADATA_CHECKED_KEYS";

/* TABLE OF CONTENT */
export const GET_TOC_DATA = "GET_TOC_DATA";
Expand Down
6 changes: 6 additions & 0 deletions dashboard/src/assets/constants/overviewConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ export const IS_DIRTY_SERVER_DELETE = "isDirtyDate";
export const IS_EDIT = "isEdit";
export const IS_ITEM_FAVORITED = "isItemFavorited";
export const IS_ITEM_SEEN = "isItemSeen";
export const KEYS_JOIN_BY = "*";
export const KEY_INDEX_REGEX = /^server[.*]index-map$/;
export const KEY_ITERATIONS_REGEX = /^dataset[.*]metalog[.*]iterations/;
export const KEY_OPERATIONS_REGEX = /^dataset[.*]operations$/;
export const KEY_TOOLS_REGEX = /^dataset[.*]metalog[.*]tools/;
export const KEY_TARBALL_PATH_REGEX = /^server[.*]tarball-path$/;
export const NAME_COPY = "name_copy";
export const NAME_ERROR_MSG = "name_errorMsg";
export const NAME_KEY = "name";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import "./index.less";

import { mapTree, onCheck } from "actions/metadataTreeActions";
import { useDispatch, useSelector } from "react-redux";

import React from "react";
import { TreeView } from "@patternfly/react-core";

const MetadataTreeView = () => {
const { treeData } = useSelector((state) => state.overview);
const dispatch = useDispatch();
const onCheckHandler = (evt, treeViewItem, dataType) => {
dispatch(onCheck(evt, treeViewItem, dataType));
};
return (
<div className="treeview-container">
{treeData &&
treeData.length > 0 &&
treeData.map((item) => {
const mapped = item.options.map((item) => mapTree(item));
return (
<div key={item.title}>
<h1 className="title">{item.title}</h1>
<TreeView
data={mapped}
hasChecks
onCheck={(evt, treeItem) =>
onCheckHandler(evt, treeItem, item.title)
}
/>
</div>
);
})}
</div>
);
};

export default MetadataTreeView;
Loading

0 comments on commit 6bd64e8

Please sign in to comment.