diff --git a/dashboard/src/actions/metadataTreeActions.js b/dashboard/src/actions/metadataTreeActions.js new file mode 100644 index 0000000000..2c294ffb1a --- /dev/null +++ b/dashboard/src/actions/metadataTreeActions.js @@ -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, + }); + }; diff --git a/dashboard/src/actions/overviewActions.js b/dashboard/src/actions/overviewActions.js index 7185a7b5c3..b09d438fdc 100644 --- a/dashboard/src/actions/overviewActions.js +++ b/dashboard/src/actions/overviewActions.js @@ -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([])); @@ -38,7 +42,6 @@ export const getDatasets = () => async (dispatch, getState) => { type: TYPES.USER_RUNS, payload: data, }); - dispatch(initializeRuns()); } } @@ -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]; @@ -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, @@ -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; +}; diff --git a/dashboard/src/actions/types.js b/dashboard/src/actions/types.js index 67080e4323..454e231268 100644 --- a/dashboard/src/actions/types.js +++ b/dashboard/src/actions/types.js @@ -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"; diff --git a/dashboard/src/assets/constants/overviewConstants.js b/dashboard/src/assets/constants/overviewConstants.js index b96111c873..b489610023 100644 --- a/dashboard/src/assets/constants/overviewConstants.js +++ b/dashboard/src/assets/constants/overviewConstants.js @@ -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"; diff --git a/dashboard/src/modules/components/OverviewComponent/MetadataTreeComponent.jsx b/dashboard/src/modules/components/OverviewComponent/MetadataTreeComponent.jsx new file mode 100644 index 0000000000..87d26db30c --- /dev/null +++ b/dashboard/src/modules/components/OverviewComponent/MetadataTreeComponent.jsx @@ -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 ( +
+ {columnNames.key} + | +{columnNames.value} | + + + {checkedItems.map((attr) => { + const levels = attr.split(CONSTANTS.KEYS_JOIN_BY); + const showKey = attr.split(CONSTANTS.KEYS_JOIN_BY).join("."); + const showValue = levels.reduce( + (a, level) => a?.[level] ?? "", + item.metadata + ); + return ( + <> + {showValue !== "" && showValue.constructor !== Object && ( +
---|---|
{showKey} | +{showValue.toString()} | +