forked from Github/frigate
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d376f6b1d2 | ||
|
|
45526a7652 | ||
|
|
cc7929932b | ||
|
|
e6516235fa | ||
|
|
40d5a9f890 | ||
|
|
ee3e744cc6 | ||
|
|
b55bd1e027 | ||
|
|
9a96df0319 | ||
|
|
e9b1618364 | ||
|
|
6dc6ed1e94 | ||
|
|
1943a49274 | ||
|
|
a8c00edc94 | ||
|
|
faa8abb2b9 | ||
|
|
f6cd2fc68e | ||
|
|
6482000d6b | ||
|
|
dcf7209706 |
2
Makefile
2
Makefile
@@ -3,7 +3,7 @@ default_target: amd64_frigate
|
||||
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
|
||||
|
||||
version:
|
||||
echo "VERSION='0.8.0-$(COMMIT_HASH)'" > frigate/version.py
|
||||
echo "VERSION='0.8.1-$(COMMIT_HASH)'" > frigate/version.py
|
||||
|
||||
web:
|
||||
docker build --tag frigate-web --file docker/Dockerfile.web web/
|
||||
|
||||
@@ -22,6 +22,9 @@ Use of a [Google Coral Accelerator](https://coral.ai/products/) is optional, but
|
||||
|
||||
View the documentation at https://blakeblackshear.github.io/frigate
|
||||
|
||||
## Donations
|
||||
If you would like to make a donation to support development, please use [Github Sponsors](https://github.com/sponsors/blakeblackshear).
|
||||
|
||||
## Screenshots
|
||||
Integration into HomeAssistant
|
||||
<div>
|
||||
|
||||
@@ -386,6 +386,29 @@ ffmpeg:
|
||||
- '1'
|
||||
```
|
||||
|
||||
### Reolink 410/520 (possibly others)
|
||||
Several users have reported success with the rtmp video from Reolink cameras.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
input_args:
|
||||
- -avoid_negative_ts
|
||||
- make_zero
|
||||
- -fflags
|
||||
- nobuffer
|
||||
- -flags
|
||||
- low_delay
|
||||
- -strict
|
||||
- experimental
|
||||
- -fflags
|
||||
- +genpts+discardcorrupt
|
||||
- -rw_timeout
|
||||
- '5000000'
|
||||
- -use_wallclock_as_timestamps
|
||||
- '1'
|
||||
```
|
||||
|
||||
|
||||
### Blue Iris RTSP Cameras
|
||||
|
||||
You will need to remove `nobuffer` flag for Blue Iris RTSP cameras
|
||||
|
||||
@@ -3,7 +3,7 @@ id: index
|
||||
title: Configuration
|
||||
---
|
||||
|
||||
HassOS users can manage their configuration directly in the addon Configuration tab. For other installations, the default location for the config file is `/config/config.yml`. This can be overridden with the `CONFIG_FILE` environment variable. Camera specific ffmpeg parameters are documented [here](/configuration/cameras.md).
|
||||
HassOS users can manage their configuration directly in the addon Configuration tab. For other installations, the default location for the config file is `/config/config.yml`. This can be overridden with the `CONFIG_FILE` environment variable. Camera specific ffmpeg parameters are documented [here](cameras.md).
|
||||
|
||||
It is recommended to start with a minimal configuration and add to it:
|
||||
|
||||
@@ -51,7 +51,7 @@ mqtt:
|
||||
|
||||
## `cameras`
|
||||
|
||||
Each of your cameras must be configured. The following is the minimum required to register a camera in Frigate. Check the [camera configuration page](cameras) for a complete list of options.
|
||||
Each of your cameras must be configured. The following is the minimum required to register a camera in Frigate. Check the [camera configuration page](cameras.md) for a complete list of options.
|
||||
|
||||
```yaml
|
||||
cameras:
|
||||
@@ -80,7 +80,8 @@ clips:
|
||||
max_seconds: 300
|
||||
# Optional: size of tmpfs mount to create for cache files (default: not set)
|
||||
# mount -t tmpfs -o size={tmpfs_cache_size} tmpfs /tmp/cache
|
||||
# Notice: If you have mounted a tmpfs volume through docker, this value should not be set in your config
|
||||
# NOTICE: Addon users must have Protection mode disabled for the addon when using this setting.
|
||||
# Also, if you have mounted a tmpfs volume through docker, this value should not be set in your config.
|
||||
tmpfs_cache_size: 256m
|
||||
# Optional: Retention settings for clips (default: shown below)
|
||||
retain:
|
||||
@@ -96,7 +97,7 @@ clips:
|
||||
```yaml
|
||||
ffmpeg:
|
||||
# Optional: global ffmpeg args (default: shown below)
|
||||
global_args: -hide_banner -loglevel fatal
|
||||
global_args: -hide_banner -loglevel warning
|
||||
# Optional: global hwaccel args (default: shown below)
|
||||
# NOTE: See hardware acceleration docs for your specific device
|
||||
hwaccel_args: []
|
||||
|
||||
@@ -17,6 +17,7 @@ HassOS users can install via the addon repository. Frigate requires an MQTT serv
|
||||
1. Add https://github.com/blakeblackshear/frigate-hass-addons
|
||||
1. Setup your configuration in the `Configuration` tab
|
||||
1. Start the addon container
|
||||
1. If you are using hardware acceleration for ffmpeg, you will need to disable "Protection mode"
|
||||
|
||||
## Docker
|
||||
|
||||
@@ -74,15 +75,27 @@ docker run --rm \
|
||||
blakeblackshear/frigate:0.8.0-beta2-amd64
|
||||
```
|
||||
|
||||
### Calculating shm-size
|
||||
|
||||
The default shm-size of 64m is fine for setups with 3 or less 1080p cameras. If frigate is exiting with "Bus error" messages, it could be because you have too many high resolution cameras and you need to specify a higher shm size.
|
||||
|
||||
You can calculate the necessary shm-size for each camera with the following formula:
|
||||
|
||||
```
|
||||
(width * height * 1.5 * 7 + 270480)/1048576 = <shm size in mb>
|
||||
```
|
||||
|
||||
The shm size cannot be set per container for HomeAssistant Addons. You must set `default-shm-size` in `/etc/docker/daemon.json` to increase the default shm size. This will increase the shm size for all of your docker containers. This may or may not cause issues with your setup. https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file
|
||||
|
||||
## Kubernetes
|
||||
|
||||
Use the [helm chart](https://github.com/k8s-at-home/charts/tree/master/charts/frigate).
|
||||
Use the [helm chart](https://github.com/blakeblackshear/blakeshome-charts/tree/master/charts/frigate).
|
||||
|
||||
## Virtualization
|
||||
|
||||
For ideal performance, Frigate needs access to underlying hardware for the Coral and GPU devices for ffmpeg decoding. Running Frigate in a VM on top of Proxmox, ESXi, Virtualbox, etc. is not recommended. The virtualization layer typically introduces a sizable amount of overhead for communication with Coral devices.
|
||||
|
||||
## Proxmox
|
||||
### Proxmox
|
||||
|
||||
Some people have had success running Frigate in LXC directly with the following config:
|
||||
|
||||
@@ -104,12 +117,6 @@ lxc.cgroup.devices.allow: a
|
||||
lxc.cap.drop:
|
||||
```
|
||||
|
||||
### Calculating shm-size
|
||||
### ESX
|
||||
For details on running Frigate under ESX, see details [here](https://github.com/blakeblackshear/frigate/issues/305).
|
||||
|
||||
The default shm-size of 64m is fine for setups with 3 or less 1080p cameras. If frigate is exiting with "Bus error" messages, it could be because you have too many high resolution cameras and you need to specify a higher shm size.
|
||||
|
||||
You can calculate the necessary shm-size for each camera with the following formula:
|
||||
|
||||
```
|
||||
(width * height * 1.5 * 7 + 270480)/1048576 = <shm size in mb>
|
||||
```
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
---
|
||||
id: troubleshooting
|
||||
title: Troubleshooting
|
||||
title: Troubleshooting and FAQ
|
||||
---
|
||||
|
||||
### How can I get sound or audio in my clips and recordings?
|
||||
By default, Frigate removes audio from clips and recordings to reduce the likelihood of failing for invalid data. If you would like to include audio, you need to override the output args to remove `-an` for where you want to include audio. The default ffmpeg args are shown [here](configuration/index#ffmpeg).
|
||||
|
||||
### My mjpeg stream or snapshots look green and crazy
|
||||
This almost always means that the width/height defined for your camera are not correct. Double check the resolution with vlc or another player. Also make sure you don't have the width and height values backwards.
|
||||
|
||||
@@ -12,15 +15,6 @@ This almost always means that the width/height defined for your camera are not c
|
||||
|
||||
These messages in the logs are expected in certain situations. Frigate checks the integrity of the video cache before assembling clips. Occasionally these cached files will be invalid and cleaned up automatically.
|
||||
|
||||
## "ffmpeg didnt return a frame. something is wrong"
|
||||
|
||||
Turn on logging for the ffmpeg process by overriding the global_args and setting the log level to `info` (the default is `fatal`). Note that all ffmpeg logs show up in the Frigate logs as `ERROR` level. This does not mean they are actually errors.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
global_args: -hide_banner -loglevel info
|
||||
```
|
||||
|
||||
## "On connect called"
|
||||
|
||||
If you see repeated "On connect called" messages in your config, check for another instance of frigate. This happens when multiple frigate containers are trying to connect to mqtt with the same client_id.
|
||||
|
||||
@@ -135,16 +135,17 @@ Version info
|
||||
|
||||
Events from the database. Accepts the following query string parameters:
|
||||
|
||||
| param | Type | Description |
|
||||
| -------------- | ---- | --------------------------------------------- |
|
||||
| `before` | int | Epoch time |
|
||||
| `after` | int | Epoch time |
|
||||
| `camera` | str | Camera name |
|
||||
| `label` | str | Label name |
|
||||
| `zone` | str | Zone name |
|
||||
| `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) |
|
||||
| param | Type | Description |
|
||||
| -------------------- | ---- | --------------------------------------------- |
|
||||
| `before` | int | Epoch time |
|
||||
| `after` | int | Epoch time |
|
||||
| `camera` | str | Camera name |
|
||||
| `label` | str | Label name |
|
||||
| `zone` | str | Zone name |
|
||||
| `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) |
|
||||
| `include_thumbnails` | int | Include thumbnails in the response (0 or 1) |
|
||||
|
||||
### `/api/events/summary`
|
||||
|
||||
@@ -159,16 +160,17 @@ Returns data for a single event.
|
||||
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.
|
||||
|
||||
### `/api/events/<id>/snapshot.jpg`
|
||||
|
||||
Returns the snapshot image for the event id. Works while the event is in progress and after completion.
|
||||
|
||||
Accepts the following query string parameters, but they are only applied when an event is in progress. After the event is completed, the saved snapshot is returned from disk without modification:
|
||||
|
||||
|param|Type|Description|
|
||||
|----|-----|--|
|
||||
|`h`|int|Height in pixels|
|
||||
|`bbox`|int|Show bounding boxes for detected objects (0 or 1)|
|
||||
|`timestamp`|int|Print the timestamp in the upper left (0 or 1)|
|
||||
|`crop`|int|Crop the snapshot to the (0 or 1)|
|
||||
| param | Type | Description |
|
||||
| ----------- | ---- | ------------------------------------------------- |
|
||||
| `h` | int | Height in pixels |
|
||||
| `bbox` | int | Show bounding boxes for detected objects (0 or 1) |
|
||||
| `timestamp` | int | Print the timestamp in the upper left (0 or 1) |
|
||||
| `crop` | int | Crop the snapshot to the (0 or 1) |
|
||||
|
||||
### `/clips/<camera>-<id>.mp4`
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ CLIPS_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
FFMPEG_GLOBAL_ARGS_DEFAULT = ['-hide_banner','-loglevel','fatal']
|
||||
FFMPEG_GLOBAL_ARGS_DEFAULT = ['-hide_banner','-loglevel','warning']
|
||||
FFMPEG_INPUT_ARGS_DEFAULT = ['-avoid_negative_ts', 'make_zero',
|
||||
'-fflags', '+genpts+discardcorrupt',
|
||||
'-rtsp_transport', 'tcp',
|
||||
|
||||
@@ -55,7 +55,7 @@ def events_summary():
|
||||
|
||||
if not has_clip is None:
|
||||
clauses.append((Event.has_clip == has_clip))
|
||||
|
||||
|
||||
if not has_snapshot is None:
|
||||
clauses.append((Event.has_snapshot == has_snapshot))
|
||||
|
||||
@@ -160,12 +160,14 @@ def events():
|
||||
camera = request.args.get('camera')
|
||||
label = request.args.get('label')
|
||||
zone = request.args.get('zone')
|
||||
after = request.args.get('after', type=int)
|
||||
before = request.args.get('before', type=int)
|
||||
after = request.args.get('after', type=float)
|
||||
before = request.args.get('before', type=float)
|
||||
has_clip = request.args.get('has_clip', type=int)
|
||||
has_snapshot = request.args.get('has_snapshot', type=int)
|
||||
include_thumbnails = request.args.get('include_thumbnails', default=1, type=int)
|
||||
|
||||
clauses = []
|
||||
excluded_fields = []
|
||||
|
||||
if camera:
|
||||
clauses.append((Event.camera == camera))
|
||||
@@ -184,10 +186,13 @@ def events():
|
||||
|
||||
if not has_clip is None:
|
||||
clauses.append((Event.has_clip == has_clip))
|
||||
|
||||
|
||||
if not has_snapshot is None:
|
||||
clauses.append((Event.has_snapshot == has_snapshot))
|
||||
|
||||
if not include_thumbnails:
|
||||
excluded_fields.append(Event.thumbnail)
|
||||
|
||||
if len(clauses) == 0:
|
||||
clauses.append((1 == 1))
|
||||
|
||||
@@ -196,7 +201,7 @@ def events():
|
||||
.order_by(Event.start_time.desc())
|
||||
.limit(limit))
|
||||
|
||||
return jsonify([model_to_dict(e) for e in events])
|
||||
return jsonify([model_to_dict(e, exclude=excluded_fields) for e in events])
|
||||
|
||||
@bp.route('/config')
|
||||
def config():
|
||||
|
||||
@@ -7,6 +7,7 @@ import queue
|
||||
import multiprocessing as mp
|
||||
from logging import handlers
|
||||
from setproctitle import setproctitle
|
||||
from collections import deque
|
||||
|
||||
|
||||
def listener_configurer():
|
||||
@@ -54,6 +55,7 @@ class LogPipe(threading.Thread):
|
||||
self.daemon = False
|
||||
self.logger = logging.getLogger(log_name)
|
||||
self.level = level
|
||||
self.deque = deque(maxlen=100)
|
||||
self.fdRead, self.fdWrite = os.pipe()
|
||||
self.pipeReader = os.fdopen(self.fdRead)
|
||||
self.start()
|
||||
@@ -67,9 +69,13 @@ class LogPipe(threading.Thread):
|
||||
"""Run the thread, logging everything.
|
||||
"""
|
||||
for line in iter(self.pipeReader.readline, ''):
|
||||
self.logger.log(self.level, line.strip('\n'))
|
||||
self.deque.append(line.strip('\n'))
|
||||
|
||||
self.pipeReader.close()
|
||||
|
||||
def dump(self):
|
||||
while len(self.deque) > 0:
|
||||
self.logger.log(self.level, self.deque.popleft())
|
||||
|
||||
def close(self):
|
||||
"""Close the write end of the pipe.
|
||||
|
||||
@@ -181,6 +181,7 @@ class CameraWatchdog(threading.Thread):
|
||||
now = datetime.datetime.now().timestamp()
|
||||
|
||||
if not self.capture_thread.is_alive():
|
||||
self.logpipe.dump()
|
||||
self.start_ffmpeg_detect()
|
||||
elif now - self.capture_thread.current_frame.value > 20:
|
||||
self.logger.info(f"No frames received from {self.camera_name} in 20 seconds. Exiting ffmpeg...")
|
||||
@@ -197,6 +198,7 @@ class CameraWatchdog(threading.Thread):
|
||||
poll = p['process'].poll()
|
||||
if poll == None:
|
||||
continue
|
||||
p['logpipe'].dump()
|
||||
p['process'] = start_or_restart_ffmpeg(p['cmd'], self.logger, p['logpipe'], ffmpeg_process=p['process'])
|
||||
|
||||
# wait a bit before checking again
|
||||
|
||||
@@ -23,6 +23,12 @@ http {
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
gzip on;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css application/json application/x-javascript application/javascript text/javascript image/svg+xml image/x-icon image/bmp image/png image/gif image/jpeg image/jpg;
|
||||
gzip_proxied no-cache no-store private expired auth;
|
||||
gzip_vary on;
|
||||
|
||||
upstream frigate_api {
|
||||
server localhost:5001;
|
||||
keepalive 1024;
|
||||
@@ -98,6 +104,7 @@ http {
|
||||
|
||||
location /api/ {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header Cache-Control "no-store";
|
||||
proxy_pass http://frigate_api/;
|
||||
proxy_pass_request_headers on;
|
||||
proxy_set_header Host $host;
|
||||
@@ -105,13 +112,23 @@ http {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
|
||||
location / {
|
||||
add_header Cache-Control "no-cache";
|
||||
|
||||
location ~* \.(?:js|css|svg|ico|png)$ {
|
||||
access_log off;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
|
||||
sub_filter 'href="/' 'href="$http_x_ingress_path/';
|
||||
sub_filter 'url(/' 'url($http_x_ingress_path/';
|
||||
sub_filter '"/js/' '"$http_x_ingress_path/js/';
|
||||
sub_filter '<body>' '<body><script>window.baseUrl="$http_x_ingress_path";</script>';
|
||||
sub_filter_types text/css application/javascript;
|
||||
sub_filter_once off;
|
||||
|
||||
root /opt/frigate/web;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
289
web/package-lock.json
generated
289
web/package-lock.json
generated
@@ -93,13 +93,6 @@
|
||||
"@babel/helper-validator-option": "^7.12.1",
|
||||
"browserslist": "^4.14.5",
|
||||
"semver": "^5.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/helper-create-class-features-plugin": {
|
||||
@@ -871,13 +864,6 @@
|
||||
"@babel/types": "^7.12.11",
|
||||
"core-js-compat": "^3.8.0",
|
||||
"semver": "^5.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/preset-modules": {
|
||||
@@ -968,22 +954,12 @@
|
||||
}
|
||||
},
|
||||
"@npmcli/move-file": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.0.tgz",
|
||||
"integrity": "sha512-Iv2iq0JuyYjKeFkSR4LPaCdDZwlGK9X2cP/01nJcp3yMJ1FjNd9vpiEYvLUgzBxKPg2SFmaOhizoQsPc0LWeOQ==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.1.tgz",
|
||||
"integrity": "sha512-LtWTicuF2wp7PNTuyCwABx7nNG+DnzSE8gN0iWxkC6mpgm/iOPu0ZMTkXuCxmJxtWFsDxUaixM9COSNJEMUfuQ==",
|
||||
"requires": {
|
||||
"mkdirp": "^1.0.4",
|
||||
"rimraf": "^2.7.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"rimraf": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
||||
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
}
|
||||
"rimraf": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"@prefresh/babel-plugin": {
|
||||
@@ -1013,24 +989,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@prefresh/utils/-/utils-1.1.1.tgz",
|
||||
"integrity": "sha512-MUhT5m2XNN5NsZl4GnpuvlzLo6VSTa/+wBfBd3fiWUvHGhv0GF9hnA1pd//v0uJaKwUnVRQ1hYElxCV7DtYsCQ=="
|
||||
},
|
||||
"@snowpack/plugin-optimize": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/@snowpack/plugin-optimize/-/plugin-optimize-0.2.13.tgz",
|
||||
"integrity": "sha512-DW0+iaSc80vWZKDIKbdR5pqDkNDsv3ZbH8eVB8yppK/JlBmOqmsUGdKlZ2eq8XDxE23/V5+gmA9RXLWcTMJ1eQ==",
|
||||
"requires": {
|
||||
"csso": "^4.1.0",
|
||||
"es-module-lexer": "^0.3.25",
|
||||
"esbuild": "^0.8.0",
|
||||
"glob": "^7.1.6",
|
||||
"html-minifier": "^4.0.0",
|
||||
"hypertag": "^0.0.3",
|
||||
"kleur": "^4.1.3",
|
||||
"mkdirp": "^1.0.4",
|
||||
"node-inject-html": "^0.0.5",
|
||||
"object.fromentries": "^2.0.2",
|
||||
"p-queue": "^6.6.1"
|
||||
}
|
||||
},
|
||||
"@snowpack/plugin-postcss": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@snowpack/plugin-postcss/-/plugin-postcss-1.1.0.tgz",
|
||||
@@ -1061,14 +1019,14 @@
|
||||
}
|
||||
},
|
||||
"@types/json-schema": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
|
||||
"integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw=="
|
||||
"version": "7.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
|
||||
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "14.14.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.21.tgz",
|
||||
"integrity": "sha512-cHYfKsnwllYhjOzuC5q1VpguABBeecUp24yFluHpn/BQaVxB1CuQ1FSRZCzrPxrkIfWISXV2LbeoBthLWg0+0A=="
|
||||
"version": "14.14.22",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz",
|
||||
"integrity": "sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw=="
|
||||
},
|
||||
"@types/parse-json": {
|
||||
"version": "4.0.0",
|
||||
@@ -2104,16 +2062,16 @@
|
||||
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
|
||||
},
|
||||
"core-js": {
|
||||
"version": "3.8.2",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.2.tgz",
|
||||
"integrity": "sha512-FfApuSRgrR6G5s58casCBd9M2k+4ikuu4wbW6pJyYU7bd9zvFc9qf7vr5xmrZOhT9nn+8uwlH1oRR9jTnFoA3A=="
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.3.tgz",
|
||||
"integrity": "sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q=="
|
||||
},
|
||||
"core-js-compat": {
|
||||
"version": "3.8.2",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.2.tgz",
|
||||
"integrity": "sha512-LO8uL9lOIyRRrQmZxHZFl1RV+ZbcsAkFWTktn5SmH40WgLtSNYN4m4W2v9ONT147PxBY/XrRhrWq8TlvObyUjQ==",
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.3.tgz",
|
||||
"integrity": "sha512-1sCb0wBXnBIL16pfFG1Gkvei6UzvKyTNYpiC41yrdjEv0UoJoq9E/abTMzyYJ6JpTkAj15dLjbqifIzEBDVvog==",
|
||||
"requires": {
|
||||
"browserslist": "^4.16.0",
|
||||
"browserslist": "^4.16.1",
|
||||
"semver": "7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -2824,9 +2782,9 @@
|
||||
}
|
||||
},
|
||||
"entities": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
|
||||
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w=="
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
|
||||
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="
|
||||
},
|
||||
"errno": {
|
||||
"version": "0.1.8",
|
||||
@@ -2863,11 +2821,6 @@
|
||||
"string.prototype.trimstart": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"es-module-lexer": {
|
||||
"version": "0.3.26",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.3.26.tgz",
|
||||
"integrity": "sha512-Va0Q/xqtrss45hWzP8CZJwzGSZJjDM5/MJRE3IXXnUCcVLElR9BRaE9F62BopysASyc4nM3uwhSW7FFB9nlWAA=="
|
||||
},
|
||||
"es-to-primitive": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
|
||||
@@ -2944,11 +2897,6 @@
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
|
||||
},
|
||||
"eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
|
||||
},
|
||||
"events": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz",
|
||||
@@ -3575,11 +3523,6 @@
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="
|
||||
},
|
||||
"hypertag": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/hypertag/-/hypertag-0.0.3.tgz",
|
||||
"integrity": "sha512-Z/cAsLCihKj+QJGQt5X19L/YY8PW5l8/A9Ju7s4g1ty4kwDdSzGp2QdRoKb266kFglJw6+CSsR/zNpkrOEo54A=="
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
@@ -3631,6 +3574,11 @@
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
|
||||
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw=="
|
||||
},
|
||||
"immer": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz",
|
||||
"integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA=="
|
||||
},
|
||||
"import-cwd": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz",
|
||||
@@ -3928,12 +3876,9 @@
|
||||
"integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="
|
||||
},
|
||||
"is-wsl": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||
"requires": {
|
||||
"is-docker": "^2.0.0"
|
||||
}
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
|
||||
"integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0="
|
||||
},
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
@@ -4102,11 +4047,6 @@
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
|
||||
},
|
||||
"kleur": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.3.tgz",
|
||||
"integrity": "sha512-H1tr8QP2PxFTNwAFM74Mui2b6ovcY9FoxJefgrwxY+OCJcq01k5nvhf4M/KnizzrJvLRap5STUy7dgDV35iUBw=="
|
||||
},
|
||||
"last-call-webpack-plugin": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz",
|
||||
@@ -4227,6 +4167,13 @@
|
||||
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||
"requires": {
|
||||
"semver": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"map-cache": {
|
||||
@@ -4320,23 +4267,15 @@
|
||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
|
||||
},
|
||||
"mini-css-extract-plugin": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.4.tgz",
|
||||
"integrity": "sha512-dNjqyeogUd8ucUgw5sxm1ahvSfSUgef7smbmATRSbDm4EmNx5kQA6VdUEhEeCKSjX6CTYjb5vxgMUvRjqP3uHg==",
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.5.tgz",
|
||||
"integrity": "sha512-tvmzcwqJJXau4OQE5vT72pRT18o2zF+tQJp8CWchqvfQnTlflkzS+dANYcRdyPRWUWRkfmeNTKltx0NZI/b5dQ==",
|
||||
"requires": {
|
||||
"loader-utils": "^2.0.0",
|
||||
"schema-utils": "^3.0.0",
|
||||
"webpack-sources": "^1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"json5": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
|
||||
"integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
@@ -4469,11 +4408,6 @@
|
||||
"resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.0.0.tgz",
|
||||
"integrity": "sha512-1lM+BMLGuDfsdwf3rsgBSrxJwAZHFIrQ8YR61xIqdHo0uNKI9M52wNpHSrliZATJp51On6JD0AfRxd4YGSU0lw=="
|
||||
},
|
||||
"moo": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz",
|
||||
"integrity": "sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w=="
|
||||
},
|
||||
"move-concurrently": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||
@@ -4560,14 +4494,6 @@
|
||||
"lodash.toarray": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"node-inject-html": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/node-inject-html/-/node-inject-html-0.0.5.tgz",
|
||||
"integrity": "sha512-YUjHKq6sjD5rru/lqxQ0BHLvgJP33T54/ehsxgi1sShUkA5cU+Kb/eZ8UnbjaZeKxueA5D75vwAmyc+B5Kof1Q==",
|
||||
"requires": {
|
||||
"moo": "^0.5.1"
|
||||
}
|
||||
},
|
||||
"node-libs-browser": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
|
||||
@@ -4729,17 +4655,6 @@
|
||||
"has": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"object.fromentries": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.3.tgz",
|
||||
"integrity": "sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"define-properties": "^1.1.3",
|
||||
"es-abstract": "^1.18.0-next.1",
|
||||
"has": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"object.getownpropertydescriptors": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz",
|
||||
@@ -4792,6 +4707,16 @@
|
||||
"requires": {
|
||||
"is-docker": "^2.0.0",
|
||||
"is-wsl": "^2.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-wsl": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||
"requires": {
|
||||
"is-docker": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"optimize-css-assets-webpack-plugin": {
|
||||
@@ -4821,11 +4746,6 @@
|
||||
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
|
||||
"integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc="
|
||||
},
|
||||
"p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
@@ -4850,23 +4770,6 @@
|
||||
"aggregate-error": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"p-queue": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
|
||||
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
|
||||
"requires": {
|
||||
"eventemitter3": "^4.0.4",
|
||||
"p-timeout": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"p-timeout": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
|
||||
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
|
||||
"requires": {
|
||||
"p-finally": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
@@ -6671,9 +6574,9 @@
|
||||
}
|
||||
},
|
||||
"rollup": {
|
||||
"version": "2.36.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.36.2.tgz",
|
||||
"integrity": "sha512-qjjiuJKb+/8n0EZyQYVW+gFU4bNRBcZaXVzUgSVrGw0HlQBlK2aWyaOMMs1Ufic1jV69b9kW3u3i9B+hISDm3A==",
|
||||
"version": "2.38.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.38.1.tgz",
|
||||
"integrity": "sha512-q07T6vU/V1kqM8rGRRyCgEvIQcIAXoKIE5CpkYAlHhfiWM1Iuh4dIPWpIbqFngCK6lwAB2aYHiUVhIbSWHQWhw==",
|
||||
"requires": {
|
||||
"fsevents": "~2.1.2"
|
||||
},
|
||||
@@ -6741,14 +6644,14 @@
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
},
|
||||
"serialize-javascript": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
|
||||
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz",
|
||||
"integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==",
|
||||
"requires": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
@@ -7013,9 +6916,9 @@
|
||||
}
|
||||
},
|
||||
"ssri": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz",
|
||||
"integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==",
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
|
||||
"integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==",
|
||||
"requires": {
|
||||
"minipass": "^3.1.1"
|
||||
}
|
||||
@@ -7330,11 +7233,6 @@
|
||||
"source-map-support": "~0.5.19"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
|
||||
@@ -7375,14 +7273,6 @@
|
||||
"ajv": "^6.12.5",
|
||||
"ajv-keywords": "^3.5.2"
|
||||
}
|
||||
},
|
||||
"serialize-javascript": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz",
|
||||
"integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==",
|
||||
"requires": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -7924,26 +7814,6 @@
|
||||
"binary-extensions": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"is-number": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
|
||||
"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"kind-of": "^3.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"kind-of": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
|
||||
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-buffer": "^1.1.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"micromatch": {
|
||||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
|
||||
@@ -8082,11 +7952,6 @@
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
|
||||
},
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
|
||||
@@ -8126,29 +7991,6 @@
|
||||
"locate-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"is-number": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
|
||||
"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
|
||||
"requires": {
|
||||
"kind-of": "^3.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"kind-of": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
|
||||
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
|
||||
"requires": {
|
||||
"is-buffer": "^1.1.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"is-wsl": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
|
||||
"integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0="
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
|
||||
@@ -8247,10 +8089,13 @@
|
||||
"ajv-keywords": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
"serialize-javascript": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
|
||||
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
|
||||
"requires": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"ssri": {
|
||||
"version": "6.0.1",
|
||||
|
||||
@@ -8,17 +8,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@prefresh/snowpack": "^3.0.1",
|
||||
"@snowpack/plugin-optimize": "^0.2.13",
|
||||
"@snowpack/plugin-postcss": "^1.1.0",
|
||||
"@snowpack/plugin-webpack": "^2.3.0",
|
||||
"autoprefixer": "^10.2.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"immer": "^8.0.1",
|
||||
"postcss": "^8.2.2",
|
||||
"postcss-cli": "^8.3.1",
|
||||
"preact": "^10.5.9",
|
||||
"preact-router": "^3.2.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"snowpack": "^3.0.0",
|
||||
"snowpack": "^3.0.11",
|
||||
"tailwindcss": "^2.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,6 @@ module.exports = {
|
||||
plugins: [
|
||||
'@snowpack/plugin-postcss',
|
||||
'@prefresh/snowpack',
|
||||
[
|
||||
'@snowpack/plugin-optimize',
|
||||
{
|
||||
preloadModules: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'@snowpack/plugin-webpack',
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { h } from 'preact';
|
||||
import ActivityIndicator from './components/ActivityIndicator';
|
||||
import Camera from './Camera';
|
||||
import CameraMap from './CameraMap';
|
||||
import Cameras from './Cameras';
|
||||
@@ -7,36 +8,27 @@ import Event from './Event';
|
||||
import Events from './Events';
|
||||
import { Router } from 'preact-router';
|
||||
import Sidebar from './Sidebar';
|
||||
import { ApiHost, Config } from './context';
|
||||
import { useContext, useEffect, useState } from 'preact/hooks';
|
||||
import Api, { FetchStatus, useConfig } from './api';
|
||||
|
||||
export default function App() {
|
||||
const apiHost = useContext(ApiHost);
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
useEffect(async () => {
|
||||
const response = await fetch(`${apiHost}/api/config`);
|
||||
const data = response.ok ? await response.json() : {};
|
||||
setConfig(data);
|
||||
}, []);
|
||||
|
||||
return !config ? (
|
||||
<div />
|
||||
const { data, status } = useConfig();
|
||||
return status !== FetchStatus.LOADED ? (
|
||||
<div className="flex flex-grow-1 min-h-screen justify-center items-center">
|
||||
<ActivityIndicator />
|
||||
</div>
|
||||
) : (
|
||||
<Config.Provider value={config}>
|
||||
<div className="md:flex flex-col md:flex-row md:min-h-screen w-full bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white">
|
||||
<Sidebar />
|
||||
<div className="flex-auto p-4 lg:pl-8 lg:pr-8 min-w-0">
|
||||
<Router>
|
||||
<CameraMap path="/cameras/:camera/editor" />
|
||||
<Camera path="/cameras/:camera" />
|
||||
<Event path="/events/:eventId" />
|
||||
<Events path="/events" />
|
||||
<Debug path="/debug" />
|
||||
<Cameras default path="/" />
|
||||
</Router>
|
||||
</div>
|
||||
<div className="md:flex flex-col md:flex-row md:min-h-screen w-full bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white">
|
||||
<Sidebar />
|
||||
<div className="flex-auto p-2 md:p-4 lg:pl-8 lg:pr-8 min-w-0">
|
||||
<Router>
|
||||
<CameraMap path="/cameras/:camera/editor" />
|
||||
<Camera path="/cameras/:camera" />
|
||||
<Event path="/events/:eventId" />
|
||||
<Events path="/events" />
|
||||
<Debug path="/debug" />
|
||||
<Cameras default path="/" />
|
||||
</Router>
|
||||
</div>
|
||||
</Config.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ import Link from './components/Link';
|
||||
import Switch from './components/Switch';
|
||||
import { route } from 'preact-router';
|
||||
import { useCallback, useContext } from 'preact/hooks';
|
||||
import { ApiHost, Config } from './context';
|
||||
import { useApiHost, useConfig } from './api';
|
||||
|
||||
export default function Camera({ camera, url }) {
|
||||
const config = useContext(Config);
|
||||
const apiHost = useContext(ApiHost);
|
||||
const { data: config } = useConfig();
|
||||
const apiHost = useApiHost();
|
||||
|
||||
if (!(camera in config.cameras)) {
|
||||
if (!config) {
|
||||
return <div>{`No camera named ${camera}`}</div>;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ import Heading from './components/Heading';
|
||||
import Switch from './components/Switch';
|
||||
import { route } from 'preact-router';
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||
import { ApiHost, Config } from './context';
|
||||
import { useApiHost, useConfig } from './api';
|
||||
|
||||
export default function CameraMasks({ camera, url }) {
|
||||
const config = useContext(Config);
|
||||
const apiHost = useContext(ApiHost);
|
||||
const { data: config } = useConfig();
|
||||
const apiHost = useApiHost();
|
||||
const imageRef = useRef(null);
|
||||
const [imageScale, setImageScale] = useState(1);
|
||||
const [snap, setSnap] = useState(true);
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { h } from 'preact';
|
||||
import ActivityIndicator from './components/ActivityIndicator';
|
||||
import Box from './components/Box';
|
||||
import CameraImage from './components/CameraImage';
|
||||
import Events from './Events';
|
||||
import Heading from './components/Heading';
|
||||
import { route } from 'preact-router';
|
||||
import { useContext } from 'preact/hooks';
|
||||
import { ApiHost, Config } from './context';
|
||||
import { useConfig } from './api';
|
||||
|
||||
export default function Cameras() {
|
||||
const config = useContext(Config);
|
||||
const { data: config, status } = useConfig();
|
||||
|
||||
if (!config.cameras) {
|
||||
if (!config) {
|
||||
return <p>loading…</p>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
import { h } from 'preact';
|
||||
import ActivityIndicator from './components/ActivityIndicator';
|
||||
import Box from './components/Box';
|
||||
import Button from './components/Button';
|
||||
import Heading from './components/Heading';
|
||||
import Link from './components/Link';
|
||||
import { ApiHost, Config } from './context';
|
||||
import { FetchStatus, useConfig, useStats } from './api';
|
||||
import { Table, Tbody, Thead, Tr, Th, Td } from './components/Table';
|
||||
import { useCallback, useContext, useEffect, useState } from 'preact/hooks';
|
||||
import { useCallback, useEffect, useState } from 'preact/hooks';
|
||||
|
||||
export default function Debug() {
|
||||
const apiHost = useContext(ApiHost);
|
||||
const config = useContext(Config);
|
||||
const [stats, setStats] = useState({});
|
||||
const config = useConfig();
|
||||
|
||||
const [timeoutId, setTimeoutId] = useState(null);
|
||||
|
||||
const fetchStats = useCallback(async () => {
|
||||
const statsResponse = await fetch(`${apiHost}/api/stats`);
|
||||
const stats = statsResponse.ok ? await statsResponse.json() : {};
|
||||
setStats(stats);
|
||||
setTimeoutId(setTimeout(fetchStats, 1000));
|
||||
}, [setStats]);
|
||||
const forceUpdate = useCallback(async () => {
|
||||
setTimeoutId(setTimeout(forceUpdate, 1000));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats();
|
||||
forceUpdate();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -29,11 +26,13 @@ export default function Debug() {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [timeoutId]);
|
||||
const { data: stats, status } = useStats(null, timeoutId);
|
||||
|
||||
if (stats === null && (status === FetchStatus.LOADING || status === FetchStatus.NONE)) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
|
||||
const { detectors, detection_fps, service, ...cameras } = stats;
|
||||
if (!service) {
|
||||
return 'loading…';
|
||||
}
|
||||
|
||||
const detectorNames = Object.keys(detectors);
|
||||
const detectorDataKeys = Object.keys(detectors[detectorNames[0]]);
|
||||
|
||||
@@ -1,28 +1,17 @@
|
||||
import { h, Fragment } from 'preact';
|
||||
import { ApiHost } from './context';
|
||||
import ActivityIndicator from './components/ActivityIndicator';
|
||||
import Box from './components/Box';
|
||||
import Heading from './components/Heading';
|
||||
import Link from './components/Link';
|
||||
import { FetchStatus, useApiHost, useEvent } from './api';
|
||||
import { Table, Thead, Tbody, Tfoot, Th, Tr, Td } from './components/Table';
|
||||
import { useContext, useEffect, useState } from 'preact/hooks';
|
||||
|
||||
export default function Event({ eventId }) {
|
||||
const apiHost = useContext(ApiHost);
|
||||
const [data, setData] = useState(null);
|
||||
const apiHost = useApiHost();
|
||||
const { data, status } = useEvent(eventId);
|
||||
|
||||
useEffect(async () => {
|
||||
const response = await fetch(`${apiHost}/api/events/${eventId}`);
|
||||
const data = response.ok ? await response.json() : null;
|
||||
setData(data);
|
||||
}, [apiHost, eventId]);
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div>
|
||||
<Heading>{eventId}</Heading>
|
||||
<p>loading…</p>
|
||||
</div>
|
||||
);
|
||||
if (status !== FetchStatus.LOADED) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
|
||||
const startime = new Date(data.start_time * 1000);
|
||||
@@ -57,34 +46,36 @@ export default function Event({ eventId }) {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Table>
|
||||
<Thead>
|
||||
<Th>Key</Th>
|
||||
<Th>Value</Th>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
<Tr>
|
||||
<Td>Camera</Td>
|
||||
<Td>
|
||||
<Link href={`/cameras/${data.camera}`}>{data.camera}</Link>
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr index={1}>
|
||||
<Td>Timeframe</Td>
|
||||
<Td>
|
||||
{startime.toLocaleString()} – {endtime.toLocaleString()}
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<Td>Score</Td>
|
||||
<Td>{(data.top_score * 100).toFixed(2)}%</Td>
|
||||
</Tr>
|
||||
<Tr index={1}>
|
||||
<Td>Zones</Td>
|
||||
<Td>{data.zones.join(', ')}</Td>
|
||||
</Tr>
|
||||
</Tbody>
|
||||
</Table>
|
||||
<Box>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Th>Key</Th>
|
||||
<Th>Value</Th>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
<Tr>
|
||||
<Td>Camera</Td>
|
||||
<Td>
|
||||
<Link href={`/cameras/${data.camera}`}>{data.camera}</Link>
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr index={1}>
|
||||
<Td>Timeframe</Td>
|
||||
<Td>
|
||||
{startime.toLocaleString()} – {endtime.toLocaleString()}
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<Td>Score</Td>
|
||||
<Td>{(data.top_score * 100).toFixed(2)}%</Td>
|
||||
</Tr>
|
||||
<Tr index={1}>
|
||||
<Td>Zones</Td>
|
||||
<Td>{data.zones.join(', ')}</Td>
|
||||
</Tr>
|
||||
</Tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,49 +1,123 @@
|
||||
import { h } from 'preact';
|
||||
import { ApiHost } from './context';
|
||||
import ActivityIndicator from './components/ActivityIndicator';
|
||||
import Box from './components/Box';
|
||||
import Heading from './components/Heading';
|
||||
import Link from './components/Link';
|
||||
import produce from 'immer';
|
||||
import { route } from 'preact-router';
|
||||
import { FetchStatus, useApiHost, useConfig, useEvents } from './api';
|
||||
import { Table, Thead, Tbody, Tfoot, Th, Tr, Td } from './components/Table';
|
||||
import { useCallback, useContext, useEffect, useState } from 'preact/hooks';
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useReducer, useState } from 'preact/hooks';
|
||||
|
||||
export default function Events({ url } = {}) {
|
||||
const apiHost = useContext(ApiHost);
|
||||
const [events, setEvents] = useState([]);
|
||||
const API_LIMIT = 25;
|
||||
|
||||
const { pathname, searchParams } = new URL(`${window.location.protocol}//${window.location.host}${url || '/events'}`);
|
||||
const searchParamsString = searchParams.toString();
|
||||
const initialState = Object.freeze({ events: [], reachedEnd: false, searchStrings: {} });
|
||||
const reducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case 'APPEND_EVENTS': {
|
||||
const {
|
||||
meta: { searchString },
|
||||
payload,
|
||||
} = action;
|
||||
return produce(state, (draftState) => {
|
||||
draftState.searchStrings[searchString] = true;
|
||||
draftState.events.push(...payload);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(async () => {
|
||||
const response = await fetch(`${apiHost}/api/events?${searchParamsString}`);
|
||||
const data = response.ok ? await response.json() : {};
|
||||
setEvents(data);
|
||||
}, [searchParamsString]);
|
||||
case 'REACHED_END': {
|
||||
const {
|
||||
meta: { searchString },
|
||||
} = action;
|
||||
return produce(state, (draftState) => {
|
||||
draftState.reachedEnd = true;
|
||||
draftState.searchStrings[searchString] = true;
|
||||
});
|
||||
}
|
||||
|
||||
const searchKeys = Array.from(searchParams.keys());
|
||||
case 'RESET':
|
||||
return initialState;
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const defaultSearchString = `include_thumbnails=0&limit=${API_LIMIT}`;
|
||||
function removeDefaultSearchKeys(searchParams) {
|
||||
searchParams.delete('limit');
|
||||
searchParams.delete('include_thumbnails');
|
||||
searchParams.delete('before');
|
||||
}
|
||||
|
||||
export default function Events({ path: pathname } = {}) {
|
||||
const apiHost = useApiHost();
|
||||
const [{ events, reachedEnd, searchStrings }, dispatch] = useReducer(reducer, initialState);
|
||||
const { searchParams: initialSearchParams } = new URL(window.location);
|
||||
const [searchString, setSearchString] = useState(`${defaultSearchString}&${initialSearchParams.toString()}`);
|
||||
const { data, status } = useEvents(searchString);
|
||||
|
||||
useEffect(() => {
|
||||
if (data && !(searchString in searchStrings)) {
|
||||
dispatch({ type: 'APPEND_EVENTS', payload: data, meta: { searchString } });
|
||||
}
|
||||
if (Array.isArray(data) && data.length < API_LIMIT) {
|
||||
dispatch({ type: 'REACHED_END', meta: { searchString } });
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const observer = useRef(
|
||||
new IntersectionObserver((entries, observer) => {
|
||||
window.requestAnimationFrame(() => {
|
||||
if (entries.length === 0) {
|
||||
return;
|
||||
}
|
||||
// under certain edge cases, a ref may be applied / in memory twice
|
||||
// avoid fetching twice by grabbing the last observed entry only
|
||||
const entry = entries[entries.length - 1];
|
||||
if (entry.isIntersecting) {
|
||||
const { startTime } = entry.target.dataset;
|
||||
const { searchParams } = new URL(window.location);
|
||||
searchParams.set('before', parseFloat(startTime) - 0.0001);
|
||||
|
||||
setSearchString(`${defaultSearchString}&${searchParams.toString()}`);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const lastCellRef = useCallback(
|
||||
(node) => {
|
||||
if (node !== null) {
|
||||
observer.current.disconnect();
|
||||
if (!reachedEnd) {
|
||||
observer.current.observe(node);
|
||||
}
|
||||
}
|
||||
},
|
||||
[observer.current, reachedEnd]
|
||||
);
|
||||
|
||||
const handleFilter = useCallback(
|
||||
(searchParams) => {
|
||||
dispatch({ type: 'RESET' });
|
||||
removeDefaultSearchKeys(searchParams);
|
||||
setSearchString(`${defaultSearchString}&${searchParams.toString()}`);
|
||||
route(`${pathname}?${searchParams.toString()}`);
|
||||
},
|
||||
[pathname, setSearchString]
|
||||
);
|
||||
|
||||
const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 w-full">
|
||||
<Heading>Events</Heading>
|
||||
|
||||
{searchKeys.length ? (
|
||||
<Box>
|
||||
<Heading size="sm">Filters</Heading>
|
||||
<div className="flex flex-wrap space-x-2">
|
||||
{searchKeys.map((filterKey) => (
|
||||
<UnFilterable
|
||||
name={`${filterKey}: ${searchParams.get(filterKey)}`}
|
||||
paramName={filterKey}
|
||||
pathname={pathname}
|
||||
searchParams={searchParamsString}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Box>
|
||||
) : null}
|
||||
<Filters onChange={handleFilter} searchParams={searchParams} />
|
||||
|
||||
<Box className="min-w-0 overflow-auto">
|
||||
<Table className="w-full">
|
||||
<Table className="min-w-full table-fixed">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th></Th>
|
||||
@@ -64,25 +138,33 @@ export default function Events({ url } = {}) {
|
||||
) => {
|
||||
const start = new Date(parseInt(startTime * 1000, 10));
|
||||
const end = new Date(parseInt(endTime * 1000, 10));
|
||||
const ref = i === events.length - 1 ? lastCellRef : undefined;
|
||||
return (
|
||||
<Tr key={id} index={i}>
|
||||
<Td>
|
||||
<a href={`/events/${id}`}>
|
||||
<img className="w-32 max-w-none" src={`data:image/jpeg;base64,${thumbnail}`} />
|
||||
<Td className="w-40">
|
||||
<a href={`/events/${id}`} ref={ref} data-start-time={startTime} data-reached-end={reachedEnd}>
|
||||
<img
|
||||
width="150"
|
||||
height="150"
|
||||
style="min-height: 48px; min-width: 48px;"
|
||||
src={`${apiHost}/api/events/${id}/thumbnail.jpg`}
|
||||
/>
|
||||
</a>
|
||||
</Td>
|
||||
<Td>
|
||||
<Filterable
|
||||
onFilter={handleFilter}
|
||||
pathname={pathname}
|
||||
searchParams={searchParamsString}
|
||||
searchParams={searchParams}
|
||||
paramName="camera"
|
||||
name={camera}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<Filterable
|
||||
onFilter={handleFilter}
|
||||
pathname={pathname}
|
||||
searchParams={searchParamsString}
|
||||
searchParams={searchParams}
|
||||
paramName="label"
|
||||
name={label}
|
||||
/>
|
||||
@@ -93,8 +175,9 @@ export default function Events({ url } = {}) {
|
||||
{zones.map((zone) => (
|
||||
<li>
|
||||
<Filterable
|
||||
onFilter={handleFilter}
|
||||
pathname={pathname}
|
||||
searchParams={searchParamsString}
|
||||
searchParams={searchString}
|
||||
paramName="zone"
|
||||
name={zone}
|
||||
/>
|
||||
@@ -110,27 +193,108 @@ export default function Events({ url } = {}) {
|
||||
}
|
||||
)}
|
||||
</Tbody>
|
||||
<Tfoot>
|
||||
<Tr>
|
||||
<Td className="text-center p-4" colspan="8">
|
||||
{status === FetchStatus.LOADING ? <ActivityIndicator /> : reachedEnd ? 'No more events' : null}
|
||||
</Td>
|
||||
</Tr>
|
||||
</Tfoot>
|
||||
</Table>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Filterable({ pathname, searchParams, paramName, name }) {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set(paramName, name);
|
||||
return <Link href={`${pathname}?${params.toString()}`}>{name}</Link>;
|
||||
}
|
||||
function Filterable({ onFilter, pathname, searchParams, paramName, name }) {
|
||||
const href = useMemo(() => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set(paramName, name);
|
||||
removeDefaultSearchKeys(params);
|
||||
return `${pathname}?${params.toString()}`;
|
||||
}, [searchParams]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
route(href, true);
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set(paramName, name);
|
||||
onFilter(params);
|
||||
},
|
||||
[href, searchParams]
|
||||
);
|
||||
|
||||
function UnFilterable({ pathname, searchParams, paramName, name }) {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.delete(paramName);
|
||||
return (
|
||||
<a
|
||||
className="bg-gray-700 text-white px-3 py-1 rounded-md hover:bg-gray-300 hover:text-gray-900 dark:bg-gray-300 dark:text-gray-900 dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
href={`${pathname}?${params.toString()}`}
|
||||
>
|
||||
<Link href={href} onclick={handleClick}>
|
||||
{name}
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function Filters({ onChange, searchParams }) {
|
||||
const { data } = useConfig();
|
||||
|
||||
const cameras = useMemo(() => Object.keys(data.cameras), [data]);
|
||||
|
||||
const zones = useMemo(
|
||||
() =>
|
||||
Object.values(data.cameras)
|
||||
.reduce((memo, camera) => {
|
||||
memo = memo.concat(Object.keys(camera.zones));
|
||||
return memo;
|
||||
}, [])
|
||||
.filter((value, i, self) => self.indexOf(value) === i),
|
||||
[data]
|
||||
);
|
||||
|
||||
const labels = useMemo(() => {
|
||||
return Object.values(data.cameras)
|
||||
.reduce((memo, camera) => {
|
||||
memo = memo.concat(camera.objects?.track || []);
|
||||
return memo;
|
||||
}, data.objects?.track || [])
|
||||
.filter((value, i, self) => self.indexOf(value) === i);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<Box className="flex space-y-0 space-x-8 flex-wrap">
|
||||
<Filter onChange={onChange} options={cameras} paramName="camera" searchParams={searchParams} />
|
||||
<Filter onChange={onChange} options={zones} paramName="zone" searchParams={searchParams} />
|
||||
<Filter onChange={onChange} options={labels} paramName="label" searchParams={searchParams} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function Filter({ onChange, searchParams, paramName, options }) {
|
||||
const handleSelect = useCallback(
|
||||
(event) => {
|
||||
const newParams = new URLSearchParams(searchParams.toString());
|
||||
const value = event.target.value;
|
||||
if (value) {
|
||||
newParams.set(paramName, event.target.value);
|
||||
} else {
|
||||
newParams.delete(paramName);
|
||||
}
|
||||
|
||||
onChange(newParams);
|
||||
},
|
||||
[searchParams, paramName, onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<label>
|
||||
<span className="block uppercase text-sm">{paramName}</span>
|
||||
<select className="border-solid border border-gray-500 rounded dark:text-gray-900" onChange={handleSelect}>
|
||||
<option>All</option>
|
||||
{options.map((opt) => {
|
||||
return (
|
||||
<option value={opt} selected={searchParams.get(paramName) === opt}>
|
||||
{opt}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
108
web/src/api/index.jsx
Normal file
108
web/src/api/index.jsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { h, createContext } from 'preact';
|
||||
import produce from 'immer';
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useReducer, useState } from 'preact/hooks';
|
||||
|
||||
export const ApiHost = createContext(import.meta.env.SNOWPACK_PUBLIC_API_HOST || window.baseUrl || '');
|
||||
|
||||
export const FetchStatus = {
|
||||
NONE: 'none',
|
||||
LOADING: 'loading',
|
||||
LOADED: 'loaded',
|
||||
ERROR: 'error',
|
||||
};
|
||||
|
||||
const initialState = Object.freeze({
|
||||
host: import.meta.env.SNOWPACK_PUBLIC_API_HOST || window.baseUrl || '',
|
||||
queries: {},
|
||||
});
|
||||
export const Api = createContext(initialState);
|
||||
export default Api;
|
||||
|
||||
function reducer(state, { type, payload, meta }) {
|
||||
switch (type) {
|
||||
case 'REQUEST': {
|
||||
const { url, request } = payload;
|
||||
const data = state.queries[url]?.data || null;
|
||||
return produce(state, (draftState) => {
|
||||
draftState.queries[url] = { status: FetchStatus.LOADING, data };
|
||||
});
|
||||
}
|
||||
|
||||
case 'RESPONSE': {
|
||||
const { url, ok, data } = payload;
|
||||
return produce(state, (draftState) => {
|
||||
draftState.queries[url] = { status: ok ? FetchStatus.LOADED : FetchStatus.ERROR, data };
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const ApiProvider = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
return <Api.Provider value={{ state, dispatch }}>{children}</Api.Provider>;
|
||||
};
|
||||
|
||||
function shouldFetch(state, url, forceRefetch = false) {
|
||||
if (forceRefetch || !(url in state.queries)) {
|
||||
return true;
|
||||
}
|
||||
const { status } = state.queries[url];
|
||||
|
||||
return status !== FetchStatus.LOADING && status !== FetchStatus.LOADED;
|
||||
}
|
||||
|
||||
export function useFetch(url, forceRefetch) {
|
||||
const { state, dispatch } = useContext(Api);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldFetch(state, url, forceRefetch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
async function fetchConfig() {
|
||||
await dispatch({ type: 'REQUEST', payload: { url } });
|
||||
const response = await fetch(`${state.host}${url}`);
|
||||
const data = await response.json();
|
||||
await dispatch({ type: 'RESPONSE', payload: { url, ok: response.ok, data } });
|
||||
}
|
||||
|
||||
fetchConfig();
|
||||
}, [url, forceRefetch]);
|
||||
|
||||
if (!(url in state.queries)) {
|
||||
return { data: null, status: FetchStatus.NONE };
|
||||
}
|
||||
|
||||
const data = state.queries[url].data || null;
|
||||
const status = state.queries[url].status;
|
||||
|
||||
return { data, status };
|
||||
}
|
||||
|
||||
export function useApiHost() {
|
||||
const { state, dispatch } = useContext(Api);
|
||||
return state.host;
|
||||
}
|
||||
|
||||
export function useEvents(searchParams, forceRefetch) {
|
||||
const url = `/api/events${searchParams ? `?${searchParams.toString()}` : ''}`;
|
||||
return useFetch(url, forceRefetch);
|
||||
}
|
||||
|
||||
export function useEvent(eventId, forceRefetch) {
|
||||
const url = `/api/events/${eventId}`;
|
||||
return useFetch(url, forceRefetch);
|
||||
}
|
||||
|
||||
export function useConfig(searchParams, forceRefetch) {
|
||||
const url = `/api/config${searchParams ? `?${searchParams.toString()}` : ''}`;
|
||||
return useFetch(url, forceRefetch);
|
||||
}
|
||||
|
||||
export function useStats(searchParams, forceRefetch) {
|
||||
const url = `/api/stats${searchParams ? `?${searchParams.toString()}` : ''}`;
|
||||
return useFetch(url, forceRefetch);
|
||||
}
|
||||
15
web/src/components/ActivityIndicator.jsx
Normal file
15
web/src/components/ActivityIndicator.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { h } from 'preact';
|
||||
|
||||
const sizes = {
|
||||
sm: 'h-4 w-4 border-2 border-t-2',
|
||||
md: 'h-8 w-8 border-4 border-t-4',
|
||||
lg: 'h-16 w-16 border-8 border-t-8',
|
||||
};
|
||||
|
||||
export default function ActivityIndicator({ size = 'md' }) {
|
||||
return (
|
||||
<div className="w-full flex items-center justify-center" aria-label="Loading…">
|
||||
<div className={`activityindicator ease-in rounded-full border-gray-200 text-blue-500 ${sizes[size]}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { h } from 'preact';
|
||||
import CameraImage from './CameraImage';
|
||||
import { ApiHost, Config } from '../context';
|
||||
import { useCallback, useState } from 'preact/hooks';
|
||||
|
||||
const MIN_LOAD_TIMEOUT_MS = 200;
|
||||
|
||||
@@ -4,7 +4,7 @@ export default function Box({ children, className = '', hover = false, href, ...
|
||||
const Element = href ? 'a' : 'div';
|
||||
return (
|
||||
<Element
|
||||
className={`bg-white dark:bg-gray-700 shadow-lg rounded-lg p-4 ${
|
||||
className={`bg-white dark:bg-gray-700 shadow-lg rounded-lg p-2 lg:p-4 ${
|
||||
hover ? 'hover:bg-gray-300 hover:dark:bg-gray-500 dark:hover:text-gray-900 dark:hover:text-gray-900' : ''
|
||||
} ${className}`}
|
||||
href={href}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { h } from 'preact';
|
||||
import { ApiHost, Config } from '../context';
|
||||
import ActivityIndicator from './ActivityIndicator';
|
||||
import { useApiHost, useConfig } from '../api';
|
||||
import { useCallback, useEffect, useContext, useMemo, useRef, useState } from 'preact/hooks';
|
||||
|
||||
export default function CameraImage({ camera, onload, searchParams = '' }) {
|
||||
const config = useContext(Config);
|
||||
const apiHost = useContext(ApiHost);
|
||||
const { data: config } = useConfig();
|
||||
const apiHost = useApiHost();
|
||||
const [availableWidth, setAvailableWidth] = useState(0);
|
||||
const [loadedSrc, setLoadedSrc] = useState(null);
|
||||
const containerRef = useRef(null);
|
||||
@@ -38,7 +39,7 @@ export default function CameraImage({ camera, onload, searchParams = '' }) {
|
||||
const img = useMemo(() => new Image(), [camera]);
|
||||
img.onload = useCallback(
|
||||
(event) => {
|
||||
const src = event.path[0].currentSrc;
|
||||
const src = event.srcElement.currentSrc;
|
||||
setLoadedSrc(src);
|
||||
onload && onload(event);
|
||||
},
|
||||
@@ -54,7 +55,11 @@ export default function CameraImage({ camera, onload, searchParams = '' }) {
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
{loadedSrc ? <img width={scaledHeight * aspectRatio} height={scaledHeight} src={loadedSrc} alt={name} /> : null}
|
||||
{loadedSrc ? (
|
||||
<img width={scaledHeight * aspectRatio} height={scaledHeight} src={loadedSrc} alt={name} />
|
||||
) : (
|
||||
<ActivityIndicator />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ export function Table({ children, className = '' }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function Thead({ children, className = '' }) {
|
||||
return <thead className={`${className}`}>{children}</thead>;
|
||||
export function Thead({ children, className }) {
|
||||
return <thead className={className}>{children}</thead>;
|
||||
}
|
||||
|
||||
export function Tbody({ children, className = '' }) {
|
||||
return <tbody className={`${className}`}>{children}</tbody>;
|
||||
export function Tbody({ children, className }) {
|
||||
return <tbody className={className}>{children}</tbody>;
|
||||
}
|
||||
|
||||
export function Tfoot({ children, className = '' }) {
|
||||
@@ -19,13 +19,21 @@ export function Tfoot({ children, className = '' }) {
|
||||
}
|
||||
|
||||
export function Tr({ children, className = '', index }) {
|
||||
return <tr className={`${index % 2 ? 'bg-gray-200 dark:bg-gray-700' : ''} ${className}`}>{children}</tr>;
|
||||
return <tr className={`${index % 2 ? 'bg-gray-200 dark:bg-gray-600' : ''} ${className}`}>{children}</tr>;
|
||||
}
|
||||
|
||||
export function Th({ children, className = '' }) {
|
||||
return <th className={`border-b-2 border-gray-400 p-4 text-left ${className}`}>{children}</th>;
|
||||
export function Th({ children, className = '', colspan }) {
|
||||
return (
|
||||
<th className={`border-b-2 border-gray-400 p-1 md:p-2 text-left ${className}`} colspan={colspan}>
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
export function Td({ children, className = '' }) {
|
||||
return <td className={`p-4 ${className}`}>{children}</td>;
|
||||
export function Td({ children, className = '', colspan }) {
|
||||
return (
|
||||
<td className={`p-1 md:p-2 ${className}`} colspan={colspan}>
|
||||
{children}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { createContext } from 'preact';
|
||||
|
||||
export const Config = createContext({});
|
||||
|
||||
export const ApiHost = createContext(import.meta.env.SNOWPACK_PUBLIC_API_HOST || window.baseUrl || '');
|
||||
@@ -1,3 +1,27 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.activityindicator {
|
||||
border-top-color: currentColor;
|
||||
-webkit-animation: spinner 0.75s linear infinite;
|
||||
animation: spinner 0.75s linear infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes spinner {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import App from './App';
|
||||
import { ApiProvider } from './api';
|
||||
import { h, render } from 'preact';
|
||||
import 'preact/devtools';
|
||||
import './index.css';
|
||||
|
||||
render(
|
||||
<App />,
|
||||
<ApiProvider>
|
||||
<App />
|
||||
</ApiProvider>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user