Skip to content

Commit db49886

Browse files
committed
Merge branch 'main' into fix/2488-facets-values-dancing
# Conflicts: # keep-ui/app/alerts/alert-table-alert-facets.tsx
2 parents c6e9861 + 384ce0f commit db49886

File tree

7 files changed

+923
-25
lines changed

7 files changed

+923
-25
lines changed

keep-ui/app/alerts/TitleAndFilters.tsx

+65-15
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { DateRangePicker, DateRangePickerValue, Title } from "@tremor/react";
33
import { AlertDto } from "./models";
44
import ColumnSelection from "./ColumnSelection";
55
import { ThemeSelection } from "./ThemeSelection";
6+
import EnhancedDateRangePicker from "@/components/ui/DateRangePicker";
7+
import { useState } from "react";
68

79
type Theme = {
810
[key: string]: string;
@@ -21,22 +23,65 @@ export const TitleAndFilters = ({
2123
table,
2224
onThemeChange,
2325
}: TableHeaderProps) => {
24-
const onDateRangePickerChange = ({
25-
from: start,
26-
to: end,
27-
}: DateRangePickerValue) => {
28-
table.setColumnFilters((existingFilters) => {
29-
// remove any existing "lastReceived" filters
30-
const filteredArrayFromLastReceived = existingFilters.filter(
31-
({ id }) => id !== "lastReceived"
32-
);
26+
const [timeFrame, setTimeFrame] = useState<{
27+
start: Date | null;
28+
end: Date | null;
29+
paused: boolean;
30+
isFromCalendar: boolean;
31+
}>({
32+
start: null,
33+
end: null,
34+
paused: true,
35+
isFromCalendar: false,
36+
});
3337

34-
return filteredArrayFromLastReceived.concat({
35-
id: "lastReceived",
36-
value: { start, end },
37-
});
38+
const handleTimeFrameChange = (newTimeFrame: {
39+
start: Date | null;
40+
end: Date | null;
41+
paused?: boolean;
42+
isFromCalendar?: boolean;
43+
}) => {
44+
setTimeFrame({
45+
start: newTimeFrame.start,
46+
end: newTimeFrame.end,
47+
paused: newTimeFrame.paused ?? true,
48+
isFromCalendar: newTimeFrame.isFromCalendar ?? false,
3849
});
3950

51+
// Only apply date filter if both start and end dates exist
52+
if (newTimeFrame.start && newTimeFrame.end) {
53+
const adjustedTimeFrame = {
54+
...newTimeFrame,
55+
end: new Date(newTimeFrame.end.getTime()),
56+
paused: newTimeFrame.paused ?? true,
57+
isFromCalendar: newTimeFrame.isFromCalendar ?? false,
58+
};
59+
60+
if (adjustedTimeFrame.isFromCalendar) {
61+
adjustedTimeFrame.end.setHours(23, 59, 59, 999);
62+
}
63+
64+
table.setColumnFilters((existingFilters) => {
65+
const filteredArrayFromLastReceived = existingFilters.filter(
66+
({ id }) => id !== "lastReceived"
67+
);
68+
69+
return filteredArrayFromLastReceived.concat({
70+
id: "lastReceived",
71+
value: {
72+
start: adjustedTimeFrame.start,
73+
end: adjustedTimeFrame.end,
74+
},
75+
});
76+
});
77+
} else {
78+
// Remove date filter if no dates are selected
79+
table.setColumnFilters((existingFilters) =>
80+
existingFilters.filter(({ id }) => id !== "lastReceived")
81+
);
82+
}
83+
84+
table.resetRowSelection();
4085
table.resetPagination();
4186
};
4287

@@ -46,8 +91,13 @@ export const TitleAndFilters = ({
4691
<Title className="capitalize inline">{presetName}</Title>
4792
</div>
4893
<div className="grid grid-cols-[auto_auto] grid-rows-[auto_auto] gap-4">
49-
<DateRangePicker
50-
onValueChange={onDateRangePickerChange}
94+
<EnhancedDateRangePicker
95+
timeFrame={timeFrame}
96+
setTimeFrame={handleTimeFrameChange}
97+
hasPlay={false}
98+
hasRewind={false}
99+
hasForward={false}
100+
hasZoomOut={false}
51101
enableYearNavigation
52102
/>
53103
<div className="flex items-center">

keep-ui/app/alerts/alert-table-alert-facets.tsx

+11-2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ export const AlertFacets: React.FC<AlertFacetsProps> = ({
2727
className,
2828
table,
2929
}) => {
30+
const timeRangeFilter = table
31+
.getState()
32+
.columnFilters.find((filter) => filter.id === "lastReceived");
33+
34+
const timeRange = timeRangeFilter?.value as
35+
| { start: Date; end: Date; isFromCalendar: boolean }
36+
| undefined;
37+
3038
const presetName = window.location.pathname.split("/").pop() || "default";
3139

3240
const [isModalOpen, setIsModalOpen] = useLocalStorage<boolean>(
@@ -72,7 +80,8 @@ export const AlertFacets: React.FC<AlertFacetsProps> = ({
7280
const filteredAlerts = getFilteredAlertsForFacet(
7381
alerts,
7482
facetFilters,
75-
key
83+
key,
84+
timeRange
7685
);
7786
const valueMap = new Map<string, number>();
7887
let nullCount = 0;
@@ -150,7 +159,7 @@ export const AlertFacets: React.FC<AlertFacetsProps> = ({
150159

151160
return values;
152161
},
153-
[alerts, facetFilters]
162+
[alerts, facetFilters, timeRange]
154163
);
155164

156165
const staticFacets = [

keep-ui/app/alerts/alert-table-facet-utils.tsx

+22-4
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,39 @@ import {
1111
BellSlashIcon,
1212
FireIcon,
1313
} from "@heroicons/react/24/outline";
14+
import { isQuickPresetRange } from "@/components/ui/DateRangePicker";
1415

1516
export const getFilteredAlertsForFacet = (
1617
alerts: AlertDto[],
1718
facetFilters: FacetFilters,
18-
excludeFacet: string
19-
): AlertDto[] => {
19+
currentFacetKey: string,
20+
timeRange?: { start: Date; end: Date; isFromCalendar: boolean }
21+
) => {
2022
return alerts.filter((alert) => {
23+
// Only apply time range filter if both start and end dates exist
24+
if (timeRange?.start && timeRange?.end) {
25+
const lastReceived = new Date(alert.lastReceived);
26+
const rangeStart = new Date(timeRange.start);
27+
const rangeEnd = new Date(timeRange.end);
28+
29+
if (!isQuickPresetRange(timeRange)) {
30+
rangeEnd.setHours(23, 59, 59, 999);
31+
}
32+
33+
if (lastReceived < rangeStart || lastReceived > rangeEnd) {
34+
return false;
35+
}
36+
}
37+
38+
// Then apply facet filters, excluding the current facet
2139
return Object.entries(facetFilters).every(([facetKey, includedValues]) => {
22-
if (facetKey === excludeFacet || includedValues.length === 0) {
40+
// Skip filtering by current facet to avoid circular dependency
41+
if (facetKey === currentFacetKey || includedValues.length === 0) {
2342
return true;
2443
}
2544

2645
let value;
2746
if (facetKey.includes(".")) {
28-
// Handle nested keys like "labels.job"
2947
const [parentKey, childKey] = facetKey.split(".");
3048
const parentValue = alert[parentKey as keyof AlertDto];
3149

keep-ui/components/ui/Calendar.tsx

+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import * as React from "react";
2+
import { DayPicker, DateRange } from "react-day-picker";
3+
4+
export function cn(...classes: (string | undefined)[]) {
5+
return classes.filter(Boolean).join(" ");
6+
}
7+
8+
interface CalendarSingleProps {
9+
mode: "single";
10+
selected?: Date;
11+
onSelect?: (date: Date | undefined) => void;
12+
className?: string;
13+
classNames?: Record<string, string>;
14+
showOutsideDays?: boolean;
15+
}
16+
17+
interface CalendarRangeProps {
18+
mode: "range";
19+
selected?: DateRange;
20+
onSelect?: (date: DateRange | undefined) => void;
21+
className?: string;
22+
classNames?: Record<string, string>;
23+
showOutsideDays?: boolean;
24+
}
25+
26+
type CalendarProps =
27+
| (CalendarSingleProps &
28+
Omit<
29+
React.ComponentProps<typeof DayPicker>,
30+
| "mode"
31+
| "selected"
32+
| "onSelect"
33+
| "className"
34+
| "classNames"
35+
| "showOutsideDays"
36+
>)
37+
| (CalendarRangeProps &
38+
Omit<
39+
React.ComponentProps<typeof DayPicker>,
40+
| "mode"
41+
| "selected"
42+
| "onSelect"
43+
| "className"
44+
| "classNames"
45+
| "showOutsideDays"
46+
>);
47+
48+
function Calendar({
49+
className,
50+
classNames,
51+
showOutsideDays = true,
52+
mode = "single",
53+
onSelect,
54+
selected,
55+
...props
56+
}: CalendarProps) {
57+
const [hoveredDay, setHoveredDay] = React.useState<Date | undefined>();
58+
const [internalSelected, setInternalSelected] = React.useState<
59+
Date | DateRange | undefined
60+
>(selected);
61+
62+
React.useEffect(() => {
63+
setInternalSelected(selected);
64+
}, [selected]);
65+
66+
const handleDayMouseEnter = (day: Date) => {
67+
setHoveredDay(day);
68+
};
69+
70+
const handleDayMouseLeave = () => {
71+
setHoveredDay(undefined);
72+
};
73+
74+
const isInHoverRange = (day: Date) => {
75+
if (
76+
!internalSelected ||
77+
!("from" in internalSelected) ||
78+
internalSelected.to ||
79+
!hoveredDay
80+
)
81+
return false;
82+
83+
const start = internalSelected.from;
84+
if (!start) return false;
85+
86+
const end = hoveredDay;
87+
88+
return (
89+
(start < end && day > start && day <= end) ||
90+
(start > end && day < start && day >= end)
91+
);
92+
};
93+
94+
const handleDaySelect = (value: Date | DateRange | undefined) => {
95+
setInternalSelected(value);
96+
if (
97+
mode === "single" &&
98+
!(value instanceof Date) &&
99+
"from" in (value || {})
100+
) {
101+
(onSelect as (date: Date | undefined) => void)?.(
102+
(value as DateRange)?.from
103+
);
104+
} else if (mode === "range" && (value instanceof Date || !value)) {
105+
(onSelect as (date: DateRange | undefined) => void)?.(undefined);
106+
} else {
107+
(onSelect as any)?.(value);
108+
}
109+
};
110+
111+
const dayPickerProps = {
112+
...props,
113+
mode,
114+
selected: internalSelected,
115+
onSelect: handleDaySelect,
116+
onDayMouseEnter: handleDayMouseEnter,
117+
onDayMouseLeave: handleDayMouseLeave,
118+
showOutsideDays,
119+
className: cn("p-3", className),
120+
classNames: {
121+
months: "flex flex-col space-y-4",
122+
month: "space-y-4 w-full",
123+
caption: "flex justify-center pt-1 relative items-center",
124+
caption_label: "text-sm font-medium",
125+
nav: "space-x-1 flex items-center",
126+
nav_button: cn("h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"),
127+
nav_button_previous: "absolute left-1",
128+
nav_button_next: "absolute right-1",
129+
table: "w-full border-collapse space-y-1",
130+
head_row: "flex",
131+
head_cell:
132+
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
133+
row: "flex w-full mt-2",
134+
cell: cn(
135+
"text-center text-sm p-0 relative",
136+
"[&:has([aria-selected])]:bg-accent/50",
137+
"first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md",
138+
"[&:has(>.day-range-end)]:rounded-r-md",
139+
"[&:has(>.day-range-start)]:rounded-l-md",
140+
"[&:has(>.day-hover)]:bg-gray-100"
141+
),
142+
day: cn(
143+
"h-9 w-9 p-0 font-normal relative",
144+
"hover:bg-gray-100",
145+
"focus-visible:bg-accent focus-visible:text-accent-foreground",
146+
"[&.day-range-start]:bg-primary [&.day-range-start]:text-primary-foreground",
147+
"[&.day-range-end]:bg-primary [&.day-range-end]:text-primary-foreground",
148+
"[&.day-range-middle]:bg-accent/50",
149+
"[&.day-hover]:bg-gray-100"
150+
),
151+
day_range_start: "day-range-start",
152+
day_range_end: "day-range-end",
153+
day_selected:
154+
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground",
155+
day_today: "bg-accent text-accent-foreground",
156+
day_outside: "text-muted-foreground opacity-50",
157+
day_disabled: "text-muted-foreground opacity-50",
158+
day_range_middle: "day-range-middle",
159+
day_hidden: "invisible",
160+
...classNames,
161+
},
162+
modifiers: {
163+
...props.modifiers,
164+
hover: (day: Date) => isInHoverRange(day),
165+
range:
166+
internalSelected && "from" in internalSelected && internalSelected.to
167+
? [{ after: internalSelected.from, before: internalSelected.to }]
168+
: [],
169+
rangeStart:
170+
internalSelected && "from" in internalSelected && internalSelected.from
171+
? [internalSelected.from]
172+
: [],
173+
rangeEnd:
174+
internalSelected && "from" in internalSelected && internalSelected.to
175+
? [internalSelected.to]
176+
: [],
177+
},
178+
modifiersStyles: {
179+
...props.modifiersStyles,
180+
hover: { backgroundColor: "rgb(243 244 246)" },
181+
range: { backgroundColor: "rgb(243 244 246)" },
182+
rangeStart: {
183+
color: "white",
184+
backgroundColor: "rgb(63 63 70)",
185+
borderTopLeftRadius: "4px",
186+
borderBottomLeftRadius: "4px",
187+
},
188+
rangeEnd: {
189+
color: "white",
190+
backgroundColor: "rgb(63 63 70)",
191+
borderTopRightRadius: "4px",
192+
borderBottomRightRadius: "4px",
193+
},
194+
},
195+
};
196+
197+
return <DayPicker {...(dayPickerProps as any)} />;
198+
}
199+
200+
Calendar.displayName = "Calendar";
201+
202+
export { Calendar };

0 commit comments

Comments
 (0)