mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-02 17:25:22 +03:00
Merge branch 'release-0.11.0' of https://github.com/blakeblackshear/frigate into better-object-jpg
This commit is contained in:
commit
0429ec0b14
127
docker/Dockerfile
Normal file
127
docker/Dockerfile
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
FROM blakeblackshear/frigate-nginx:1.0.2 as nginx
|
||||||
|
|
||||||
|
FROM node:14 as web
|
||||||
|
|
||||||
|
WORKDIR /opt/frigate
|
||||||
|
|
||||||
|
COPY web/ .
|
||||||
|
|
||||||
|
RUN npm install && npm run build
|
||||||
|
|
||||||
|
FROM debian:11 as wheels
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Use a separate container to build wheels to prevent build dependencies in final image
|
||||||
|
RUN apt-get -qq update \
|
||||||
|
&& apt-get -qq install -y \
|
||||||
|
apt-transport-https \
|
||||||
|
gnupg \
|
||||||
|
wget \
|
||||||
|
&& wget -O - http://archive.raspberrypi.org/debian/raspberrypi.gpg.key | apt-key add - \
|
||||||
|
&& echo "deb http://archive.raspberrypi.org/debian/ bullseye main" | tee /etc/apt/sources.list.d/raspi.list \
|
||||||
|
&& apt-get -qq update \
|
||||||
|
&& apt-get -qq install -y \
|
||||||
|
python3 \
|
||||||
|
python3-dev \
|
||||||
|
wget \
|
||||||
|
# opencv dependencies
|
||||||
|
build-essential cmake git pkg-config libgtk-3-dev \
|
||||||
|
libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \
|
||||||
|
libxvidcore-dev libx264-dev libjpeg-dev libpng-dev libtiff-dev \
|
||||||
|
gfortran openexr libatlas-base-dev libssl-dev\
|
||||||
|
libtbb2 libtbb-dev libdc1394-22-dev libopenexr-dev \
|
||||||
|
libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev \
|
||||||
|
# scipy dependencies
|
||||||
|
gcc gfortran libopenblas-dev liblapack-dev
|
||||||
|
|
||||||
|
RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
|
||||||
|
&& python3 get-pip.py "pip"
|
||||||
|
|
||||||
|
RUN pip3 install scikit-build
|
||||||
|
|
||||||
|
# TODO: lock with requirements.txt
|
||||||
|
RUN pip3 wheel --wheel-dir=/wheels \
|
||||||
|
opencv-python-headless \
|
||||||
|
numpy \
|
||||||
|
imutils \
|
||||||
|
scipy \
|
||||||
|
psutil \
|
||||||
|
Flask \
|
||||||
|
paho-mqtt \
|
||||||
|
PyYAML \
|
||||||
|
matplotlib \
|
||||||
|
click \
|
||||||
|
setproctitle \
|
||||||
|
peewee \
|
||||||
|
peewee_migrate \
|
||||||
|
pydantic \
|
||||||
|
zeroconf \
|
||||||
|
ws4py
|
||||||
|
|
||||||
|
# Frigate Container
|
||||||
|
FROM debian:11-slim
|
||||||
|
ARG TARGETARCH
|
||||||
|
ARG S6_OVERLAY_VERSION=3.0.0.2
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV FLASK_ENV=development
|
||||||
|
|
||||||
|
COPY --from=wheels /wheels /wheels
|
||||||
|
|
||||||
|
# Install ffmpeg
|
||||||
|
RUN apt-get -qq update \
|
||||||
|
&& apt-get -qq install --no-install-recommends -y \
|
||||||
|
apt-transport-https \
|
||||||
|
gnupg \
|
||||||
|
wget \
|
||||||
|
unzip tzdata libxml2 xz-utils \
|
||||||
|
python3-pip \
|
||||||
|
# add raspberry pi repo
|
||||||
|
&& wget -O - http://archive.raspberrypi.org/debian/raspberrypi.gpg.key | apt-key add - \
|
||||||
|
&& echo "deb http://archive.raspberrypi.org/debian/ bullseye main" | tee /etc/apt/sources.list.d/raspi.list \
|
||||||
|
# add coral repo
|
||||||
|
&& APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn apt-key adv --fetch-keys https://packages.cloud.google.com/apt/doc/apt-key.gpg \
|
||||||
|
&& echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" > /etc/apt/sources.list.d/coral-edgetpu.list \
|
||||||
|
&& echo "libedgetpu1-max libedgetpu/accepted-eula select true" | debconf-set-selections \
|
||||||
|
&& apt-get -qq update \
|
||||||
|
&& apt-get -qq install --no-install-recommends -y \
|
||||||
|
ffmpeg \
|
||||||
|
# coral drivers
|
||||||
|
libedgetpu1-max python3-tflite-runtime python3-pycoral \
|
||||||
|
&& pip3 install -U /wheels/*.whl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /wheels \
|
||||||
|
&& (apt-get autoremove -y; apt-get autoclean -y)
|
||||||
|
|
||||||
|
COPY --from=nginx /usr/local/nginx/ /usr/local/nginx/
|
||||||
|
|
||||||
|
# get model and labels
|
||||||
|
COPY labelmap.txt /labelmap.txt
|
||||||
|
RUN wget -q https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite -O /edgetpu_model.tflite
|
||||||
|
RUN wget -q https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess.tflite -O /cpu_model.tflite
|
||||||
|
|
||||||
|
WORKDIR /opt/frigate/
|
||||||
|
ADD frigate frigate/
|
||||||
|
ADD migrations migrations/
|
||||||
|
|
||||||
|
COPY --from=web /opt/frigate/build web/
|
||||||
|
|
||||||
|
COPY docker/rootfs/ /
|
||||||
|
|
||||||
|
# s6-overlay
|
||||||
|
RUN S6_ARCH="${TARGETARCH}" \
|
||||||
|
&& if [ "${TARGETARCH}" = "amd64" ]; then S6_ARCH="amd64"; fi \
|
||||||
|
&& if [ "${TARGETARCH}" = "arm64" ]; then S6_ARCH="aarch64"; fi \
|
||||||
|
&& wget -O /tmp/s6-overlay-installer "https://github.com/just-containers/s6-overlay/releases/download/v2.2.0.3/s6-overlay-${S6_ARCH}-installer" \
|
||||||
|
&& chmod +x /tmp/s6-overlay-installer && /tmp/s6-overlay-installer /
|
||||||
|
# && wget -O - "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch-${S6_OVERLAY_VERSION}.tar.xz" \
|
||||||
|
# | tar -C / -Jxpf - \
|
||||||
|
# && wget -O - "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}-${S6_OVERLAY_VERSION}.tar.xz" \
|
||||||
|
# | tar -C / -Jxpf -
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
EXPOSE 1935
|
||||||
|
|
||||||
|
ENTRYPOINT ["/init"]
|
||||||
|
|
||||||
|
CMD ["python3", "-u", "-m", "frigate"]
|
||||||
@ -90,6 +90,20 @@ VSCode will start the docker compose file for you and open a terminal window con
|
|||||||
|
|
||||||
After closing VSCode, you may still have containers running. To close everything down, just run `docker-compose down -v` to cleanup all containers.
|
After closing VSCode, you may still have containers running. To close everything down, just run `docker-compose down -v` to cleanup all containers.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
#### FFMPEG Hardware Acceleration
|
||||||
|
|
||||||
|
The following commands are used inside the container to ensure hardware acceleration is working properly.
|
||||||
|
|
||||||
|
**Raspberry Pi (64bit)**
|
||||||
|
|
||||||
|
This should show <50% CPU in top, and ~80% CPU without `-c:v h264_v4l2m2m`.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
ffmpeg -c:v h264_v4l2m2m -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
|
||||||
|
```
|
||||||
|
|
||||||
## Web Interface
|
## Web Interface
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|||||||
@ -189,6 +189,14 @@ Returns data for a single event.
|
|||||||
|
|
||||||
Permanently deletes the event along with any clips/snapshots.
|
Permanently deletes the event along with any clips/snapshots.
|
||||||
|
|
||||||
|
### `POST /api/events/<id>/retain`
|
||||||
|
|
||||||
|
Sets retain to true for the event id.
|
||||||
|
|
||||||
|
### `DELETE /api/events/<id>/retain`
|
||||||
|
|
||||||
|
Sets retain to false for the event id (event may be deleted quickly after removing).
|
||||||
|
|
||||||
### `GET /api/events/<id>/thumbnail.jpg`
|
### `GET /api/events/<id>/thumbnail.jpg`
|
||||||
|
|
||||||
Returns a thumbnail for the event id optimized for notifications. Works while the event is in progress and after completion. Passing `?format=android` will convert the thumbnail to 2:1 aspect ratio.
|
Returns a thumbnail for the event id optimized for notifications. Works while the event is in progress and after completion. Passing `?format=android` will convert the thumbnail to 2:1 aspect ratio.
|
||||||
|
|||||||
@ -147,6 +147,7 @@ class EventCleanup(threading.Thread):
|
|||||||
Event.camera.not_in(self.camera_keys),
|
Event.camera.not_in(self.camera_keys),
|
||||||
Event.start_time < expire_after,
|
Event.start_time < expire_after,
|
||||||
Event.label == l.label,
|
Event.label == l.label,
|
||||||
|
Event.retain_indefinitely == False,
|
||||||
)
|
)
|
||||||
# delete the media from disk
|
# delete the media from disk
|
||||||
for event in expired_events:
|
for event in expired_events:
|
||||||
@ -166,6 +167,7 @@ class EventCleanup(threading.Thread):
|
|||||||
Event.camera.not_in(self.camera_keys),
|
Event.camera.not_in(self.camera_keys),
|
||||||
Event.start_time < expire_after,
|
Event.start_time < expire_after,
|
||||||
Event.label == l.label,
|
Event.label == l.label,
|
||||||
|
Event.retain_indefinitely == False,
|
||||||
)
|
)
|
||||||
update_query.execute()
|
update_query.execute()
|
||||||
|
|
||||||
@ -192,6 +194,7 @@ class EventCleanup(threading.Thread):
|
|||||||
Event.camera == name,
|
Event.camera == name,
|
||||||
Event.start_time < expire_after,
|
Event.start_time < expire_after,
|
||||||
Event.label == l.label,
|
Event.label == l.label,
|
||||||
|
Event.retain_indefinitely == False,
|
||||||
)
|
)
|
||||||
# delete the grabbed clips from disk
|
# delete the grabbed clips from disk
|
||||||
for event in expired_events:
|
for event in expired_events:
|
||||||
@ -210,6 +213,7 @@ class EventCleanup(threading.Thread):
|
|||||||
Event.camera == name,
|
Event.camera == name,
|
||||||
Event.start_time < expire_after,
|
Event.start_time < expire_after,
|
||||||
Event.label == l.label,
|
Event.label == l.label,
|
||||||
|
Event.retain_indefinitely == False,
|
||||||
)
|
)
|
||||||
update_query.execute()
|
update_query.execute()
|
||||||
|
|
||||||
|
|||||||
@ -120,6 +120,40 @@ def event(id):
|
|||||||
return "Event not found", 404
|
return "Event not found", 404
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/events/<id>/retain", methods=("POST",))
|
||||||
|
def set_retain(id):
|
||||||
|
try:
|
||||||
|
event = Event.get(Event.id == id)
|
||||||
|
except DoesNotExist:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Event" + id + " not found"}), 404
|
||||||
|
)
|
||||||
|
|
||||||
|
event.retain_indefinitely = True
|
||||||
|
event.save()
|
||||||
|
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": True, "message": "Event" + id + " retained"}), 200
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/events/<id>/retain", methods=("DELETE",))
|
||||||
|
def delete_retain(id):
|
||||||
|
try:
|
||||||
|
event = Event.get(Event.id == id)
|
||||||
|
except DoesNotExist:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Event" + id + " not found"}), 404
|
||||||
|
)
|
||||||
|
|
||||||
|
event.retain_indefinitely = False
|
||||||
|
event.save()
|
||||||
|
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": True, "message": "Event" + id + " un-retained"}), 200
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/events/<id>", methods=("DELETE",))
|
@bp.route("/events/<id>", methods=("DELETE",))
|
||||||
def delete_event(id):
|
def delete_event(id):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -18,6 +18,7 @@ class Event(Model):
|
|||||||
region = JSONField()
|
region = JSONField()
|
||||||
box = JSONField()
|
box = JSONField()
|
||||||
area = IntegerField()
|
area = IntegerField()
|
||||||
|
retain_indefinitely = BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
class Recordings(Model):
|
class Recordings(Model):
|
||||||
|
|||||||
@ -623,7 +623,8 @@ def process_frames(
|
|||||||
idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
|
idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
|
||||||
|
|
||||||
for index in idxs:
|
for index in idxs:
|
||||||
obj = group[index[0]]
|
index = index if isinstance(index, np.int32) else index[0]
|
||||||
|
obj = group[index]
|
||||||
if clipped(obj, frame_shape):
|
if clipped(obj, frame_shape):
|
||||||
box = obj[2]
|
box = obj[2]
|
||||||
# calculate a new region that will hopefully get the entire object
|
# calculate a new region that will hopefully get the entire object
|
||||||
|
|||||||
46
migrations/007_add_retain_indefinitely.py
Normal file
46
migrations/007_add_retain_indefinitely.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"""Peewee migrations -- 007_add_retain_indefinitely.py.
|
||||||
|
|
||||||
|
Some examples (model - class or model name)::
|
||||||
|
|
||||||
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
|
> migrator.sql(sql) # Run custom SQL
|
||||||
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
> migrator.change_fields(model, **fields) # Change fields
|
||||||
|
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||||
|
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||||
|
> migrator.rename_table(model, new_table_name)
|
||||||
|
> migrator.add_index(model, *col_names, unique=False)
|
||||||
|
> migrator.drop_index(model, *col_names)
|
||||||
|
> migrator.add_not_null(model, *field_names)
|
||||||
|
> migrator.drop_not_null(model, *field_names)
|
||||||
|
> migrator.add_default(model, field_name, default)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
|
import peewee as pw
|
||||||
|
from playhouse.sqlite_ext import *
|
||||||
|
from decimal import ROUND_HALF_EVEN
|
||||||
|
from frigate.models import Event
|
||||||
|
|
||||||
|
try:
|
||||||
|
import playhouse.postgres_ext as pw_pext
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
SQL = pw.SQL
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(migrator, database, fake=False, **kwargs):
|
||||||
|
migrator.add_fields(
|
||||||
|
Event,
|
||||||
|
retain_indefinitely=pw.BooleanField(default=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(migrator, database, fake=False, **kwargs):
|
||||||
|
migrator.remove_fields(Event, ["retain_indefinitely"])
|
||||||
@ -117,6 +117,24 @@ export function useDelete() {
|
|||||||
return deleteEvent;
|
return deleteEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useRetain() {
|
||||||
|
const { state } = useContext(Api);
|
||||||
|
|
||||||
|
async function retainEvent(eventId, shouldRetain) {
|
||||||
|
if (!eventId) return null;
|
||||||
|
|
||||||
|
if (shouldRetain) {
|
||||||
|
const response = await fetch(`${state.host}/api/events/${eventId}/retain`, { method: 'POST' });
|
||||||
|
return await (response.status < 300 ? response.json() : { success: true });
|
||||||
|
} else {
|
||||||
|
const response = await fetch(`${state.host}/api/events/${eventId}/retain`, { method: 'DELETE' });
|
||||||
|
return await (response.status < 300 ? response.json() : { success: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return retainEvent;
|
||||||
|
}
|
||||||
|
|
||||||
export function useApiHost() {
|
export function useApiHost() {
|
||||||
const { state } = useContext(Api);
|
const { state } = useContext(Api);
|
||||||
return state.host;
|
return state.host;
|
||||||
|
|||||||
@ -17,6 +17,13 @@ const ButtonColors = {
|
|||||||
text:
|
text:
|
||||||
'text-red-500 hover:bg-red-500 hover:bg-opacity-20 focus:bg-red-500 focus:bg-opacity-40 active:bg-red-500 active:bg-opacity-40',
|
'text-red-500 hover:bg-red-500 hover:bg-opacity-20 focus:bg-red-500 focus:bg-opacity-40 active:bg-red-500 active:bg-opacity-40',
|
||||||
},
|
},
|
||||||
|
yellow: {
|
||||||
|
contained: 'bg-yellow-500 focus:bg-yellow-400 active:bg-yellow-600 ring-yellow-300',
|
||||||
|
outlined:
|
||||||
|
'text-yellow-500 border-2 border-yellow-500 hover:bg-yellow-500 hover:bg-opacity-20 focus:bg-yellow-500 focus:bg-opacity-40 active:bg-yellow-500 active:bg-opacity-40',
|
||||||
|
text:
|
||||||
|
'text-yellow-500 hover:bg-yellow-500 hover:bg-opacity-20 focus:bg-yellow-500 focus:bg-opacity-40 active:bg-yellow-500 active:bg-opacity-40',
|
||||||
|
},
|
||||||
green: {
|
green: {
|
||||||
contained: 'bg-green-500 focus:bg-green-400 active:bg-green-600 ring-green-300',
|
contained: 'bg-green-500 focus:bg-green-400 active:bg-green-600 ring-green-300',
|
||||||
outlined:
|
outlined:
|
||||||
|
|||||||
12
web/src/icons/StarRecording.jsx
Normal file
12
web/src/icons/StarRecording.jsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { memo } from 'preact/compat';
|
||||||
|
|
||||||
|
export function StarRecording({ className = '' }) {
|
||||||
|
return (
|
||||||
|
<svg className={`fill-current ${className}`} viewBox="0 0 24 24">
|
||||||
|
<path d="M14 2H6a2 2 0 00-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6m.5 16.9L12 17.5 9.5 19l.7-2.8L8 14.3l2.9-.2 1.1-2.7 1.1 2.6 2.9.2-2.2 1.9.7 2.8M13 9V3.5L18.5 9H13z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(StarRecording);
|
||||||
@ -7,16 +7,21 @@ import ArrowDown from '../icons/ArrowDropdown';
|
|||||||
import ArrowDropup from '../icons/ArrowDropup';
|
import ArrowDropup from '../icons/ArrowDropup';
|
||||||
import Clip from '../icons/Clip';
|
import Clip from '../icons/Clip';
|
||||||
import Close from '../icons/Close';
|
import Close from '../icons/Close';
|
||||||
|
import StarRecording from '../icons/StarRecording';
|
||||||
import Delete from '../icons/Delete';
|
import Delete from '../icons/Delete';
|
||||||
import Snapshot from '../icons/Snapshot';
|
import Snapshot from '../icons/Snapshot';
|
||||||
import Dialog from '../components/Dialog';
|
import Dialog from '../components/Dialog';
|
||||||
import Heading from '../components/Heading';
|
import Heading from '../components/Heading';
|
||||||
import VideoPlayer from '../components/VideoPlayer';
|
import VideoPlayer from '../components/VideoPlayer';
|
||||||
import { Table, Thead, Tbody, Th, Tr, Td } from '../components/Table';
|
import { Table, Thead, Tbody, Th, Tr, Td } from '../components/Table';
|
||||||
import { FetchStatus, useApiHost, useEvent, useDelete } from '../api';
|
import { FetchStatus, useApiHost, useEvent, useDelete, useRetain } from '../api';
|
||||||
|
|
||||||
const ActionButtonGroup = ({ className, handleClickDelete, close }) => (
|
const ActionButtonGroup = ({ className, isRetained, handleClickRetain, handleClickDelete, close }) => (
|
||||||
<div className={`space-y-2 space-x-2 sm:space-y-0 xs:space-x-4 ${className}`}>
|
<div className={`space-y-2 space-x-2 sm:space-y-0 xs:space-x-4 ${className}`}>
|
||||||
|
<Button className="xs:w-auto" color={isRetained ? 'red' : 'yellow'} onClick={handleClickRetain}>
|
||||||
|
<StarRecording className="w-6" />
|
||||||
|
{isRetained ? ('Un-retain event') : ('Retain event')}
|
||||||
|
</Button>
|
||||||
<Button className="xs:w-auto" color="red" onClick={handleClickDelete}>
|
<Button className="xs:w-auto" color="red" onClick={handleClickDelete}>
|
||||||
<Delete className="w-6" /> Delete event
|
<Delete className="w-6" /> Delete event
|
||||||
</Button>
|
</Button>
|
||||||
@ -54,6 +59,8 @@ export default function Event({ eventId, close, scrollRef }) {
|
|||||||
const [showDetails, setShowDetails] = useState(false);
|
const [showDetails, setShowDetails] = useState(false);
|
||||||
const [shouldScroll, setShouldScroll] = useState(true);
|
const [shouldScroll, setShouldScroll] = useState(true);
|
||||||
const [deleteStatus, setDeleteStatus] = useState(FetchStatus.NONE);
|
const [deleteStatus, setDeleteStatus] = useState(FetchStatus.NONE);
|
||||||
|
const [isRetained, setIsRetained] = useState(false);
|
||||||
|
const setRetainEvent = useRetain();
|
||||||
const setDeleteEvent = useDelete();
|
const setDeleteEvent = useDelete();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -71,6 +78,22 @@ export default function Event({ eventId, close, scrollRef }) {
|
|||||||
};
|
};
|
||||||
}, [data, scrollRef, eventId, shouldScroll]);
|
}, [data, scrollRef, eventId, shouldScroll]);
|
||||||
|
|
||||||
|
const handleClickRetain = useCallback(async () => {
|
||||||
|
let success;
|
||||||
|
try {
|
||||||
|
success = await setRetainEvent(eventId, !isRetained);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
setIsRetained(!isRetained);
|
||||||
|
|
||||||
|
// Need to reload page otherwise retain button state won't stick if event is collapsed and re-opened.
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}, [eventId, isRetained, setRetainEvent]);
|
||||||
|
|
||||||
const handleClickDelete = () => {
|
const handleClickDelete = () => {
|
||||||
setShowDialog(true);
|
setShowDialog(true);
|
||||||
};
|
};
|
||||||
@ -98,6 +121,7 @@ export default function Event({ eventId, close, scrollRef }) {
|
|||||||
return <ActivityIndicator />;
|
return <ActivityIndicator />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsRetained(data.retain_indefinitely);
|
||||||
const startime = new Date(data.start_time * 1000);
|
const startime = new Date(data.start_time * 1000);
|
||||||
const endtime = data.end_time ? new Date(data.end_time * 1000) : null;
|
const endtime = data.end_time ? new Date(data.end_time * 1000) : null;
|
||||||
return (
|
return (
|
||||||
@ -119,7 +143,7 @@ export default function Event({ eventId, close, scrollRef }) {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ActionButtonGroup handleClickDelete={handleClickDelete} close={close} className="hidden sm:block" />
|
<ActionButtonGroup isRetained={isRetained} handleClickRetain={handleClickRetain} handleClickDelete={handleClickDelete} close={close} className="hidden sm:block" />
|
||||||
{showDialog ? (
|
{showDialog ? (
|
||||||
<Dialog
|
<Dialog
|
||||||
onDismiss={handleDismissDeleteDialog}
|
onDismiss={handleDismissDeleteDialog}
|
||||||
@ -210,7 +234,7 @@ export default function Event({ eventId, close, scrollRef }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 xs:space-y-0">
|
<div className="space-y-2 xs:space-y-0">
|
||||||
<DownloadButtonGroup apiHost={apiHost} eventId={eventId} className="block sm:hidden" />
|
<DownloadButtonGroup apiHost={apiHost} eventId={eventId} className="block sm:hidden" />
|
||||||
<ActionButtonGroup handleClickDelete={handleClickDelete} close={close} className="block sm:hidden" />
|
<ActionButtonGroup isRetained={isRetained} handleClickRetain={handleClickRetain} handleClickDelete={handleClickDelete} close={close} className="block sm:hidden" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -9,6 +9,7 @@ const TableHead = () => (
|
|||||||
<Th>Label</Th>
|
<Th>Label</Th>
|
||||||
<Th>Score</Th>
|
<Th>Score</Th>
|
||||||
<Th>Zones</Th>
|
<Th>Zones</Th>
|
||||||
|
<Th>Retain</Th>
|
||||||
<Th>Date</Th>
|
<Th>Date</Th>
|
||||||
<Th>Start</Th>
|
<Th>Start</Th>
|
||||||
<Th>End</Th>
|
<Th>End</Th>
|
||||||
|
|||||||
@ -22,6 +22,7 @@ const EventsRow = memo(
|
|||||||
label,
|
label,
|
||||||
top_score: score,
|
top_score: score,
|
||||||
zones,
|
zones,
|
||||||
|
retain_indefinitely
|
||||||
}) => {
|
}) => {
|
||||||
const [viewEvent, setViewEvent] = useState(null);
|
const [viewEvent, setViewEvent] = useState(null);
|
||||||
const { searchString, removeDefaultSearchKeys } = useSearchString(limit);
|
const { searchString, removeDefaultSearchKeys } = useSearchString(limit);
|
||||||
@ -100,6 +101,7 @@ const EventsRow = memo(
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</Td>
|
</Td>
|
||||||
|
<Td>{retain_indefinitely ? 'True' : 'False'}</Td>
|
||||||
<Td>{start.toLocaleDateString()}</Td>
|
<Td>{start.toLocaleDateString()}</Td>
|
||||||
<Td>{start.toLocaleTimeString()}</Td>
|
<Td>{start.toLocaleTimeString()}</Td>
|
||||||
<Td>{end === null ? 'In progress' : end.toLocaleTimeString()}</Td>
|
<Td>{end === null ? 'In progress' : end.toLocaleTimeString()}</Td>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user