diff --git a/client/src/app/components/FilterToolbar/DateRangeFilter.tsx b/client/src/app/components/FilterToolbar/DateRangeFilter.tsx
new file mode 100644
index 000000000..be39d1604
--- /dev/null
+++ b/client/src/app/components/FilterToolbar/DateRangeFilter.tsx
@@ -0,0 +1,133 @@
+import React, { FormEvent, useState } from "react";
+
+import {
+ DatePicker,
+ InputGroup,
+ isValidDate as isValidJSDate,
+ ToolbarChip,
+ ToolbarChipGroup,
+ ToolbarFilter,
+ Tooltip,
+} from "@patternfly/react-core";
+
+import { IFilterControlProps } from "./FilterControl";
+import {
+ abbreviateInterval,
+ dateFormat,
+ dateParse,
+ isValidDate,
+ isValidInterval,
+ parseInterval,
+ toISODateInterval,
+} from "./dateUtils";
+
+/**
+ * This Filter type enables selecting an closed date range.
+ * Precisely given range [A,B] a date X in the range if A <= X <= B.
+ *
+ * **Props are interpreted as follows**:
+ * 1) filterValue - date range encoded as ISO 8601 time interval string ("dateFrom/dateTo"). Only date part is used (no time).
+ * 2) setFilterValue - accepts the list of ranges.
+ *
+ */
+
+export const DateRangeFilter = ({
+ category,
+ filterValue,
+ setFilterValue,
+ showToolbarItem,
+ isDisabled = false,
+}: React.PropsWithChildren<
+ IFilterControlProps
+>): JSX.Element | null => {
+ const selectedFilters = filterValue ?? [];
+
+ const validFilters =
+ selectedFilters?.filter((interval) =>
+ isValidInterval(parseInterval(interval))
+ ) ?? [];
+ const [from, setFrom] = useState();
+ const [to, setTo] = useState();
+
+ const rangeToOption = (range: string) => {
+ const abbr = abbreviateInterval(range);
+ return {
+ key: range,
+ node: (
+
+ {abbr ?? ""}
+
+ ),
+ };
+ };
+
+ const clearSingleRange = (
+ category: string | ToolbarChipGroup,
+ option: string | ToolbarChip
+ ) => {
+ const target = (option as ToolbarChip)?.key;
+ setFilterValue([...validFilters.filter((range) => range !== target)]);
+ };
+
+ const onFromDateChange = (
+ event: FormEvent,
+ value: string
+ ) => {
+ if (isValidDate(value)) {
+ setFrom(dateParse(value));
+ setTo(undefined);
+ }
+ };
+
+ const onToDateChange = (even: FormEvent, value: string) => {
+ if (isValidDate(value)) {
+ const newTo = dateParse(value);
+ setTo(newTo);
+ const target = toISODateInterval(from, newTo);
+ if (target) {
+ setFilterValue([
+ ...validFilters.filter((range) => range !== target),
+ target,
+ ]);
+ }
+ }
+ };
+
+ return (
+ setFilterValue([])}
+ categoryName={category.title}
+ showToolbarItem={showToolbarItem}
+ >
+
+
+
+
+
+ );
+};
diff --git a/client/src/app/components/FilterToolbar/FilterControl.tsx b/client/src/app/components/FilterToolbar/FilterControl.tsx
index 52a0f73b4..71940b608 100644
--- a/client/src/app/components/FilterToolbar/FilterControl.tsx
+++ b/client/src/app/components/FilterToolbar/FilterControl.tsx
@@ -11,6 +11,7 @@ import {
import { SelectFilterControl } from "./SelectFilterControl";
import { SearchFilterControl } from "./SearchFilterControl";
import { MultiselectFilterControl } from "./MultiselectFilterControl";
+import { DateRangeFilter } from "./DateRangeFilter";
export interface IFilterControlProps {
category: FilterCategory;
@@ -58,5 +59,8 @@ export const FilterControl = ({
/>
);
}
+ if (category.type === FilterType.dateRange) {
+ return ;
+ }
return null;
};
diff --git a/client/src/app/components/FilterToolbar/FilterToolbar.tsx b/client/src/app/components/FilterToolbar/FilterToolbar.tsx
index eaa169a2d..0e0af511c 100644
--- a/client/src/app/components/FilterToolbar/FilterToolbar.tsx
+++ b/client/src/app/components/FilterToolbar/FilterToolbar.tsx
@@ -18,6 +18,7 @@ export enum FilterType {
multiselect = "multiselect",
search = "search",
numsearch = "numsearch",
+ dateRange = "dateRange",
}
export type FilterValue = string[] | undefined | null;
diff --git a/client/src/app/components/FilterToolbar/dateUtils.ts b/client/src/app/components/FilterToolbar/dateUtils.ts
new file mode 100644
index 000000000..cd74a69b1
--- /dev/null
+++ b/client/src/app/components/FilterToolbar/dateUtils.ts
@@ -0,0 +1,30 @@
+import dayjs from "dayjs";
+
+export const isValidDate = (val: string) => dayjs(val).isValid();
+
+export const dateFormat = (val: Date) => dayjs(val).format("MM/DD/YYYY");
+
+export const dateParse = (val: string) => dayjs(val).toDate();
+
+// i.e.'1970-01-01/1970-01-01'
+export const toISODateInterval = (from?: Date, to?: Date) => {
+ const [start, end] = [dayjs(from), dayjs(to)];
+ if (!isValidInterval([start, end])) {
+ return undefined;
+ }
+ return `${start.format("YYYY-MM-dd")}/${end.format("YYYY-MM-dd")}`;
+};
+
+export const parseInterval = (interval: string): dayjs.Dayjs[] =>
+ interval?.split("/").map(dayjs) ?? [];
+
+export const isValidInterval = ([from, to]: dayjs.Dayjs[]) =>
+ from?.isValid() && to?.isValid() && from?.isSameOrBefore(to);
+
+export const abbreviateInterval = (interval: string) => {
+ const [start, end] = parseInterval(interval);
+ if (!isValidInterval([start, end])) {
+ return undefined;
+ }
+ return `${start.format("MM-dd")}/${end.format("MM-dd")}`;
+};