mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	feat(web): Delete events from Event page and API (#991)
Co-authored-by: Scott Roach <scott@thinkpivot.io> Co-authored-by: Paul Armstrong <paul@spaceyak.com>
This commit is contained in:
		
							parent
							
								
									482399d82f
								
							
						
					
					
						commit
						ebb6d348a3
					
				@ -21,13 +21,13 @@ RUN apt-get -qq update \
 | 
				
			|||||||
    && apt-get -qq install --no-install-recommends -y \
 | 
					    && apt-get -qq install --no-install-recommends -y \
 | 
				
			||||||
    gnupg wget unzip tzdata nginx libnginx-mod-rtmp \
 | 
					    gnupg wget unzip tzdata nginx libnginx-mod-rtmp \
 | 
				
			||||||
    && apt-get -qq install --no-install-recommends -y \
 | 
					    && apt-get -qq install --no-install-recommends -y \
 | 
				
			||||||
        python3-pip \
 | 
					    python3-pip \
 | 
				
			||||||
    && pip3 install -U /wheels/*.whl \
 | 
					    && pip3 install -U /wheels/*.whl \
 | 
				
			||||||
    && APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn apt-key adv --fetch-keys https://packages.cloud.google.com/apt/doc/apt-key.gpg \
 | 
					    && 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 "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 \
 | 
					    && echo "libedgetpu1-max libedgetpu/accepted-eula select true" | debconf-set-selections \
 | 
				
			||||||
    && apt-get -qq update && apt-get -qq install --no-install-recommends -y \
 | 
					    && apt-get -qq update && apt-get -qq install --no-install-recommends -y \
 | 
				
			||||||
        libedgetpu1-max=15.0 \
 | 
					    libedgetpu1-max=15.0 \
 | 
				
			||||||
    && rm -rf /var/lib/apt/lists/* /wheels \
 | 
					    && rm -rf /var/lib/apt/lists/* /wheels \
 | 
				
			||||||
    && (apt-get autoremove -y; apt-get autoclean -y)
 | 
					    && (apt-get autoremove -y; apt-get autoclean -y)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -35,7 +35,7 @@ RUN pip3 wheel --wheel-dir=/wheels \
 | 
				
			|||||||
    click \
 | 
					    click \
 | 
				
			||||||
    setproctitle \
 | 
					    setproctitle \
 | 
				
			||||||
    peewee \
 | 
					    peewee \
 | 
				
			||||||
    gevent
 | 
					    gevent 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
FROM scratch
 | 
					FROM scratch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,7 @@ title: HTTP API
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
A web server is available on port 5000 with the following endpoints.
 | 
					A web server is available on port 5000 with the following endpoints.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `/api/<camera_name>`
 | 
					### `GET /api/<camera_name>`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
An mjpeg stream for debugging. Keep in mind the mjpeg endpoint is for debugging only and will put additional load on the system when in use.
 | 
					An mjpeg stream for debugging. Keep in mind the mjpeg endpoint is for debugging only and will put additional load on the system when in use.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -24,7 +24,7 @@ Accepts the following query string parameters:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
You can access a higher resolution mjpeg stream by appending `h=height-in-pixels` to the endpoint. For example `http://localhost:5000/back?h=1080`. You can also increase the FPS by appending `fps=frame-rate` to the URL such as `http://localhost:5000/back?fps=10` or both with `?fps=10&h=1000`.
 | 
					You can access a higher resolution mjpeg stream by appending `h=height-in-pixels` to the endpoint. For example `http://localhost:5000/back?h=1080`. You can also increase the FPS by appending `fps=frame-rate` to the URL such as `http://localhost:5000/back?fps=10` or both with `?fps=10&h=1000`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `/api/<camera_name>/<object_name>/best.jpg[?h=300&crop=1]`
 | 
					### `GET /api/<camera_name>/<object_name>/best.jpg[?h=300&crop=1]`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The best snapshot for any object type. It is a full resolution image by default.
 | 
					The best snapshot for any object type. It is a full resolution image by default.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -33,7 +33,7 @@ Example parameters:
 | 
				
			|||||||
- `h=300`: resizes the image to 300 pixes tall
 | 
					- `h=300`: resizes the image to 300 pixes tall
 | 
				
			||||||
- `crop=1`: crops the image to the region of the detection rather than returning the entire image
 | 
					- `crop=1`: crops the image to the region of the detection rather than returning the entire image
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `/api/<camera_name>/latest.jpg[?h=300]`
 | 
					### `GET /api/<camera_name>/latest.jpg[?h=300]`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The most recent frame that frigate has finished processing. It is a full resolution image by default.
 | 
					The most recent frame that frigate has finished processing. It is a full resolution image by default.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -53,7 +53,7 @@ Example parameters:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
- `h=300`: resizes the image to 300 pixes tall
 | 
					- `h=300`: resizes the image to 300 pixes tall
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `/api/stats`
 | 
					### `GET /api/stats`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Contains some granular debug info that can be used for sensors in HomeAssistant.
 | 
					Contains some granular debug info that can be used for sensors in HomeAssistant.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -150,15 +150,15 @@ Sample response:
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `/api/config`
 | 
					### `GET /api/config`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
A json representation of your configuration
 | 
					A json representation of your configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `/api/version`
 | 
					### `GET /api/version`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Version info
 | 
					Version info
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `/api/events`
 | 
					### `GET /api/events`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Events from the database. Accepts the following query string parameters:
 | 
					Events from the database. Accepts the following query string parameters:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -174,19 +174,23 @@ Events from the database. Accepts the following query string parameters:
 | 
				
			|||||||
| `has_clip`           | int  | Filter to events that have clips (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)   |
 | 
					| `include_thumbnails` | int  | Include thumbnails in the response (0 or 1)   |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `/api/events/summary`
 | 
					### `GET /api/events/summary`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Returns summary data for events in the database. Used by the HomeAssistant integration.
 | 
					Returns summary data for events in the database. Used by the HomeAssistant integration.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `/api/events/<id>`
 | 
					### `GET /api/events/<id>`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Returns data for a single event.
 | 
					Returns data for a single event.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `/api/events/<id>/thumbnail.jpg`
 | 
					### `DELETE /api/events/<id>`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Permanently deletes the event along with any clips/snapshots.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `/api/events/<id>/snapshot.jpg`
 | 
					### `GET /api/events/<id>/snapshot.jpg`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Returns the snapshot image for the event id. Works while the event is in progress and after completion.
 | 
					Returns the snapshot image for the event id. Works while the event is in progress and after completion.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ import logging
 | 
				
			|||||||
import os
 | 
					import os
 | 
				
			||||||
import time
 | 
					import time
 | 
				
			||||||
from functools import reduce
 | 
					from functools import reduce
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import cv2
 | 
					import cv2
 | 
				
			||||||
import gevent
 | 
					import gevent
 | 
				
			||||||
@ -178,15 +179,36 @@ def events_summary():
 | 
				
			|||||||
    return jsonify([e for e in groups.dicts()])
 | 
					    return jsonify([e for e in groups.dicts()])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route("/events/<id>")
 | 
					@bp.route("/events/<id>", methods=("GET",))
 | 
				
			||||||
def event(id):
 | 
					def event(id):
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        return model_to_dict(Event.get(Event.id == id))
 | 
					        return model_to_dict(Event.get(Event.id == id))
 | 
				
			||||||
    except DoesNotExist:
 | 
					    except DoesNotExist:
 | 
				
			||||||
        return "Event not found", 404
 | 
					        return "Event not found", 404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/events/<id>', methods=('DELETE',))
 | 
				
			||||||
 | 
					def delete_event(id):
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        event = Event.get(Event.id == id)
 | 
				
			||||||
 | 
					    except DoesNotExist:
 | 
				
			||||||
 | 
					        return make_response(jsonify({"success": False, "message": "Event"  + id + " not found"}),404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route("/events/<id>/thumbnail.jpg")
 | 
					
 | 
				
			||||||
 | 
					    media_name = f"{event.camera}-{event.id}"
 | 
				
			||||||
 | 
					    if event.has_snapshot:
 | 
				
			||||||
 | 
					        media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg")
 | 
				
			||||||
 | 
					        media.unlink(missing_ok=True)
 | 
				
			||||||
 | 
					    if event.has_clip:
 | 
				
			||||||
 | 
					        media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4")
 | 
				
			||||||
 | 
					        media.unlink(missing_ok=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    event.delete_instance()
 | 
				
			||||||
 | 
					    return make_response(jsonify({"success": True, "message": "Event"  + id + " deleted"}),200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/events/<id>/thumbnail.jpg')
 | 
				
			||||||
def event_thumbnail(id):
 | 
					def event_thumbnail(id):
 | 
				
			||||||
    format = request.args.get("format", "ios")
 | 
					    format = request.args.get("format", "ios")
 | 
				
			||||||
    thumbnail_bytes = None
 | 
					    thumbnail_bytes = None
 | 
				
			||||||
 | 
				
			|||||||
@ -112,6 +112,7 @@ http {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        location /api/ {
 | 
					        location /api/ {
 | 
				
			||||||
            add_header 'Access-Control-Allow-Origin' '*';
 | 
					            add_header 'Access-Control-Allow-Origin' '*';
 | 
				
			||||||
 | 
					            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
 | 
				
			||||||
            add_header Cache-Control "no-store";
 | 
					            add_header Cache-Control "no-store";
 | 
				
			||||||
            proxy_pass http://frigate_api/;
 | 
					            proxy_pass http://frigate_api/;
 | 
				
			||||||
            proxy_pass_request_headers on;
 | 
					            proxy_pass_request_headers on;
 | 
				
			||||||
 | 
				
			|||||||
@ -15,6 +15,7 @@
 | 
				
			|||||||
  </head>
 | 
					  </head>
 | 
				
			||||||
  <body>
 | 
					  <body>
 | 
				
			||||||
    <div id="root" class="z-0"></div>
 | 
					    <div id="root" class="z-0"></div>
 | 
				
			||||||
 | 
					    <div id="dialogs" class="z-0"></div>
 | 
				
			||||||
    <div id="menus" class="z-0"></div>
 | 
					    <div id="menus" class="z-0"></div>
 | 
				
			||||||
    <div id="tooltips" class="z-0"></div>
 | 
					    <div id="tooltips" class="z-0"></div>
 | 
				
			||||||
    <noscript>You need to enable JavaScript to run this app.</noscript>
 | 
					    <noscript>You need to enable JavaScript to run this app.</noscript>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										47
									
								
								web/src/components/Dialog.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								web/src/components/Dialog.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					import { h, Fragment } from 'preact';
 | 
				
			||||||
 | 
					import Button from './Button';
 | 
				
			||||||
 | 
					import Heading from './Heading';
 | 
				
			||||||
 | 
					import { createPortal } from 'preact/compat';
 | 
				
			||||||
 | 
					import { useState, useEffect } from 'preact/hooks';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Dialog({ actions = [], portalRootID = 'dialogs', title, text }) {
 | 
				
			||||||
 | 
					  const portalRoot = portalRootID && document.getElementById(portalRootID);
 | 
				
			||||||
 | 
					  const [show, setShow] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    window.requestAnimationFrame(() => {
 | 
				
			||||||
 | 
					      setShow(true);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const dialog = (
 | 
				
			||||||
 | 
					    <Fragment>
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        data-testid="scrim"
 | 
				
			||||||
 | 
					        key="scrim"
 | 
				
			||||||
 | 
					        className="absolute inset-0 z-10 flex justify-center items-center bg-black bg-opacity-40"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          role="modal"
 | 
				
			||||||
 | 
					          className={`absolute rounded shadow-2xl bg-white dark:bg-gray-700 max-w-sm text-gray-900 dark:text-white transition-transform transition-opacity duration-75 transform scale-90 opacity-0 ${
 | 
				
			||||||
 | 
					            show ? 'scale-100 opacity-100' : ''
 | 
				
			||||||
 | 
					          }`}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div className="p-4">
 | 
				
			||||||
 | 
					            <Heading size="lg">{title}</Heading>
 | 
				
			||||||
 | 
					            <p>{text}</p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div className="p-2 flex justify-start flex-row-reverse space-x-2">
 | 
				
			||||||
 | 
					            {actions.map(({ color, text, onClick, ...props }, i) => (
 | 
				
			||||||
 | 
					              <Button className="ml-2" color={color} key={i} onClick={onClick} type="text" {...props}>
 | 
				
			||||||
 | 
					                {text}
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </Fragment>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return portalRoot ? createPortal(dialog, portalRoot) : dialog;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										38
									
								
								web/src/components/__tests__/Dialog.test.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								web/src/components/__tests__/Dialog.test.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					import { h } from 'preact';
 | 
				
			||||||
 | 
					import Dialog from '../Dialog';
 | 
				
			||||||
 | 
					import { fireEvent, render, screen } from '@testing-library/preact';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('Dialog', () => {
 | 
				
			||||||
 | 
					  let portal;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeAll(() => {
 | 
				
			||||||
 | 
					    portal = document.createElement('div');
 | 
				
			||||||
 | 
					    portal.id = 'dialogs';
 | 
				
			||||||
 | 
					    document.body.appendChild(portal);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  afterAll(() => {
 | 
				
			||||||
 | 
					    document.body.removeChild(portal);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  test('renders to a portal', async () => {
 | 
				
			||||||
 | 
					    render(<Dialog title="Tacos" text="This is the dialog" />);
 | 
				
			||||||
 | 
					    expect(screen.getByText('Tacos')).toBeInTheDocument();
 | 
				
			||||||
 | 
					    expect(screen.getByRole('modal').closest('#dialogs')).not.toBeNull();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  test('renders action buttons', async () => {
 | 
				
			||||||
 | 
					    const handleClick = jest.fn();
 | 
				
			||||||
 | 
					    render(
 | 
				
			||||||
 | 
					      <Dialog
 | 
				
			||||||
 | 
					        actions={[
 | 
				
			||||||
 | 
					          { color: 'red', text: 'Delete' },
 | 
				
			||||||
 | 
					          { text: 'Okay', onClick: handleClick },
 | 
				
			||||||
 | 
					        ]}
 | 
				
			||||||
 | 
					        title="Tacos"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    fireEvent.click(screen.getByRole('button', { name: 'Okay' }));
 | 
				
			||||||
 | 
					    expect(handleClick).toHaveBeenCalled();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										13
									
								
								web/src/icons/Delete.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								web/src/icons/Delete.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					import { h } from 'preact';
 | 
				
			||||||
 | 
					import { memo } from 'preact/compat';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function Delete({ className = '' }) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <svg className={`fill-current ${className}`} viewBox="0 0 24 24">
 | 
				
			||||||
 | 
					      <path d="M0 0h24v24H0V0z" fill="none" />
 | 
				
			||||||
 | 
					      <path d="M6 21h12V7H6v14zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
 | 
				
			||||||
 | 
					    </svg>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default memo(Delete);
 | 
				
			||||||
@ -1,5 +1,10 @@
 | 
				
			|||||||
import { h, Fragment } from 'preact';
 | 
					import { h, Fragment } from 'preact';
 | 
				
			||||||
 | 
					import { useCallback, useState } from 'preact/hooks';
 | 
				
			||||||
 | 
					import { route } from 'preact-router';
 | 
				
			||||||
import ActivityIndicator from '../components/ActivityIndicator';
 | 
					import ActivityIndicator from '../components/ActivityIndicator';
 | 
				
			||||||
 | 
					import Button from '../components/Button';
 | 
				
			||||||
 | 
					import Delete from '../icons/Delete'
 | 
				
			||||||
 | 
					import Dialog from '../components/Dialog';
 | 
				
			||||||
import Heading from '../components/Heading';
 | 
					import Heading from '../components/Heading';
 | 
				
			||||||
import Link from '../components/Link';
 | 
					import Link from '../components/Link';
 | 
				
			||||||
import { FetchStatus, useApiHost, useEvent } from '../api';
 | 
					import { FetchStatus, useApiHost, useEvent } from '../api';
 | 
				
			||||||
@ -8,9 +13,39 @@ import { Table, Thead, Tbody, Th, Tr, Td } from '../components/Table';
 | 
				
			|||||||
export default function Event({ eventId }) {
 | 
					export default function Event({ eventId }) {
 | 
				
			||||||
  const apiHost = useApiHost();
 | 
					  const apiHost = useApiHost();
 | 
				
			||||||
  const { data, status } = useEvent(eventId);
 | 
					  const { data, status } = useEvent(eventId);
 | 
				
			||||||
 | 
					  const [showDialog, setShowDialog] = useState(false);
 | 
				
			||||||
 | 
					  const [deleteStatus, setDeleteStatus] = useState(FetchStatus.NONE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleClickDelete = () => {
 | 
				
			||||||
 | 
					    setShowDialog(true);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleDismissDeleteDialog = () => {
 | 
				
			||||||
 | 
					    setShowDialog(false);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleClickDeleteDialog = useCallback(async () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let success;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const response = await fetch(`${apiHost}/api/events/${eventId}`, { method: 'DELETE' });
 | 
				
			||||||
 | 
					      success = await (response.status < 300 ? response.json() : { success: true });
 | 
				
			||||||
 | 
					      setDeleteStatus(success ? FetchStatus.LOADED : FetchStatus.ERROR);
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      setDeleteStatus(FetchStatus.ERROR);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (success) {
 | 
				
			||||||
 | 
					      setDeleteStatus(FetchStatus.LOADED);
 | 
				
			||||||
 | 
					      setShowDialog(false);
 | 
				
			||||||
 | 
					      route('/events', true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [apiHost, eventId, setShowDialog]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (status !== FetchStatus.LOADED) {
 | 
					  if (status !== FetchStatus.LOADED) {
 | 
				
			||||||
    return <ActivityIndicator />;
 | 
					    return <ActivityIndicator />
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const startime = new Date(data.start_time * 1000);
 | 
					  const startime = new Date(data.start_time * 1000);
 | 
				
			||||||
@ -18,9 +53,27 @@ export default function Event({ eventId }) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className="space-y-4">
 | 
					    <div className="space-y-4">
 | 
				
			||||||
      <Heading>
 | 
					      <div className="flex">
 | 
				
			||||||
        {data.camera} {data.label} <span className="text-sm">{startime.toLocaleString()}</span>
 | 
					        <Heading className="flex-grow">
 | 
				
			||||||
      </Heading>
 | 
					          {data.camera} {data.label} <span className="text-sm">{startime.toLocaleString()}</span>
 | 
				
			||||||
 | 
					        </Heading>
 | 
				
			||||||
 | 
					        <Button className="self-start" color="red" onClick={handleClickDelete}>
 | 
				
			||||||
 | 
					          <Delete className="w-6" /> Delete event
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					        {showDialog ? (
 | 
				
			||||||
 | 
					          <Dialog
 | 
				
			||||||
 | 
					            onDismiss={handleDismissDeleteDialog}
 | 
				
			||||||
 | 
					            title="Delete Event?"
 | 
				
			||||||
 | 
					            text="This event will be permanently deleted along with any related clips and snapshots"
 | 
				
			||||||
 | 
					            actions={[
 | 
				
			||||||
 | 
					              deleteStatus !== FetchStatus.LOADING
 | 
				
			||||||
 | 
					                ? { text: 'Delete', color: 'red', onClick: handleClickDeleteDialog }
 | 
				
			||||||
 | 
					                : { text: 'Deleting…', color: 'red', disabled: true },
 | 
				
			||||||
 | 
					              { text: 'Cancel', onClick: handleDismissDeleteDialog },
 | 
				
			||||||
 | 
					            ]}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        ) : null}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <Table class="w-full">
 | 
					      <Table class="w-full">
 | 
				
			||||||
        <Thead>
 | 
					        <Thead>
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@ import { h } from 'preact';
 | 
				
			|||||||
import ArrowDropdown from '../icons/ArrowDropdown';
 | 
					import ArrowDropdown from '../icons/ArrowDropdown';
 | 
				
			||||||
import ArrowDropup from '../icons/ArrowDropup';
 | 
					import ArrowDropup from '../icons/ArrowDropup';
 | 
				
			||||||
import Button from '../components/Button';
 | 
					import Button from '../components/Button';
 | 
				
			||||||
 | 
					import Dialog from '../components/Dialog';
 | 
				
			||||||
import Heading from '../components/Heading';
 | 
					import Heading from '../components/Heading';
 | 
				
			||||||
import Select from '../components/Select';
 | 
					import Select from '../components/Select';
 | 
				
			||||||
import Switch from '../components/Switch';
 | 
					import Switch from '../components/Switch';
 | 
				
			||||||
@ -10,6 +11,7 @@ import { useCallback, useState } from 'preact/hooks';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export default function StyleGuide() {
 | 
					export default function StyleGuide() {
 | 
				
			||||||
  const [switches, setSwitches] = useState({ 0: false, 1: true, 2: false, 3: false });
 | 
					  const [switches, setSwitches] = useState({ 0: false, 1: true, 2: false, 3: false });
 | 
				
			||||||
 | 
					  const [showDialog, setShowDialog] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleSwitch = useCallback(
 | 
					  const handleSwitch = useCallback(
 | 
				
			||||||
    (id, checked) => {
 | 
					    (id, checked) => {
 | 
				
			||||||
@ -18,6 +20,10 @@ export default function StyleGuide() {
 | 
				
			|||||||
    [switches]
 | 
					    [switches]
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleDismissDialog = () => {
 | 
				
			||||||
 | 
					    setShowDialog(false);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
      <Heading size="md">Button</Heading>
 | 
					      <Heading size="md">Button</Heading>
 | 
				
			||||||
@ -59,6 +65,26 @@ export default function StyleGuide() {
 | 
				
			|||||||
        </Button>
 | 
					        </Button>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <Heading size="md">Dialog</Heading>
 | 
				
			||||||
 | 
					      <Button
 | 
				
			||||||
 | 
					        onClick={() => {
 | 
				
			||||||
 | 
					          setShowDialog(true);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        Show Dialog
 | 
				
			||||||
 | 
					      </Button>
 | 
				
			||||||
 | 
					      {showDialog ? (
 | 
				
			||||||
 | 
					        <Dialog
 | 
				
			||||||
 | 
					          onDismiss={handleDismissDialog}
 | 
				
			||||||
 | 
					          title="This is a dialog"
 | 
				
			||||||
 | 
					          text="Would you like to see more?"
 | 
				
			||||||
 | 
					          actions={[
 | 
				
			||||||
 | 
					            { text: 'Yes', color: 'red', onClick: handleDismissDialog },
 | 
				
			||||||
 | 
					            { text: 'No', onClick: handleDismissDialog },
 | 
				
			||||||
 | 
					          ]}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      ) : null}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <Heading size="md">Switch</Heading>
 | 
					      <Heading size="md">Switch</Heading>
 | 
				
			||||||
      <div className="flex-col space-y-4 max-w-4xl">
 | 
					      <div className="flex-col space-y-4 max-w-4xl">
 | 
				
			||||||
        <Switch label="Disabled, off" labelPosition="after" />
 | 
					        <Switch label="Disabled, off" labelPosition="after" />
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user