Skip to content

Commit

Permalink
Multi-run comparison UI
Browse files Browse the repository at this point in the history
This adds the basic UI to support comparison of the metrics of two InstructLab
runs. This compares only the primary metrics of the two runs, in a relative
timeline graph.

This is backed by cloud-bulldozer#125, which is backed by cloud-bulldozer#124, which is backed by cloud-bulldozer#123,
which is backed by cloud-bulldozer#122. These represent a series of steps towards a complete
InstructLab UI and API, and will be reviewed and merged from cloud-bulldozer#122 forward.
  • Loading branch information
MVarshini authored and dbutenhof committed Oct 24, 2024
1 parent 7215bb2 commit 1fb02a0
Show file tree
Hide file tree
Showing 11 changed files with 512 additions and 158 deletions.
2 changes: 1 addition & 1 deletion frontend/src/actions/filterActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export const setDateFilter = (date, key, navigation, currType) => {
dispatch(setTelcoDateFilter(date, key, navigation));
} else if (currType === "ilab") {
dispatch(setIlabDateFilter(date, key, navigation));
dispatch(fetchILabJobs());
dispatch(fetchILabJobs(true));
}
};

Expand Down
112 changes: 105 additions & 7 deletions frontend/src/actions/ilabActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ export const fetchPeriods = (uid) => async (dispatch) => {
};

export const fetchGraphData =
(uid, metric, primary_metric) => async (dispatch, getState) => {
(uid, metric = null) =>
async (dispatch, getState) => {
try {
const periods = getState().ilab.periods.find((i) => i.uid == uid);
const graphData = cloneDeep(getState().ilab.graphData);
Expand All @@ -136,15 +137,17 @@ export const fetchGraphData =
let graphs = [];
periods?.periods?.forEach((p) => {
graphs.push({ metric: p.primary_metric, periods: [p.id] });
graphs.push({
metric,
aggregate: true,
periods: [p.id],
});
if (metric) {
graphs.push({
metric,
aggregate: true,
periods: [p.id],
});
}
});
const response = await API.post(`/api/v1/ilab/runs/multigraph`, {
run: uid,
name: primary_metric,
name: `graph ${uid}`,
graphs,
});
if (response.status === 200) {
Expand All @@ -169,6 +172,86 @@ export const fetchGraphData =
dispatch({ type: TYPES.GRAPH_COMPLETED });
};

export const handleMultiGraph = (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
})
);

dispatch(fetchMultiGraphData(uids));
} catch (error) {
console.error(
`ERROR (${error?.response?.status}): ${JSON.stringify(
error?.response?.data
)}`
);
dispatch(showFailureToast());
}
};
export const fetchMultiGraphData = (uids) => async (dispatch, getState) => {
try {
dispatch({ type: TYPES.LOADING });
const periods = getState().ilab.periods;
const filterPeriods = periods.filter((item) => uids.includes(item.uid));

let graphs = [];
uids.forEach(async (uid) => {
const periods = filterPeriods.find((i) => i.uid == uid);
periods?.periods?.forEach((p) => {
graphs.push({
run: uid,
metric: p.primary_metric,
periods: [p.id],
});
// graphs.push({
// run: uid,
// metric,
// aggregate: true,
// periods: [p.id],
// });
});
});
console.log(graphs);
const response = await API.post(`/api/v1/ilab/runs/multigraph`, {
name: "comparison",
relative: true,
graphs,
});
if (response.status === 200) {
response.data.layout["showlegend"] = true;
response.data.layout["responsive"] = "true";
response.data.layout["autosize"] = "true";
response.data.layout["legend"] = { x: 0, y: 1.5 };
const graphData = [];
graphData.push({
data: response.data.data,
layout: response.data.layout,
});
dispatch({
type: TYPES.SET_ILAB_MULTIGRAPH_DATA,
payload: graphData,
});
}
} catch (error) {
console.error(
`ERROR (${error?.response?.status}): ${JSON.stringify(
error?.response?.data
)}`
);
dispatch(showFailureToast());
}
dispatch({ type: TYPES.COMPLETED });
};

