Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: better filter ux #207

Merged
merged 5 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 20 additions & 29 deletions packages/diracx-web-components/components/shared/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ export function DataTable(props: DataTableProps) {
const { getAllParam, getParam, setParam } = useSearchParamsUtils();
const appId = getParam("appId");

const [appliedFilters, setAppliedFilters] = React.useState<Filter[]>(filters);

const updateFiltersAndUrl = React.useCallback(
(newFilters: Filter[]) => {
// Update the filters in the URL using the setParam function
Expand Down Expand Up @@ -364,42 +366,27 @@ export function DataTable(props: DataTableProps) {
}));
setSearchBody({ search: jsonFilters });
setPage(0);
setAppliedFilters(filters);

// Update the filters in the URL
updateFiltersAndUrl(filters);
// Update the filters in the sections
updateSectionFilters(filters);
};

const SectionItem = React.useMemo(
() =>
sections
.find((section) => section.items.some((item) => item.id === appId))
?.items.find((item) => item.id === appId),
[appId, sections],
);

React.useEffect(() => {
// Function to parse the filters from the URL search params
const parseFiltersFromUrl = () => {
const filterStrings = getAllParam("filter");
return filterStrings.map((filterString: string) => {
const [id, column, operator, value] = filterString.split("_");
return { id: Number(id), column, operator, value };
});
};

const item = sections
.find((section) => section.items.some((item) => item.id === appId))
?.items.find((item) => item.id === appId);

if (getParam("filter")) {
// Parse the filters when the component mounts or when the searchParams change
const initialFilters = parseFiltersFromUrl();
// Set the filters (they will be displayed in the UI)
setFilters(initialFilters);
// Apply the filters to get the filtered data
const jsonFilters = initialFilters.map((filter) => ({
parameter: filter.column,
operator: filter.operator,
value: filter.value,
}));
setSearchBody({ search: jsonFilters });
} else if (item?.data?.filters) {
setFilters(item.data.filters);
const jsonFilters = item.data.filters.map(
if (SectionItem?.data?.filters) {
setFilters(SectionItem.data.filters);
setAppliedFilters(SectionItem.data.filters);
const jsonFilters = SectionItem.data.filters.map(
(filter: {
id: number;
column: string;
Expand All @@ -416,7 +403,7 @@ export function DataTable(props: DataTableProps) {
setFilters([]);
setSearchBody({ search: [] });
}
}, [appId, getAllParam, getParam, sections, setFilters, setSearchBody]);
}, [SectionItem?.data?.filters, setFilters, setSearchBody]);

// Manage sorting
const handleRequestSort = (
Expand Down Expand Up @@ -498,6 +485,7 @@ export function DataTable(props: DataTableProps) {
columns={columns}
filters={filters}
setFilters={setFilters}
appliedFilters={appliedFilters}
handleApplyFilters={handleApplyFilters}
/>
<Box sx={{ width: "100%", p: 1 }} data-testid="skeleton">
Expand All @@ -520,6 +508,7 @@ export function DataTable(props: DataTableProps) {
columns={columns}
filters={filters}
setFilters={setFilters}
appliedFilters={appliedFilters}
handleApplyFilters={handleApplyFilters}
/>
<Box sx={{ width: "100%", marginTop: 2 }}>
Expand All @@ -539,6 +528,7 @@ export function DataTable(props: DataTableProps) {
columns={columns}
filters={filters}
setFilters={setFilters}
appliedFilters={appliedFilters}
handleApplyFilters={handleApplyFilters}
/>
<Box sx={{ width: "100%", marginTop: 2 }}>
Expand All @@ -556,6 +546,7 @@ export function DataTable(props: DataTableProps) {
columns={columns}
filters={filters}
setFilters={setFilters}
appliedFilters={appliedFilters}
handleApplyFilters={handleApplyFilters}
/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,18 @@ export const Default: Story = {
{ id: "name", label: "Name" },
{ id: "age", label: "Age" },
],
filters: [{ id: 0, column: "id", operator: "eq", value: "1" }],
filters: [
{ id: 0, column: "id", operator: "eq", value: "1" },
{ id: 1, column: "id", operator: "neq", value: "2" },
],
setFilters: () => {},
handleApplyFilters: () => {},
appliedFilters: [{ id: 0, column: "id", operator: "eq", value: "1" }],
},
render: (props) => {
const [, updateArgs] = useArgs();
const [{ filters }, updateArgs] = useArgs();
props.setFilters = (filters) => updateArgs({ filters });
props.handleApplyFilters = () => updateArgs({ appliedFilters: filters });
return <FilterToolbar {...props} />;
},
};
90 changes: 63 additions & 27 deletions packages/diracx-web-components/components/shared/FilterToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import React from "react";
import { FilterList, Delete, Send } from "@mui/icons-material";
import Chip from "@mui/material/Chip";
import Button from "@mui/material/Button";
import { Popover, Stack, Tooltip } from "@mui/material";
import { Alert, Popover, Stack, Tooltip } from "@mui/material";
import { Filter } from "@/types/Filter";
import { Column } from "@/types/Column";
import { FilterForm } from "./FilterForm";
import "@/hooks/theme";

/**
* Filter toolbar component
Expand All @@ -18,6 +19,8 @@ interface FilterToolbarProps {
filters: Filter[];
/** The function to set the filters */
setFilters: React.Dispatch<React.SetStateAction<Filter[]>>;
/** The applied filters */
appliedFilters: Filter[];
/** The function to apply the filters */
handleApplyFilters: () => void;
}
Expand All @@ -28,7 +31,8 @@ interface FilterToolbarProps {
* @returns a FilterToolbar component
*/
export function FilterToolbar(props: FilterToolbarProps) {
const { columns, filters, setFilters, handleApplyFilters } = props;
const { columns, filters, setFilters, appliedFilters, handleApplyFilters } =
props;
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
const [selectedFilter, setSelectedFilter] = React.useState<Filter | null>(
null,
Expand Down Expand Up @@ -82,6 +86,17 @@ export function FilterToolbar(props: FilterToolbarProps) {
setFilters(filters.filter((_, i) => i !== index));
};

const changesUnapplied = React.useCallback(() => {
return JSON.stringify(filters) !== JSON.stringify(appliedFilters);
}, [filters, appliedFilters]);

const isApplied = React.useCallback(
(filter: Filter) => {
return appliedFilters.some((f) => f.id == filter.id);
},
[appliedFilters],
);

// Keyboard shortcuts
React.useEffect(() => {
function debounce(func: (...args: any[]) => void, wait: number) {
Expand Down Expand Up @@ -142,37 +157,51 @@ export function FilterToolbar(props: FilterToolbarProps) {

return (
<>
<Stack direction="row" spacing={1} sx={{ m: 1 }}>
<Stack direction="row" spacing={1} sx={{ m: 1 }} alignItems={"center"}>
<Tooltip title="Alt+Shift+a" placement="top">
<Button
variant="text"
startIcon={<FilterList />}
onClick={handleAddFilter}
ref={addFilterButtonRef}
>
<span>Add filter</span>
</Button>
<span>
<Button
variant="text"
startIcon={<FilterList />}
onClick={handleAddFilter}
ref={addFilterButtonRef}
>
<span>Add filter</span>
</Button>
</span>
</Tooltip>
<Tooltip title="Alt+Shift+p" placement="top">
<Button
variant="text"
startIcon={<Send />}
onClick={() => handleApplyFilters()}
>
<span>Apply filters</span>
</Button>
<span>
<Button
variant="text"
startIcon={<Send />}
onClick={() => handleApplyFilters()}
disabled={!changesUnapplied()}
>
<span>Apply filters</span>
</Button>
</span>
</Tooltip>
<Tooltip title="Alt+Shift+c" placement="top">
<Button
variant="text"
startIcon={<Delete />}
onClick={handleRemoveAllFilters}
>
<span>Clear all filters</span>
</Button>
<span>
<Button
variant="text"
startIcon={<Delete />}
onClick={handleRemoveAllFilters}
disabled={filters.length === 0}
>
<span>Clear all filters</span>
</Button>
</span>
</Tooltip>
</Stack>
<Stack direction="row" spacing={1} sx={{ m: 1, flexWrap: "wrap" }}>
<Stack
direction="row"
spacing={1}
flexWrap="wrap"
useFlexGap
sx={{ m: 1 }}
>
{filters.map((filter: Filter, index: number) => (
<Chip
key={index}
Expand All @@ -184,10 +213,11 @@ export function FilterToolbar(props: FilterToolbarProps) {
onDelete={() => {
handleRemoveFilter(index);
}}
color="primary"
color={isApplied(filter) ? "chipColor" : "default"}
sx={{ m: 0.5 }}
/>
))}

<Popover
open={open}
onClose={handleFilterMenuClose}
Expand All @@ -207,6 +237,12 @@ export function FilterToolbar(props: FilterToolbarProps) {
/>
</Popover>
</Stack>
{changesUnapplied() && (
<Alert severity="info">
Some filter changes have not been applied. Please click on &quot;Apply
filters&quot; to update your results.
</Alert>
)}
</>
);
}
14 changes: 10 additions & 4 deletions packages/diracx-web-components/contexts/ApplicationsProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import React, { createContext, useCallback, useEffect, useState } from "react";
import React, {
createContext,
useCallback,
useMemo,
useEffect,
useState,
} from "react";
import { Dashboard, FolderCopy, Monitor } from "@mui/icons-material";
import JSONCrush from "jsoncrush";
import { useSearchParamsUtils } from "@/hooks/searchParamsUtils";
Expand Down Expand Up @@ -36,9 +42,6 @@ export const ApplicationsProvider = ({

const { getParam, setParam } = useSearchParamsUtils();

// get user sections from searchParams
const sectionsParams = getParam("sections");

// save user sections to searchParams (but not icons)
const setSectionsParams = useCallback(
(sections: UserSection[] | ((prev: UserSection[]) => UserSection[])) => {
Expand All @@ -61,6 +64,9 @@ export const ApplicationsProvider = ({
[setParam, userSections],
);

// get user sections from searchParams
const sectionsParams = useMemo(() => getParam("sections"), [getParam]);

useEffect(() => {
if (userSections.length !== 0) return;
if (sectionsParams) {
Expand Down
46 changes: 34 additions & 12 deletions packages/diracx-web-components/hooks/theme.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
import { PaletteMode } from "@mui/material";
import { grey, lightGreen, cyan } from "@mui/material/colors";
import { createTheme, darken, lighten } from "@mui/material/styles";
import { cyan, grey, lightGreen } from "@mui/material/colors";
import {
createTheme,
darken,
lighten,
getContrastRatio,
} from "@mui/material/styles";
import { useContext } from "react";
import { ThemeContext } from "@/contexts/ThemeProvider";

declare module "@mui/material/styles" {
interface Palette {
chipColor: Palette["primary"];
}

interface PaletteOptions {
chipColor?: PaletteOptions["primary"];
}
}

declare module "@mui/material/Chip" {
interface ChipPropsColorOverrides {
chipColor: true;
}
}

/**
* Custom hook to access the theme context
* @returns the theme context
Expand All @@ -26,19 +47,27 @@ export const useTheme = () => {
export const useMUITheme = () => {
const { theme } = useTheme();

const primary = lightGreen[700];
const secondary = cyan[500];

const chipColor =
theme === "light" ? lighten(primary, 0.5) : darken(primary, 0.5);

// Create a Material-UI theme based on the current mode
const muiTheme = createTheme({
palette: {
mode: theme as PaletteMode,
primary: {
main: "#ffffff",
},
chipColor: {
main: chipColor,
contrastText:
getContrastRatio(chipColor, "#fff") > 4.5 ? "#fff" : "#111",
},
},
});

const primary = lightGreen[700];
const secondary = cyan[500];

const scrollbarBackground = theme === "dark" ? "#333" : "#f1f1f1";
const scrollbarThumbBackground = theme === "dark" ? "#888" : "#ccc";
const scrollbarThumbHoverBackground = theme === "dark" ? "#555" : "#999";
Expand Down Expand Up @@ -101,13 +130,6 @@ export const useMUITheme = () => {
},
},
},
MuiChip: {
styleOverrides: {
root: {
backgroundColor: lighten(primary, 0.75),
},
},
},
MuiInputLabel: {
styleOverrides: {
root: {
Expand Down
Loading