diff --git a/frontend/src/actions/ilabActions.js b/frontend/src/actions/ilabActions.js index 53f051f..d715c72 100644 --- a/frontend/src/actions/ilabActions.js +++ b/frontend/src/actions/ilabActions.js @@ -78,8 +78,11 @@ export const setIlabDateFilter = appendQueryString({ ...appliedFilters, start_date, end_date }, navigate); }; -export const fetchMetricsInfo = (uid) => async (dispatch) => { +export const fetchMetricsInfo = (uid) => async (dispatch, getState) => { try { + if (getState().ilab.metrics?.find((i) => i.uid == uid)) { + return; + } dispatch({ type: TYPES.LOADING }); const response = await API.get(`/api/v1/ilab/runs/${uid}/metrics`); if (response.status === 200) { @@ -87,9 +90,10 @@ export const fetchMetricsInfo = (uid) => async (dispatch) => { response.data.constructor === Object && Object.keys(response.data).length > 0 ) { + const metrics = Object.keys(response.data).sort(); dispatch({ type: TYPES.SET_ILAB_METRICS, - payload: { uid, metrics: Object.keys(response.data).sort() }, + payload: { uid, metrics }, }); } } @@ -100,8 +104,11 @@ export const fetchMetricsInfo = (uid) => async (dispatch) => { dispatch({ type: TYPES.COMPLETED }); }; -export const fetchPeriods = (uid) => async (dispatch) => { +export const fetchPeriods = (uid) => async (dispatch, getState) => { try { + if (getState().ilab.periods?.find((i) => i.uid == uid)) { + return; + } dispatch({ type: TYPES.LOADING }); const response = await API.get(`/api/v1/ilab/runs/${uid}/periods`); if (response.status === 200) { @@ -121,146 +128,147 @@ export const fetchPeriods = (uid) => async (dispatch) => { dispatch({ type: TYPES.COMPLETED }); }; -export const fetchSummaryData = - (uid, metric = null) => - async (dispatch, getState) => { - try { - const periods = getState().ilab.periods.find((i) => i.uid == uid); - const metrics = getState().ilab.metrics_selected[uid]; - dispatch({ type: TYPES.SET_ILAB_SUMMARY_LOADING }); - let summaries = []; - periods?.periods?.forEach((p) => { - if (p.is_primary) { - summaries.push({ - run: uid, - metric: p.primary_metric, - periods: [p.id], - }); - } - if (metrics) { - metrics.forEach((metric) => +export const fetchSummaryData = (uid) => async (dispatch, getState) => { + try { + const periods = getState().ilab.periods.find((i) => i.uid == uid); + const metrics = getState().ilab.metrics_selected; + const avail_metrics = getState().ilab.metrics; + dispatch({ type: TYPES.SET_ILAB_SUMMARY_LOADING }); + let summaries = []; + periods?.periods?.forEach((p) => { + if (p.is_primary) { + summaries.push({ + run: uid, + metric: p.primary_metric, + periods: [p.id], + }); + } + if (metrics) { + metrics.forEach((metric) => { + if ( + avail_metrics.find((m) => m.uid == uid)?.metrics?.includes(metric) + ) { summaries.push({ run: uid, metric, aggregate: true, periods: [p.id], - }) - ); - } - }); - const response = await API.post( - `/api/v1/ilab/runs/multisummary`, - summaries - ); - if (response.status === 200) { - dispatch({ - type: TYPES.SET_ILAB_SUMMARY_DATA, - payload: { uid, data: response.data }, + }); + } }); } - } catch (error) { - console.error( - `ERROR (${error?.response?.status}): ${JSON.stringify( - error?.response?.data - )}` - ); - dispatch(showFailureToast()); - } - dispatch({ type: TYPES.SET_ILAB_SUMMARY_COMPLETE }); - }; - -export const handleSummaryData = - (uids, metric = null) => - async (dispatch, getState) => { - try { - const periods = getState().ilab.periods; - const pUids = periods.map((i) => i.uid); - const missingPeriods = uids.filter(function (x) { - return pUids.indexOf(x) < 0; + }); + const response = await API.post( + `/api/v1/ilab/runs/multisummary`, + summaries + ); + if (response.status === 200) { + dispatch({ + type: TYPES.SET_ILAB_SUMMARY_DATA, + payload: { uid, data: response.data }, }); - console.log(`Missing periods for ${missingPeriods}`); - await Promise.all( - missingPeriods.map(async (uid) => { - console.log(`Fetching periods for ${uid}`); - await dispatch(fetchPeriods(uid)); // Dispatch each item - }) - ); - await Promise.all( - uids.map(async (uid) => { - console.log(`Fetching summary data for ${uid}`); - await dispatch(fetchSummaryData(uid, metric)); - }) - ); - } catch (error) { - console.error(`ERROR: ${JSON.stringify(error)}`); - dispatch(showFailureToast()); } - }; + } catch (error) { + console.error( + `ERROR (${error?.response?.status}): ${JSON.stringify( + error?.response?.data + )}` + ); + dispatch(showFailureToast()); + } + dispatch({ type: TYPES.SET_ILAB_SUMMARY_COMPLETE }); +}; -export const fetchGraphData = - (uid, metric = null) => - async (dispatch, getState) => { - try { - const periods = getState().ilab.periods.find((i) => i.uid == uid); - const graphData = cloneDeep(getState().ilab.graphData); - const filterData = graphData.filter((i) => i.uid !== uid); - const metrics = getState().ilab.metrics_selected[uid]; - dispatch({ - type: TYPES.SET_ILAB_GRAPH_DATA, - payload: filterData, - }); - const copyData = cloneDeep(filterData); - dispatch({ type: TYPES.GRAPH_LOADING }); - let graphs = []; - periods?.periods?.forEach((p) => { - if (p.is_primary) { - graphs.push({ run: uid, metric: p.primary_metric, periods: [p.id] }); - } - if (metrics) { - metrics.forEach((metric) => +export const handleSummaryData = (uids) => async (dispatch, getState) => { + try { + const periods = getState().ilab.periods; + const pUids = periods.map((i) => i.uid); + const missingPeriods = uids.filter(function (x) { + return pUids.indexOf(x) < 0; + }); + await Promise.all( + missingPeriods.map(async (uid) => { + await dispatch(fetchPeriods(uid)); // Dispatch each item + }) + ); + await Promise.all( + uids.map(async (uid) => { + await dispatch(fetchSummaryData(uid)); + }) + ); + } catch (error) { + console.error(`ERROR: ${JSON.stringify(error)}`); + dispatch(showFailureToast()); + } +}; + +export const fetchGraphData = (uid) => async (dispatch, getState) => { + try { + const periods = getState().ilab.periods.find((i) => i.uid == uid); + const graphData = cloneDeep(getState().ilab.graphData); + const filterData = graphData.filter((i) => i.uid !== uid); + const metrics = getState().ilab.metrics_selected; + const avail_metrics = getState().ilab.metrics; + dispatch({ + type: TYPES.SET_ILAB_GRAPH_DATA, + payload: filterData, + }); + const copyData = cloneDeep(filterData); + dispatch({ type: TYPES.GRAPH_LOADING }); + let graphs = []; + periods?.periods?.forEach((p) => { + if (p.is_primary) { + graphs.push({ run: uid, metric: p.primary_metric, periods: [p.id] }); + } + if (metrics) { + metrics.forEach((metric) => { + if ( + avail_metrics.find((m) => m.uid == uid)?.metrics?.includes(metric) + ) { graphs.push({ run: uid, metric, aggregate: true, periods: [p.id], - }) - ); - } - }); - const response = await API.post(`/api/v1/ilab/runs/multigraph`, { - name: `graph ${uid}`, - graphs, - }); - if (response.status === 200) { - response.data.layout["showlegend"] = true; - response.data.layout["responsive"] = "true"; - response.data.layout["autosize"] = "true"; - response.data.layout["legend"] = { - orientation: "h", - xanchor: "left", - yanchor: "top", - y: -0.1, - }; - copyData.push({ - uid, - data: response.data.data, - layout: response.data.layout, - }); - dispatch({ - type: TYPES.SET_ILAB_GRAPH_DATA, - payload: copyData, + }); + } }); } - } catch (error) { - console.error( - `ERROR (${error?.response?.status}): ${JSON.stringify( - error?.response?.data - )}` - ); - dispatch(showFailureToast()); + }); + const response = await API.post(`/api/v1/ilab/runs/multigraph`, { + name: `graph ${uid}`, + graphs, + }); + if (response.status === 200) { + response.data.layout["showlegend"] = true; + response.data.layout["responsive"] = "true"; + response.data.layout["autosize"] = "true"; + response.data.layout["legend"] = { + orientation: "h", + xanchor: "left", + yanchor: "top", + y: -0.1, + }; + copyData.push({ + uid, + data: response.data.data, + layout: response.data.layout, + }); + dispatch({ + type: TYPES.SET_ILAB_GRAPH_DATA, + payload: copyData, + }); } - dispatch({ type: TYPES.GRAPH_COMPLETED }); - }; + } catch (error) { + console.error( + `ERROR (${error?.response?.status}): ${JSON.stringify( + error?.response?.data + )}` + ); + dispatch(showFailureToast()); + } + dispatch({ type: TYPES.GRAPH_COMPLETED }); +}; export const handleMultiGraph = (uids) => async (dispatch, getState) => { try { @@ -292,6 +300,8 @@ export const fetchMultiGraphData = (uids) => async (dispatch, getState) => { dispatch({ type: TYPES.LOADING }); const periods = getState().ilab.periods; const filterPeriods = periods.filter((item) => uids.includes(item.uid)); + const get_metrics = getState().ilab.metrics_selected; + const avail_metrics = getState().ilab.metrics; let graphs = []; uids.forEach(async (uid) => { @@ -304,12 +314,20 @@ export const fetchMultiGraphData = (uids) => async (dispatch, getState) => { periods: [p.id], }); } - // graphs.push({ - // run: uid, - // metric, - // aggregate: true, - // periods: [p.id], - // }); + if (get_metrics) { + get_metrics.forEach((m) => { + if ( + avail_metrics.find((m) => m.uid == uid)?.metrics?.includes(metric) + ) { + graphs.push({ + run: uid, + metric: m, + aggregate: true, + periods: [p.id], + }); + } + }); + } }); }); console.log(graphs); @@ -374,15 +392,13 @@ export const checkIlabJobs = (newPage) => (dispatch, getState) => { } }; -export const toggleSelectedMetric = (id, metric) => (dispatch, getState) => { - const metrics_selected = cloneDeep(getState().ilab.metrics_selected); - var new_selected = metrics_selected[id] ? metrics_selected[id] : []; - if (new_selected.includes(metric)) { - new_selected = new_selected.filter((m) => m !== metric); +export const toggleSelectedMetric = (metric) => (dispatch, getState) => { + let metrics_selected = getState().ilab.metrics_selected; + if (metrics_selected.includes(metric)) { + metrics_selected = metrics_selected.filter((m) => m !== metric); } else { - new_selected = [...new_selected, metric]; + metrics_selected = [...metrics_selected, metric]; } - metrics_selected[id] = new_selected; dispatch({ type: TYPES.SET_ILAB_SELECTED_METRICS, payload: metrics_selected, diff --git a/frontend/src/components/templates/ILab/ILabSummary.jsx b/frontend/src/components/templates/ILab/ILabSummary.jsx index 25526a7..45e1fd9 100644 --- a/frontend/src/components/templates/ILab/ILabSummary.jsx +++ b/frontend/src/components/templates/ILab/ILabSummary.jsx @@ -11,9 +11,9 @@ const ILabSummary = (props) => { const data = summaryData?.find((a) => a.uid === id); return data; }; - const hasSummaryData = (ids) => { + const hasSummaryData = (runs) => { const hasData = Boolean( - summaryData.filter((i) => ids.includes(i.uid)).length === ids.length + summaryData.filter((i) => runs.includes(i.uid)).length === ids.length ); return hasData; }; @@ -23,11 +23,12 @@ const ILabSummary = (props) => { {hasSummaryData(ids) ? ( - + {ids.length > 1 ? : <>} @@ -40,14 +41,12 @@ const ILabSummary = (props) => { {ids.map((id, ridx) => getSummaryData(id).data.map((stat, sidx) => ( - + {ids.length > 1 && sidx === 0 ? ( - ) : undefined} + ) : ( + <> + )}
RunMetric
{ridx + 1}{stat.title} {typeof stat.min === "number" @@ -84,6 +83,6 @@ const ILabSummary = (props) => { }; ILabSummary.propTypes = { - item: PropType.object, + ids: PropType.array, }; export default ILabSummary; diff --git a/frontend/src/components/templates/ILab/IlabCompareComponent.jsx b/frontend/src/components/templates/ILab/IlabCompareComponent.jsx index 19b8364..96bebd0 100644 --- a/frontend/src/components/templates/ILab/IlabCompareComponent.jsx +++ b/frontend/src/components/templates/ILab/IlabCompareComponent.jsx @@ -18,11 +18,17 @@ import Plot from "react-plotly.js"; import PropTypes from "prop-types"; import RenderPagination from "@/components/organisms/Pagination"; import { cloneDeep } from "lodash"; -import { handleMultiGraph, handleSummaryData } from "@/actions/ilabActions.js"; +import { + fetchPeriods, + handleMultiGraph, + handleSummaryData, + fetchMetricsInfo, +} from "@/actions/ilabActions.js"; import { uid } from "@/utils/helper"; import { useState } from "react"; import ILabSummary from "./ILabSummary"; import ILabMetadata from "./ILabMetadata"; +import MetricsSelect from "./MetricsDropdown"; const IlabCompareComponent = () => { const { page, perPage, totalItems, tableData } = useSelector( @@ -42,6 +48,8 @@ const IlabCompareComponent = () => { setSelectedItems(selectedItems.filter((id) => id !== item)); } else { setSelectedItems([...selectedItems, item]); + dispatch(fetchPeriods(item)); + dispatch(fetchMetricsInfo(item)); } }; const dummy = () => { @@ -76,9 +84,8 @@ const IlabCompareComponent = () => { Metadata} + headerContent={Metadata} appendTo={() => document.body} - // hasAutoWidth hasNoPadding position="auto" className="mini-metadata" @@ -109,6 +116,9 @@ const IlabCompareComponent = () => { /> + + + {isSummaryLoading ? (
diff --git a/frontend/src/components/templates/ILab/IlabExpandedRow.jsx b/frontend/src/components/templates/ILab/IlabExpandedRow.jsx index 06078fe..99f095a 100644 --- a/frontend/src/components/templates/ILab/IlabExpandedRow.jsx +++ b/frontend/src/components/templates/ILab/IlabExpandedRow.jsx @@ -14,6 +14,7 @@ import MetricsSelect from "./MetricsDropdown"; import PropTypes from "prop-types"; import { setMetaRowExpanded } from "@/actions/ilabActions"; import ILabMetadata from "./ILabMetadata"; +import { uid } from "@/utils/helper"; const IlabRowContent = (props) => { const { item } = props; @@ -34,7 +35,7 @@ const IlabRowContent = (props) => { }; return ( - + { onToggle(`metadata-toggle-${item.id}`); @@ -52,7 +53,7 @@ const IlabRowContent = (props) => { - + { onToggle(`graph-toggle-${item.id}`); @@ -67,12 +68,12 @@ const IlabRowContent = (props) => { isHidden={!metaRowExpanded.includes(`graph-toggle-${item.id}`)} >
Metrics:
- + - + - + diff --git a/frontend/src/components/templates/ILab/MetricsDropdown.jsx b/frontend/src/components/templates/ILab/MetricsDropdown.jsx index 4bf4194..68f738d 100644 --- a/frontend/src/components/templates/ILab/MetricsDropdown.jsx +++ b/frontend/src/components/templates/ILab/MetricsDropdown.jsx @@ -8,22 +8,19 @@ import { } from "@patternfly/react-core"; import { fetchGraphData, + fetchMultiGraphData, fetchSummaryData, + handleSummaryData, toggleSelectedMetric, } from "@/actions/ilabActions"; import { useDispatch, useSelector } from "react-redux"; import PropTypes from "prop-types"; -import { cloneDeep } from "lodash"; -import { uid } from "@/utils/helper"; import { useState } from "react"; const MetricsSelect = (props) => { const { metrics, metrics_selected } = useSelector((state) => state.ilab); - const { item } = props; - var current_metrics = metrics_selected[item.id] - ? metrics_selected[item.id] - : []; + const { ids } = props; /* Metrics select */ const [isOpen, setIsOpen] = useState(false); @@ -34,51 +31,82 @@ const MetricsSelect = (props) => { ref={toggleRef} onClick={onToggleClick} isExpanded={isOpen} - badge={{`${current_metrics.length} selected`}} + badge={{`${metrics_selected.length} selected`}} > Additional metrics ); - const onToggleClick = () => { + const onToggleClick = async () => { setIsOpen(!isOpen); }; - const onSelect = (_event, value) => { - const [run, metric] = value; - dispatch(toggleSelectedMetric(run, metric)); - dispatch(fetchGraphData(run, metric)); - dispatch(fetchSummaryData(run, metric)); + const onSelect = (_event, metric) => { + dispatch(toggleSelectedMetric(metric)); + }; + + const onOpenChange = async (nextOpen) => { + if (!nextOpen) { + // If we're closing, fetch data + if (ids.length === 1) { + await Promise.all([ + await dispatch(fetchGraphData(ids[0])), + await dispatch(fetchSummaryData(ids[0])), + ]); + } else { + await Promise.all([ + await dispatch(fetchMultiGraphData(ids)), + await dispatch(handleSummaryData(ids)), + ]); + } + }; + setIsOpen(nextOpen); }; - const metricsDataCopy = cloneDeep(metrics); const getMetricsData = (id) => { - const data = metricsDataCopy?.filter((a) => a.uid === id); - return data; + const data = metrics?.filter((a) => a.uid === id); + return data?.metrics; }; - const hasMetricsData = (uuid) => { - const hasData = getMetricsData(uuid).length > 0; + const hasAllMetricsData = (runs) => { + const hasData = Boolean( + metrics?.filter((i) => runs.includes(i.uid)).length === runs.length + ); return hasData; }; + + // de-dup a "set" using object keys + var collector = {}; + if (hasAllMetricsData(ids)) { + const datas = metrics.filter((a) => ids.includes(a.uid)); + if (datas) { + datas.forEach((a) => { + if (a.metrics) { + a.metrics.forEach((k) => (collector[k] = true)); + } + }); + } + } + const all_metrics = Object.keys(collector).sort(); + /* Metrics select */ return ( <> - {hasMetricsData(item.id) ? ( + {hasAllMetricsData(ids) ? (