Skip to content

Commit 20f250d

Browse files
authored
ILAB Filter Component Integration (#254)
Implement filtering for the ILAB tab.
1 parent 8ab06c7 commit 20f250d

File tree

8 files changed

+442
-55
lines changed

8 files changed

+442
-55
lines changed

frontend/src/actions/ilabActions.js

Lines changed: 172 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import { appendDateFilter, appendQueryString } from "@/utils/helper";
99

1010
import API from "@/utils/axiosInstance";
1111
import { cloneDeep } from "lodash";
12-
import { showFailureToast } from "@/actions/toastActions";
12+
import { deleteAppliedFilters } from "./commonActions";
13+
import { showFailureToast } from "./toastActions";
1314

1415
/**
1516
* Fetch and store InstructLab jobs based on configured filters.
@@ -21,13 +22,15 @@ export const fetchIlabJobs =
2122
async (dispatch, getState) => {
2223
try {
2324
dispatch({ type: TYPES.LOADING });
24-
const { start_date, end_date, size, offset, results } = getState().ilab;
25+
const { start_date, end_date, size, offset, results, appliedFiltersStr } =
26+
getState().ilab;
2527
const response = await API.get(API_ROUTES.ILABS_JOBS_API_V1, {
2628
params: {
2729
...(start_date && { start_date }),
2830
...(end_date && { end_date }),
2931
...(size && { size }),
3032
...(offset && { offset }),
33+
...(appliedFiltersStr && { filter: appliedFiltersStr }),
3134
},
3235
});
3336
if (response.status === 200) {
@@ -74,7 +77,7 @@ export const fetchIlabJobs =
7477
export const applyFilters = () => (dispatch) => {
7578
dispatch(setIlabOffset(INITAL_OFFSET));
7679
dispatch(setIlabPage(START_PAGE));
77-
dispatch(fetchIlabJobs());
80+
dispatch(fetchIlabJobs(true));
7881
dispatch(tableReCalcValues());
7982
};
8083

@@ -101,20 +104,116 @@ export const setIlabDateFilter =
101104
/**
102105
* Set the category filters.
103106
*
104-
* TODO: currently unimplemented/unused for ILAB
107+
* @param {string} category
108+
*/
109+
export const setIlabCatFilters = (category) => (dispatch, getState) => {
110+
const filterData = [...getState().ilab.filterData];
111+
const options = filterData.filter((item) => item.groupLabel === category)[0]
112+
.data;
113+
114+
dispatch({
115+
type: TYPES.SET_ILAB_CATEGORY_FILTER,
116+
payload: category,
117+
});
118+
dispatch({
119+
type: TYPES.SET_ILAB_FILTER_OPTIONS,
120+
payload: options,
121+
});
122+
dispatch(resetSubCategoryFilters());
123+
dispatch(resetTypeFilters());
124+
};
125+
126+
/**
127+
* Set the sub-category filters.
105128
*
106129
* @param {string} category
107130
*/
108-
export const setIlabCatFilters = () => () => {};
131+
export const setIlabSubCatFilters = (category) => (dispatch, getState) => {
132+
const subCategoryOptions = [...getState().ilab.subCategoryOptions];
133+
const options = subCategoryOptions.filter((item) => item.key === category)[0]
134+
.value;
135+
136+
dispatch({
137+
type: TYPES.SET_ILAB_SUB_CATEGORY_FILTER,
138+
payload: category,
139+
});
140+
dispatch({
141+
type: TYPES.SET_ILAB_TYPE_FILTER_OPTIONS,
142+
payload: options,
143+
});
144+
dispatch(resetTypeFilters());
145+
};
109146

110147
/**
111-
* Set applied filters.
148+
* Reset SubCategory and Type Filters when the category changes
149+
*/
150+
export const resetSubCategoryFilters = () => ({
151+
type: TYPES.SET_ILAB_SUB_CATEGORY_FILTER,
152+
payload: "",
153+
});
154+
/**
155+
* Reset Type Filters when the sub-category changes
156+
*/
157+
export const resetTypeFilters = () => ({
158+
type: TYPES.SET_ILAB_TYPE_FILTER,
159+
payload: "",
160+
});
161+
/**
162+
* Set Type filter value
163+
*/
164+
export const setIlabTypeFilter = (filterValue, navigate) => (dispatch) => {
165+
dispatch({ type: TYPES.SET_ILAB_TYPE_FILTER, payload: filterValue });
166+
dispatch(setIlabAppliedFilters(navigate));
167+
};
168+
/**
169+
* Parses a comma-separated filter string and groups values by their filter keys
170+
* @param {string} appliedFilter A string representing filters in the format `key:value` (e.g., "status:active,type:user").
171+
* @returns {Object} An object where each key maps to an array of its corresponding filter values.
112172
*
113-
* TODO: currently unimplemented/unused for ILAB
173+
* @example
174+
* getGroupedFilters("status:active,type:user,status:inactive")
175+
* // Returns: { status: ['active', 'inactive'], type: ['user'] }
176+
*/
177+
const getGroupedFilters = (appliedFilter) => {
178+
const grouped = appliedFilter.split(",").reduce((acc, item) => {
179+
const [key, rest] = item.trim().split(":");
180+
if (!rest) return acc;
181+
182+
const value = rest;
183+
if (!acc[key]) acc[key] = [];
184+
185+
acc[key].push(value);
186+
return acc;
187+
}, {});
188+
return grouped;
189+
};
190+
/**
191+
* Set applied filters.
114192
*
115193
* @param {function} navigate hook
116194
*/
117-
export const setIlabAppliedFilters = () => () => {};
195+
export const setIlabAppliedFilters = (navigate) => (dispatch, getState) => {
196+
const {
197+
typeFilterValue,
198+
subCategoryFilterValue,
199+
categoryFilterValue,
200+
appliedFiltersStr,
201+
} = getState().ilab;
202+
203+
const filterStr = `${categoryFilterValue}:${subCategoryFilterValue}=${typeFilterValue}`;
204+
const updatedFilter = appliedFiltersStr
205+
? `${appliedFiltersStr},${filterStr}`
206+
: filterStr;
207+
dispatch({
208+
type: TYPES.SET_ILAB_APPLIED_FILTER,
209+
payload: {
210+
filterStr: updatedFilter,
211+
filter: getGroupedFilters(updatedFilter),
212+
},
213+
});
214+
dispatch(applyFilters());
215+
dispatch(updateURL(navigate));
216+
};
118217

119218
/**
120219
* Set summery filters for non-[success|failure]
@@ -164,18 +263,51 @@ export const tableReCalcValues = () => (dispatch, getState) => {
164263

165264
dispatch(setIlabPageOptions(page, perPage));
166265
};
167-
266+
/**
267+
* Converts the grouped object to string
268+
* @param {Object} grouped
269+
* @returns string
270+
*/
271+
const convertGroupedToString = (grouped) => {
272+
return Object.entries(grouped)
273+
.flatMap(([key, values]) => values.map((value) => `${key}:${value}`))
274+
.join(",");
275+
};
168276
/**
169277
* Remove applied filters
170278
*
171279
* @param {*} filterKey
172280
* @param {*} filterValue
173281
* @param {*} navigate
174282
*/
175-
export const removeIlabAppliedFilters = () => (dispatch) => {
283+
export const removeIlabAppliedFilters =
284+
(filterKey, filterValue, navigate) => (dispatch) => {
285+
const appliedFilters = dispatch(
286+
deleteAppliedFilters(filterKey, filterValue, "ilab")
287+
);
288+
289+
dispatch({
290+
type: TYPES.SET_ILAB_APPLIED_FILTER,
291+
payload: {
292+
filterStr: convertGroupedToString(appliedFilters),
293+
filter: appliedFilters,
294+
},
295+
});
296+
dispatch(updateURL(navigate));
297+
dispatch(applyFilters());
298+
};
299+
export const removeAllFilters = (navigate) => (dispatch) => {
300+
dispatch({
301+
type: TYPES.SET_ILAB_APPLIED_FILTER,
302+
payload: {
303+
filterStr: "",
304+
filter: {},
305+
},
306+
});
307+
dispatch(updateURL(navigate));
176308
dispatch(applyFilters());
177-
};
178309

310+
};
179311
/**
180312
* Apply a new date filter, resetting pagination
181313
*
@@ -194,10 +326,27 @@ export const applyIlabDateFilter =
194326
/**
195327
* Fetch the set of possible InstructLab run filters and store them.
196328
*/
197-
export const fetchIlabFilters = () => async (dispatch) => {
329+
export const fetchIlabFilters = () => async (dispatch, getState) => {
198330
try {
331+
const { categoryFilterValue } = getState().ilab;
199332
const response = await API.get(`/api/v1/ilab/runs/filters`);
200-
dispatch({ type: TYPES.SET_ILAB_RUN_FILTERS, payload: response.data });
333+
if (response.status === 200) {
334+
let filterData = [];
335+
336+
for (const main in response.data) {
337+
const p = response.data[main];
338+
const a = [];
339+
340+
Object.entries(p).forEach(([key, value]) => {
341+
a.push({ key, value, name: key });
342+
});
343+
344+
filterData.push({ groupLabel: main, data: a });
345+
}
346+
const activeFilter = categoryFilterValue || filterData[0].groupLabel;
347+
dispatch({ type: TYPES.SET_ILAB_RUN_FILTERS, payload: filterData });
348+
dispatch(setIlabCatFilters(activeFilter));
349+
}
201350
} catch (error) {
202351
console.error(error);
203352
dispatch(showFailureToast());
@@ -666,8 +815,15 @@ export const setMetaRowExpanded = (expandedItems) => ({
666815
});
667816

668817
export const updateURL = (navigate) => (dispatch, getState) => {
669-
const { perPage, offset, page, comparisonSwitch, start_date, end_date } =
670-
getState().ilab;
818+
const {
819+
perPage,
820+
offset,
821+
page,
822+
comparisonSwitch,
823+
start_date,
824+
end_date,
825+
appliedFiltersStr,
826+
} = getState().ilab;
671827

672828
appendQueryString(
673829
{
@@ -677,6 +833,7 @@ export const updateURL = (navigate) => (dispatch, getState) => {
677833
comparisonSwitch,
678834
...(start_date ? { start_date } : {}),
679835
...(end_date ? { end_date } : {}),
836+
...(appliedFiltersStr ? { filter: appliedFiltersStr } : {}),
680837
},
681838
navigate
682839
);

frontend/src/actions/types.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,9 @@ export const SET_EXPANDED_METAROW = "SET_EXPANDED_METAROW";
136136
export const SET_SELECTED_METRICS_PER_RUN = "SET_SELECTED_METRICS_PER_RUN";
137137
export const SET_MODAL_OPEN = "SET_MODAL_OPEN";
138138
export const SET_MODAL_METADATA_ITEM = "SET_MODAL_METADATA_ITEM";
139+
export const SET_ILAB_CATEGORY_FILTER = "SET_ILAB_CATEGORY_FILTER";
140+
export const SET_ILAB_FILTER_OPTIONS = "SET_ILAB_FILTER_OPTIONS";
141+
export const SET_ILAB_SUB_CATEGORY_FILTER = "SET_ILAB_SUB_CATEGORY_FILTER";
142+
export const SET_ILAB_TYPE_FILTER_OPTIONS = "SET_ILAB_TYPE_FILTER_OPTIONS";
143+
export const SET_ILAB_TYPE_FILTER = "SET_ILAB_TYPE_FILTER";
144+
export const SET_ILAB_APPLIED_FILTER = "SET_ILAB_APPLIED_FILTER";

frontend/src/components/molecules/SelectBox/index.jsx

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import PropTypes from "prop-types";
99
import { useState } from "react";
1010

1111
const SelectBox = (props) => {
12+
const { options, type, selected } = props;
1213
const [isOpen, setIsOpen] = useState(false);
1314

1415
const onToggleClick = () => {
@@ -18,6 +19,51 @@ const SelectBox = (props) => {
1819
setIsOpen(false);
1920
props.onChange(_event, value);
2021
};
22+
const renderOptions = () => {
23+
switch (type) {
24+
case "ilab":
25+
return (
26+
<SelectList>
27+
{options.map(({ groupLabel }) => (
28+
<SelectOption value={groupLabel} key={groupLabel}>
29+
{groupLabel}
30+
</SelectOption>
31+
))}
32+
</SelectList>
33+
);
34+
case "ilab_subcategory":
35+
return (
36+
<SelectList>
37+
{options.map(({ key, name }) => (
38+
<SelectOption value={key} key={key}>
39+
{name}
40+
</SelectOption>
41+
))}
42+
</SelectList>
43+
);
44+
case "ilab_type_filter":
45+
return (
46+
<SelectList>
47+
{options.map((option) => (
48+
<SelectOption value={option} key={option}>
49+
{option}
50+
</SelectOption>
51+
))}
52+
</SelectList>
53+
);
54+
default:
55+
return (
56+
<SelectList>
57+
{options.map(({ key, name }) => (
58+
<SelectOption value={name} key={key}>
59+
{name}
60+
</SelectOption>
61+
))}
62+
</SelectList>
63+
);
64+
}
65+
};
66+
2167
const toggle = (toggleRef) => (
2268
<MenuToggle
2369
ref={toggleRef}
@@ -29,29 +75,21 @@ const SelectBox = (props) => {
2975
height: "36px",
3076
}}
3177
>
32-
{props.selected}
78+
{selected}
3379
</MenuToggle>
3480
);
3581
return (
36-
<>
37-
<Select
38-
className="select-box"
39-
isOpen={isOpen}
40-
selected={props.selected}
41-
onSelect={onSelect}
42-
onOpenChange={(isOpen) => setIsOpen(isOpen)}
43-
toggle={toggle}
44-
shouldFocusToggleOnSelect
45-
>
46-
<SelectList>
47-
{props.options.map((option) => (
48-
<SelectOption value={option.name} key={option.key}>
49-
{option.name}
50-
</SelectOption>
51-
))}
52-
</SelectList>
53-
</Select>
54-
</>
82+
<Select
83+
className="select-box"
84+
isOpen={isOpen}
85+
selected={selected}
86+
onSelect={onSelect}
87+
onOpenChange={setIsOpen}
88+
toggle={toggle}
89+
shouldFocusToggleOnSelect
90+
>
91+
{renderOptions()}
92+
</Select>
5593
);
5694
};
5795

@@ -61,5 +99,6 @@ SelectBox.propTypes = {
6199
selected: PropTypes.string,
62100
width: PropTypes.string,
63101
icon: PropTypes.any,
102+
type: PropTypes.string,
64103
};
65104
export default SelectBox;

0 commit comments

Comments
 (0)