diff --git a/web/src/routes/Events.jsx b/web/src/routes/Events.jsx
index 3336ec32b..1a40044e4 100644
--- a/web/src/routes/Events.jsx
+++ b/web/src/routes/Events.jsx
@@ -67,13 +67,13 @@ export default function Events({ path, ...props }) {
const { data: config } = useSWR('config');
- const cameras = useMemo(() => Object.keys(config.cameras), [config]);
+ const cameras = useMemo(() => Object.keys(config?.cameras || {}), [config]);
const zones = useMemo(
() =>
- Object.values(config.cameras)
+ Object.values(config?.cameras || {})
.reduce((memo, camera) => {
- memo = memo.concat(Object.keys(camera.zones));
+ memo = memo.concat(Object.keys(camera?.zones || {}));
return memo;
}, [])
.filter((value, i, self) => self.indexOf(value) === i),
@@ -81,11 +81,11 @@ export default function Events({ path, ...props }) {
);
const labels = useMemo(() => {
- return Object.values(config.cameras)
+ return Object.values(config?.cameras || {})
.reduce((memo, camera) => {
- memo = memo.concat(camera.objects?.track || []);
+ memo = memo.concat(camera?.objects?.track || []);
return memo;
- }, config.objects?.track || [])
+ }, config?.objects?.track || [])
.filter((value, i, self) => self.indexOf(value) === i);
}, [config]);
@@ -123,6 +123,7 @@ export default function Events({ path, ...props }) {
const handleSelectDateRange = useCallback(
(dates) => {
+ console.log(dates);
setSearchParams({ ...searchParams, before: dates.before, after: dates.after });
setShowDatePicker(false);
},
diff --git a/web/src/routes/__tests__/Camera.test.jsx b/web/src/routes/__tests__/Camera.test.jsx
index 5840affef..26347a2bb 100644
--- a/web/src/routes/__tests__/Camera.test.jsx
+++ b/web/src/routes/__tests__/Camera.test.jsx
@@ -1,10 +1,10 @@
import { h } from 'preact';
import * as AutoUpdatingCameraImage from '../../components/AutoUpdatingCameraImage';
-import * as Api from '../../api';
import * as Context from '../../context';
+import * as Mqtt from '../../api/mqtt';
import Camera from '../Camera';
import * as JSMpegPlayer from '../../components/JSMpegPlayer';
-import { fireEvent, render, screen } from '@testing-library/preact';
+import { fireEvent, render, screen, waitForElementToBeRemoved } from 'testing-library';
describe('Camera Route', () => {
let mockUsePersistence, mockSetOptions;
@@ -12,16 +12,13 @@ describe('Camera Route', () => {
beforeEach(() => {
mockSetOptions = jest.fn();
mockUsePersistence = jest.spyOn(Context, 'usePersistence').mockImplementation(() => [{}, mockSetOptions]);
- jest.spyOn(Api, 'useConfig').mockImplementation(() => ({
- data: { cameras: { front: { name: 'front', detect: {width: 1280, height: 720}, live: {height: 720}, objects: { track: ['taco', 'cat', 'dog'] } } } },
- }));
- jest.spyOn(Api, 'useApiHost').mockImplementation(() => 'http://base-url.local:5000');
jest.spyOn(AutoUpdatingCameraImage, 'default').mockImplementation(({ searchParams }) => {
return
{searchParams.toString()}
;
});
jest.spyOn(JSMpegPlayer, 'default').mockImplementation(() => {
return
;
});
+ jest.spyOn(Mqtt, 'MqttProvider').mockImplementation(({ children }) => children);
});
test('reads camera feed options from persistence', async () => {
@@ -39,6 +36,8 @@ describe('Camera Route', () => {
render(
);
+ await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading…'));
+
fireEvent.click(screen.queryByText('Debug'));
fireEvent.click(screen.queryByText('Show Options'));
expect(screen.queryByTestId('mock-image')).toHaveTextContent(
@@ -48,6 +47,7 @@ describe('Camera Route', () => {
test('updates camera feed options to persistence', async () => {
mockUsePersistence
+ .mockReturnValueOnce([{}, mockSetOptions])
.mockReturnValueOnce([{}, mockSetOptions])
.mockReturnValueOnce([{}, mockSetOptions])
.mockReturnValueOnce([{ bbox: true }, mockSetOptions])
@@ -55,13 +55,15 @@ describe('Camera Route', () => {
render(
);
+ await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading…'));
+
fireEvent.click(screen.queryByText('Debug'));
fireEvent.click(screen.queryByText('Show Options'));
fireEvent.change(screen.queryByTestId('bbox-input'), { target: { checked: true } });
fireEvent.change(screen.queryByTestId('timestamp-input'), { target: { checked: true } });
fireEvent.click(screen.queryByText('Hide Options'));
- expect(mockUsePersistence).toHaveBeenCalledTimes(4);
+ expect(mockUsePersistence).toHaveBeenCalledTimes(5);
expect(mockSetOptions).toHaveBeenCalledTimes(2);
expect(mockSetOptions).toHaveBeenCalledWith({ bbox: true, timestamp: true });
expect(screen.queryByTestId('mock-image')).toHaveTextContent('bbox=1×tamp=1');
diff --git a/web/src/routes/__tests__/Cameras.test.jsx b/web/src/routes/__tests__/Cameras.test.jsx
index cdb0c400d..cbfe5a5f9 100644
--- a/web/src/routes/__tests__/Cameras.test.jsx
+++ b/web/src/routes/__tests__/Cameras.test.jsx
@@ -1,30 +1,16 @@
import { h } from 'preact';
-import * as Api from '../../api';
import * as CameraImage from '../../components/CameraImage';
import * as Mqtt from '../../api/mqtt';
import Cameras from '../Cameras';
-import { fireEvent, render, screen } from '@testing-library/preact';
+import { fireEvent, render, screen, waitForElementToBeRemoved } from 'testing-library';
describe('Cameras Route', () => {
- let useConfigMock;
-
beforeEach(() => {
- useConfigMock = jest.spyOn(Api, 'useConfig').mockImplementation(() => ({
- data: {
- cameras: {
- front: { name: 'front', objects: { track: ['taco', 'cat', 'dog'] }, record: { enabled: true } },
- side: { name: 'side', objects: { track: ['taco', 'cat', 'dog'] }, record: { enabled: false } },
- },
- },
- status: 'loaded',
- }));
- jest.spyOn(Api, 'useApiHost').mockImplementation(() => 'http://base-url.local:5000');
jest.spyOn(CameraImage, 'default').mockImplementation(() =>
);
jest.spyOn(Mqtt, 'useMqtt').mockImplementation(() => ({ value: { payload: 'OFF' }, send: jest.fn() }));
});
test('shows an ActivityIndicator if not yet loaded', async () => {
- useConfigMock.mockReturnValueOnce(() => ({ status: 'loading' }));
render(
);
expect(screen.queryByLabelText('Loading…')).toBeInTheDocument();
});
@@ -32,7 +18,7 @@ describe('Cameras Route', () => {
test('shows cameras', async () => {
render(
);
- expect(screen.queryByLabelText('Loading…')).not.toBeInTheDocument();
+ await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading…'));
expect(screen.queryByText('front')).toBeInTheDocument();
expect(screen.queryByText('front').closest('a')).toHaveAttribute('href', '/cameras/front');
@@ -44,7 +30,7 @@ describe('Cameras Route', () => {
test('shows recordings link', async () => {
render(
);
- expect(screen.queryByLabelText('Loading…')).not.toBeInTheDocument();
+ await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading…'));
expect(screen.queryAllByText('Recordings')).toHaveLength(2);
});
@@ -65,6 +51,8 @@ describe('Cameras Route', () => {
render(
);
+ await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading…'));
+
fireEvent.click(screen.getAllByLabelText('Toggle detect off')[0]);
expect(sendDetect).toHaveBeenCalledWith('OFF');
expect(sendDetect).toHaveBeenCalledTimes(1);
diff --git a/web/src/routes/__tests__/Debug.test.jsx b/web/src/routes/__tests__/Debug.test.jsx
index e28f033c1..212d3a291 100644
--- a/web/src/routes/__tests__/Debug.test.jsx
+++ b/web/src/routes/__tests__/Debug.test.jsx
@@ -1,41 +1,19 @@
import { h } from 'preact';
-import * as Api from '../../api';
-import * as Mqtt from '../../api/mqtt';
import Debug from '../Debug';
-import { render, screen } from '@testing-library/preact';
+import { render, screen, waitForElementToBeRemoved } from 'testing-library';
describe('Debug Route', () => {
- let useStatsMock, useMqttMock;
-
- beforeEach(() => {
- jest.spyOn(Api, 'useConfig').mockImplementation(() => ({
- data: {
- service: {
- version: '0.8.3',
- },
- cameras: {
- front: { name: 'front', objects: { track: ['taco', 'cat', 'dog'] } },
- side: { name: 'side', objects: { track: ['taco', 'cat', 'dog'] } },
- },
- mqtt: {
- stats_interva: 60,
- },
- },
- status: 'loaded',
- }));
- useStatsMock = jest.spyOn(Api, 'useStats').mockImplementation(() => ({ data: statsMock }));
- useMqttMock = jest.spyOn(Mqtt, 'useMqtt').mockImplementation(() => ({ value: { payload: null } }));
- });
+ beforeEach(() => {});
test('shows an ActivityIndicator if stats are null', async () => {
- useStatsMock.mockReturnValue({ data: null });
render(
);
expect(screen.queryByLabelText('Loading…')).toBeInTheDocument();
});
test('shows stats and config', async () => {
render(
);
- expect(screen.queryByLabelText('Loading…')).not.toBeInTheDocument();
+
+ await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading…'));
expect(screen.queryByTestId('detectors')).toBeInTheDocument();
expect(screen.queryByText('coral')).toBeInTheDocument();
@@ -47,32 +25,4 @@ describe('Debug Route', () => {
expect(screen.queryByText('Config')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Copy to Clipboard' })).toBeInTheDocument();
});
-
- test('updates the stats from mqtt', async () => {
- const { rerender } = render(
);
- expect(useMqttMock).toHaveBeenCalledWith('stats');
- useMqttMock.mockReturnValue({
- value: {
- payload: { ...statsMock, detectors: { coral: { ...statsMock.detectors.coral, inference_speed: 42.4242 } } },
- },
- });
- rerender(
);
-
- expect(screen.queryByText('42.4242')).toBeInTheDocument();
- });
});
-
-const statsMock = {
- detection_fps: 0.0,
- detectors: { coral: { detection_start: 0.0, inference_speed: 8.94, pid: 52 } },
- front: { camera_fps: 5.0, capture_pid: 64, detection_fps: 0.0, pid: 54, process_fps: 0.0, skipped_fps: 0.0 },
- side: {
- camera_fps: 6.9,
- capture_pid: 71,
- detection_fps: 0.0,
- pid: 60,
- process_fps: 0.0,
- skipped_fps: 0.0,
- },
- service: { uptime: 34812, version: '0.8.1-d376f6b' },
-};
diff --git a/web/src/routes/__tests__/Events.test.jsx b/web/src/routes/__tests__/Events.test.jsx
index 9f033859d..3fef6ed85 100644
--- a/web/src/routes/__tests__/Events.test.jsx
+++ b/web/src/routes/__tests__/Events.test.jsx
@@ -1,28 +1,9 @@
import { h } from 'preact';
-import * as Api from '../../api';
-import * as Hooks from '../../hooks';
import Events from '../Events';
-import { render, screen } from '@testing-library/preact';
+import { render, screen, waitForElementToBeRemoved } from 'testing-library';
describe('Events Route', () => {
- let useEventsMock, useIntersectionMock;
-
- beforeEach(() => {
- useEventsMock = jest.spyOn(Api, 'useEvents').mockImplementation(() => ({
- data: null,
- status: 'loading',
- }));
- jest.spyOn(Api, 'useConfig').mockImplementation(() => ({
- data: {
- cameras: {
- front: { name: 'front', objects: { track: ['taco', 'cat', 'dog'] }, zones: [] },
- side: { name: 'side', objects: { track: ['taco', 'cat', 'dog'] }, zones: [] },
- },
- },
- }));
- jest.spyOn(Api, 'useApiHost').mockImplementation(() => 'http://localhost:5000');
- useIntersectionMock = jest.spyOn(Hooks, 'useIntersectionObserver').mockImplementation(() => [null, jest.fn()]);
- });
+ beforeEach(() => {});
test('shows an ActivityIndicator if not yet loaded', async () => {
render(
);
@@ -30,53 +11,10 @@ describe('Events Route', () => {
});
test('does not show ActivityIndicator after loaded', async () => {
- useEventsMock.mockReturnValue({ data: mockEvents, status: 'loaded' });
render(
);
+
+ await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading…'));
+
expect(screen.queryByLabelText('Loading…')).not.toBeInTheDocument();
});
-
- test('loads more when the intersectionObserver fires', async () => {
- const setIntersectionNode = jest.fn();
- useIntersectionMock.mockReturnValue([null, setIntersectionNode]);
- useEventsMock.mockImplementation((searchString) => {
- if (searchString.includes('before=')) {
- const params = new URLSearchParams(searchString);
- const before = parseFloat(params.get('before'));
- const index = mockEvents.findIndex((el) => el.start_time === before + 0.0001);
- return { data: mockEvents.slice(index, index + 5), status: 'loaded' };
- }
-
- return { data: mockEvents.slice(0, 5), status: 'loaded' };
- });
-
- const { rerender } = render(
);
- expect(setIntersectionNode).toHaveBeenCalled();
- expect(useEventsMock).toHaveBeenCalledWith('include_thumbnails=0&limit=5&');
- expect(screen.queryAllByTestId(/event-\d+/)).toHaveLength(5);
-
- useIntersectionMock.mockReturnValue([
- {
- isIntersecting: true,
- target: { dataset: { startTime: mockEvents[4].start_time } },
- },
- setIntersectionNode,
- ]);
- rerender(
);
- expect(useEventsMock).toHaveBeenCalledWith(
- `include_thumbnails=0&limit=5&before=${mockEvents[4].start_time - 0.0001}`
- );
- expect(screen.queryAllByTestId(/event-\d+/)).toHaveLength(10);
- });
});
-
-const mockEvents = new Array(12).fill(null).map((v, i) => ({
- end_time: 1613257337 + i,
- has_clip: true,
- has_snapshot: true,
- id: i,
- label: 'person',
- start_time: 1613257326 + i,
- top_score: Math.random(),
- zones: ['front_patio'],
- thumbnail: '/9j/4aa...',
-}));
diff --git a/web/src/utils/Timeline/timelineEventUtils.ts b/web/src/utils/Timeline/timelineEventUtils.ts
index ac3d70290..484afbd40 100644
--- a/web/src/utils/Timeline/timelineEventUtils.ts
+++ b/web/src/utils/Timeline/timelineEventUtils.ts
@@ -70,4 +70,5 @@ export const getTimelineWidthFromBlocks = (blocks: TimelineEventBlock[], offset:
const timelineDurationLong = epochToLong(endTimeEpoch - startTimeEpoch);
return timelineDurationLong + offset * 2;
}
+ return 0;
};
diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts
new file mode 100644
index 000000000..11f02fe2a
--- /dev/null
+++ b/web/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/web/tailwind.config.js b/web/tailwind.config.js
index e23eb8267..93e713e59 100644
--- a/web/tailwind.config.js
+++ b/web/tailwind.config.js
@@ -1,30 +1,24 @@
module.exports = {
- mode: 'jit',
- content: [
- "./public/**/*.html",
- "./src/**/*.{js,jsx,ts,tsx}",
- ],
- darkMode: 'class',
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
screens: {
- xs: '480px',
- '2xl': '1536px',
- '3xl': '1720px',
+ xs: "480px",
+ "2xl": "1536px",
+ "3xl": "1720px",
},
},
boxShadow: {
- sm: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)',
- DEFAULT: '0px 2px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 3px 0px rgb(0 0 0 / 12%);',
- md: '0px 2px 2px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 3px 0px rgb(0 0 0 / 12%);',
- lg: '0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23)',
- xl: '0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22)',
- '2xl': '0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22)',
- '3xl': '',
- none: '',
+ sm: "0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)",
+ DEFAULT:
+ "0px 2px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 3px 0px rgb(0 0 0 / 12%);",
+ md: "0px 2px 2px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 3px 0px rgb(0 0 0 / 12%);",
+ lg: "0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23)",
+ xl: "0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22)",
+ "2xl": "0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22)",
+ "3xl": "",
+ none: "",
},
},
- plugins: [
- require('@tailwindcss/forms'),
- ],
+ plugins: [require("@tailwindcss/forms")],
};
diff --git a/web/tsconfig.json b/web/tsconfig.json
index e656a92cb..0a24dec18 100644
--- a/web/tsconfig.json
+++ b/web/tsconfig.json
@@ -1,28 +1,23 @@
{
- "include": ["./src/**/*.tsx", "./src/**/*.ts"],
"compilerOptions": {
- "module": "esnext",
- "target": "esnext",
- "moduleResolution": "node",
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
"jsx": "preserve",
"jsxFactory": "h",
- "baseUrl": "./",
- /* paths - import rewriting/resolving */
- "paths": {
- // If you configured any Snowpack aliases, add them here.
- // Add this line to get types for streaming imports (packageOptions.source="remote"):
- // "*": [".snowpack/types/*"]
- // More info: https://www.snowpack.dev/guides/streaming-imports
- },
- /* noEmit - Snowpack builds (emits) files, not tsc. */
- "noEmit": true,
- /* Additional Options */
- "strict": false,
- "skipLibCheck": true,
- // "types": ["mocha", "snowpack-env"],
- "forceConsistentCasingInFileNames": true,
- "resolveJsonModule": true,
- "allowSyntheticDefaultImports": true,
- "importsNotUsedAsValues": "error"
- }
-}
\ No newline at end of file
+ "jsxFragmentFactory": "Fragment"
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json
new file mode 100644
index 000000000..e993792cb
--- /dev/null
+++ b/web/tsconfig.node.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "module": "esnext",
+ "moduleResolution": "node"
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/web/vite.config.ts b/web/vite.config.ts
new file mode 100644
index 000000000..29b326faf
--- /dev/null
+++ b/web/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import preact from '@preact/preset-vite'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [preact()],
+})