export const setIlabPage = (pageNo) => ({
type: TYPES.SET_ILAB_PAGE,
payload: pageNo,
Expand Down Expand Up @@ -210,4 +293,19 @@ export const tableReCalcValues = () => (dispatch, getState) => {
const startIdx = page !== 1 ? (page - 1) * perPage : 0;
const endIdx = page !== 1 ? page * perPage - 1 : perPage;
dispatch(sliceIlabTableRows(startIdx, endIdx));
dispatch(getMetaRowdId());
};

export const getMetaRowdId = () => (dispatch, getState) => {
const tableData = getState().ilab.tableData;
const metaId = tableData.map((item) => `metadata-toggle-${item.id}`);
dispatch(setMetaRowExpanded(metaId));
};
export const toggleComparisonSwitch = () => ({
type: TYPES.TOGGLE_COMPARISON_SWITCH,
});

export const setMetaRowExpanded = (expandedItems) => ({
type: TYPES.SET_EXPANDED_METAROW,
payload: expandedItems,
});
3 changes: 3 additions & 0 deletions frontend/src/actions/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export const SET_TELCO_GRAPH_DATA = "SET_TELCO_GRAPH_DATA";
export const SET_ILAB_JOBS_DATA = "SET_ILAB_JOBS_DATA";
export const SET_ILAB_DATE_FILTER = "SET_ILAB_DATE_FILTER";
export const SET_ILAB_GRAPH_DATA = "SET_ILAB_GRAPH_DATA";
export const SET_ILAB_MULTIGRAPH_DATA = "SET_ILAB_MULTIGRAPH_DATA";
export const SET_ILAB_TOTAL_ITEMS = "SET_ILAB_TOTAL_ITEMS";
export const SET_ILAB_OFFSET = "SET_ILAB_OFFSET";
export const SET_ILAB_PAGE = "SET_ILAB_PAGE";
Expand All @@ -89,3 +90,5 @@ export const SET_ILAB_METRICS = "SET_ILAB_METRICS";
export const SET_ILAB_SELECTED_METRICS = "SET_ILAB_SELECTED_METRICS";
export const SET_ILAB_PERIODS = "SET_ILAB_PERIODS";
export const SET_ILAB_INIT_JOBS = "SET_ILAB_INIT_JOBS";
export const TOGGLE_COMPARISON_SWITCH = "TOGGLE_COMPARISON_SWITCH";
export const SET_EXPANDED_METAROW = "SET_EXPANDED_METAROW";
19 changes: 18 additions & 1 deletion frontend/src/components/organisms/TableFilters/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "./index.less";
import {
Chip,
ChipGroup,
Switch,
Toolbar,
ToolbarContent,
ToolbarItem,
Expand Down Expand Up @@ -39,6 +40,8 @@ const TableFilter = (props) => {
setColumns,
selectedFilters,
updateSelectedFilter,
onSwitchChange,
isSwitchChecked,
} = props;

const category =
Expand Down Expand Up @@ -66,7 +69,7 @@ const TableFilter = (props) => {
setDateFilter(date, key, navigation, type);
};
const endDateChangeHandler = (date, key) => {
setDateFilter(key, date, navigation, type);
setDateFilter(date, key, navigation, type);
};

return (
Expand Down Expand Up @@ -123,6 +126,18 @@ const TableFilter = (props) => {
</ToolbarItem>
)}
</ToolbarContent>
{type === "ilab" && (
<ToolbarContent id="comparison-switch">
<ToolbarItem>
<Switch
label="Comparison"
isChecked={isSwitchChecked}
onChange={onSwitchChange}
ouiaId="Comparison Switch"
/>
</ToolbarItem>
</ToolbarContent>
)}
</Toolbar>
{appliedFilters &&
Object.keys(appliedFilters).length > 0 &&
Expand Down Expand Up @@ -154,5 +169,7 @@ TableFilter.propTypes = {
selectedFilters: PropTypes.array,
updateSelectedFilter: PropTypes.func,
navigation: PropTypes.func,
isSwitchChecked: PropTypes.bool,
onSwitchChange: PropTypes.func,
};
export default TableFilter;
4 changes: 4 additions & 0 deletions frontend/src/components/organisms/TableFilters/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@
.to-text {
padding: 5px 0;
}
#comparison-switch {
margin-left: auto;
align-content: center;
}
}
117 changes: 117 additions & 0 deletions frontend/src/components/templates/ILab/IlabCompareComponent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import "./index.less";

import {
Button,
Menu,
MenuContent,
MenuItem,
MenuItemAction,
MenuList,
Title,
} from "@patternfly/react-core";
import { useDispatch, useSelector } from "react-redux";

import { InfoCircleIcon } from "@patternfly/react-icons";
import Plot from "react-plotly.js";
import PropTypes from "prop-types";
import RenderPagination from "@/components/organisms/Pagination";
import { cloneDeep } from "lodash";
import { handleMultiGraph } from "@/actions/ilabActions.js";
import { uid } from "@/utils/helper";
import { useState } from "react";

const IlabCompareComponent = () => {
// const { data } = props;
const { page, perPage, totalItems, tableData } = useSelector(
(state) => state.ilab
);
const dispatch = useDispatch();
const [selectedItems, setSelectedItems] = useState([]);
const { multiGraphData } = useSelector((state) => state.ilab);
const isGraphLoading = useSelector((state) => state.loading.isGraphLoading);
const graphDataCopy = cloneDeep(multiGraphData);

const onSelect = (_event, itemId) => {
const item = itemId;
if (selectedItems.includes(item)) {
setSelectedItems(selectedItems.filter((id) => id !== item));
} else {
setSelectedItems([...selectedItems, item]);
}
};
const dummy = () => {
dispatch(handleMultiGraph(selectedItems));
};
return (
<div className="comparison-container">
<div className="metrics-container">
<Title headingLevel="h3" className="title">
Metrics
</Title>
<Button
className="compare-btn"
isDisabled={selectedItems.length < 2}
isBlock
onClick={dummy}
>
Compare
</Button>
<Menu onSelect={onSelect} selected={selectedItems}>
<MenuContent>
<MenuList>
{tableData.map((item) => {
return (
<MenuItem
key={uid()}
hasCheckbox
itemId={item.id}
isSelected={selectedItems.includes(item.id)}
actions={
<MenuItemAction
icon={<InfoCircleIcon aria-hidden />}
actionId="code"
onClick={() => console.log("clicked on code icon")}
aria-label="Code"
/>
}
>
{`${new Date(item.begin_date).toLocaleDateString()} ${
item.primary_metrics[0]
}`}
</MenuItem>
);
})}
</MenuList>
</MenuContent>
</Menu>
<RenderPagination
items={totalItems}
page={page}
perPage={perPage}
type={"ilab"}
/>
</div>
<div className="chart-container">
{isGraphLoading ? (
<div className="loader"></div>
) : graphDataCopy?.length > 0 &&
graphDataCopy?.[0]?.data?.length > 0 ? (
<div className="chart-box">
<Plot
data={graphDataCopy[0]?.data}
layout={graphDataCopy[0]?.layout}
key={uid()}
/>
</div>
) : (
<div>No data to compare</div>
)}
</div>
</div>
);
};

IlabCompareComponent.propTypes = {
data: PropTypes.array,
};
export default IlabCompareComponent;
Loading

0 comments on commit 1fb02a0

Please sign in to comment.