forked from Github/frigate
test(web): add eslint and PR lint validation
This commit is contained in:
committed by
Blake Blackshear
parent
513a099c24
commit
daa759cc55
@@ -6,31 +6,26 @@ import Heading from '../components/Heading';
|
||||
import Link from '../components/Link';
|
||||
import SettingsIcon from '../icons/Settings';
|
||||
import Switch from '../components/Switch';
|
||||
import { route } from 'preact-router';
|
||||
import { usePersistence } from '../context';
|
||||
import { useCallback, useContext, useMemo, useState } from 'preact/hooks';
|
||||
import { useCallback, useMemo, useState } from 'preact/hooks';
|
||||
import { useApiHost, useConfig } from '../api';
|
||||
|
||||
const emptyObject = Object.freeze({});
|
||||
|
||||
export default function Camera({ camera }) {
|
||||
const { data: config } = useConfig();
|
||||
const apiHost = useApiHost();
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
if (!config) {
|
||||
return <div>{`No camera named ${camera}`}</div>;
|
||||
}
|
||||
|
||||
const cameraConfig = config.cameras[camera];
|
||||
const [options, setOptions, optionsLoaded] = usePersistence(`${camera}-feed`, Object.freeze({}));
|
||||
|
||||
const objectCount = useMemo(() => cameraConfig.objects.track.length, [cameraConfig]);
|
||||
const cameraConfig = config?.cameras[camera];
|
||||
const [options, setOptions] = usePersistence(`${camera}-feed`, emptyObject);
|
||||
|
||||
const handleSetOption = useCallback(
|
||||
(id, value) => {
|
||||
const newOptions = { ...options, [id]: value };
|
||||
setOptions(newOptions);
|
||||
},
|
||||
[options]
|
||||
[options, setOptions]
|
||||
);
|
||||
|
||||
const searchParams = useMemo(
|
||||
@@ -41,7 +36,7 @@ export default function Camera({ camera }) {
|
||||
return memo;
|
||||
}, [])
|
||||
),
|
||||
[camera, options]
|
||||
[options]
|
||||
);
|
||||
|
||||
const handleToggleSettings = useCallback(() => {
|
||||
@@ -52,27 +47,27 @@ export default function Camera({ camera }) {
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
<div className="flex space-x-3">
|
||||
<Switch checked={options['bbox']} id="bbox" onChange={handleSetOption} />
|
||||
<span class="inline-flex">Bounding box</span>
|
||||
<span className="inline-flex">Bounding box</span>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<Switch checked={options['timestamp']} id="timestamp" onChange={handleSetOption} />
|
||||
<span class="inline-flex">Timestamp</span>
|
||||
<span className="inline-flex">Timestamp</span>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<Switch checked={options['zones']} id="zones" onChange={handleSetOption} />
|
||||
<span class="inline-flex">Zones</span>
|
||||
<span className="inline-flex">Zones</span>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<Switch checked={options['mask']} id="mask" onChange={handleSetOption} />
|
||||
<span class="inline-flex">Masks</span>
|
||||
<span className="inline-flex">Masks</span>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<Switch checked={options['motion']} id="motion" onChange={handleSetOption} />
|
||||
<span class="inline-flex">Motion boxes</span>
|
||||
<span className="inline-flex">Motion boxes</span>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<Switch checked={options['regions']} id="regions" onChange={handleSetOption} />
|
||||
<span class="inline-flex">Regions</span>
|
||||
<span className="inline-flex">Regions</span>
|
||||
</div>
|
||||
<Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link>
|
||||
</div>
|
||||
@@ -81,14 +76,12 @@ export default function Camera({ camera }) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Heading size="2xl">{camera}</Heading>
|
||||
{optionsLoaded ? (
|
||||
<div>
|
||||
<AutoUpdatingCameraImage camera={camera} searchParams={searchParams} />
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
<AutoUpdatingCameraImage camera={camera} searchParams={searchParams} />
|
||||
</div>
|
||||
|
||||
<Button onClick={handleToggleSettings} type="text">
|
||||
<span class="w-5 h-5">
|
||||
<span className="w-5 h-5">
|
||||
<SettingsIcon />
|
||||
</span>{' '}
|
||||
<span>{showSettings ? 'Hide' : 'Show'} Options</span>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { h } from 'preact';
|
||||
import Card from '../components/Card';
|
||||
import Button from '../components/Button';
|
||||
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 Card from '../components/Card.jsx';
|
||||
import Button from '../components/Button.jsx';
|
||||
import Heading from '../components/Heading.jsx';
|
||||
import Switch from '../components/Switch.jsx';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||
import { useApiHost, useConfig } from '../api';
|
||||
|
||||
export default function CameraMasks({ camera, url }) {
|
||||
@@ -14,10 +13,6 @@ export default function CameraMasks({ camera, url }) {
|
||||
const [imageScale, setImageScale] = useState(1);
|
||||
const [snap, setSnap] = useState(true);
|
||||
|
||||
if (!(camera in config.cameras)) {
|
||||
return <div>{`No camera named ${camera}`}</div>;
|
||||
}
|
||||
|
||||
const cameraConfig = config.cameras[camera];
|
||||
const {
|
||||
width,
|
||||
@@ -38,7 +33,7 @@ export default function CameraMasks({ camera, url }) {
|
||||
}
|
||||
});
|
||||
}),
|
||||
[camera, width, setImageScale]
|
||||
[width, setImageScale]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -46,14 +41,14 @@ export default function CameraMasks({ camera, url }) {
|
||||
return;
|
||||
}
|
||||
resizeObserver.observe(imageRef.current);
|
||||
}, [resizeObserver, imageRef.current]);
|
||||
}, [resizeObserver, imageRef]);
|
||||
|
||||
const [motionMaskPoints, setMotionMaskPoints] = useState(
|
||||
Array.isArray(motionMask)
|
||||
? motionMask.map((mask) => getPolylinePoints(mask))
|
||||
: motionMask
|
||||
? [getPolylinePoints(motionMask)]
|
||||
: []
|
||||
? [getPolylinePoints(motionMask)]
|
||||
: []
|
||||
);
|
||||
|
||||
const [zonePoints, setZonePoints] = useState(
|
||||
@@ -67,8 +62,8 @@ export default function CameraMasks({ camera, url }) {
|
||||
[name]: Array.isArray(objectFilters[name].mask)
|
||||
? objectFilters[name].mask.map((mask) => getPolylinePoints(mask))
|
||||
: objectFilters[name].mask
|
||||
? [getPolylinePoints(objectFilters[name].mask)]
|
||||
: [],
|
||||
? [getPolylinePoints(objectFilters[name].mask)]
|
||||
: [],
|
||||
}),
|
||||
{}
|
||||
)
|
||||
@@ -94,26 +89,6 @@ export default function CameraMasks({ camera, url }) {
|
||||
[editing]
|
||||
);
|
||||
|
||||
const handleSelectEditable = useCallback(
|
||||
(name) => {
|
||||
setEditing(name);
|
||||
},
|
||||
[setEditing]
|
||||
);
|
||||
|
||||
const handleRemoveEditable = useCallback(
|
||||
(name) => {
|
||||
const filteredZonePoints = Object.keys(zonePoints)
|
||||
.filter((zoneName) => zoneName !== name)
|
||||
.reduce((memo, name) => {
|
||||
memo[name] = zonePoints[name];
|
||||
return memo;
|
||||
}, {});
|
||||
setZonePoints(filteredZonePoints);
|
||||
},
|
||||
[zonePoints, setZonePoints]
|
||||
);
|
||||
|
||||
// Motion mask methods
|
||||
const handleAddMask = useCallback(() => {
|
||||
const newMotionMaskPoints = [...motionMaskPoints, []];
|
||||
@@ -171,11 +146,11 @@ ${motionMaskPoints.map((mask, i) => ` - ${polylinePointsToPolyline(mask)}`)
|
||||
const handleCopyZones = useCallback(async () => {
|
||||
await window.navigator.clipboard.writeText(` zones:
|
||||
${Object.keys(zonePoints)
|
||||
.map(
|
||||
(zoneName) => ` ${zoneName}:
|
||||
.map(
|
||||
(zoneName) => ` ${zoneName}:
|
||||
coordinates: ${polylinePointsToPolyline(zonePoints[zoneName])}`
|
||||
)
|
||||
.join('\n')}`);
|
||||
)
|
||||
.join('\n')}`);
|
||||
}, [zonePoints]);
|
||||
|
||||
// Object methods
|
||||
@@ -207,14 +182,14 @@ ${Object.keys(zonePoints)
|
||||
await window.navigator.clipboard.writeText(` objects:
|
||||
filters:
|
||||
${Object.keys(objectMaskPoints)
|
||||
.map((objectName) =>
|
||||
objectMaskPoints[objectName].length
|
||||
? ` ${objectName}:
|
||||
.map((objectName) =>
|
||||
objectMaskPoints[objectName].length
|
||||
? ` ${objectName}:
|
||||
mask: ${polylinePointsToPolyline(objectMaskPoints[objectName])}`
|
||||
: ''
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join('\n')}`);
|
||||
: ''
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join('\n')}`);
|
||||
}, [objectMaskPoints]);
|
||||
|
||||
const handleAddToObjectMask = useCallback(
|
||||
@@ -239,7 +214,7 @@ ${Object.keys(objectMaskPoints)
|
||||
);
|
||||
|
||||
return (
|
||||
<div class="flex-col space-y-4">
|
||||
<div className="flex-col space-y-4">
|
||||
<Heading size="2xl">{camera} mask & zone creator</Heading>
|
||||
|
||||
<Card
|
||||
@@ -265,12 +240,12 @@ ${Object.keys(objectMaskPoints)
|
||||
height={height}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex space-x-4">
|
||||
<div className="flex space-x-4">
|
||||
<span>Snap to edges</span> <Switch checked={snap} onChange={handleChangeSnap} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-col space-y-4">
|
||||
<div className="flex-col space-y-4">
|
||||
<MaskValues
|
||||
editing={editing}
|
||||
title="Motion masks"
|
||||
@@ -314,7 +289,7 @@ ${Object.keys(objectMaskPoints)
|
||||
}
|
||||
|
||||
function maskYamlKeyPrefix(points) {
|
||||
return ` - `;
|
||||
return ' - ';
|
||||
}
|
||||
|
||||
function zoneYamlKeyPrefix(points, key) {
|
||||
@@ -323,43 +298,40 @@ function zoneYamlKeyPrefix(points, key) {
|
||||
}
|
||||
|
||||
function objectYamlKeyPrefix(points, key, subkey) {
|
||||
return ` - `;
|
||||
return ' - ';
|
||||
}
|
||||
|
||||
const MaskInset = 20;
|
||||
|
||||
function EditableMask({ onChange, points, scale, snap, width, height }) {
|
||||
if (!points) {
|
||||
return null;
|
||||
}
|
||||
const boundingRef = useRef(null);
|
||||
|
||||
function boundedSize(value, maxValue) {
|
||||
const newValue = Math.min(Math.max(0, Math.round(value)), maxValue);
|
||||
if (snap) {
|
||||
if (newValue <= MaskInset) {
|
||||
return 0;
|
||||
} else if (maxValue - newValue <= MaskInset) {
|
||||
return maxValue;
|
||||
}
|
||||
function boundedSize(value, maxValue, snap) {
|
||||
const newValue = Math.min(Math.max(0, Math.round(value)), maxValue);
|
||||
if (snap) {
|
||||
if (newValue <= MaskInset) {
|
||||
return 0;
|
||||
} else if (maxValue - newValue <= MaskInset) {
|
||||
return maxValue;
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
|
||||
function EditableMask({ onChange, points, scale, snap, width, height }) {
|
||||
const boundingRef = useRef(null);
|
||||
|
||||
const handleMovePoint = useCallback(
|
||||
(index, newX, newY) => {
|
||||
if (newX < 0 && newY < 0) {
|
||||
return;
|
||||
}
|
||||
let x = boundedSize(newX / scale, width, snap);
|
||||
let y = boundedSize(newY / scale, height, snap);
|
||||
const x = boundedSize(newX / scale, width, snap);
|
||||
const y = boundedSize(newY / scale, height, snap);
|
||||
|
||||
const newPoints = [...points];
|
||||
newPoints[index] = [x, y];
|
||||
onChange(newPoints);
|
||||
},
|
||||
[scale, points, snap]
|
||||
[height, width, onChange, scale, points, snap]
|
||||
);
|
||||
|
||||
// Add a new point between the closest two other points
|
||||
@@ -370,7 +342,6 @@ function EditableMask({ onChange, points, scale, snap, width, height }) {
|
||||
const scaledY = boundedSize((offsetY - MaskInset) / scale, height, snap);
|
||||
const newPoint = [scaledX, scaledY];
|
||||
|
||||
let closest;
|
||||
const { index } = points.reduce(
|
||||
(result, point, i) => {
|
||||
const nextPoint = points.length === i + 1 ? points[0] : points[i + 1];
|
||||
@@ -385,7 +356,7 @@ function EditableMask({ onChange, points, scale, snap, width, height }) {
|
||||
newPoints.splice(index, 0, newPoint);
|
||||
onChange(newPoints);
|
||||
},
|
||||
[scale, points, onChange, snap]
|
||||
[height, width, scale, points, onChange, snap]
|
||||
);
|
||||
|
||||
const handleRemovePoint = useCallback(
|
||||
@@ -407,16 +378,16 @@ function EditableMask({ onChange, points, scale, snap, width, height }) {
|
||||
{!scaledPoints
|
||||
? null
|
||||
: scaledPoints.map(([x, y], i) => (
|
||||
<PolyPoint
|
||||
boundingRef={boundingRef}
|
||||
index={i}
|
||||
onMove={handleMovePoint}
|
||||
onRemove={handleRemovePoint}
|
||||
x={x + MaskInset}
|
||||
y={y + MaskInset}
|
||||
/>
|
||||
))}
|
||||
<div className="absolute inset-0 right-0 bottom-0" onclick={handleAddPoint} ref={boundingRef} />
|
||||
<PolyPoint
|
||||
boundingRef={boundingRef}
|
||||
index={i}
|
||||
onMove={handleMovePoint}
|
||||
onRemove={handleRemovePoint}
|
||||
x={x + MaskInset}
|
||||
y={y + MaskInset}
|
||||
/>
|
||||
))}
|
||||
<div className="absolute inset-0 right-0 bottom-0" onClick={handleAddPoint} ref={boundingRef} />
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
@@ -488,15 +459,15 @@ function MaskValues({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden" onmouseover={handleMousein} onmouseout={handleMouseout}>
|
||||
<div class="flex space-x-4">
|
||||
<div className="overflow-hidden" onMouseOver={handleMousein} onMouseOut={handleMouseout}>
|
||||
<div className="flex space-x-4">
|
||||
<Heading className="flex-grow self-center" size="base">
|
||||
{title}
|
||||
</Heading>
|
||||
<Button onClick={onCopy}>Copy</Button>
|
||||
<Button onClick={onCreate}>Add</Button>
|
||||
</div>
|
||||
<pre class="relative overflow-auto font-mono text-gray-900 dark:text-gray-100 rounded bg-gray-100 dark:bg-gray-800 p-2">
|
||||
<pre className="relative overflow-auto font-mono text-gray-900 dark:text-gray-100 rounded bg-gray-100 dark:bg-gray-800 p-2">
|
||||
{yamlPrefix}
|
||||
{Object.keys(points).map((mainkey) => {
|
||||
if (isMulti) {
|
||||
@@ -522,20 +493,19 @@ function MaskValues({
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Item
|
||||
mainkey={mainkey}
|
||||
editing={editing}
|
||||
handleAdd={onAdd ? handleAdd : undefined}
|
||||
handleEdit={handleEdit}
|
||||
handleRemove={handleRemove}
|
||||
points={points[mainkey]}
|
||||
showButtons={showButtons}
|
||||
yamlKeyPrefix={yamlKeyPrefix}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Item
|
||||
mainkey={mainkey}
|
||||
editing={editing}
|
||||
handleAdd={onAdd ? handleAdd : undefined}
|
||||
handleEdit={handleEdit}
|
||||
handleRemove={handleRemove}
|
||||
points={points[mainkey]}
|
||||
showButtons={showButtons}
|
||||
yamlKeyPrefix={yamlKeyPrefix}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -613,18 +583,18 @@ function PolyPoint({ boundingRef, index, x, y, onMove, onRemove }) {
|
||||
}
|
||||
onMove(index, event.layerX - PolyPointRadius * 2, event.layerY - PolyPointRadius * 2);
|
||||
},
|
||||
[onMove, index, boundingRef.current]
|
||||
[onMove, index, boundingRef]
|
||||
);
|
||||
|
||||
const handleDragStart = useCallback(() => {
|
||||
boundingRef.current && boundingRef.current.addEventListener('dragover', handleDragOver, false);
|
||||
setHidden(true);
|
||||
}, [setHidden, boundingRef.current, handleDragOver]);
|
||||
}, [setHidden, boundingRef, handleDragOver]);
|
||||
|
||||
const handleDragEnd = useCallback(() => {
|
||||
boundingRef.current && boundingRef.current.removeEventListener('dragover', handleDragOver);
|
||||
setHidden(false);
|
||||
}, [setHidden, boundingRef.current, handleDragOver]);
|
||||
}, [setHidden, boundingRef, handleDragOver]);
|
||||
|
||||
const handleRightClick = useCallback(
|
||||
(event) => {
|
||||
@@ -644,10 +614,10 @@ function PolyPoint({ boundingRef, index, x, y, onMove, onRemove }) {
|
||||
className={`${hidden ? 'opacity-0' : ''} bg-gray-900 rounded-full absolute z-20`}
|
||||
style={`top: ${y - PolyPointRadius}px; left: ${x - PolyPointRadius}px; width: 20px; height: 20px;`}
|
||||
draggable
|
||||
onclick={handleClick}
|
||||
oncontextmenu={handleRightClick}
|
||||
ondragstart={handleDragStart}
|
||||
ondragend={handleDragEnd}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleRightClick}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,19 +2,15 @@ import { h } from 'preact';
|
||||
import ActivityIndicator from '../components/ActivityIndicator';
|
||||
import Card from '../components/Card';
|
||||
import CameraImage from '../components/CameraImage';
|
||||
import Heading from '../components/Heading';
|
||||
import { route } from 'preact-router';
|
||||
import { useConfig } from '../api';
|
||||
import { useConfig, FetchStatus } from '../api';
|
||||
import { useMemo } from 'preact/hooks';
|
||||
|
||||
export default function Cameras() {
|
||||
const { data: config, status } = useConfig();
|
||||
|
||||
if (!config) {
|
||||
return <p>loading…</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
return status !== FetchStatus.LOADED ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<div className="grid grid-cols-1 3xl:grid-cols-3 md:grid-cols-2 gap-4">
|
||||
{Object.keys(config.cameras).map((camera) => (
|
||||
<Camera name={camera} />
|
||||
|
||||
@@ -3,53 +3,56 @@ import ActivityIndicator from '../components/ActivityIndicator';
|
||||
import Button from '../components/Button';
|
||||
import Heading from '../components/Heading';
|
||||
import Link from '../components/Link';
|
||||
import { FetchStatus, useConfig, useStats } from '../api';
|
||||
import { useConfig, useStats } from '../api';
|
||||
import { Table, Tbody, Thead, Tr, Th, Td } from '../components/Table';
|
||||
import { useCallback, useEffect, useState } from 'preact/hooks';
|
||||
|
||||
const emptyObject = Object.freeze({});
|
||||
|
||||
export default function Debug() {
|
||||
const config = useConfig();
|
||||
|
||||
const [timeoutId, setTimeoutId] = useState(null);
|
||||
const { data: stats } = useStats(null, timeoutId);
|
||||
|
||||
const forceUpdate = useCallback(async () => {
|
||||
setTimeoutId(setTimeout(forceUpdate, 1000));
|
||||
const forceUpdate = useCallback(() => {
|
||||
const timeoutId = setTimeout(forceUpdate, 1000);
|
||||
setTimeoutId(timeoutId);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
forceUpdate();
|
||||
}, []);
|
||||
}, [forceUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [timeoutId]);
|
||||
const { data: stats, status } = useStats(null, timeoutId);
|
||||
|
||||
if (stats === null && (status === FetchStatus.LOADING || status === FetchStatus.NONE)) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
const { detectors, service, detection_fps, ...cameras } = stats || emptyObject;
|
||||
|
||||
const { detectors, detection_fps, service, ...cameras } = stats;
|
||||
const detectorNames = Object.keys(detectors || emptyObject);
|
||||
const detectorDataKeys = Object.keys(detectors ? detectors[detectorNames[0]] : emptyObject);
|
||||
const cameraNames = Object.keys(cameras || emptyObject);
|
||||
const cameraDataKeys = Object.keys(cameras[cameraNames[0]] || emptyObject);
|
||||
|
||||
const detectorNames = Object.keys(detectors);
|
||||
const detectorDataKeys = Object.keys(detectors[detectorNames[0]]);
|
||||
|
||||
const cameraNames = Object.keys(cameras);
|
||||
const cameraDataKeys = Object.keys(cameras[cameraNames[0]]);
|
||||
|
||||
const handleCopyConfig = useCallback(async () => {
|
||||
await window.navigator.clipboard.writeText(JSON.stringify(config, null, 2));
|
||||
const handleCopyConfig = useCallback(() => {
|
||||
async function copy() {
|
||||
await window.navigator.clipboard.writeText(JSON.stringify(config, null, 2));
|
||||
}
|
||||
copy();
|
||||
}, [config]);
|
||||
|
||||
return (
|
||||
<div class="space-y-4">
|
||||
return stats === null ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<Heading>
|
||||
Debug <span className="text-sm">{service.version}</span>
|
||||
</Heading>
|
||||
|
||||
<div class="min-w-0 overflow-auto">
|
||||
<div className="min-w-0 overflow-auto">
|
||||
<Table className="w-full">
|
||||
<Thead>
|
||||
<Tr>
|
||||
@@ -72,7 +75,7 @@ export default function Debug() {
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 overflow-auto">
|
||||
<div className="min-w-0 overflow-auto">
|
||||
<Table className="w-full">
|
||||
<Thead>
|
||||
<Tr>
|
||||
|
||||
@@ -3,7 +3,7 @@ import ActivityIndicator from '../components/ActivityIndicator';
|
||||
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 { Table, Thead, Tbody, Th, Tr, Td } from '../components/Table';
|
||||
|
||||
export default function Event({ eventId }) {
|
||||
const apiHost = useApiHost();
|
||||
@@ -54,7 +54,7 @@ export default function Event({ eventId }) {
|
||||
{data.has_clip ? (
|
||||
<Fragment>
|
||||
<Heading size="sm">Clip</Heading>
|
||||
<video autoplay className="w-100" src={`${apiHost}/clips/${data.camera}-${eventId}.mp4`} controls />
|
||||
<video autoPlay className="w-100" src={`${apiHost}/clips/${data.camera}-${eventId}.mp4`} controls />
|
||||
</Fragment>
|
||||
) : (
|
||||
<p>No clip available</p>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { h } from 'preact';
|
||||
import ActivityIndicator from '../components/ActivityIndicator';
|
||||
import Card from '../components/Card';
|
||||
import Heading from '../components/Heading';
|
||||
import Link from '../components/Link';
|
||||
import Select from '../components/Select';
|
||||
@@ -8,39 +7,39 @@ 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, useMemo, useRef, useReducer, useState } from 'preact/hooks';
|
||||
import { useCallback, useEffect, useMemo, useRef, useReducer, useState } from 'preact/hooks';
|
||||
|
||||
const API_LIMIT = 25;
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
case 'APPEND_EVENTS': {
|
||||
const {
|
||||
meta: { searchString },
|
||||
payload,
|
||||
} = action;
|
||||
return produce(state, (draftState) => {
|
||||
draftState.searchStrings[searchString] = true;
|
||||
draftState.events.push(...payload);
|
||||
});
|
||||
}
|
||||
|
||||
case 'REACHED_END': {
|
||||
const {
|
||||
meta: { searchString },
|
||||
} = action;
|
||||
return produce(state, (draftState) => {
|
||||
draftState.reachedEnd = true;
|
||||
draftState.searchStrings[searchString] = true;
|
||||
});
|
||||
}
|
||||
case 'REACHED_END': {
|
||||
const {
|
||||
meta: { searchString },
|
||||
} = action;
|
||||
return produce(state, (draftState) => {
|
||||
draftState.reachedEnd = true;
|
||||
draftState.searchStrings[searchString] = true;
|
||||
});
|
||||
}
|
||||
|
||||
case 'RESET':
|
||||
return initialState;
|
||||
case 'RESET':
|
||||
return initialState;
|
||||
|
||||
default:
|
||||
return state;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -65,7 +64,7 @@ export default function Events({ path: pathname } = {}) {
|
||||
if (Array.isArray(data) && data.length < API_LIMIT) {
|
||||
dispatch({ type: 'REACHED_END', meta: { searchString } });
|
||||
}
|
||||
}, [data]);
|
||||
}, [data, searchString, searchStrings]);
|
||||
|
||||
const observer = useRef(
|
||||
new IntersectionObserver((entries, observer) => {
|
||||
@@ -96,7 +95,7 @@ export default function Events({ path: pathname } = {}) {
|
||||
}
|
||||
}
|
||||
},
|
||||
[observer.current, reachedEnd]
|
||||
[observer, reachedEnd]
|
||||
);
|
||||
|
||||
const handleFilter = useCallback(
|
||||
@@ -121,7 +120,7 @@ export default function Events({ path: pathname } = {}) {
|
||||
<Table className="min-w-full table-fixed">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th></Th>
|
||||
<Th />
|
||||
<Th>Camera</Th>
|
||||
<Th>Label</Th>
|
||||
<Th>Score</Th>
|
||||
@@ -213,7 +212,7 @@ function Filterable({ onFilter, pathname, searchParams, paramName, name }) {
|
||||
params.set(paramName, name);
|
||||
removeDefaultSearchKeys(params);
|
||||
return `${pathname}?${params.toString()}`;
|
||||
}, [searchParams]);
|
||||
}, [searchParams, paramName, pathname, name]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event) => {
|
||||
@@ -223,7 +222,7 @@ function Filterable({ onFilter, pathname, searchParams, paramName, name }) {
|
||||
params.set(paramName, name);
|
||||
onFilter(params);
|
||||
},
|
||||
[href, searchParams]
|
||||
[href, searchParams, onFilter, paramName, name]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { h } from 'preact';
|
||||
import ArrowDropdown from '../icons/ArrowDropdown';
|
||||
import ArrowDropup from '../icons/ArrowDropup';
|
||||
import Card from '../components/Card';
|
||||
import Button from '../components/Button';
|
||||
import Heading from '../components/Heading';
|
||||
import Select from '../components/Select';
|
||||
@@ -22,13 +21,13 @@ export default function StyleGuide() {
|
||||
return (
|
||||
<div>
|
||||
<Heading size="md">Button</Heading>
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<div className="flex space-x-4 mb-4">
|
||||
<Button>Default</Button>
|
||||
<Button color="red">Danger</Button>
|
||||
<Button color="green">Save</Button>
|
||||
<Button disabled>Disabled</Button>
|
||||
</div>
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<div className="flex space-x-4 mb-4">
|
||||
<Button type="text">Default</Button>
|
||||
<Button color="red" type="text">
|
||||
Danger
|
||||
@@ -40,7 +39,7 @@ export default function StyleGuide() {
|
||||
Disabled
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<div className="flex space-x-4 mb-4">
|
||||
<Button type="outlined">Default</Button>
|
||||
<Button color="red" type="outlined">
|
||||
Danger
|
||||
@@ -54,7 +53,7 @@ export default function StyleGuide() {
|
||||
</div>
|
||||
|
||||
<Heading size="md">Switch</Heading>
|
||||
<div class="flex">
|
||||
<div className="flex">
|
||||
<div>
|
||||
<p>Disabled, off</p>
|
||||
<Switch />
|
||||
@@ -74,12 +73,12 @@ export default function StyleGuide() {
|
||||
</div>
|
||||
|
||||
<Heading size="md">Select</Heading>
|
||||
<div class="flex space-x-4 mb-4 max-w-4xl">
|
||||
<div className="flex space-x-4 mb-4 max-w-4xl">
|
||||
<Select label="Basic select box" options={['All', 'None', 'Other']} selected="None" />
|
||||
</div>
|
||||
|
||||
<Heading size="md">TextField</Heading>
|
||||
<div class="flex-col space-y-4 max-w-4xl">
|
||||
<div className="flex-col space-y-4 max-w-4xl">
|
||||
<TextField label="Default text field" />
|
||||
<TextField label="Pre-filled" value="This is my pre-filled value" />
|
||||
<TextField label="With help" helpText="This is some help text" />
|
||||
|
||||
Reference in New Issue
Block a user