ui/src/pages/namespace/Events/History/components/Filters/index.tsx
import { Plus, X } from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "~/design/Popover";
import Button from "~/design/Button";
import { ButtonBar } from "~/design/ButtonBar";
import DatePicker from "~/components/Filters/DatePicker";
import { FiltersObj } from "~/api/events/query/get";
import RefineTime from "~/components/Filters/RefineTime";
import { SelectFieldMenu } from "~/components/Filters/SelectFieldMenu";
import TextInput from "~/components/Filters/TextInput";
import moment from "moment";
import { useState } from "react";
import { useTranslation } from "react-i18next";
type FiltersProps = {
filters: FiltersObj;
onUpdate: (filters: FiltersObj) => void;
};
type MenuAnchor =
| "main"
| "TYPE"
| "TEXT"
| "AFTER"
| "BEFORE"
| "AFTER.time"
| "BEFORE.time";
const fieldsInMenu: Array<keyof FiltersObj> = [
"TYPE",
"TEXT",
"AFTER",
"BEFORE",
];
const Filters = ({ filters, onUpdate }: FiltersProps) => {
const { t } = useTranslation();
// activeMenu controls which popover component is opened (there are
// separate popovers triggered by the respective buttons)
const [activeMenu, setActiveMenu] = useState<MenuAnchor | null>(null);
// selectedField controls which submenu is shown in the main menu
const [selectedField, setSelectedField] = useState<keyof FiltersObj | null>(
null
);
const handleOpenChange = (isOpening: boolean, menu: MenuAnchor) => {
if (!isOpening) {
setSelectedField(null);
}
toggleMenu(menu);
};
const toggleMenu = (value: MenuAnchor) => {
if (activeMenu === value) {
return setActiveMenu(null);
}
setActiveMenu(value);
};
const resetMenu = () => {
setActiveMenu(null);
setSelectedField(null);
};
const setFilter = (newFilter: FiltersObj) => {
const newFilters = { ...filters, ...newFilter };
onUpdate(newFilters);
resetMenu();
};
const clearFilter = (field: keyof FiltersObj) => {
const newFilters = { ...filters };
delete newFilters[field];
onUpdate(newFilters);
};
const currentFilterKeys = Object.keys(filters) as Array<keyof FiltersObj>;
const hasFilters = !!currentFilterKeys.length;
const undefinedFilters = fieldsInMenu.filter(
(field) => !currentFilterKeys.includes(field)
);
return (
<div className="m-2 flex flex-row flex-wrap gap-2">
{currentFilterKeys.map((field) => {
// For type safety, one separate return is required below for every type
// so it is possible to assert filters[field]?.value is defined and TS
// does not merge the different possible types of filters[field]?.value
if (field === "TYPE" || field === "TEXT") {
return (
<ButtonBar key={field}>
<Button variant="outline" asChild>
<label>
{t([`pages.events.history.filter.field.${field}`])}
</label>
</Button>
<Popover
open={activeMenu === field}
onOpenChange={(state) => handleOpenChange(state, field)}
>
<PopoverTrigger asChild>
<Button variant="outline">{filters[field]?.value}</Button>
</PopoverTrigger>
<PopoverContent align="start">
<TextInput
value={filters[field]?.value}
onSubmit={(value) => {
if (value) {
setFilter({
[field]: { value, type: "CONTAINS" },
});
} else {
clearFilter(field);
}
}}
heading={t(
`pages.events.history.filter.menuHeading.${field}`
)}
placeholder={t(
`pages.events.history.filter.placeholder.${field}`
)}
/>
</PopoverContent>
</Popover>
<Button
variant="outline"
icon
data-testid={`clear-filter-${field}`}
>
<X onClick={() => clearFilter(field)} />
</Button>
</ButtonBar>
);
}
if (field === "AFTER" || field == "BEFORE") {
const dateValue = filters[field]?.value;
if (!dateValue) {
console.error("Early return: dateValue is not defined");
return null;
}
return (
<ButtonBar key={field}>
<Button variant="outline" asChild>
<label>
{t([`pages.events.history.filter.field.${field}`])}
</label>
</Button>
<Popover
open={activeMenu === field}
onOpenChange={(state) => handleOpenChange(state, field)}
>
<PopoverTrigger asChild>
<Button variant="outline" className="px-2">
{moment(filters[field]?.value).format("YYYY-MM-DD")}
</Button>
</PopoverTrigger>
<PopoverContent align="start">
{(field === "AFTER" || field === "BEFORE") && (
<DatePicker
field={field}
date={filters[field]?.value}
setFilter={setFilter}
/>
)}
</PopoverContent>
</Popover>
<Popover
open={activeMenu === `${field}.time`}
onOpenChange={(state) =>
handleOpenChange(state, `${field}.time`)
}
>
<PopoverTrigger asChild>
<Button variant="outline" className="px-2">
{moment(filters[field]?.value).format("HH:mm:ss")}
</Button>
</PopoverTrigger>
<PopoverContent align="start" className="w-min">
<RefineTime
field={field}
date={dateValue}
setFilter={setFilter}
/>
</PopoverContent>
</Popover>
<Button
variant="outline"
icon
data-testid={`clear-filter-${field}`}
>
<X onClick={() => clearFilter(field)} />
</Button>
</ButtonBar>
);
}
})}
{!!undefinedFilters.length && (
<Popover
open={activeMenu === "main"}
onOpenChange={(state) => handleOpenChange(state, "main")}
>
<PopoverTrigger asChild>
{hasFilters ? (
<Button
data-testid="add-filter"
variant="outline"
icon
onClick={() => toggleMenu("main")}
>
<Plus />
</Button>
) : (
<Button
data-testid="add-filter"
variant="outline"
onClick={() => toggleMenu("main")}
>
<Plus />
{t("pages.events.history.filter.filterButton")}
</Button>
)}
</PopoverTrigger>
<PopoverContent align="start">
{(selectedField === null && (
<SelectFieldMenu<keyof FiltersObj>
options={undefinedFilters.map((option) => ({
value: option,
label: t(`pages.events.history.filter.field.${option}`),
}))}
onSelect={(value) => setSelectedField(value)}
heading={t("pages.events.history.filter.menuHeading.main")}
placeholder={t(
"pages.events.history.filter.placeholder.mainMenu"
)}
/>
)) ||
((selectedField === "TYPE" || selectedField === "TEXT") && (
<TextInput
value={filters[selectedField]?.value}
onSubmit={(value) => {
if (value) {
setFilter({
[selectedField]: { value, type: "CONTAINS" },
});
} else {
clearFilter(selectedField);
}
}}
heading={t(
`pages.events.history.filter.menuHeading.${selectedField}`
)}
placeholder={t(
`pages.events.history.filter.placeholder.${selectedField}`
)}
/>
)) ||
((selectedField === "AFTER" || selectedField === "BEFORE") && (
<DatePicker
field={selectedField}
date={filters[selectedField]?.value}
setFilter={setFilter}
/>
))}
</PopoverContent>
</Popover>
)}
</div>
);
};
export default Filters;