diff --git a/dashboard/src/App.js b/dashboard/src/App.js index 4dcd94707f..2acf1e5def 100644 --- a/dashboard/src/App.js +++ b/dashboard/src/App.js @@ -96,17 +96,16 @@ const App = () => { path={APP_ROUTES.ANALYSIS} element={} /> + } + /> } /> - } - /> - } /> diff --git a/dashboard/src/actions/comparisonActions.js b/dashboard/src/actions/comparisonActions.js index aec2ee1ddc..b43fff38bc 100644 --- a/dashboard/src/actions/comparisonActions.js +++ b/dashboard/src/actions/comparisonActions.js @@ -37,17 +37,16 @@ export const getQuisbyData = (dataset) => async (dispatch, getState) => { dispatch(parseChartData()); } } catch (error) { - if (error?.response && error.response?.data) { - const errorMsg = error.response.data?.message; - const isUnsupportedType = errorMsg + if ( + error?.response?.data && + error.response.data?.message ?.toLowerCase() - .includes("unsupported benchmark"); - if (isUnsupportedType) { - dispatch({ - type: TYPES.IS_UNSUPPORTED_TYPE, - payload: errorMsg, - }); - } + .includes("unsupported benchmark") + ) { + dispatch({ + type: TYPES.IS_UNSUPPORTED_TYPE, + payload: error.response.data.message, + }); } else { dispatch(showToast(DANGER, ERROR_MSG)); } @@ -55,10 +54,12 @@ export const getQuisbyData = (dataset) => async (dispatch, getState) => { } dispatch({ type: TYPES.COMPLETED }); }; - +const COLORS = ["#8BC1F7", "#0066CC", "#519DE9", "#004B95", "#002F5D"]; export const parseChartData = () => (dispatch, getState) => { const response = getState().comparison.data.data; + const isCompareSwitchChecked = getState().comparison.isCompareSwitchChecked; const chartData = []; + let i = 0; for (const run of response) { const options = { @@ -97,26 +98,130 @@ export const parseChartData = () => (dispatch, getState) => { }, }; - const datasets = [ - { - label: run.instances[0].dataset_name, - data: run.instances.map((i) => i.time_taken), - backgroundColor: "#8BC1F7", - }, - ]; - + const datasets = []; const data = { - labels: run.instances.map((i) => i.name), + labels: [...new Set(run.instances.map((i) => i.name))], id: `${run.test_name}_${run.metrics_unit}`, datasets, }; + const result = run.instances.reduce(function (r, a) { + r[a.dataset_name] = r[a.dataset_name] || []; + r[a.dataset_name].push(a); + return r; + }, Object.create(null)); + + for (const [key, value] of Object.entries(result)) { + console.log(key); + + const map = {}; + for (const element of value) { + map[element.name] = element.time_taken.trim(); + } + const mappedData = data.labels.map((label) => { + return map[label]; + }); + const obj = { label: key, backgroundColor: COLORS[i], data: mappedData }; + i++; + datasets.push(obj); + } const obj = { options, data }; chartData.push(obj); + i = 0; } + const type = isCompareSwitchChecked + ? TYPES.SET_COMPARE_DATA + : TYPES.SET_PARSED_DATA; dispatch({ - type: TYPES.SET_PARSED_DATA, + type, payload: chartData, }); }; + +export const toggleCompareSwitch = () => (dispatch, getState) => { + dispatch({ + type: TYPES.TOGGLE_COMPARE_SWITCH, + payload: !getState().comparison.isCompareSwitchChecked, + }); +}; + +export const setSelectedId = (isChecked, rId) => (dispatch, getState) => { + let selectedIds = [...getState().comparison.selectedResourceIds]; + if (isChecked) { + selectedIds = [...selectedIds, rId]; + } else { + selectedIds = selectedIds.filter((item) => item !== rId); + } + dispatch({ + type: TYPES.SET_SELECTED_RESOURCE_ID, + payload: selectedIds, + }); +}; + +export const compareMultipleDatasets = () => async (dispatch, getState) => { + try { + dispatch({ type: TYPES.LOADING }); + + const endpoints = getState().apiEndpoint.endpoints; + const selectedIds = [...getState().comparison.selectedResourceIds]; + + const params = new URLSearchParams(); + params.append("datasets", selectedIds.toString()); + const response = await API.get( + uriTemplate(endpoints, "datasets_compare", {}), + { params } + ); + if (response.status === 200 && response.data.json_data) { + dispatch({ + type: TYPES.SET_QUISBY_DATA, + payload: response.data.json_data, + }); + dispatch({ + type: TYPES.UNMATCHED_BENCHMARK_TYPES, + payload: "", + }); + dispatch(parseChartData()); + } + } catch (error) { + if ( + error?.response?.data && + error.response.data?.message + ?.toLowerCase() + .includes("benchmarks must match") + ) { + dispatch({ + type: TYPES.UNMATCHED_BENCHMARK_TYPES, + payload: error.response.data.message, + }); + } else { + dispatch(showToast(DANGER, ERROR_MSG)); + } + dispatch({ type: TYPES.NETWORK_ERROR }); + } + dispatch({ type: TYPES.COMPLETED }); +}; + +export const setChartModalContent = (chartId) => (dispatch, getState) => { + const isCompareSwitchChecked = getState().comparison.isCompareSwitchChecked; + const data = isCompareSwitchChecked + ? getState().comparison.compareChartData + : getState().comparison.chartData; + + const activeChart = data.filter((item) => item.data.id === chartId)[0]; + + dispatch({ + type: TYPES.SET_CURRENT_CHARTID, + payload: activeChart, + }); +}; + +export const setChartModal = (isOpen) => ({ + type: TYPES.SET_CHART_MODAL, + payload: isOpen, +}); + +export const setSearchValue = (value) => ({ + type: TYPES.SET_SEARCH_VALUE, + payload: value, +}); diff --git a/dashboard/src/actions/types.js b/dashboard/src/actions/types.js index 7f1d9ccd00..a9cc6a9d98 100644 --- a/dashboard/src/actions/types.js +++ b/dashboard/src/actions/types.js @@ -65,3 +65,10 @@ export const SET_QUISBY_DATA = "SET_QUISBY_DATA"; export const SET_PARSED_DATA = "SET_PARSED_DATA"; export const SET_ACTIVE_RESOURCEID = "SET_ACTIVE_RESOURCEID"; export const IS_UNSUPPORTED_TYPE = "IS_UNSUPPORTED_TYPE"; +export const TOGGLE_COMPARE_SWITCH = "TOGGLE_COMPARE_SWITCH"; +export const SET_SELECTED_RESOURCE_ID = "SET_SELECTED_RESOURCE_ID"; +export const UNMATCHED_BENCHMARK_TYPES = "UNMATCHED_BENCHMARK_TYPES"; +export const SET_CHART_MODAL = "SET_CHART_MODAL"; +export const SET_CURRENT_CHARTID = "SET_CURRENT_CHARTID"; +export const SET_COMPARE_DATA = "SET_COMPARE_DATA"; +export const SET_SEARCH_VALUE = "SET_SEARCH_VALUE"; diff --git a/dashboard/src/modules/components/ComparisonComponent/ChartGallery.jsx b/dashboard/src/modules/components/ComparisonComponent/ChartGallery.jsx index 5aeea0c3ef..43bf0ef287 100644 --- a/dashboard/src/modules/components/ComparisonComponent/ChartGallery.jsx +++ b/dashboard/src/modules/components/ComparisonComponent/ChartGallery.jsx @@ -7,18 +7,13 @@ import { Title, Tooltip, } from "chart.js"; -import { - Card, - EmptyState, - EmptyStateBody, - EmptyStateVariant, - Gallery, - GalleryItem, -} from "@patternfly/react-core"; +import { Card, Gallery, GalleryItem } from "@patternfly/react-core"; +import { setChartModal, setChartModalContent } from "actions/comparisonActions"; import { Bar } from "react-chartjs-2"; +import { ExpandArrowsAltIcon } from "@patternfly/react-icons"; import React from "react"; -import { useSelector } from "react-redux"; +import { useDispatch } from "react-redux"; ChartJS.register( BarElement, @@ -28,55 +23,46 @@ ChartJS.register( CategoryScale, LinearScale ); -const EmptyStateExtraSmall = (props) => ( - -
{props.message}
- Benchmark type is currently unsupported! -
-); -const ChartGallery = () => { - const { chartData, unsupportedType } = useSelector( - (state) => state.comparison - ); + +const ChartGallery = (props) => { + const dispatch = useDispatch(); + + const handleExpandClick = (chartId) => { + dispatch(setChartModal(true)); + dispatch(setChartModalContent(chartId)); + }; return ( <> - {unsupportedType ? ( - - ) : ( - <> - {chartData && chartData.length > 0 && ( - - {chartData.map((chart) => ( - 0 && ( + + {props.dataToPlot.map((chart) => ( + +
+
handleExpandClick(chart.data.id)} > - - - - - ))} - - )} - + +
+
+ + + +
+ ))} +
)} ); diff --git a/dashboard/src/modules/components/ComparisonComponent/ChartModal.jsx b/dashboard/src/modules/components/ComparisonComponent/ChartModal.jsx new file mode 100644 index 0000000000..dce19c157e --- /dev/null +++ b/dashboard/src/modules/components/ComparisonComponent/ChartModal.jsx @@ -0,0 +1,85 @@ +import { AngleLeftIcon, AngleRightIcon } from "@patternfly/react-icons"; +import { + BarElement, + CategoryScale, + Chart as ChartJS, + Legend, + LinearScale, + Title, + Tooltip, +} from "chart.js"; +import { Button, Modal, ModalVariant } from "@patternfly/react-core"; +import { setChartModal, setChartModalContent } from "actions/comparisonActions"; +import { useDispatch, useSelector } from "react-redux"; + +import { Bar } from "react-chartjs-2"; +import React from "react"; + +ChartJS.register( + BarElement, + Title, + Tooltip, + Legend, + CategoryScale, + LinearScale +); +const ChartModal = (props) => { + const { isChartModalOpen, activeChart } = useSelector( + (state) => state.comparison + ); + + const dispatch = useDispatch(); + const handleModalToggle = () => { + dispatch(setChartModal(false)); + }; + const currIndex = props.dataToPlot.findIndex( + (item) => item.data.id === activeChart.data.id + ); + const prevId = props.dataToPlot[currIndex - 1]?.data?.id; + const nextId = props.dataToPlot[currIndex + 1]?.data?.id; + return ( + + {activeChart && ( +
+
+ +
+
+ +
+
+ +
+
+ )} +
+ ); +}; + +export default ChartModal; diff --git a/dashboard/src/modules/components/ComparisonComponent/PanelContent.jsx b/dashboard/src/modules/components/ComparisonComponent/PanelContent.jsx index 305b04d7f3..a104b7b8a7 100644 --- a/dashboard/src/modules/components/ComparisonComponent/PanelContent.jsx +++ b/dashboard/src/modules/components/ComparisonComponent/PanelContent.jsx @@ -1,36 +1,82 @@ import "./index.less"; -import { List, ListItem } from "@patternfly/react-core"; +import { Checkbox, List, ListItem } from "@patternfly/react-core"; +import React, { useCallback, useMemo } from "react"; +import { getQuisbyData, setSelectedId } from "actions/comparisonActions"; import { useDispatch, useSelector } from "react-redux"; -import React from "react"; -import { getQuisbyData } from "actions/comparisonActions"; - const PanelConent = () => { const dispatch = useDispatch(); const { datasets } = useSelector((state) => state.overview); - const { activeResourceId } = useSelector((state) => state.comparison); + const { + activeResourceId, + isCompareSwitchChecked, + selectedResourceIds, + searchValue, + } = useSelector((state) => state.comparison); + const onFilter = useCallback( + (item) => { + if (searchValue === "") { + return true; + } + let input; + try { + input = new RegExp(searchValue, "i"); + } catch (err) { + input = new RegExp( + searchValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), + "i" + ); + } + return item.name.search(input) >= 0; + }, + [searchValue] + ); + const filteredDatasets = useMemo( + () => datasets.filter(onFilter), + [datasets, onFilter] + ); return ( <> - {datasets.length > 0 && ( - - {datasets.map((item) => { - const isActiveItem = item.resource_id === activeResourceId; - const itemClassName = isActiveItem - ? "dataset-item active-item" - : "dataset-item"; - return ( - dispatch(getQuisbyData(item))} - key={item.resource_id} - > - {item.name} - - ); - })} - + {filteredDatasets.length > 0 && ( +
+ {isCompareSwitchChecked ? ( +
+ {filteredDatasets.map((item) => { + return ( + + dispatch(setSelectedId(checked, item.resource_id)) + } + /> + ); + })} +
+ ) : ( + + {filteredDatasets.map((item) => { + const isActiveItem = item.resource_id === activeResourceId; + const itemClassName = isActiveItem + ? "dataset-item active-item" + : "dataset-item"; + return ( + dispatch(getQuisbyData(item))} + key={item.resource_id} + > + {item.name} + + ); + })} + + )} +
)} ); diff --git a/dashboard/src/modules/components/ComparisonComponent/common-components.jsx b/dashboard/src/modules/components/ComparisonComponent/common-components.jsx new file mode 100644 index 0000000000..eed50803b5 --- /dev/null +++ b/dashboard/src/modules/components/ComparisonComponent/common-components.jsx @@ -0,0 +1,69 @@ +import { + EmptyState, + EmptyStateBody, + EmptyStateVariant, + SearchInput, +} from "@patternfly/react-core"; +import { useDispatch, useSelector } from "react-redux"; + +import ChartGallery from "./ChartGallery"; +import ChartModal from "./ChartModal"; +import React from "react"; +import { setSearchValue } from "actions/comparisonActions"; + +export const UnsupportedTextComponent = (props) => ( + +
{props.title}
+ {props.message} +
+); + +export const MainContent = () => { + const { + isCompareSwitchChecked, + compareChartData, + chartData, + unsupportedType, + unmatchedMessage, + activeChart, + } = useSelector((state) => state.comparison); + + const message = isCompareSwitchChecked + ? "Benchmarks are of non-compatabile types!" + : "Benchmark type is currently unsupported!"; + const data = isCompareSwitchChecked ? compareChartData : chartData; + return ( + <> + {isCompareSwitchChecked ? ( + unmatchedMessage ? ( + + ) : ( + + ) + ) : unsupportedType ? ( + + ) : ( + + )} + {activeChart && } + + ); +}; + +export const SearchByName = () => { + const dispatch = useDispatch(); + const onSearchChange = (value) => { + dispatch(setSearchValue(value)); + }; + const { searchValue } = useSelector((state) => state.comparison); + return ( + onSearchChange(value)} + /> + ); +}; diff --git a/dashboard/src/modules/components/ComparisonComponent/index.jsx b/dashboard/src/modules/components/ComparisonComponent/index.jsx index 2e9c6e3663..505c3de5de 100644 --- a/dashboard/src/modules/components/ComparisonComponent/index.jsx +++ b/dashboard/src/modules/components/ComparisonComponent/index.jsx @@ -1,20 +1,26 @@ import "./index.less"; import { + Button, Divider, Flex, FlexItem, Sidebar, SidebarContent, SidebarPanel, + Switch, } from "@patternfly/react-core"; +import { MainContent, SearchByName } from "./common-components"; import React, { useEffect } from "react"; +import { + compareMultipleDatasets, + getQuisbyData, + toggleCompareSwitch, +} from "actions/comparisonActions"; import { useDispatch, useSelector } from "react-redux"; -import ChartGallery from "./ChartGallery"; import PanelConent from "./PanelContent"; import { getDatasets } from "actions/overviewActions"; -import { getQuisbyData } from "actions/comparisonActions"; import { useNavigate } from "react-router-dom"; const ComparisonComponent = () => { @@ -22,7 +28,9 @@ const ComparisonComponent = () => { const navigate = useNavigate(); const { datasets } = useSelector((state) => state.overview); - + const { isCompareSwitchChecked, selectedResourceIds } = useSelector( + (state) => state.comparison + ); useEffect(() => { if (datasets && datasets.length > 0) { dispatch(getQuisbyData(datasets[0])); @@ -33,17 +41,38 @@ const ComparisonComponent = () => { return (
- Data comparison + Data Visualization -
Datasets
+
+
Datasets
+
+ dispatch(toggleCompareSwitch())} + /> +
+
+ {isCompareSwitchChecked && ( + + )} +
Results
- +
diff --git a/dashboard/src/modules/components/ComparisonComponent/index.less b/dashboard/src/modules/components/ComparisonComponent/index.less index 2eccaa7b75..2af0d428ed 100644 --- a/dashboard/src/modules/components/ComparisonComponent/index.less +++ b/dashboard/src/modules/components/ComparisonComponent/index.less @@ -6,6 +6,15 @@ margin-right: 2.5vh; } .pf-c-sidebar { + .sidepanel-heading-container { + display: flex; + justify-content: space-between; + margin-bottom: 1vh; + } + .pf-c-text-input-group { + width: 22vw; + margin-top: 1vh; + } .heading { margin-bottom: 0.5vh; font-weight: 600; @@ -16,16 +25,42 @@ .active-item { border-left: 4px solid #06c; padding-left: 10px; + padding-bottom: 10px; margin-left: 0px; background-color: #f0f0f0; margin-top: 0; } + .datasets-container { + height: 80vh; + overflow-y: auto; + overflow-x: hidden; + width: 22vw; + padding-top: 1.5vh; + } + .dataset-list-checkbox { + padding-left: 24px; + .pf-c-check { + padding-top: 15px; + border-bottom: 1px solid #d2d2d2; + padding-bottom: 10px; + } + } } .chart-card { display: flex; - align-items: center; justify-content: center; - height: 30vh; + height: 33vh; + .expand-icon-container { + display: flex; + justify-content: flex-end; + .icon-wrapper { + padding: 10px 10px 0; + cursor: pointer; + svg { + fill: #6a6e73; + } + } + } } .heading-container { padding: 2vh 2vh 0; @@ -46,3 +81,16 @@ .dataset-item:hover { cursor: pointer; } +.chart-expand-modal-container { + .pf-c-modal-box__body { + margin-right: 0 !important; + } + .chart-modal-wrapper { + display: flex; + justify-content: space-between; + .modalBtn { + display: flex; + align-items: center; + } + } +} diff --git a/dashboard/src/modules/components/SidebarComponent/sideMenuOptions.js b/dashboard/src/modules/components/SidebarComponent/sideMenuOptions.js index 8a8b67af3b..f9e341ffcf 100644 --- a/dashboard/src/modules/components/SidebarComponent/sideMenuOptions.js +++ b/dashboard/src/modules/components/SidebarComponent/sideMenuOptions.js @@ -31,11 +31,11 @@ export const menuOptions = [ link: "results", }, { - name: "Comparison", + name: "Visualization", submenu: true, - key: "comparison", + key: "visualization", submenuOf: "dashboard", - link: "comparison", + link: "visualization", }, ], }, diff --git a/dashboard/src/reducers/comparisonReducer.js b/dashboard/src/reducers/comparisonReducer.js index 901b004681..77c2852c8b 100644 --- a/dashboard/src/reducers/comparisonReducer.js +++ b/dashboard/src/reducers/comparisonReducer.js @@ -5,6 +5,13 @@ const initialState = { chartData: [], activeResourceId: "", unsupportedType: "", + isCompareSwitchChecked: false, + selectedResourceIds: [], + unmatchedMessage: "", + isChartModalOpen: false, + activeChart: "", + compareChartData: [], + searchValue: "", }; const ComparisonReducer = (state = initialState, action = {}) => { @@ -30,6 +37,41 @@ const ComparisonReducer = (state = initialState, action = {}) => { ...state, unsupportedType: payload, }; + case TYPES.TOGGLE_COMPARE_SWITCH: + return { + ...state, + isCompareSwitchChecked: payload, + }; + case TYPES.SET_SELECTED_RESOURCE_ID: + return { + ...state, + selectedResourceIds: payload, + }; + case TYPES.UNMATCHED_BENCHMARK_TYPES: + return { + ...state, + unmatchedMessage: payload, + }; + case TYPES.SET_CHART_MODAL: + return { + ...state, + isChartModalOpen: payload, + }; + case TYPES.SET_CURRENT_CHARTID: + return { + ...state, + activeChart: payload, + }; + case TYPES.SET_COMPARE_DATA: + return { + ...state, + compareChartData: payload, + }; + case TYPES.SET_SEARCH_VALUE: + return { + ...state, + searchValue: payload, + }; default: return state; } diff --git a/dashboard/src/utils/routeConstants.js b/dashboard/src/utils/routeConstants.js index 92933bba9e..4c5b080de1 100644 --- a/dashboard/src/utils/routeConstants.js +++ b/dashboard/src/utils/routeConstants.js @@ -10,4 +10,4 @@ export const ANALYSIS = "analysis"; export const SEARCH = "search"; export const RESULTS = "results"; export const EXPLORE = "explore"; -export const COMPARISON = "comparison"; +export const VISUALIZATION = "visualization";