switch from create-react-app to vite
create-react-app is apparently deprecated, so the cool kids use vite, I guess.
This commit is contained in:
parent
79af39f35e
commit
24880a5c2d
|
@ -63,7 +63,7 @@ jobs:
|
||||||
name: Node ${{ matrix.node }}
|
name: Node ${{ matrix.node }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: [ "14", "16", "18" ]
|
node: [ "18", "20", "21" ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
|
@ -35,7 +35,7 @@ jobs:
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: moonfire-nvr-ui-${{ github.ref_name }}
|
name: moonfire-nvr-ui-${{ github.ref_name }}
|
||||||
path: ui/build
|
path: ui/dist
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
@ -69,7 +69,7 @@ jobs:
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: moonfire-nvr-ui-${{ github.ref_name }}
|
name: moonfire-nvr-ui-${{ github.ref_name }}
|
||||||
path: ui/build
|
path: ui/dist
|
||||||
|
|
||||||
# actions-rust-cross doesn't actually use cross for x86_64.
|
# actions-rust-cross doesn't actually use cross for x86_64.
|
||||||
# Install the needed musl-tools in the host.
|
# Install the needed musl-tools in the host.
|
||||||
|
@ -79,7 +79,7 @@ jobs:
|
||||||
- name: Build
|
- name: Build
|
||||||
uses: houseabsolute/actions-rust-cross@v0
|
uses: houseabsolute/actions-rust-cross@v0
|
||||||
env:
|
env:
|
||||||
UI_BUILD_DIR: ../ui/build
|
UI_BUILD_DIR: ../ui/dist
|
||||||
|
|
||||||
# cross doesn't install `git` within its Docker container, so plumb
|
# cross doesn't install `git` within its Docker container, so plumb
|
||||||
# the version through rather than try `git describe` from `build.rs`.
|
# the version through rather than try `git describe` from `build.rs`.
|
||||||
|
|
|
@ -38,7 +38,9 @@ can skip this if compiling with `--features=rusqlite/bundled` and don't
|
||||||
mind the `moonfire-nvr sql` command not working.
|
mind the `moonfire-nvr sql` command not working.
|
||||||
|
|
||||||
To build the UI, you'll need a [nodejs](https://nodejs.org/en/download/) release
|
To build the UI, you'll need a [nodejs](https://nodejs.org/en/download/) release
|
||||||
in "Maintenance" or "LTS" status: currently v14, v16, or v18.
|
in "Maintenance", "LTS", or "Current" status on the
|
||||||
|
[Release Schedule](https://github.com/nodejs/release#release-schedule):
|
||||||
|
currently v18, v20, or v21.
|
||||||
|
|
||||||
On recent Ubuntu or Raspbian Linux, the following command will install
|
On recent Ubuntu or Raspbian Linux, the following command will install
|
||||||
most non-Rust dependencies:
|
most non-Rust dependencies:
|
||||||
|
@ -90,7 +92,7 @@ $ npm install
|
||||||
$ npm run build
|
$ npm run build
|
||||||
$ sudo mkdir /usr/local/lib/moonfire-nvr
|
$ sudo mkdir /usr/local/lib/moonfire-nvr
|
||||||
$ cd ..
|
$ cd ..
|
||||||
$ sudo rsync --recursive --delete --chmod=D755,F644 ui/build/ /usr/local/lib/moonfire-nvr/ui
|
$ sudo rsync --recursive --delete --chmod=D755,F644 ui/dist/ /usr/local/lib/moonfire-nvr/ui
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running interactively straight from the working copy
|
### Running interactively straight from the working copy
|
||||||
|
@ -100,7 +102,7 @@ the binaries in the working copy will run via just `nvr`:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ sudo mkdir /usr/local/lib/moonfire-nvr
|
$ sudo mkdir /usr/local/lib/moonfire-nvr
|
||||||
$ sudo ln -s `pwd`/ui/build /usr/local/lib/moonfire-nvr/ui
|
$ sudo ln -s `pwd`/ui/dist /usr/local/lib/moonfire-nvr/ui
|
||||||
$ sudo mkdir /var/lib/moonfire-nvr
|
$ sudo mkdir /var/lib/moonfire-nvr
|
||||||
$ sudo chown $USER: /var/lib/moonfire-nvr
|
$ sudo chown $USER: /var/lib/moonfire-nvr
|
||||||
$ ln -s `pwd`/server/target/release/moonfire-nvr $HOME/bin/moonfire-nvr
|
$ ln -s `pwd`/server/target/release/moonfire-nvr $HOME/bin/moonfire-nvr
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
The UI is presented from a single HTML page (index.html) and any number
|
The UI is presented from a single HTML page (index.html) and any number
|
||||||
of Javascript files, css files, images, etc. These are "packed" together
|
of Javascript files, css files, images, etc. These are "packed" together
|
||||||
using [webpack](https://webpack.js.org).
|
using [vite](https://vitejs.dev/).
|
||||||
|
|
||||||
For ongoing development it is possible to have the UI running in a web
|
For ongoing development it is possible to have the UI running in a web
|
||||||
browser using "hot loading". This means that as you make changes to source
|
browser using "hot loading". This means that as you make changes to source
|
||||||
|
@ -26,12 +26,12 @@ Checkout the branch you want to work on and type
|
||||||
|
|
||||||
```
|
```
|
||||||
$ cd ui
|
$ cd ui
|
||||||
$ npm run start
|
$ npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
This will pack and prepare a development setup. By default the development
|
This will pack and prepare a development setup. By default the development
|
||||||
server that serves up the web page(s) will listen on
|
server that serves up the web page(s) will listen on
|
||||||
[http://localhost:3000/](http://localhost:3000/) so you can direct your browser
|
[http://localhost:5173/](http://localhost:5173/) so you can direct your browser
|
||||||
there. It assumes the Moonfire NVR server is running at
|
there. It assumes the Moonfire NVR server is running at
|
||||||
[http://localhost:8080/](http://localhost:8080/) and will proxy API requests
|
[http://localhost:8080/](http://localhost:8080/) and will proxy API requests
|
||||||
there.
|
there.
|
||||||
|
@ -45,32 +45,25 @@ process, but some will show up in the browser console, or both.
|
||||||
|
|
||||||
## Overriding defaults
|
## Overriding defaults
|
||||||
|
|
||||||
The UI is setup with [Create React App](https://create-react-app.dev/).
|
Currently there's only one supported environment variable override defined in
|
||||||
`npm run start` will honor any of the environment variables described in their
|
`ui/vite.config.ts`:
|
||||||
[Advanced Configuration](https://create-react-app.dev/docs/advanced-configuration/),
|
|
||||||
as well as Moonfire NVR's custom `PROXY_TARGET` variable. Quick reference:
|
|
||||||
|
|
||||||
| variable | description | default |
|
| variable | description | default |
|
||||||
| :------------- | :----------------------------------------------------------------------- | :----------------------- |
|
| :------------- | :------------------------------------------ | :----------------------- |
|
||||||
| `PROXY_TARGET` | base URL of the backing Moonfire NVR server (see `ui/src/setupProxy.js`) | `http://localhost:8080/` |
|
| `PROXY_TARGET` | base URL of the backing Moonfire NVR server | `http://localhost:8080/` |
|
||||||
| `PORT` | port to listen on | 3000 |
|
|
||||||
| `HOST` | host/IP to listen on (or `0.0.0.0` for all) | `0.0.0.0` |
|
|
||||||
|
|
||||||
Thus one could connect to a remote Moonfire NVR by specifying its URL as
|
Thus one could connect to a remote Moonfire NVR by specifying its URL as
|
||||||
follows:
|
follows:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ PROXY_TARGET=https://nvr.example.com/ npm run start
|
$ PROXY_TARGET=https://nvr.example.com/ npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
This allows you to test a new UI against your stable, production Moonfire NVR
|
This allows you to test a new UI against your stable, production Moonfire NVR
|
||||||
installation with real data.
|
installation with real data.
|
||||||
|
|
||||||
**Note:** the live stream currently does not work in combination with
|
|
||||||
`PROXY_TARGET` due to [#290](https://github.com/scottlamb/moonfire-nvr/issues/290).
|
|
||||||
|
|
||||||
You can also set environment variables in `.env` files, as described in
|
You can also set environment variables in `.env` files, as described in
|
||||||
[Adding Custom Environment Variables](https://create-react-app.dev/docs/adding-custom-environment-variables/).
|
[vitejs.dev: Env Variables and Modes](https://vitejs.dev/guide/env-and-mode).
|
||||||
|
|
||||||
## A note on `https`
|
## A note on `https`
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,8 @@ use std::fmt::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
const UI_BUILD_DIR_ENV_VAR: &str = "UI_BUILD_DIR";
|
const UI_DIST_DIR_ENV_VAR: &str = "UI_DIST_DIR";
|
||||||
const DEFAULT_UI_BUILD_DIR: &str = "../ui/build";
|
const DEFAULT_UI_DIST_DIR: &str = "../ui/dist";
|
||||||
|
|
||||||
type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
|
type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ impl FileEncoding {
|
||||||
/// Map of "bare path" to the best representation.
|
/// Map of "bare path" to the best representation.
|
||||||
///
|
///
|
||||||
/// A "bare path" has no prefix for the root and no suffix for encoding, e.g.
|
/// A "bare path" has no prefix for the root and no suffix for encoding, e.g.
|
||||||
/// `favicons/blah.ico` rather than `../../ui/build/favicons/blah.ico.gz`.
|
/// `favicons/blah.ico` rather than `../../ui/dist/favicons/blah.ico.gz`.
|
||||||
///
|
///
|
||||||
/// The best representation is gzipped if available, uncompressed otherwise.
|
/// The best representation is gzipped if available, uncompressed otherwise.
|
||||||
type FileMap = fnv::FnvHashMap<String, File>;
|
type FileMap = fnv::FnvHashMap<String, File>;
|
||||||
|
@ -78,10 +78,10 @@ fn handle_bundled_ui() -> Result<(), BoxError> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let ui_dir =
|
let ui_dir =
|
||||||
std::env::var(UI_BUILD_DIR_ENV_VAR).unwrap_or_else(|_| DEFAULT_UI_BUILD_DIR.to_owned());
|
std::env::var(UI_DIST_DIR_ENV_VAR).unwrap_or_else(|_| DEFAULT_UI_DIST_DIR.to_owned());
|
||||||
|
|
||||||
// If the feature is on, also re-run if the actual UI files change.
|
// If the feature is on, also re-run if the actual UI files change.
|
||||||
println!("cargo:rerun-if-env-changed={UI_BUILD_DIR_ENV_VAR}");
|
println!("cargo:rerun-if-env-changed={UI_DIST_DIR_ENV_VAR}");
|
||||||
println!("cargo:rerun-if-changed={ui_dir}");
|
println!("cargo:rerun-if-changed={ui_dir}");
|
||||||
|
|
||||||
let out_dir: PathBuf = std::env::var_os("OUT_DIR")
|
let out_dir: PathBuf = std::env::var_os("OUT_DIR")
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/dist
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||||
|
// Copyright (C) 2023 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
||||||
|
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
|
||||||
|
|
||||||
|
// Environment based on `jsdom` with some extra globals, inspired by
|
||||||
|
// the following comment:
|
||||||
|
// https://github.com/jsdom/jsdom/issues/1724#issuecomment-1446858041
|
||||||
|
|
||||||
|
import JSDOMEnvironment from "jest-environment-jsdom";
|
||||||
|
|
||||||
|
// https://github.com/facebook/jest/blob/v29.4.3/website/versioned_docs/version-29.4/Configuration.md#testenvironment-string
|
||||||
|
export default class FixJSDOMEnvironment extends JSDOMEnvironment {
|
||||||
|
constructor(...args: ConstructorParameters<typeof JSDOMEnvironment>) {
|
||||||
|
super(...args);
|
||||||
|
|
||||||
|
// Tests use fetch calls with relative URLs + msw to intercept.
|
||||||
|
this.global.fetch = (
|
||||||
|
resource: RequestInfo | URL,
|
||||||
|
options?: RequestInit
|
||||||
|
) => {
|
||||||
|
throw "must use msw to fetch: " + resource;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MyRequest extends Request {
|
||||||
|
constructor(input: RequestInfo | URL, init?: RequestInit | undefined) {
|
||||||
|
input = new URL(input as string, "http://localhost");
|
||||||
|
super(input, init);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.global.Headers = Headers;
|
||||||
|
this.global.Request = MyRequest;
|
||||||
|
this.global.Response = Response;
|
||||||
|
|
||||||
|
// `src/LiveCamera/parser.ts` uses TextDecoder.
|
||||||
|
this.global.TextDecoder = TextDecoder;
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,24 +11,24 @@
|
||||||
<link
|
<link
|
||||||
rel="apple-touch-icon"
|
rel="apple-touch-icon"
|
||||||
sizes="180x180"
|
sizes="180x180"
|
||||||
href="%PUBLIC_URL%/favicons/apple-touch-icon-94a09b5d2ddb5af47.png"
|
href="favicons/apple-touch-icon-94a09b5d2ddb5af47.png"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="icon"
|
rel="icon"
|
||||||
type="image/png"
|
type="image/png"
|
||||||
sizes="32x32"
|
sizes="32x32"
|
||||||
href="%PUBLIC_URL%/favicons/favicon-32x32-ab95901a9e0d040e2.png"
|
href="favicons/favicon-32x32-ab95901a9e0d040e2.png"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="icon"
|
rel="icon"
|
||||||
type="image/png"
|
type="image/png"
|
||||||
sizes="16x16"
|
sizes="16x16"
|
||||||
href="%PUBLIC_URL%/favicons/favicon-16x16-b16b3f2883aacf9f1.png"
|
href="favicons/favicon-16x16-b16b3f2883aacf9f1.png"
|
||||||
/>
|
/>
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" />
|
<link rel="manifest" href="site.webmanifest" />
|
||||||
<link
|
<link
|
||||||
rel="mask-icon"
|
rel="mask-icon"
|
||||||
href="%PUBLIC_URL%/favicons/safari-pinned-tab-9792c2c82f04639f8.svg"
|
href="favicons/safari-pinned-tab-9792c2c82f04639f8.svg"
|
||||||
color="#e04e1b"
|
color="#e04e1b"
|
||||||
/>
|
/>
|
||||||
<meta name="theme-color" content="#e04e1b" />
|
<meta name="theme-color" content="#e04e1b" />
|
||||||
|
@ -42,5 +42,6 @@
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root" style="display: flex; flex-direction: column"></div>
|
<div id="root" style="display: flex; flex-direction: column"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -0,0 +1,36 @@
|
||||||
|
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||||
|
// Copyright (C) 2023 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
||||||
|
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
|
||||||
|
|
||||||
|
import type { Config } from "jest";
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
testEnvironment: "./FixJSDomEnvironment.ts",
|
||||||
|
|
||||||
|
transform: {
|
||||||
|
// https://github.com/swc-project/jest
|
||||||
|
"\\.[tj]sx?$": [
|
||||||
|
"@swc/jest",
|
||||||
|
{
|
||||||
|
// https://swc.rs/docs/configuration/compilation
|
||||||
|
// https://github.com/swc-project/jest/issues/167#issuecomment-1809868077
|
||||||
|
jsc: {
|
||||||
|
transform: {
|
||||||
|
react: {
|
||||||
|
runtime: "automatic",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
|
||||||
|
|
||||||
|
// https://github.com/jaredLunde/react-hook/issues/300#issuecomment-1845227937
|
||||||
|
moduleNameMapper: {
|
||||||
|
"@react-hook/(.*)": "<rootDir>/node_modules/@react-hook/$1/dist/main",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
File diff suppressed because it is too large
Load Diff
106
ui/package.json
106
ui/package.json
|
@ -2,6 +2,7 @@
|
||||||
"name": "ui",
|
"name": "ui",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.8.2",
|
"@emotion/react": "^11.8.2",
|
||||||
"@emotion/styled": "^11.8.1",
|
"@emotion/styled": "^11.8.1",
|
||||||
|
@ -10,36 +11,50 @@
|
||||||
"@mui/lab": "^5.0.0-alpha.102",
|
"@mui/lab": "^5.0.0-alpha.102",
|
||||||
"@mui/material": "^5.10.8",
|
"@mui/material": "^5.10.8",
|
||||||
"@mui/x-date-pickers": "^6.16.3",
|
"@mui/x-date-pickers": "^6.16.3",
|
||||||
"@react-hook/resize-observer": "^1.2.5",
|
"@react-hook/resize-observer": "^1.2.6",
|
||||||
"@types/node": "^18.8.1",
|
|
||||||
"@types/react": "^18.0.26",
|
|
||||||
"@types/react-dom": "^18.0.10",
|
|
||||||
"date-fns": "^2.28.0",
|
"date-fns": "^2.28.0",
|
||||||
"date-fns-tz": "^1.3.0",
|
"date-fns-tz": "^2.0.0",
|
||||||
"gzipper": "^7.0.0",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.41.5",
|
"react-hook-form": "^7.41.5",
|
||||||
"react-hook-form-mui": "^6.5.2",
|
"react-hook-form-mui": "^6.5.2",
|
||||||
"react-router-dom": "^6.2.2",
|
"react-router-dom": "^6.2.2"
|
||||||
"react-scripts": "^5.0.0",
|
|
||||||
"typescript": "^4.9.4"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"check-format": "prettier --check --ignore-path .gitignore .",
|
||||||
"build": "react-scripts build && gzipper compress --exclude=png,woff2 --remove-larger ./build",
|
"dev": "vite",
|
||||||
"test": "react-scripts test",
|
"build": "tsc && vite build",
|
||||||
"eject": "react-scripts eject",
|
"format": "prettier --write --ignore-path .gitignore .",
|
||||||
"format": "prettier --write src/ public/",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"check-format": "prettier --check src/ public/",
|
"preview": "vite preview",
|
||||||
"lint": "eslint src"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
"react-app",
|
"eslint:recommended",
|
||||||
"react-app/jest"
|
"plugin:jest/recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react/jsx-runtime",
|
||||||
|
"plugin:react-hooks/recommended"
|
||||||
],
|
],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.ts",
|
||||||
|
"*.tsx"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"no-undef": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"jest/no-disabled-tests": "off",
|
||||||
"no-restricted-imports": [
|
"no-restricted-imports": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
|
@ -50,29 +65,52 @@
|
||||||
"name": "@mui/icons-material",
|
"name": "@mui/icons-material",
|
||||||
"message": "Please use the 'import MenuIcon from \"material-ui/icons/Menu\";' style instead; see https://material-ui.com/guides/minimizing-bundle-size/#option-1"
|
"message": "Please use the 'import MenuIcon from \"material-ui/icons/Menu\";' style instead; see https://material-ui.com/guides/minimizing-bundle-size/#option-1"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"args": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"react/no-unescaped-entities": "off"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.23.5",
|
||||||
|
"@babel/preset-env": "^7.23.6",
|
||||||
|
"@babel/preset-react": "^7.23.3",
|
||||||
|
"@babel/preset-typescript": "^7.23.3",
|
||||||
|
"@swc/core": "^1.3.100",
|
||||||
|
"@swc/jest": "^0.2.29",
|
||||||
"@testing-library/dom": "^8.11.3",
|
"@testing-library/dom": "^8.11.3",
|
||||||
"@testing-library/jest-dom": "^5.16.2",
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^14.4.3",
|
"@testing-library/user-event": "^14.4.3",
|
||||||
"@types/jest": "^29.2.5",
|
"@types/jest": "^29.5.11",
|
||||||
|
"@types/node": "^18.8.1",
|
||||||
|
"@types/react": "^18.0.26",
|
||||||
|
"@types/react-dom": "^18.0.10",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||||
|
"@typescript-eslint/parser": "^6.14.0",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
|
"eslint": "^8.55.0",
|
||||||
|
"eslint-plugin-jest": "^27.6.0",
|
||||||
|
"eslint-plugin-react": "^7.33.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
"http-proxy-middleware": "^2.0.4",
|
"http-proxy-middleware": "^2.0.4",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"msw": "^1.3.2",
|
"msw": "^1.3.2",
|
||||||
"prettier": "^2.6.0"
|
"prettier": "^2.6.0",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.1.0",
|
||||||
|
"vite": "^5.0.8",
|
||||||
|
"vite-plugin-compression": "^0.5.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,17 @@
|
||||||
import { screen } from "@testing-library/react";
|
import { screen } from "@testing-library/react";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import { renderWithCtx } from "./testutil";
|
import { renderWithCtx } from "./testutil";
|
||||||
|
import { rest } from "msw";
|
||||||
|
import { setupServer } from "msw/node";
|
||||||
|
|
||||||
|
const server = setupServer(
|
||||||
|
rest.get("/api/", (req, res, ctx) => {
|
||||||
|
return res(ctx.status(503), ctx.text("server error"));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
|
||||||
|
afterEach(() => server.resetHandlers());
|
||||||
|
afterAll(() => server.close());
|
||||||
|
|
||||||
test("instantiate", async () => {
|
test("instantiate", async () => {
|
||||||
renderWithCtx(<App />);
|
renderWithCtx(<App />);
|
||||||
|
|
|
@ -62,9 +62,9 @@ const ChangePassword = ({ user, open, handleClose }: Props) => {
|
||||||
if (loading === null) {
|
if (loading === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let abort = new AbortController();
|
const abort = new AbortController();
|
||||||
const send = async (signal: AbortSignal) => {
|
const send = async (signal: AbortSignal) => {
|
||||||
let response = await api.updateUser(
|
const response = await api.updateUser(
|
||||||
loading.userId,
|
loading.userId,
|
||||||
{
|
{
|
||||||
csrf: loading.csrf,
|
csrf: loading.csrf,
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { render, screen } from "@testing-library/react";
|
||||||
import ErrorBoundary from "./ErrorBoundary";
|
import ErrorBoundary from "./ErrorBoundary";
|
||||||
|
|
||||||
const ThrowsLiteralComponent = () => {
|
const ThrowsLiteralComponent = () => {
|
||||||
throw "simple string error"; // eslint-disable-line no-throw-literal
|
throw "simple string error";
|
||||||
};
|
};
|
||||||
|
|
||||||
test("renders string error", () => {
|
test("renders string error", () => {
|
||||||
|
|
|
@ -43,7 +43,7 @@ class MoonfireErrorBoundary extends React.Component<Props, State> {
|
||||||
const { children } = this.props;
|
const { children } = this.props;
|
||||||
|
|
||||||
if (this.state.error !== null) {
|
if (this.state.error !== null) {
|
||||||
var error;
|
let error;
|
||||||
if (this.state.error.stack !== undefined) {
|
if (this.state.error.stack !== undefined) {
|
||||||
error = <pre>{this.state.error.stack}</pre>;
|
error = <pre>{this.state.error.stack}</pre>;
|
||||||
} else if (this.state.error instanceof Error) {
|
} else if (this.state.error instanceof Error) {
|
||||||
|
|
|
@ -8,7 +8,6 @@ import InputLabel from "@mui/material/InputLabel";
|
||||||
import FormControl from "@mui/material/FormControl";
|
import FormControl from "@mui/material/FormControl";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import Select from "@mui/material/Select";
|
import Select from "@mui/material/Select";
|
||||||
import React from "react";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
|
|
||||||
|
|
|
@ -298,7 +298,7 @@ function daysStateReducer(old: DaysState, op: DaysOp): DaysState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "set-end-day":
|
case "set-end-day": {
|
||||||
const millis = toMillis(op.newEndDate);
|
const millis = toMillis(op.newEndDate);
|
||||||
if (
|
if (
|
||||||
state.rangeMillis === null ||
|
state.rangeMillis === null ||
|
||||||
|
@ -310,6 +310,7 @@ function daysStateReducer(old: DaysState, op: DaysOp): DaysState {
|
||||||
state.rangeMillis[1] = millis;
|
state.rangeMillis[1] = millis;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "set-end-type":
|
case "set-end-type":
|
||||||
state.endType = op.newEndType;
|
state.endType = op.newEndType;
|
||||||
if (state.endType === "same-day" && state.rangeMillis !== null) {
|
if (state.endType === "same-day" && state.rangeMillis !== null) {
|
||||||
|
|
|
@ -29,7 +29,7 @@ export function parsePart(raw: Uint8Array): ParseResult {
|
||||||
// Parse into headers and body.
|
// Parse into headers and body.
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
let pos = 0;
|
let pos = 0;
|
||||||
while (true) {
|
for (;;) {
|
||||||
const cr = raw.indexOf(CR, pos);
|
const cr = raw.indexOf(CR, pos);
|
||||||
if (cr === -1 || raw.length === cr + 1 || raw[cr + 1] !== NL) {
|
if (cr === -1 || raw.length === cr + 1 || raw[cr + 1] !== NL) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -74,7 +74,7 @@ test("success", async () => {
|
||||||
// I think the problem is that npmjs doesn't really support aborting requests,
|
// I think the problem is that npmjs doesn't really support aborting requests,
|
||||||
// so the delay("infinite") request just sticks around, even though the fetch
|
// so the delay("infinite") request just sticks around, even though the fetch
|
||||||
// has been aborted. Maybe https://github.com/mswjs/msw/pull/585 will fix it.
|
// has been aborted. Maybe https://github.com/mswjs/msw/pull/585 will fix it.
|
||||||
xtest("close while pending", async () => {
|
test.skip("close while pending", async () => {
|
||||||
const handleClose = jest.fn().mockName("handleClose");
|
const handleClose = jest.fn().mockName("handleClose");
|
||||||
const onSuccess = jest.fn().mockName("handleOpen");
|
const onSuccess = jest.fn().mockName("handleOpen");
|
||||||
const { rerender } = renderWithCtx(
|
const { rerender } = renderWithCtx(
|
||||||
|
@ -95,7 +95,7 @@ xtest("close while pending", async () => {
|
||||||
|
|
||||||
// TODO: fix and re-enable this test.
|
// TODO: fix and re-enable this test.
|
||||||
// It depends on the timers; see TODO above.
|
// It depends on the timers; see TODO above.
|
||||||
xtest("bad credentials", async () => {
|
test.skip("bad credentials", async () => {
|
||||||
const handleClose = jest.fn().mockName("handleClose");
|
const handleClose = jest.fn().mockName("handleClose");
|
||||||
const onSuccess = jest.fn().mockName("handleOpen");
|
const onSuccess = jest.fn().mockName("handleOpen");
|
||||||
renderWithCtx(
|
renderWithCtx(
|
||||||
|
@ -109,7 +109,7 @@ xtest("bad credentials", async () => {
|
||||||
|
|
||||||
// TODO: fix and re-enable this test.
|
// TODO: fix and re-enable this test.
|
||||||
// It depends on the timers; see TODO above.
|
// It depends on the timers; see TODO above.
|
||||||
xtest("server error", async () => {
|
test.skip("server error", async () => {
|
||||||
const handleClose = jest.fn().mockName("handleClose");
|
const handleClose = jest.fn().mockName("handleClose");
|
||||||
const onSuccess = jest.fn().mockName("handleOpen");
|
const onSuccess = jest.fn().mockName("handleOpen");
|
||||||
renderWithCtx(
|
renderWithCtx(
|
||||||
|
@ -126,7 +126,7 @@ xtest("server error", async () => {
|
||||||
|
|
||||||
// TODO: fix and re-enable this test.
|
// TODO: fix and re-enable this test.
|
||||||
// It depends on the timers; see TODO above.
|
// It depends on the timers; see TODO above.
|
||||||
xtest("network error", async () => {
|
test.skip("network error", async () => {
|
||||||
const handleClose = jest.fn().mockName("handleClose");
|
const handleClose = jest.fn().mockName("handleClose");
|
||||||
const onSuccess = jest.fn().mockName("handleOpen");
|
const onSuccess = jest.fn().mockName("handleOpen");
|
||||||
renderWithCtx(
|
renderWithCtx(
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
|
||||||
// Copyright (C) 2021 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
|
||||||
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
|
|
||||||
|
|
||||||
// https://create-react-app.dev/docs/proxying-api-requests-in-development/
|
|
||||||
|
|
||||||
const { createProxyMiddleware } = require("http-proxy-middleware");
|
|
||||||
|
|
||||||
const target = process.env.PROXY_TARGET || "http://localhost:8080/";
|
|
||||||
|
|
||||||
module.exports = (app) => {
|
|
||||||
app.use(
|
|
||||||
"/api",
|
|
||||||
|
|
||||||
// Note: the `/api` here seems redundant with that above, but without it, the
|
|
||||||
// `ws: true` here appears to break react-script's automatic websocket reloading.
|
|
||||||
createProxyMiddleware("/api", {
|
|
||||||
target,
|
|
||||||
ws: true,
|
|
||||||
|
|
||||||
// XXX: this doesn't appear to work for websocket requests. See
|
|
||||||
// <https://github.com/scottlamb/moonfire-nvr/issues/290>
|
|
||||||
changeOrigin: true,
|
|
||||||
|
|
||||||
// If the backing host is https, Moonfire NVR will set a 'secure'
|
|
||||||
// attribute on cookie responses, so that the browser will only send
|
|
||||||
// them over https connections. This is a good security practice, but
|
|
||||||
// it means a non-https development proxy server won't work. Strip out
|
|
||||||
// this attribute in the proxy with code from here:
|
|
||||||
// https://github.com/chimurai/http-proxy-middleware/issues/169#issuecomment-575027907
|
|
||||||
// See also discussion in guide/developing-ui.md.
|
|
||||||
onProxyRes: (proxyRes, req, res) => {
|
|
||||||
const sc = proxyRes.headers["set-cookie"];
|
|
||||||
if (Array.isArray(sc)) {
|
|
||||||
proxyRes.headers["set-cookie"] = sc.map((sc) => {
|
|
||||||
return sc
|
|
||||||
.split(";")
|
|
||||||
.filter((v) => v.trim().toLowerCase() !== "secure")
|
|
||||||
.join("; ");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// The `changeOrigin` above doesn't appear to apply to WebSocket requests.
|
|
||||||
// This has a similar effect.
|
|
||||||
// <https://github.com/scottlamb/moonfire-nvr/issues/290>
|
|
||||||
onProxyReqWs: (proxyReq, req, socket, options, head) => {
|
|
||||||
proxyReq.setHeader("origin", target);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -7,18 +7,3 @@
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import { TextDecoder } from "util";
|
|
||||||
|
|
||||||
// LiveCamera/parser.ts uses TextDecoder, which works fine from the browser
|
|
||||||
// but isn't available from node.js without a little help.
|
|
||||||
// https://create-react-app.dev/docs/running-tests/#initializing-test-environment
|
|
||||||
// https://stackoverflow.com/questions/51090515/global-functions-in-typescript-for-jest-testing#comment89270564_51091150
|
|
||||||
declare let global: any;
|
|
||||||
|
|
||||||
// TODO: There's likely an elegant way to add TextDecoder to global's type.
|
|
||||||
// Some promising links:
|
|
||||||
// https://www.typescriptlang.org/docs/handbook/declaration-merging.html#global-augmentation
|
|
||||||
// https://stackoverflow.com/a/62011156/23584
|
|
||||||
// https://github.com/facebook/create-react-app/issues/6553#issuecomment-475491096
|
|
||||||
|
|
||||||
global.TextDecoder = TextDecoder;
|
|
||||||
|
|
|
@ -1,21 +1,24 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "ES2020",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"useDefineForClassFields": true,
|
||||||
"downlevelIteration": true,
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
"allowJs": true,
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
/* Bundler mode */
|
||||||
"strict": true,
|
"moduleResolution": "bundler",
|
||||||
"forceConsistentCasingInFileNames": true,
|
"allowImportingTsExtensions": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||||
|
// Copyright (C) 2023 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
||||||
|
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
|
||||||
|
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react-swc";
|
||||||
|
import viteCompression from "vite-plugin-compression";
|
||||||
|
|
||||||
|
const target = process.env.PROXY_TARGET ?? "http://localhost:8080/";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), viteCompression()],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target,
|
||||||
|
|
||||||
|
// Moonfire NVR needs WebSocket connections for live connections (and
|
||||||
|
// likely more in the future:
|
||||||
|
// <https://github.com/scottlamb/moonfire-nvr/issues/40>.)
|
||||||
|
ws: true,
|
||||||
|
changeOrigin: true,
|
||||||
|
|
||||||
|
// If the backing host is https, Moonfire NVR will set a `secure`
|
||||||
|
// attribute on cookie responses, so that the browser will only send
|
||||||
|
// them over https connections. This is a good security practice, but
|
||||||
|
// it means a non-https development proxy server won't work. Strip out
|
||||||
|
// this attribute in the proxy with code from here:
|
||||||
|
// https://github.com/chimurai/http-proxy-middleware/issues/169#issuecomment-575027907
|
||||||
|
// See also discussion in guide/developing-ui.md.
|
||||||
|
configure: (proxy, options) => {
|
||||||
|
// The `changeOrigin` above doesn't appear to apply to websocket
|
||||||
|
// requests. This has a similar effect.
|
||||||
|
proxy.on("proxyReqWs", (proxyReq, req, socket, options, head) => {
|
||||||
|
proxyReq.setHeader("origin", target);
|
||||||
|
});
|
||||||
|
|
||||||
|
proxy.on("proxyRes", (proxyRes, req, res) => {
|
||||||
|
const sc = proxyRes.headers["set-cookie"];
|
||||||
|
if (Array.isArray(sc)) {
|
||||||
|
proxyRes.headers["set-cookie"] = sc.map((sc) => {
|
||||||
|
return sc
|
||||||
|
.split(";")
|
||||||
|
.filter((v) => v.trim().toLowerCase() !== "secure")
|
||||||
|
.join("; ");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in New Issue