Skip to content

Commit

Permalink
Add multiselect filtering to events view
Browse files Browse the repository at this point in the history
  • Loading branch information
NickM-27 committed Jun 30, 2022
1 parent c2465a4 commit bd3d0bb
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 60 deletions.
6 changes: 3 additions & 3 deletions docs/docs/integrations/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,9 @@ Events from the database. Accepts the following query string parameters:
| -------------------- | ---- | --------------------------------------------- |
| `before` | int | Epoch time |
| `after` | int | Epoch time |
| `camera` | str | Camera name |
| `label` | str | Label name |
| `zone` | str | Zone name |
| `cameras` | str | , separated list of cameras |
| `labels` | str | , separated list of labels |
| `zones` | str | , separated list of zones |
| `limit` | int | Limit the number of events returned |
| `has_snapshot` | int | Filter to events that have snapshots (0 or 1) |
| `has_clip` | int | Filter to events that have clips (0 or 1) |
Expand Down
72 changes: 65 additions & 7 deletions frigate/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ def set_sub_label(id):

@bp.route("/sub_labels")
def get_sub_labels():
split_joined = request.args.get("split_joined", type=int)

try:
events = Event.select(Event.sub_label).distinct()
except Exception as e:
Expand All @@ -260,6 +262,16 @@ def get_sub_labels():
if None in sub_labels:
sub_labels.remove(None)

if split_joined:
for label in sub_labels:
if "," in label:
sub_labels.remove(label)
parts = label.split(",")

for part in parts:
if not (part.strip()) in sub_labels:
sub_labels.append(part.strip())

return jsonify(sub_labels)


Expand Down Expand Up @@ -489,11 +501,35 @@ def event_clip(id):

@bp.route("/events")
def events():
limit = request.args.get("limit", 100)
camera = request.args.get("camera", "all")
cameras = request.args.get("cameras", "all")

# handle old camera arg
if cameras == "all" and camera != "all":
cameras = camera

label = request.args.get("label", "all")
labels = request.args.get("labels", "all")

# handle old label arg
if labels == "all" and label != "all":
labels = label

sub_label = request.args.get("sub_label", "all")
sub_labels = request.args.get("sub_labels", "all")

# handle old sub_label arg
if sub_labels == "all" and sub_label != "all":
sub_labels = sub_label

zone = request.args.get("zone", "all")
zones = request.args.get("zones", "all")

# handle old label arg
if zones == "all" and zone != "all":
zones = zone

limit = request.args.get("limit", 100)
after = request.args.get("after", type=float)
before = request.args.get("before", type=float)
has_clip = request.args.get("has_clip", type=int)
Expand Down Expand Up @@ -521,14 +557,36 @@ def events():
if camera != "all":
clauses.append((Event.camera == camera))

if label != "all":
clauses.append((Event.label == label))
if cameras != "all":
camera_list = cameras.split(",")
clauses.append((Event.camera << camera_list))

if labels != "all":
label_list = labels.split(",")
clauses.append((Event.label << label_list))

if sub_labels != "all":
# use matching so joined sub labels are included
# for example a sub label 'bob' would get events
# with sub labels 'bob' and 'bob, john'
sub_label_clauses = []

for label in sub_labels.split(","):
sub_label_clauses.append((Event.sub_label.cast("text") % f"*{label}*"))

sub_label_clause = reduce(operator.or_, sub_label_clauses)
clauses.append((sub_label_clause))

if zones != "all":
# use matching so events with multiple zones
# still match on a search where any zone matches
zone_clauses = []

if sub_label != "all":
clauses.append((Event.sub_label == sub_label))
for zone in zones.split(","):
zone_clauses.append((Event.zones.cast("text") % f'*"{zone}"*'))

if zone != "all":
clauses.append((Event.zones.cast("text") % f'*"{zone}"*'))
zone_clause = reduce(operator.or_, zone_clauses)
clauses.append((zone_clause))

if after:
clauses.append((Event.start_time > after))
Expand Down
43 changes: 43 additions & 0 deletions web/src/components/MultiSelect.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { h } from 'preact';
import { useRef, useState } from 'preact/hooks';
import Menu from './Menu';
import { ArrowDropdown } from '../icons/ArrowDropdown';
import Heading from './Heading';

