mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-07 11:45:24 +03:00
Get history filtering working for cameras and labels
This commit is contained in:
parent
70ef2c0508
commit
dac8c1f975
@ -23,6 +23,16 @@ export default function HistoryFilterPopover({
|
|||||||
onUpdateFilter,
|
onUpdateFilter,
|
||||||
}: HistoryFilterPopoverProps) {
|
}: HistoryFilterPopoverProps) {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const disabledDates = useMemo(() => {
|
||||||
|
const tomorrow = new Date();
|
||||||
|
tomorrow.setHours(tomorrow.getHours() + 24, -1, 0, 0);
|
||||||
|
const future = new Date();
|
||||||
|
future.setFullYear(2032);
|
||||||
|
return { from: tomorrow, to: future };
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { data: allLabels } = useSWR<string[]>(["labels"], {
|
const { data: allLabels } = useSWR<string[]>(["labels"], {
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
});
|
});
|
||||||
@ -42,10 +52,21 @@ export default function HistoryFilterPopover({
|
|||||||
const [selectedFilters, setSelectedFilters] = useState({
|
const [selectedFilters, setSelectedFilters] = useState({
|
||||||
cameras: filter == undefined ? [] : filter.cameras,
|
cameras: filter == undefined ? [] : filter.cameras,
|
||||||
labels: filter == undefined ? [] : filter.labels,
|
labels: filter == undefined ? [] : filter.labels,
|
||||||
|
before: filter?.before,
|
||||||
|
after: filter?.after,
|
||||||
});
|
});
|
||||||
|
const dateRange = useMemo(() => {
|
||||||
|
return selectedFilters?.before == undefined ||
|
||||||
|
selectedFilters?.after == undefined
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
from: new Date(selectedFilters.after * 1000),
|
||||||
|
to: new Date(selectedFilters.before * 1000),
|
||||||
|
};
|
||||||
|
}, [selectedFilters]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover open={open} onOpenChange={(open) => setOpen(open)}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button>
|
<Button>
|
||||||
<LuFilter className="mx-1" />
|
<LuFilter className="mx-1" />
|
||||||
@ -57,7 +78,9 @@ export default function HistoryFilterPopover({
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button className="capitalize" variant="outline">
|
<Button className="capitalize" variant="outline">
|
||||||
{""?.replaceAll("_", " ") || "All Cameras"}
|
{selectedFilters.cameras.length == 0
|
||||||
|
? "All Cameras"
|
||||||
|
: `${selectedFilters.cameras.length} Cameras`}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
@ -107,7 +130,9 @@ export default function HistoryFilterPopover({
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button className="capitalize" variant="outline">
|
<Button className="capitalize" variant="outline">
|
||||||
{""?.replaceAll("_", " ") || "All Labels"}
|
{selectedFilters.labels.length == 0
|
||||||
|
? "All Labels"
|
||||||
|
: `${selectedFilters.labels.length} Labels`}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
@ -155,10 +180,35 @@ export default function HistoryFilterPopover({
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
<Calendar mode="range" />
|
<Calendar
|
||||||
|
mode="range"
|
||||||
|
disabled={disabledDates}
|
||||||
|
selected={dateRange}
|
||||||
|
onSelect={(range) => {
|
||||||
|
let afterTime = undefined;
|
||||||
|
if (range?.from != undefined) {
|
||||||
|
afterTime = range.from.getTime() / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
// need to make sure the day selected for before covers the entire day
|
||||||
|
let beforeTime = undefined;
|
||||||
|
if (range?.from != undefined) {
|
||||||
|
const beforeDate = range.to ?? range.from;
|
||||||
|
beforeDate.setHours(beforeDate.getHours() + 24, -1, 0, 0);
|
||||||
|
beforeTime = beforeDate.getTime() / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedFilters({
|
||||||
|
...selectedFilters,
|
||||||
|
after: afterTime,
|
||||||
|
before: beforeTime,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onUpdateFilter(selectedFilters);
|
onUpdateFilter(selectedFilters);
|
||||||
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
|
|||||||
@ -1,17 +1,42 @@
|
|||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
type useApiFilterReturn<F extends FilterType> = [
|
type useApiFilterReturn<F extends FilterType> = [
|
||||||
filter: F | undefined,
|
filter: F | undefined,
|
||||||
setFilter: (filter: F) => void,
|
setFilter: (filter: F) => void,
|
||||||
searchParams: {
|
searchParams:
|
||||||
|
| {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
},
|
}
|
||||||
|
| undefined,
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function useApiFilter<
|
export default function useApiFilter<
|
||||||
F extends FilterType,
|
F extends FilterType,
|
||||||
>(): useApiFilterReturn<F> {
|
>(): useApiFilterReturn<F> {
|
||||||
const [filter, setFilter] = useState<F>();
|
const [filter, setFilter] = useState<F | undefined>(undefined);
|
||||||
|
const searchParams = useMemo(() => {
|
||||||
return [filter, setFilter, {}];
|
if (filter == undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const search: { [key: string]: string } = {};
|
||||||
|
|
||||||
|
Object.entries(filter).forEach(([key, value]) => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (value.length == 0) {
|
||||||
|
// empty array means all so ignore
|
||||||
|
} else {
|
||||||
|
search[key] = value.join(",");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (value != undefined) {
|
||||||
|
search[key] = `${value}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return search;
|
||||||
|
}, [filter]);
|
||||||
|
|
||||||
|
return [filter, setFilter, searchParams];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,15 +40,30 @@ function History() {
|
|||||||
return axios.get(path, { params }).then((res) => res.data);
|
return axios.get(path, { params }).then((res) => res.data);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getKey = useCallback((index: number, prevData: HourlyTimeline) => {
|
const getKey = useCallback(
|
||||||
|
(index: number, prevData: HourlyTimeline) => {
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
const lastDate = prevData.end;
|
const lastDate = prevData.end;
|
||||||
const pagedParams = { before: lastDate, timezone, limit: API_LIMIT };
|
const pagedParams =
|
||||||
|
historySearchParams == undefined
|
||||||
|
? { before: lastDate, timezone, limit: API_LIMIT }
|
||||||
|
: {
|
||||||
|
...historySearchParams,
|
||||||
|
before: lastDate,
|
||||||
|
timezone,
|
||||||
|
limit: API_LIMIT,
|
||||||
|
};
|
||||||
return ["timeline/hourly", pagedParams];
|
return ["timeline/hourly", pagedParams];
|
||||||
}
|
}
|
||||||
|
|
||||||
return ["timeline/hourly", { timezone, limit: API_LIMIT }];
|
const params =
|
||||||
}, []);
|
historySearchParams == undefined
|
||||||
|
? { timezone, limit: API_LIMIT }
|
||||||
|
: { ...historySearchParams, timezone, limit: API_LIMIT };
|
||||||
|
return ["timeline/hourly", params];
|
||||||
|
},
|
||||||
|
[historySearchParams]
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: timelinePages,
|
data: timelinePages,
|
||||||
@ -188,7 +203,7 @@ function History() {
|
|||||||
return (
|
return (
|
||||||
<div key={day}>
|
<div key={day}>
|
||||||
<Heading
|
<Heading
|
||||||
className="sticky py-2 -top-4 left-0 bg-background w-full z-10"
|
className="sticky py-2 -top-4 left-0 bg-background w-full z-20"
|
||||||
as="h3"
|
as="h3"
|
||||||
>
|
>
|
||||||
{formatUnixTimestampToDateTime(parseInt(day), {
|
{formatUnixTimestampToDateTime(parseInt(day), {
|
||||||
|
|||||||
@ -1,45 +1,47 @@
|
|||||||
type CardsData = {
|
type CardsData = {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
[key: string]: Card
|
[key: string]: Card;
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
type Card = {
|
type Card = {
|
||||||
camera: string,
|
camera: string;
|
||||||
time: number,
|
time: number;
|
||||||
entries: Timeline[],
|
entries: Timeline[];
|
||||||
uniqueKeys: string[],
|
uniqueKeys: string[];
|
||||||
}
|
};
|
||||||
|
|
||||||
type Preview = {
|
type Preview = {
|
||||||
camera: string,
|
camera: string;
|
||||||
src: string,
|
src: string;
|
||||||
type: string,
|
type: string;
|
||||||
start: number,
|
start: number;
|
||||||
end: number,
|
end: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
type Timeline = {
|
type Timeline = {
|
||||||
camera: string,
|
camera: string;
|
||||||
timestamp: number,
|
timestamp: number;
|
||||||
data: {
|
data: {
|
||||||
[key: string]: any
|
[key: string]: any;
|
||||||
},
|
};
|
||||||
class_type: string,
|
class_type: string;
|
||||||
source_id: string,
|
source_id: string;
|
||||||
source: string,
|
source: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
type HourlyTimeline = {
|
type HourlyTimeline = {
|
||||||
start: number,
|
start: number;
|
||||||
end: number,
|
end: number;
|
||||||
count: number,
|
count: number;
|
||||||
hours: { [key: string]: Timeline[] };
|
hours: { [key: string]: Timeline[] };
|
||||||
}
|
};
|
||||||
|
|
||||||
interface HistoryFilter extends FilterType {
|
interface HistoryFilter extends FilterType {
|
||||||
cameras: string[],
|
cameras: string[];
|
||||||
labels: string[],
|
labels: string[];
|
||||||
|
before: number | undefined;
|
||||||
|
after: number | undefined;
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user