Skip to content

Commit 3c55b41

Browse files
committed
[FEATURE] Table: Add fitler to the table
Signed-off-by: Mahmoud Shahrokni <seyedmahmoud.shahrokni@amadeus.com> Signed-off-by: Mahmoud Shahrokni <seyedmahmoud.shahrokni@amadeus.com> Signed-off-by: Mahmoud Shahrokni <seyedmahmoud.shahrokni@amadeus.com> Signed-off-by: Mahmoud Shahrokni <seyedmahmoud.shahrokni@amadeus.com> Signed-off-by: Mahmoud Shahrokni <seyedmahmoud.shahrokni@amadeus.com> Signed-off-by: Mahmoud Shahrokni <seyedmahmoud.shahrokni@amadeus.com>
1 parent 9ebdd4f commit 3c55b41

5 files changed

Lines changed: 461 additions & 18 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Copyright The Perses Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
import { Box, ButtonBase, Typography, useTheme } from '@mui/material';
15+
import { ReactElement, useMemo, useRef, useState } from 'react';
16+
import { ColumnFilterDropdown } from './ColumnFilterDropDown';
17+
import { TableColumnConfig } from './model/table-model';
18+
import { FilterColumns } from './TableFilters';
19+
20+
interface Props<TableData> extends FilterColumns {
21+
id: string;
22+
width?: number | 'auto';
23+
filters: Array<string | number>;
24+
borderRight: string;
25+
column: TableColumnConfig<TableData>;
26+
columnUniqueValues: Record<string, Array<string | number>>;
27+
openFilterColumn?: string;
28+
setOpenFilterColumn: (columnId?: string) => void;
29+
}
30+
31+
export function ColumnFilter<TableData>({
32+
id,
33+
width,
34+
filters,
35+
column,
36+
setColumnFilters,
37+
columnFilters,
38+
borderRight,
39+
columnUniqueValues,
40+
openFilterColumn,
41+
setOpenFilterColumn,
42+
}: Props<TableData>): ReactElement {
43+
const theme = useTheme();
44+
const dropdownId = id.concat('-dropdown');
45+
46+
const [filterAnchorEl, setFilterAnchorEl] = useState<{ [key: string]: HTMLElement | undefined }>({});
47+
const [calculatedWidth, setCalculatedWidth] = useState<string>('0px');
48+
49+
const handleFilterClick = (event: React.MouseEvent<HTMLButtonElement>, columnId: string): void => {
50+
event.preventDefault();
51+
event.stopPropagation();
52+
setFilterAnchorEl({ ...filterAnchorEl, [columnId]: event.currentTarget });
53+
setOpenFilterColumn(columnId);
54+
};
55+
56+
const handleFilterClose = (): void => {
57+
setFilterAnchorEl({});
58+
setOpenFilterColumn(undefined);
59+
};
60+
61+
const updateColumnFilter = (columnId: string, values: Array<string | number>): void => {
62+
const newFilters = columnFilters.filter((f) => f.id !== columnId);
63+
if (values.length) {
64+
newFilters.push({ id: columnId, value: values });
65+
}
66+
setColumnFilters(newFilters);
67+
};
68+
69+
const mainContainerRef = useRef<HTMLDivElement>(null);
70+
const [mainContainerDimension, setMainContainerDimension] = useState<{ width: number; height: number }>({
71+
width: 0,
72+
height: 0,
73+
});
74+
75+
const observeDimensionChanges = (htmlElements: ResizeObserverEntry[]): void => {
76+
if (htmlElements?.length) {
77+
const targetElement = htmlElements[0]?.target as HTMLElement;
78+
const width = targetElement.offsetWidth;
79+
const height = targetElement.offsetHeight;
80+
setMainContainerDimension({ width, height });
81+
}
82+
};
83+
84+
/**
85+
* Width is taken from the optional column.width. Therefore, it could be possibly undefined
86+
* To handle this, we need the actual width of the container to adjust the width of the dropdown. They need to be perfectly aligned
87+
* Also, using an observer is necessary due to the effects of the toggle view mode which changes the table dimension
88+
*/
89+
const observer = useRef(new ResizeObserver(observeDimensionChanges));
90+
if (mainContainerRef.current) {
91+
observer.current.observe(mainContainerRef.current);
92+
}
93+
94+
useMemo(() => {
95+
if (width !== undefined) {
96+
setCalculatedWidth(typeof width === 'number' ? `${width}px` : width);
97+
} else if (mainContainerDimension) {
98+
setCalculatedWidth(`${mainContainerDimension.width}px`);
99+
}
100+
}, [width, mainContainerDimension]);
101+
102+
return (
103+
<Box
104+
key={id}
105+
data-testid={id}
106+
ref={mainContainerRef}
107+
sx={{
108+
padding: '8px',
109+
borderRight: borderRight,
110+
width: width,
111+
minWidth: width,
112+
maxWidth: width,
113+
display: 'flex',
114+
alignItems: 'center',
115+
position: 'relative',
116+
boxSizing: 'border-box',
117+
flex: typeof width === 'number' ? 'none' : '1 1 auto',
118+
}}
119+
>
120+
<Typography
121+
variant="body2"
122+
color="text.secondary"
123+
noWrap
124+
component="span"
125+
sx={{
126+
mr: 1,
127+
flex: 1,
128+
fontSize: '12px',
129+
minWidth: '100px',
130+
}}
131+
>
132+
{filters.length ? `${filters.length} items` : 'All'}
133+
</Typography>
134+
135+
<ButtonBase
136+
onClick={(e) => handleFilterClick(e, column.accessorKey as string)}
137+
sx={{
138+
border: '1px solid',
139+
borderColor: 'divider',
140+
backgroundColor: 'background.paper',
141+
fontSize: '12px',
142+
color: filters.length ? 'primary.main' : 'text.secondary',
143+
px: 1,
144+
py: 0.5,
145+
borderRadius: 1,
146+
minWidth: '20px',
147+
height: '24px',
148+
flexShrink: 0,
149+
transition: (theme) => theme.transitions.create('all', { duration: 200 }),
150+
'&:hover': {
151+
backgroundColor: 'action.hover',
152+
},
153+
}}
154+
>
155+
156+
</ButtonBase>
157+
158+
{openFilterColumn === column.accessorKey && (
159+
<ColumnFilterDropdown
160+
id={dropdownId}
161+
width={calculatedWidth}
162+
allValues={columnUniqueValues[column.accessorKey as string] || []}
163+
selectedValues={filters}
164+
onFilterChange={(values) => updateColumnFilter(column.accessorKey as string, values)}
165+
theme={theme}
166+
handleFilterClose={handleFilterClose}
167+
/>
168+
)}
169+
</Box>
170+
);
171+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright The Perses Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
import { ReactElement } from 'react';
15+
import { Box, Checkbox, Divider, FormControlLabel, Theme, Typography, ClickAwayListener } from '@mui/material';
16+
17+
interface Props {
18+
id: string;
19+
allValues: Array<string | number>;
20+
selectedValues: Array<string | number>;
21+
onFilterChange: (values: Array<string | number>) => void;
22+
handleFilterClose: () => void;
23+
theme: Theme;
24+
width: string;
25+
}
26+
27+
export const ColumnFilterDropdown = ({
28+
id,
29+
allValues,
30+
selectedValues,
31+
onFilterChange,
32+
handleFilterClose,
33+
theme,
34+
width,
35+
}: Props): ReactElement => {
36+
const values = [...new Set(allValues)].filter((v) => v !== null).sort();
37+
38+
if (!values.length) {
39+
return (
40+
<ClickAwayListener onClickAway={handleFilterClose}>
41+
<Box
42+
sx={{
43+
position: 'absolute',
44+
top: '100%',
45+
left: 0,
46+
zIndex: 9999,
47+
marginTop: '4px',
48+
}}
49+
>
50+
<Box
51+
data-filter-dropdown
52+
data-testid={id}
53+
sx={{
54+
width: width,
55+
padding: 10,
56+
backgroundColor: theme.palette.background.paper,
57+
border: `1px solid ${theme.palette.divider}`,
58+
boxShadow: theme.shadows[4],
59+
}}
60+
>
61+
<Typography sx={{ color: theme.palette.text.secondary, fontSize: 14 }}>No values found</Typography>
62+
</Box>
63+
</Box>
64+
</ClickAwayListener>
65+
);
66+
}
67+
68+
return (
69+
<ClickAwayListener onClickAway={handleFilterClose}>
70+
<Box
71+
sx={{
72+
position: 'absolute',
73+
top: '100%',
74+
left: 0,
75+
zIndex: 9999,
76+
marginTop: '4px',
77+
}}
78+
>
79+
<Box
80+
data-filter-dropdown
81+
data-testid={id}
82+
sx={{
83+
width: width,
84+
padding: '10px',
85+
backgroundColor: theme.palette.background.paper,
86+
border: `1px solid ${theme.palette.divider}`,
87+
boxShadow: theme.shadows[4],
88+
maxHeight: 250,
89+
overflowY: 'auto',
90+
}}
91+
>
92+
<Box style={{ marginBottom: 8, fontSize: 14, fontWeight: 'bold' }}>
93+
<FormControlLabel
94+
control={
95+
<Checkbox
96+
checked={selectedValues.length === values.length && values.length > 0}
97+
onChange={(e) => onFilterChange(e.target.checked ? values : [])}
98+
indeterminate={selectedValues.length > 0 && selectedValues.length < values.length}
99+
/>
100+
}
101+
label={<Typography sx={{ color: 'text.primary' }}>Select All ({values.length})</Typography>}
102+
/>
103+
</Box>
104+
<Divider sx={{ my: 1 }} />
105+
{values.map((value, index) => (
106+
<Box key={`value-${index}`} style={{ marginBottom: 4 }}>
107+
<FormControlLabel
108+
sx={{
109+
display: 'flex',
110+
alignItems: 'center',
111+
padding: '2px 0',
112+
borderRadius: '4px',
113+
cursor: 'pointer',
114+
}}
115+
control={
116+
<Checkbox
117+
size="small"
118+
checked={selectedValues.includes(value)}
119+
onChange={(e) => {
120+
if (e.target.checked) {
121+
onFilterChange([...selectedValues, value]);
122+
} else {
123+
onFilterChange(selectedValues.filter((v) => v !== value));
124+
}
125+
}}
126+
/>
127+
}
128+
label={
129+
<Typography variant="body2" sx={{ color: 'text.primary', fontSize: 14 }}>
130+
{!value && value !== 0 ? '(empty)' : String(value)}
131+
</Typography>
132+
}
133+
/>
134+
</Box>
135+
))}
136+
</Box>
137+
</Box>
138+
</ClickAwayListener>
139+
);
140+
};

0 commit comments

Comments
 (0)