export default function MultiSelect({ className, title, options, selection, onToggle }) {

const popupRef = useRef(null);

const [state, setState] = useState({
showMenu: false,
});

return (
<div className={`${className} p-2`} ref={popupRef}>
<div
className="flex justify-between min-w-[120px]"
onClick={() => setState({ showMenu: true })}
>
<label>{title}</label>
<ArrowDropdown className="w-6" />
</div>
{state.showMenu ? (
<Menu relativeTo={popupRef} onDismiss={() => setState({ showMenu: false })}>
<Heading className="p-4 justify-center" size="md">{title}</Heading>
{options.map((item) => (
<label
className={`flex flex-shrink space-x-2 p-1 my-1 min-w-[176px] hover:bg-gray-200 dark:hover:bg-gray-800 dark:hover:text-white cursor-pointer capitalize text-sm`}
key={item}>
<input
className="mx-4 m-0 align-middle"
type="checkbox"
checked={selection == "all" || selection.indexOf(item) > -1}
onChange={() => onToggle(item)} />
{item.replaceAll("_", " ")}
</label>
))}
</Menu>
): null}
</div>
);
}
2 changes: 1 addition & 1 deletion web/src/routes/Camera.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export default function Camera({ camera }) {
className="mb-4 mr-4"
key={objectType}
header={objectType}
href={`/events?camera=${camera}&label=${objectType}`}
href={`/events?cameras=${camera}&labels=${objectType}`}
media={<img src={`${apiHost}/api/${camera}/${objectType}/thumbnail.jpg`} />}
/>
))}
Expand Down
113 changes: 64 additions & 49 deletions web/src/routes/Events.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import CalendarIcon from '../icons/Calendar';
import Calendar from '../components/Calendar';
import Button from '../components/Button';
import Dialog from '../components/Dialog';
import MultiSelect from '../components/MultiSelect';

const API_LIMIT = 25;

Expand All @@ -41,10 +42,10 @@ export default function Events({ path, ...props }) {
const [searchParams, setSearchParams] = useState({
before: null,
after: null,
camera: props.camera ?? 'all',
label: props.label ?? 'all',
zone: props.zone ?? 'all',
sub_label: props.sub_label ?? 'all',
cameras: props.cameras ?? 'all',
labels: props.labels ?? 'all',
zones: props.zones ?? 'all',
sub_labels: props.sub_labels ?? 'all',
});
const [state, setState] = useState({
showDownloadMenu: false,
Expand Down Expand Up @@ -87,7 +88,7 @@ export default function Events({ path, ...props }) {

const { data: config } = useSWR('config');

const { data: allSubLabels } = useSWR('sub_labels')
const { data: allSubLabels } = useSWR(['sub_labels', { split_joined: 1 }]);

const filterValues = useMemo(
() => ({
Expand Down Expand Up @@ -135,6 +136,40 @@ export default function Events({ path, ...props }) {
}
};

const onToggleNamedFilter = (name, item) => {
let items;

if (searchParams[name] == 'all') {
const currentItems = Array.from(filterValues[name]);

// don't remove all if only one option
if (currentItems.length > 1) {
currentItems.splice(currentItems.indexOf(item), 1);
items = currentItems.join(",");
} else {
items = ["all"];
}
} else {
let currentItems = searchParams[name].length > 0 ? searchParams[name].split(",") : [];

if (currentItems.includes(item)) {
// don't remove the last item in the filter list
if (currentItems.length > 1) {
currentItems.splice(currentItems.indexOf(item), 1);
}

items = currentItems.join(",");
} else if ((currentItems.length + 1) == filterValues[name].length) {
items = ["all"];
} else {
currentItems.push(item);
items = currentItems.join(",");
}
}

onFilter(name, items);
};

const datePicker = useRef();

const downloadButton = useRef();
Expand Down Expand Up @@ -243,56 +278,36 @@ export default function Events({ path, ...props }) {
<div className="space-y-4 p-2 px-4 w-full">
<Heading>Events</Heading>
<div className="flex flex-wrap gap-2 items-center">
<select
<MultiSelect
className="basis-1/5 cursor-pointer rounded dark:bg-slate-800"
value={searchParams.camera}
onChange={(e) => onFilter('camera', e.target.value)}
>
<option value="all">all cameras</option>
{filterValues.cameras.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
<select
title="Cameras"
options={filterValues.cameras}
selection={searchParams.cameras}
onToggle={(item) => onToggleNamedFilter("cameras", item)}
/>
<MultiSelect
className="basis-1/5 cursor-pointer rounded dark:bg-slate-800"
value={searchParams.label}
onChange={(e) => onFilter('label', e.target.value)}
>
<option value="all">all labels</option>
{filterValues.labels.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
<select
title="Labels"
options={filterValues.labels}
selection={searchParams.labels}
onToggle={(item) => onToggleNamedFilter("labels", item) }
/>
<MultiSelect
className="basis-1/5 cursor-pointer rounded dark:bg-slate-800"
value={searchParams.zone}
onChange={(e) => onFilter('zone', e.target.value)}
>
<option value="all">all zones</option>
{filterValues.zones.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
title="Zones"
options={filterValues.zones}
selection={searchParams.zones}
onToggle={(item) => onToggleNamedFilter("zones", item) }
/>
{
filterValues.sub_labels.length > 0 && (
<select
<MultiSelect
className="basis-1/5 cursor-pointer rounded dark:bg-slate-800"
value={searchParams.sub_label}
onChange={(e) => onFilter('sub_label', e.target.value)}
>
<option value="all">all sub labels</option>
{filterValues.sub_labels.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
title="Sub Labels"
options={filterValues.sub_labels}
selection={searchParams.sub_labels}
onToggle={(item) => onToggleNamedFilter("sub_labels", item) }
/>
)}
<div ref={datePicker} className="ml-auto">
<CalendarIcon
Expand Down

0 comments on commit bd3d0bb

Please sign in to comment.