Skip to content

Commit

Permalink
Merge pull request #213 from kbase/feature/boolean-filter
Browse files Browse the repository at this point in the history
Add boolean dropdown filters for heatmap
  • Loading branch information
codytodonnell authored May 21, 2024
2 parents 8d8864b + 598a6b5 commit 05e03e3
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 74 deletions.
7 changes: 7 additions & 0 deletions src/common/api/collectionsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,13 @@ export type ColumnMeta = {
max_value: undefined;
enum_values: string[];
}
| {
type: 'bool';
filter_strategy: undefined;
min_value: undefined;
max_value: undefined;
enum_values: undefined;
}
);

interface CollectionsResults {
Expand Down
4 changes: 3 additions & 1 deletion src/common/components/FilterChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const FilterChip = ({
...chipProps
}: FilterChipProps) => {
let filterString = '';
if (filter.value) {
if (filter.value !== undefined && filter.value !== null) {
if (
filter.type === 'date' ||
filter.type === 'int' ||
Expand All @@ -30,6 +30,8 @@ export const FilterChip = ({
})
.map((val) => val.toLocaleString());
filterString = `${minString} to ${maxString}`;
} else if (filter.type === 'bool') {
filterString = Boolean(filter.value).toString();
} else {
const val = filter.value;
if (typeof val === 'string') {
Expand Down
110 changes: 102 additions & 8 deletions src/features/collections/CollectionDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,15 @@ import {
Accordion,
AccordionSummary,
AccordionDetails,
FormControl,
Select,
MenuItem,
SelectChangeEvent,
} from '@mui/material';
import { useForm } from 'react-hook-form';
import { CollectionOverview } from './CollectionOverview';
import { FilterChip } from '../../common/components/FilterChip';
import { FilterContextTabs } from './Filters';
import { filterContextScope, FilterContextTabs } from './Filters';

export const detailPath = ':id';
export const detailDataProductPath = ':id/:data_product';
Expand All @@ -63,8 +67,8 @@ const pageConfig: Record<
}
> = {
samples: { features: ['filter'] },
biolog: { features: [] },
microtrait: { features: [] },
biolog: { features: ['filter'] },
microtrait: { features: ['filter'] },
genome_attribs: {
features: ['filter', 'match', 'search'],
},
Expand Down Expand Up @@ -178,7 +182,8 @@ export const CollectionDetail = () => {
!filter ||
filter.type === 'int' ||
filter.type === 'float' ||
filter.type === 'date'
filter.type === 'date' ||
filter.type === 'bool'
)
return;
if (e.target.value === filter.value) return;
Expand Down Expand Up @@ -401,6 +406,7 @@ const useFilterEntries = (collectionId: string) => {
};

const FilterChips = ({ collectionId }: { collectionId: string }) => {
const { columnMeta } = useFilters(collectionId);
const { filterEntries, clearFilterState, context } =
useFilterEntries(collectionId);
if (filterEntries.length === 0) return <></>;
Expand All @@ -411,7 +417,7 @@ const FilterChips = ({ collectionId }: { collectionId: string }) => {
<FilterChip
filter={filter}
key={`${column}-${context}-${filter.type}`}
name={column}
name={columnMeta?.[column]?.display_name || column}
onDelete={() => clearFilterState(column)}
/>
);
Expand Down Expand Up @@ -451,11 +457,19 @@ const FilterMenu = ({
setExpandedCategory(expanded ? category : '');
};

const menuLabel = {
DEFAULT: 'Filters',
genomes: 'Genome Filters',
samples: 'Sample Filters',
biolog: 'Biolog Filters',
microtrait: 'Microtrait Filters',
}[filterContextScope(context) || 'DEFAULT'];

if (open) {
return (
<div className={styles['filters_panel']}>
<Stack direction="row" className={styles['filters-panel-header']}>
<h3>Genome Filters</h3>
<h3>{menuLabel}</h3>
<Stack direction="row" spacing={1}>
<Button
size="small"
Expand Down Expand Up @@ -503,7 +517,8 @@ const FilterMenu = ({
<AccordionDetails>
<Stack spacing={3}>
{category.filters.flatMap(([column, filter]) => {
const hasVal = Boolean(filter.value);
const hasVal =
filter.value !== undefined && filter.value !== null;
return (
<Stack
className={styles['filter-container']}
Expand Down Expand Up @@ -596,6 +611,15 @@ const FilterControls = ({
collectionId={collectionId}
/>
);
} else if (filter.type === 'bool') {
return (
<BooleanFilterControls
column={column}
filter={filter}
context={context}
collectionId={collectionId}
/>
);
}
return null;
};
Expand Down Expand Up @@ -859,7 +883,7 @@ const RangeFilterControls = ({
const [filterMin, filterMax] = [filter.min_value, filter.max_value];
useEffect(() => {
//Clear when the filter is cleared
if (!filter.value) {
if (filter.value === undefined || filter.value === null) {
setValue('min', filterMin);
setValue('max', filterMax);
setSliderPosition([filterMin, filterMax]);
Expand Down Expand Up @@ -977,3 +1001,73 @@ const TextFilterControls = ({
/>
);
};

const BooleanFilterControls = ({
column,
filter,
collectionId,
context,
}: FilterControlProps & {
filter: { type: 'bool' };
}) => {
const dispatch = useAppDispatch();
// Convert boolean values to proper dropdown values
const getBooleanDropdownValue = (value?: number | boolean) => {
if (value === true || value === 1) {
return 'true';
} else if (value === false || value === 0) {
return 'false';
} else {
return 'any';
}
};
const [selectValue, setSelectValue] = useState<string>(() => {
return getBooleanDropdownValue(filter.value);
});

const handleChange = (event: SelectChangeEvent) => {
setSelectValue(event.target.value as string);
};

// In order not to create a dependency loop when filter changes within the below effect,
// use a filter ref
const filterRef = useRef(filter);
filterRef.current = filter;

useEffect(() => {
let value;
if (selectValue === 'true') {
value = 1;
} else if (selectValue === 'false') {
value = 0;
}
dispatch(
setFilter([
collectionId,
context,
column,
{ ...filterRef.current, value },
])
);
}, [selectValue, collectionId, context, column, dispatch]);

useEffect(() => {
//Clear when the filter is cleared
// Use 'any' for empty value so the select stays controlled and shows "Any" in the dropdown
if (filter.value === undefined || filter.value === null) {
setSelectValue('any');
} else if (filter.value !== filterRef.current.value) {
setSelectValue(getBooleanDropdownValue(filter.value));
}
}, [filter.value, setSelectValue]);

return (
<FormControl fullWidth>
<Select value={selectValue} onChange={handleChange}>
<MenuItem value="true">True</MenuItem>
<MenuItem value="false">False</MenuItem>
<MenuItem value="any">Any</MenuItem>
</Select>
</FormControl>
);
};
116 changes: 75 additions & 41 deletions src/features/collections/Filters.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Chip, Stack, Tab, Tabs } from '@mui/material';
import { useCallback, useEffect, useRef } from 'react';
import { toast } from 'react-hot-toast';
Expand Down Expand Up @@ -147,25 +148,22 @@ export const useContextFilterQueryManagement = (
<T extends CommonResult>(
result: T,
collectionId: string | undefined
): { filterData?: T['data']; context?: FilterContext } => {
): { filterData?: T['data'] } => {
if (result.isError) {
const err = parseError(result.error);
if (err.name !== 'AbortError')
toast('Filter Loading failed: ' + parseError(result.error).message);
}
const context = result.requestId
? requestContext.current[result.requestId]
: undefined;
if (!collectionId || !result?.data || !context || result.isError)
return {};
return { filterData: result.data, context };

if (!collectionId || !result?.data || result.isError) return {};
return { filterData: result.data };
},
[]
);

const handleTableFilters = useCallback(
<T extends typeof genomeResult | typeof sampleResult>(result: T) => {
const { filterData, context } = handleResult(result, collectionId);
const { filterData } = handleResult(result, collectionId);
if (!filterData || !collectionId || !context) return;
dispatch(clearFiltersAndColumnMeta([collectionId, context]));
filterData.columns.forEach((column) => {
Expand Down Expand Up @@ -214,50 +212,88 @@ export const useContextFilterQueryManagement = (
}
});
},
[collectionId, dispatch, handleResult]
[collectionId, dispatch, handleResult, context]
);

const handleHeatmapFilters = useCallback(
<T extends typeof biologResult | typeof microtraitResult>(result: T) => {
const { filterData, context } = handleResult(result, collectionId);
const { filterData } = handleResult(result, collectionId);
if (!filterData || !collectionId || !context) return;
dispatch(clearFiltersAndColumnMeta([collectionId, context]));
filterData.categories.forEach((category) => {
category.columns.forEach((column) => {
const current =
filtersRef.current && filtersRef.current[column.col_id];
const filterMeta: ColumnMeta = {
type: 'int',
key: column.col_id,
max_value: filterData.max_value,
min_value: filterData.min_value,
category: category.category,
description: column.description,
display_name: column.name,
filter_strategy: undefined,
enum_values: undefined,
};
dispatch(
setColumnMeta([collectionId, context, column.col_id, filterMeta])
);
dispatch(
setFilter([
collectionId,
context,
column.col_id,
{
type: filterMeta.type,
min_value: filterMeta.min_value,
max_value: filterMeta.max_value,
value:
current?.type === column.type ? current.value : undefined,
},
])
);
if (column.type === 'bool') {
dispatch(
setColumnMeta([
collectionId,
context,
column.col_id,
{
type: 'bool',
key: column.col_id,
max_value: undefined,
min_value: undefined,
category: category.category,
description: column.description,
display_name: column.name,
filter_strategy: undefined,
enum_values: undefined,
},
])
);
dispatch(
setFilter([
collectionId,
context,
column.col_id,
{
type: 'bool',
value:
current?.type === column.type
? typeof current.value !== 'undefined'
? Boolean(current.value)
: undefined
: undefined,
},
])
);
} else {
// column.type === 'count'
const filterMeta: ColumnMeta = {
type: 'int',
key: column.col_id,
max_value: filterData.max_value,
min_value: filterData.min_value,
category: category.category,
description: column.description,
display_name: column.name,
filter_strategy: undefined,
enum_values: undefined,
};
dispatch(
setColumnMeta([collectionId, context, column.col_id, filterMeta])
);
dispatch(
setFilter([
collectionId,
context,
column.col_id,
{
type: 'int',
min_value: filterMeta.min_value,
max_value: filterMeta.max_value,
value:
current?.type === column.type ? current.value : undefined,
},
])
);
}
});
});
},
[collectionId, dispatch, handleResult]
[collectionId, dispatch, handleResult, context]
);

// When the context (or collection) changes, set the filter context and trigger appropriate query
Expand All @@ -277,8 +313,6 @@ export const useContextFilterQueryManagement = (
} else {
throw new Error(`No filter query matches filter context "${context}"`);
}
requestContext.current[request.requestId] = context;

return () => {
// Abort request if context changes while running (prevents race conditions)
if (request) {
Expand Down
Loading

0 comments on commit 05e03e3

Please sign in to comment.