mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-02-24 11:59:15 -05:00
upgrade material-ui to latest beta
This is a surprisingly complex upgrade. Some relevant changes from [their CHANGELOG](https://github.com/mui-org/material-ui/blob/v5.0.0-beta.3/CHANGELOG.md): * @material-ui/core/styles no longer re-exports some stuff from @material-ui/styles * there's no more defaultTheme, so tests need to provide one * select's onChange has a new type; match that. I haven't actually tried that the string form (apparently from autofill) works correctly. * checkboxes no longer default to the secondary color; explicitly request this in some places. * checkbox no longer has a checked argument; use event.target.checked instead. * date pickers have switched to the new style system, so I had to redo how I was overridding their spacing for desktop. * LoadingButton now has a loading property, not pending * createMuiTheme is no createTheme
This commit is contained in:
parent
c55032dfcd
commit
39a63e03ae
32770
ui/package-lock.json
generated
32770
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,21 +6,22 @@
|
|||||||
"@emotion/react": "^11.1.5",
|
"@emotion/react": "^11.1.5",
|
||||||
"@emotion/styled": "^11.1.5",
|
"@emotion/styled": "^11.1.5",
|
||||||
"@fontsource/roboto": "^4.2.1",
|
"@fontsource/roboto": "^4.2.1",
|
||||||
"@material-ui/core": "^5.0.0-alpha.26",
|
"@material-ui/core": "^5.0.0-beta.3",
|
||||||
"@material-ui/icons": "^5.0.0-alpha.26",
|
"@material-ui/icons": "^5.0.0-beta.1",
|
||||||
"@material-ui/lab": "^5.0.0-alpha.26",
|
"@material-ui/lab": "^5.0.0-alpha.42",
|
||||||
|
"@material-ui/styles": "^5.0.0-beta.3",
|
||||||
"@react-hook/resize-observer": "^1.2.0",
|
"@react-hook/resize-observer": "^1.2.0",
|
||||||
"@types/jest": "^26.0.20",
|
"@types/jest": "^26.0.20",
|
||||||
"@types/node": "^14.14.22",
|
"@types/node": "^16.3.1",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.0",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.0",
|
||||||
"date-fns": "^2.18.0",
|
"date-fns": "^2.18.0",
|
||||||
"date-fns-tz": "^1.1.3",
|
"date-fns-tz": "^1.1.3",
|
||||||
"gzipper": "^4.4.0",
|
"gzipper": "^5.0.0",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-scripts": "4.0.1",
|
"react-scripts": "^4.0.3",
|
||||||
"typescript": "^4.1.3"
|
"typescript": "^4.3.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
@ -64,10 +65,10 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.2.7",
|
||||||
"@testing-library/user-event": "^12.6.3",
|
"@testing-library/user-event": "^12.8.3",
|
||||||
"http-proxy-middleware": "^1.0.6",
|
"http-proxy-middleware": "^2.0.1",
|
||||||
"msw": "^0.26.1",
|
"msw": "^0.26.2",
|
||||||
"prettier": "2.2.1"
|
"prettier": "^2.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,8 @@ import Button from "@material-ui/core/Button";
|
|||||||
import IconButton from "@material-ui/core/IconButton";
|
import IconButton from "@material-ui/core/IconButton";
|
||||||
import Menu from "@material-ui/core/Menu";
|
import Menu from "@material-ui/core/Menu";
|
||||||
import MenuItem from "@material-ui/core/MenuItem";
|
import MenuItem from "@material-ui/core/MenuItem";
|
||||||
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
import { Theme } from "@material-ui/core/styles";
|
||||||
|
import { createStyles, makeStyles } from "@material-ui/styles";
|
||||||
import Toolbar from "@material-ui/core/Toolbar";
|
import Toolbar from "@material-ui/core/Toolbar";
|
||||||
import Typography from "@material-ui/core/Typography";
|
import Typography from "@material-ui/core/Typography";
|
||||||
import AccountCircle from "@material-ui/icons/AccountCircle";
|
import AccountCircle from "@material-ui/icons/AccountCircle";
|
||||||
@ -38,10 +39,8 @@ interface Props {
|
|||||||
// https://material-ui.com/components/app-bar/
|
// https://material-ui.com/components/app-bar/
|
||||||
function MoonfireMenu(props: Props) {
|
function MoonfireMenu(props: Props) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [
|
const [accountMenuAnchor, setAccountMenuAnchor] =
|
||||||
accountMenuAnchor,
|
React.useState<null | HTMLElement>(null);
|
||||||
setAccountMenuAnchor,
|
|
||||||
] = React.useState<null | HTMLElement>(null);
|
|
||||||
|
|
||||||
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
|
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
setAccountMenuAnchor(event.currentTarget);
|
setAccountMenuAnchor(event.currentTarget);
|
||||||
@ -94,7 +93,6 @@ function MoonfireMenu(props: Props) {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
<Menu
|
<Menu
|
||||||
anchorEl={accountMenuAnchor}
|
anchorEl={accountMenuAnchor}
|
||||||
getContentAnchorEl={null}
|
|
||||||
keepMounted
|
keepMounted
|
||||||
anchorOrigin={{
|
anchorOrigin={{
|
||||||
vertical: "bottom",
|
vertical: "bottom",
|
||||||
|
@ -4,12 +4,6 @@
|
|||||||
|
|
||||||
import Avatar from "@material-ui/core/Avatar";
|
import Avatar from "@material-ui/core/Avatar";
|
||||||
import Container from "@material-ui/core/Container";
|
import Container from "@material-ui/core/Container";
|
||||||
import {
|
|
||||||
createStyles,
|
|
||||||
Theme,
|
|
||||||
WithStyles,
|
|
||||||
withStyles,
|
|
||||||
} from "@material-ui/core/styles";
|
|
||||||
import BugReportIcon from "@material-ui/icons/BugReport";
|
import BugReportIcon from "@material-ui/icons/BugReport";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
@ -17,16 +11,7 @@ interface State {
|
|||||||
error: any;
|
error: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = (theme: Theme) =>
|
interface Props {
|
||||||
createStyles({
|
|
||||||
avatar: {
|
|
||||||
backgroundColor: theme.palette.secondary.main,
|
|
||||||
float: "left",
|
|
||||||
marginRight: "1em",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
interface Props extends WithStyles<typeof styles> {
|
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +40,7 @@ class MoonfireErrorBoundary extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { classes, children } = this.props;
|
const { children } = this.props;
|
||||||
|
|
||||||
if (this.state.error !== null) {
|
if (this.state.error !== null) {
|
||||||
var error;
|
var error;
|
||||||
@ -74,7 +59,13 @@ class MoonfireErrorBoundary extends React.Component<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Avatar className={classes.avatar}>
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
float: "left",
|
||||||
|
bgcolor: "secondary.main",
|
||||||
|
marginRight: "1em",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<BugReportIcon color="primary" />
|
<BugReportIcon color="primary" />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<h1>Error</h1>
|
<h1>Error</h1>
|
||||||
@ -130,4 +121,4 @@ class MoonfireErrorBoundary extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(MoonfireErrorBoundary);
|
export default MoonfireErrorBoundary;
|
||||||
|
@ -53,7 +53,13 @@ const DisplaySelector = (props: Props) => {
|
|||||||
id="split90k"
|
id="split90k"
|
||||||
size="small"
|
size="small"
|
||||||
value={props.split90k}
|
value={props.split90k}
|
||||||
onChange={(e) => props.setSplit90k(e.target.value)}
|
onChange={(e) =>
|
||||||
|
props.setSplit90k(
|
||||||
|
typeof e.target.value === "string"
|
||||||
|
? parseInt(e.target.value)
|
||||||
|
: e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
displayEmpty
|
displayEmpty
|
||||||
>
|
>
|
||||||
{DURATIONS.map(([l, d]) => (
|
{DURATIONS.map(([l, d]) => (
|
||||||
@ -72,10 +78,9 @@ const DisplaySelector = (props: Props) => {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
checked={props.trimStartAndEnd}
|
checked={props.trimStartAndEnd}
|
||||||
size="small"
|
size="small"
|
||||||
onChange={(_, checked: boolean) =>
|
onChange={(event) => props.setTrimStartAndEnd(event.target.checked)}
|
||||||
props.setTrimStartAndEnd(checked)
|
|
||||||
}
|
|
||||||
name="trim-start-and-end"
|
name="trim-start-and-end"
|
||||||
|
color="secondary"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Trim start and end"
|
label="Trim start and end"
|
||||||
@ -87,8 +92,9 @@ const DisplaySelector = (props: Props) => {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
checked={props.timestampTrack}
|
checked={props.timestampTrack}
|
||||||
size="small"
|
size="small"
|
||||||
onChange={(_, checked: boolean) => props.setTimestampTrack(checked)}
|
onChange={(event) => props.setTimestampTrack(event.target.checked)}
|
||||||
name="timestamp-track"
|
name="timestamp-track"
|
||||||
|
color="secondary"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Timestamp track"
|
label="Timestamp track"
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
import Card from "@material-ui/core/Card";
|
import Card from "@material-ui/core/Card";
|
||||||
import { Camera, Stream, StreamType } from "../types";
|
import { Camera, Stream, StreamType } from "../types";
|
||||||
import Checkbox from "@material-ui/core/Checkbox";
|
import Checkbox from "@material-ui/core/Checkbox";
|
||||||
import { makeStyles, useTheme } from "@material-ui/core/styles";
|
import { useTheme } from "@material-ui/core/styles";
|
||||||
|
import { makeStyles } from "@material-ui/styles";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cameras: Camera[];
|
cameras: Camera[];
|
||||||
@ -86,14 +87,17 @@ const StreamMultiSelector = ({ cameras, selected, setSelected }: Props) => {
|
|||||||
function checkbox(st: StreamType) {
|
function checkbox(st: StreamType) {
|
||||||
const s = c.streams[st];
|
const s = c.streams[st];
|
||||||
if (s === undefined) {
|
if (s === undefined) {
|
||||||
return <Checkbox className={classes.check} disabled />;
|
return (
|
||||||
|
<Checkbox className={classes.check} color="secondary" disabled />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
className={classes.check}
|
className={classes.check}
|
||||||
size="small"
|
size="small"
|
||||||
checked={selected.has(s)}
|
checked={selected.has(s)}
|
||||||
onChange={(_, checked: boolean) => setStream(s, checked)}
|
color="secondary"
|
||||||
|
onChange={(event) => setStream(s, event.target.checked)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
|
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
|
||||||
|
|
||||||
import { Stream } from "../types";
|
import { Stream } from "../types";
|
||||||
import StaticDatePicker from "@material-ui/lab/StaticDatePicker";
|
import StaticDatePicker, {
|
||||||
|
StaticDatePickerProps,
|
||||||
|
} from "@material-ui/lab/StaticDatePicker";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { zonedTimeToUtc } from "date-fns-tz";
|
import { zonedTimeToUtc } from "date-fns-tz";
|
||||||
import { addDays, addMilliseconds, differenceInMilliseconds } from "date-fns";
|
import { addDays, addMilliseconds, differenceInMilliseconds } from "date-fns";
|
||||||
@ -17,6 +19,7 @@ import Radio from "@material-ui/core/Radio";
|
|||||||
import RadioGroup from "@material-ui/core/RadioGroup";
|
import RadioGroup from "@material-ui/core/RadioGroup";
|
||||||
import TimePicker, { TimePickerProps } from "@material-ui/lab/TimePicker";
|
import TimePicker, { TimePickerProps } from "@material-ui/lab/TimePicker";
|
||||||
import Collapse from "@material-ui/core/Collapse";
|
import Collapse from "@material-ui/core/Collapse";
|
||||||
|
import Box from "@material-ui/core/Box";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectedStreams: Set<Stream>;
|
selectedStreams: Set<Stream>;
|
||||||
@ -39,6 +42,47 @@ const MyTimePicker = (
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const SmallStaticDatePicker = (props: StaticDatePickerProps<Date>) => {
|
||||||
|
// The spacing defined at https://material.io/components/date-pickers#specs
|
||||||
|
// seems plenty big enough (on desktop). Not sure why material-ui wants
|
||||||
|
// to make it bigger but that doesn't work well with our layout.
|
||||||
|
// This adjustment is a fragile hack but seems to work for now.
|
||||||
|
const DATE_SIZE = 32;
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
"@media (pointer: fine)": {
|
||||||
|
"& > div": {
|
||||||
|
minWidth: 256,
|
||||||
|
},
|
||||||
|
"& > div > div, & > div > div > div, & .MuiCalendarPicker-root": {
|
||||||
|
width: 256,
|
||||||
|
},
|
||||||
|
"& .MuiTypography-caption": {
|
||||||
|
width: DATE_SIZE,
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
"& .PrivatePickersSlideTransition-root": {
|
||||||
|
minHeight: DATE_SIZE * 6,
|
||||||
|
},
|
||||||
|
'& .PrivatePickersSlideTransition-root [role="row"]': {
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
"& .MuiPickersDay-dayWithMargin": {
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
"& .MuiPickersDay-root": {
|
||||||
|
width: DATE_SIZE,
|
||||||
|
height: DATE_SIZE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StaticDatePicker {...props} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combines the date-part of <tt>dayMillis</tt> and the time part of
|
* Combines the date-part of <tt>dayMillis</tt> and the time part of
|
||||||
* <tt>time</tt>. If <tt>time</tt> is null, assume it reaches the end of the
|
* <tt>time</tt>. If <tt>time</tt> is null, assume it reaches the end of the
|
||||||
@ -260,7 +304,7 @@ const TimerangeSelector = ({
|
|||||||
<Card sx={{ padding: theme.spacing(1) }}>
|
<Card sx={{ padding: theme.spacing(1) }}>
|
||||||
<div>
|
<div>
|
||||||
<FormLabel component="legend">From</FormLabel>
|
<FormLabel component="legend">From</FormLabel>
|
||||||
<StaticDatePicker
|
<SmallStaticDatePicker
|
||||||
displayStaticWrapperAs="desktop"
|
displayStaticWrapperAs="desktop"
|
||||||
value={startDate}
|
value={startDate}
|
||||||
shouldDisableDate={shouldDisableDate}
|
shouldDisableDate={shouldDisableDate}
|
||||||
@ -299,17 +343,17 @@ const TimerangeSelector = ({
|
|||||||
>
|
>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value="same-day"
|
value="same-day"
|
||||||
control={<Radio size="small" />}
|
control={<Radio size="small" color="secondary" />}
|
||||||
label="Same day"
|
label="Same day"
|
||||||
/>
|
/>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value="other-day"
|
value="other-day"
|
||||||
control={<Radio size="small" />}
|
control={<Radio size="small" color="secondary" />}
|
||||||
label="Other day"
|
label="Other day"
|
||||||
/>
|
/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
<Collapse in={days.endType === "other-day"}>
|
<Collapse in={days.endType === "other-day"}>
|
||||||
<StaticDatePicker
|
<SmallStaticDatePicker
|
||||||
displayStaticWrapperAs="desktop"
|
displayStaticWrapperAs="desktop"
|
||||||
value={endDate}
|
value={endDate}
|
||||||
shouldDisableDate={(d: Date | null) =>
|
shouldDisableDate={(d: Date | null) =>
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
import Box from "@material-ui/core/Box";
|
import Box from "@material-ui/core/Box";
|
||||||
import Modal from "@material-ui/core/Modal";
|
import Modal from "@material-ui/core/Modal";
|
||||||
import Paper from "@material-ui/core/Paper";
|
import Paper from "@material-ui/core/Paper";
|
||||||
import { makeStyles, Theme } from "@material-ui/core/styles";
|
import { Theme } from "@material-ui/core/styles";
|
||||||
|
import { makeStyles } from "@material-ui/styles";
|
||||||
import Table from "@material-ui/core/Table";
|
import Table from "@material-ui/core/Table";
|
||||||
import TableContainer from "@material-ui/core/TableContainer";
|
import TableContainer from "@material-ui/core/TableContainer";
|
||||||
import utcToZonedTime from "date-fns-tz/utcToZonedTime";
|
import utcToZonedTime from "date-fns-tz/utcToZonedTime";
|
||||||
|
@ -2,12 +2,13 @@
|
|||||||
// Copyright (C) 2021 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
// 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
|
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
|
||||||
|
|
||||||
import Select from "@material-ui/core/Select";
|
import Select, { SelectChangeEvent } from "@material-ui/core/Select";
|
||||||
import MenuItem from "@material-ui/core/MenuItem";
|
import MenuItem from "@material-ui/core/MenuItem";
|
||||||
import React, { useReducer, useState } from "react";
|
import React, { useReducer, useState } from "react";
|
||||||
import { Camera } from "../types";
|
import { Camera } from "../types";
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/styles";
|
||||||
import useResizeObserver from "@react-hook/resize-observer";
|
import useResizeObserver from "@react-hook/resize-observer";
|
||||||
|
import { Theme } from "@material-ui/core/styles";
|
||||||
|
|
||||||
export interface Layout {
|
export interface Layout {
|
||||||
className: string;
|
className: string;
|
||||||
@ -23,7 +24,7 @@ const LAYOUTS: Layout[] = [
|
|||||||
];
|
];
|
||||||
const MAX_CAMERAS = 9;
|
const MAX_CAMERAS = 9;
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
root: {
|
root: {
|
||||||
flex: "1 0 0",
|
flex: "1 0 0",
|
||||||
color: "white",
|
color: "white",
|
||||||
@ -98,7 +99,13 @@ export const MultiviewChooser = (props: MultiviewChooserProps) => {
|
|||||||
<Select
|
<Select
|
||||||
id="layout"
|
id="layout"
|
||||||
value={props.layoutIndex}
|
value={props.layoutIndex}
|
||||||
onChange={(e) => props.onChoice(e.target.value)}
|
onChange={(e) =>
|
||||||
|
props.onChoice(
|
||||||
|
typeof e.target.value === "string"
|
||||||
|
? parseInt(e.target.value)
|
||||||
|
: e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
size="small"
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
// Hacky attempt to style for the app menu.
|
// Hacky attempt to style for the app menu.
|
||||||
@ -238,10 +245,16 @@ interface MonoviewProps {
|
|||||||
|
|
||||||
/** A single pane of a Multiview, including its camera chooser. */
|
/** A single pane of a Multiview, including its camera chooser. */
|
||||||
const Monoview = (props: MonoviewProps) => {
|
const Monoview = (props: MonoviewProps) => {
|
||||||
|
const handleChange = (event: SelectChangeEvent<number | null>) => {
|
||||||
|
const {
|
||||||
|
target: { value },
|
||||||
|
} = event;
|
||||||
|
props.onSelect(typeof value === "string" ? parseInt(value) : value);
|
||||||
|
};
|
||||||
const chooser = (
|
const chooser = (
|
||||||
<Select
|
<Select
|
||||||
value={props.cameraIndex == null ? undefined : props.cameraIndex}
|
value={props.cameraIndex == null ? undefined : props.cameraIndex}
|
||||||
onChange={(e) => props.onSelect(e.target.value ?? null)}
|
onChange={handleChange}
|
||||||
displayEmpty
|
displayEmpty
|
||||||
size="small"
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -38,8 +38,8 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("success", async () => {
|
test("success", async () => {
|
||||||
const handleClose = jest.fn();
|
const handleClose = jest.fn().mockName("handleClose");
|
||||||
const onSuccess = jest.fn();
|
const onSuccess = jest.fn().mockName("handleOpen");
|
||||||
renderWithCtx(
|
renderWithCtx(
|
||||||
<Login open={true} onSuccess={onSuccess} handleClose={handleClose} />
|
<Login open={true} onSuccess={onSuccess} handleClose={handleClose} />
|
||||||
);
|
);
|
||||||
@ -54,8 +54,8 @@ test("success", async () => {
|
|||||||
// 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 () => {
|
xtest("close while pending", async () => {
|
||||||
const handleClose = jest.fn();
|
const handleClose = jest.fn().mockName("handleClose");
|
||||||
const onSuccess = jest.fn();
|
const onSuccess = jest.fn().mockName("handleOpen");
|
||||||
const { rerender } = renderWithCtx(
|
const { rerender } = renderWithCtx(
|
||||||
<Login open={true} onSuccess={onSuccess} handleClose={handleClose} />
|
<Login open={true} onSuccess={onSuccess} handleClose={handleClose} />
|
||||||
);
|
);
|
||||||
@ -73,8 +73,8 @@ xtest("close while pending", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("bad credentials", async () => {
|
test("bad credentials", async () => {
|
||||||
const handleClose = jest.fn();
|
const handleClose = jest.fn().mockName("handleClose");
|
||||||
const onSuccess = jest.fn();
|
const onSuccess = jest.fn().mockName("handleOpen");
|
||||||
renderWithCtx(
|
renderWithCtx(
|
||||||
<Login open={true} onSuccess={onSuccess} handleClose={handleClose} />
|
<Login open={true} onSuccess={onSuccess} handleClose={handleClose} />
|
||||||
);
|
);
|
||||||
@ -85,8 +85,8 @@ test("bad credentials", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("server error", async () => {
|
test("server error", async () => {
|
||||||
const handleClose = jest.fn();
|
const handleClose = jest.fn().mockName("handleClose");
|
||||||
const onSuccess = jest.fn();
|
const onSuccess = jest.fn().mockName("handleOpen");
|
||||||
renderWithCtx(
|
renderWithCtx(
|
||||||
<Login open={true} onSuccess={onSuccess} handleClose={handleClose} />
|
<Login open={true} onSuccess={onSuccess} handleClose={handleClose} />
|
||||||
);
|
);
|
||||||
@ -100,8 +100,8 @@ test("server error", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("network error", async () => {
|
test("network error", async () => {
|
||||||
const handleClose = jest.fn();
|
const handleClose = jest.fn().mockName("handleClose");
|
||||||
const onSuccess = jest.fn();
|
const onSuccess = jest.fn().mockName("handleOpen");
|
||||||
renderWithCtx(
|
renderWithCtx(
|
||||||
<Login open={true} onSuccess={onSuccess} handleClose={handleClose} />
|
<Login open={true} onSuccess={onSuccess} handleClose={handleClose} />
|
||||||
);
|
);
|
||||||
|
@ -8,7 +8,8 @@ import DialogActions from "@material-ui/core/DialogActions";
|
|||||||
import DialogTitle from "@material-ui/core/DialogTitle";
|
import DialogTitle from "@material-ui/core/DialogTitle";
|
||||||
import FormControl from "@material-ui/core/FormControl";
|
import FormControl from "@material-ui/core/FormControl";
|
||||||
import FormHelperText from "@material-ui/core/FormHelperText";
|
import FormHelperText from "@material-ui/core/FormHelperText";
|
||||||
import { makeStyles, Theme } from "@material-ui/core/styles";
|
import { Theme } from "@material-ui/core/styles";
|
||||||
|
import { makeStyles } from "@material-ui/styles";
|
||||||
import TextField from "@material-ui/core/TextField";
|
import TextField from "@material-ui/core/TextField";
|
||||||
import LockOutlinedIcon from "@material-ui/icons/LockOutlined";
|
import LockOutlinedIcon from "@material-ui/icons/LockOutlined";
|
||||||
import LoadingButton from "@material-ui/lab/LoadingButton";
|
import LoadingButton from "@material-ui/lab/LoadingButton";
|
||||||
@ -62,15 +63,15 @@ const Login = ({ open, onSuccess, handleClose }: Props) => {
|
|||||||
const passwordRef = React.useRef<HTMLInputElement>(null);
|
const passwordRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
const [pending, setPending] = React.useState<api.LoginRequest | null>(null);
|
const [loading, setLoading] = React.useState<api.LoginRequest | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pending === null) {
|
if (loading === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let abort = new AbortController();
|
let abort = new AbortController();
|
||||||
const send = async (signal: AbortSignal) => {
|
const send = async (signal: AbortSignal) => {
|
||||||
let response = await api.login(pending, { signal });
|
let response = await api.login(loading, { signal });
|
||||||
switch (response.status) {
|
switch (response.status) {
|
||||||
case "aborted":
|
case "aborted":
|
||||||
break;
|
break;
|
||||||
@ -83,10 +84,10 @@ const Login = ({ open, onSuccess, handleClose }: Props) => {
|
|||||||
key: "login-error",
|
key: "login-error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setPending(null);
|
setLoading(null);
|
||||||
break;
|
break;
|
||||||
case "success":
|
case "success":
|
||||||
setPending(null);
|
setLoading(null);
|
||||||
onSuccess();
|
onSuccess();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -94,16 +95,16 @@ const Login = ({ open, onSuccess, handleClose }: Props) => {
|
|||||||
return () => {
|
return () => {
|
||||||
abort.abort();
|
abort.abort();
|
||||||
};
|
};
|
||||||
}, [pending, onSuccess, snackbars]);
|
}, [loading, onSuccess, snackbars]);
|
||||||
|
|
||||||
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// Suppress duplicate login attempts when latency is high.
|
// Suppress duplicate login attempts when latency is high.
|
||||||
if (pending !== null) {
|
if (loading !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setPending({
|
setLoading({
|
||||||
username: usernameRef.current!.value,
|
username: usernameRef.current!.value,
|
||||||
password: passwordRef.current!.value,
|
password: passwordRef.current!.value,
|
||||||
});
|
});
|
||||||
@ -154,7 +155,7 @@ const Login = ({ open, onSuccess, handleClose }: Props) => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
pending={pending !== null}
|
loading={loading !== null}
|
||||||
>
|
>
|
||||||
Log in
|
Log in
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
|
@ -10,48 +10,3 @@ body,
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (pointer: fine) {
|
|
||||||
/*
|
|
||||||
* The spacing defined at https://material.io/components/date-pickers#specs
|
|
||||||
* seems plenty big enough (on desktop). Not sure why material-ui wants
|
|
||||||
* to make it bigger but that doesn't work well with our layout.
|
|
||||||
*
|
|
||||||
* Defining this here (in a global .css file) is the first way I could find
|
|
||||||
* to override properties of .MuiPickerStaticWrapper-root. It's unfortunately
|
|
||||||
* a _parent_ of the element that gets the <DatePicker>'s className applied,
|
|
||||||
* and it doesn't seem to be exposed for a global style override
|
|
||||||
* <https://next.material-ui.com/customization/theme-components/#global-style-overrides>.
|
|
||||||
*/
|
|
||||||
.MuiPickersStaticWrapper-root {
|
|
||||||
min-width: 256px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Increased specificity here so it doesn't apply to the popup time picker. */
|
|
||||||
.MuiPickersStaticWrapper-root .MuiPickerView-root {
|
|
||||||
width: 256px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MuiPickersCalendar-root {
|
|
||||||
min-height: 160px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MuiPickersCalendar-weekDayLabel {
|
|
||||||
width: 32px !important;
|
|
||||||
height: 32px !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MuiPickersCalendar-week {
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MuiPickersDay-dayWithMargin {
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MuiPickersDay-root {
|
|
||||||
width: 32px !important;
|
|
||||||
height: 32px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
|
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
|
||||||
|
|
||||||
import CssBaseline from "@material-ui/core/CssBaseline";
|
import CssBaseline from "@material-ui/core/CssBaseline";
|
||||||
import { ThemeProvider, createMuiTheme } from "@material-ui/core/styles";
|
import { ThemeProvider, createTheme } from "@material-ui/core/styles";
|
||||||
import StyledEngineProvider from "@material-ui/core/StyledEngineProvider";
|
import StyledEngineProvider from "@material-ui/core/StyledEngineProvider";
|
||||||
import LocalizationProvider from "@material-ui/lab/LocalizationProvider";
|
import LocalizationProvider from "@material-ui/lab/LocalizationProvider";
|
||||||
import "@fontsource/roboto";
|
import "@fontsource/roboto";
|
||||||
@ -15,7 +15,7 @@ import { SnackbarProvider } from "./snackbars";
|
|||||||
import AdapterDateFns from "@material-ui/lab/AdapterDateFns";
|
import AdapterDateFns from "@material-ui/lab/AdapterDateFns";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
const theme = createMuiTheme({
|
const theme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
primary: {
|
primary: {
|
||||||
main: "#000000",
|
main: "#000000",
|
||||||
|
@ -93,7 +93,8 @@ const ctx = React.createContext<Snackbars | null>(null);
|
|||||||
// and I couldn't figure out a way to do that with hooks.
|
// and I couldn't figure out a way to do that with hooks.
|
||||||
export class SnackbarProvider
|
export class SnackbarProvider
|
||||||
extends React.Component<SnackbarProviderProps, State>
|
extends React.Component<SnackbarProviderProps, State>
|
||||||
implements Snackbars {
|
implements Snackbars
|
||||||
|
{
|
||||||
constructor(props: SnackbarProviderProps) {
|
constructor(props: SnackbarProviderProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { queue: [] };
|
this.state = { queue: [] };
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// Copyright (C) 2021 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
// 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
|
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
|
||||||
|
|
||||||
|
import { createTheme, ThemeProvider } from "@material-ui/core/styles";
|
||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
import { SnackbarProvider } from "./snackbars";
|
import { SnackbarProvider } from "./snackbars";
|
||||||
|
|
||||||
@ -10,7 +11,9 @@ export function renderWithCtx(
|
|||||||
): Pick<ReturnType<typeof render>, "rerender"> {
|
): Pick<ReturnType<typeof render>, "rerender"> {
|
||||||
function wrapped(children: React.ReactElement): React.ReactElement {
|
function wrapped(children: React.ReactElement): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
|
<ThemeProvider theme={createTheme()}>
|
||||||
<SnackbarProvider autoHideDuration={5000}>{children}</SnackbarProvider>
|
<SnackbarProvider autoHideDuration={5000}>{children}</SnackbarProvider>
|
||||||
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { rerender } = render(wrapped(children));
|
const { rerender } = render(wrapped(children));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user