Sync development into main (#31)

* Sync development into main

* Fix
This commit is contained in:
John Wu
2021-04-22 17:25:46 -07:00
committed by GitHub
parent 0f19a62b32
commit bf81903ecd
28 changed files with 3634 additions and 220 deletions

View File

@@ -1,6 +1,8 @@
jest.mock("../Contexts/UserContext");
jest.mock("../Contexts/FileUploadContext");
jest.mock("../Contexts/VehicleContext");
jest.mock("../Contexts/UpdatesContext");
jest.mock("../Contexts/UserContext");
jest.mock("../../services/monitoring");
import { render, screen, cleanup, waitForElementToBeRemoved } from "@testing-library/react";
import { setToken } from "../Contexts/UserContext";
@@ -18,7 +20,27 @@ const renderRoute = async (route) => {
return container;
};
const check = async (path, selector, compare) => {
const container = await renderRoute(path);
expect(container.querySelector(selector).innerHTML).toEqual(compare);
expect(container).toMatchSnapshot();
};
describe("App", () => {
beforeAll(() => {
// Stablize Table Pagination control ids
expect.addSnapshotSerializer({
test: function(val) {
return val && typeof val === "string" && val.indexOf("mui-") >= 0;
},
print: function(val) {
let str = val;
str = str.replace(/mui-[0-9]*/g, "mui-00000");
return `"${str}"`;
}
});
});
afterEach(() => {
setToken(null);
@@ -26,15 +48,15 @@ describe("App", () => {
});
it("Route / unauthenticated", async () => {
const container = await renderRoute("/");
expect(container.querySelector("span.MuiButton-label").innerHTML).toEqual("Sign In");
expect(container).toMatchSnapshot();
await check("/", "span.MuiButton-label", "Sign In");
});
it("Route /home unauthenticated", async () => {
const container = await renderRoute("/home");
expect(container.querySelector("span.MuiButton-label").innerHTML).toEqual("Sign In");
expect(container).toMatchSnapshot();
await check("/home", "span.MuiButton-label", "Sign In");
});
it("Route /package-upload unauthenticated", async () => {
await check("/package-upload", "span.MuiButton-label", "Sign In");
});
it("Route /package-upload unauthenticated", async () => {
@@ -44,49 +66,89 @@ describe("App", () => {
});
it("Route /vehicle-add unauthenticated", async () => {
const container = await renderRoute("/vehicle-add");
expect(container.querySelector("span.MuiButton-label").innerHTML).toEqual("Sign In");
expect(container).toMatchSnapshot();
await check("/vehicle-add", "span.MuiButton-label", "Sign In");
});
it("Route /updates unauthenticated", async () => {
await check("/updates", "span.MuiButton-label", "Sign In");
});
it("Route /update unauthenticated", async () => {
await check("/update/1", "span.MuiButton-label", "Sign In");
});
it("Route /carupdate-deploy unauthenticated", async () => {
await check("/carupdate-deploy/1", "span.MuiButton-label", "Sign In");
});
it("Route /carupdate-status unauthenticated", async () => {
await check("/carupdate-status/1", "span.MuiButton-label", "Sign In");
});
it("Route /vehicles unauthenticated", async () => {
await check("/vehicles", "span.MuiButton-label", "Sign In");
});
it("Route /vehicle-status unauthenticated", async () => {
await check("/vehicle-status/FISKER123", "span.MuiButton-label", "Sign In");
});
it("Route / authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderRoute("/");
expect(container.querySelector("h1").innerHTML).toEqual("Welcome John!");
expect(container).toMatchSnapshot();
await check("/", "h1", "Welcome John!");
});
it("Route /home authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderRoute("/home");
expect(container.querySelector("h1").innerHTML).toEqual("Welcome John!");
expect(container).toMatchSnapshot();
await check("/home", "h1", "Welcome John!");
});
it("Route /package-upload authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderRoute("/package-upload");
expect(container.querySelector("h1").innerHTML).toEqual("Upload Update Package");
expect(container).toMatchSnapshot();
await check("/package-upload", "h1", "Upload Update Package");
});
it("Route /vehicle-add authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderRoute("/vehicle-add");
expect(container.querySelector("h1").innerHTML).toEqual("Add Vehicle");
expect(container).toMatchSnapshot();
await check("/vehicle-add", "h1", "Add Vehicle");
});
it("Route /updates authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/updates", "h1", "Updates");
});
it("Route /update authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/update/1", "h1", "Update Package 1");
});
it("Route /carupdate-deploy authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/carupdate-deploy/1", "h1", "[1] ");
});
it("Route /carupdate-status authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/carupdate-status/1", "h1", "");
});
it("Route /vehicles authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/vehicles", "h1", "Vehicles");
});
it("Route /vehicle-status authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/vehicle-status/FISKER123", "h1", "FISKER123 Updates");
});
it("Route /page-not-found unauthenticated", async () => {
const container = await renderRoute("/page-not-found");
expect(container.querySelector("h1").innerHTML).toEqual("Page Not Found");
expect(container).toMatchSnapshot();
await check("/page-not-found", "h1", "Page Not Found");
});
it("Route /page-not-found authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderRoute("/page-not-found");
expect(container.querySelector("h1").innerHTML).toEqual("Page Not Found");
expect(container).toMatchSnapshot();
await check("/page-not-found", "h1", "Page Not Found");
});
})

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ import { StatusProvider } from "../Contexts/StatusContext";
import { CssBaseline } from "@material-ui/core";
import MenuDrawer from "../Layouts/MenuDrawer";
import SiteRoutes from "../Routes/SiteRoutes";
import {} from "../../services/monitoring";
function App() {
return (

View File

@@ -49,18 +49,12 @@ const MainForm = () => {
const handleVehiclesChange = (event) => {
setSelectedVehicles(event.target.value);
};
const getCarIDs = () => {
if (!selectedVehicles) return [];
return selectedVehicles.map((vin) => {
return vehicles.find((vehicle) => vehicle.vin === vin).id;
});
};
const onSubmit = async (event) => {
try {
event.preventDefault();
const data = {
package_id: parseInt(packageid),
car_ids: getCarIDs(),
vins: selectedVehicles,
};
await createCarUpdates(data, token);
setMessage(

View File

@@ -20,12 +20,15 @@ import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import { LocalDateTimeString } from "../../../utils/dates";
import VehicleStatus from "../../Cars/StatusModal";
const MainForm = () => {
const { packageid } = useParams();
const classes = useStyles();
const [pageSize, setPageSize] = useState(10);
const [pageIndex, setPageIndex] = useState(0);
const [viewVIN, setViewVIN] = useState(null);
const {
getCarUpdates,
carUpdates,
@@ -74,6 +77,15 @@ const MainForm = () => {
setPageIndex(0);
};
const handleViewVIN = (event) => {
event.preventDefault();
setViewVIN(event.target.innerHTML);
};
const handleCloseViewVIN = (event) => {
setViewVIN(null);
};
return (
<div className={classes.paper} style={{ height: 700, width: "100%" }}>
<Typography component="h1" variant="h5">
@@ -96,7 +108,11 @@ const MainForm = () => {
{carUpdates.map((row) => (
<TableRow key={row.id}>
<TableCell align="center">{row.id}</TableCell>
<TableCell align="center">{`${row.car.vin} ${row.car.model} ${row.car.year}`}</TableCell>
<TableCell align="center">
<span className={classes.link} onClick={handleViewVIN}>
{row.vin}
</span>
</TableCell>
<TableCell align="center">{row.status}</TableCell>
<TableCell align="center">
{LocalDateTimeString(row.created)}
@@ -126,6 +142,7 @@ const MainForm = () => {
</TableFooter>
</Table>
</TableContainer>
<VehicleStatus vin={viewVIN} handleClose={handleCloseViewVIN} />
</div>
);
};

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import {
Table,
TableBody,
@@ -65,7 +66,6 @@ const MainForm = () => {
<Table>
<TableHead>
<TableRow>
<TableCell align="center">ID</TableCell>
<TableCell align="center">VIN</TableCell>
<TableCell align="center">Model</TableCell>
<TableCell align="center">Year</TableCell>
@@ -75,9 +75,10 @@ const MainForm = () => {
</TableHead>
<TableBody>
{vehicles.map((row) => (
<TableRow key={row.id}>
<TableCell align="center">{row.id}</TableCell>
<TableCell align="center">{row.vin}</TableCell>
<TableRow key={row.vin}>
<TableCell align="center">
<Link to={`/vehicle-status/${row.vin}`}>{row.vin}</Link>
</TableCell>
<TableCell align="center">{row.model}</TableCell>
<TableCell align="center">{row.year}</TableCell>
<TableCell align="center">

View File

@@ -0,0 +1,122 @@
import React, { useEffect, useState } from "react";
import { useParams } from "react-router";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableFooter,
TableHead,
TablePagination,
TableRow,
Typography,
} from "@material-ui/core";
import {
UpdatesProvider,
useUpdatesContext,
} from "../../Contexts/UpdatesContext";
import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import { LocalDateTimeString } from "../../../utils/dates";
const MainForm = () => {
const { vin } = useParams();
const classes = useStyles();
const [pageSize, setPageSize] = useState(10);
const [pageIndex, setPageIndex] = useState(0);
const { getCarUpdates, carUpdates, totalCarUpdates } = useUpdatesContext();
const { setMessage } = useStatusContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
useEffect(() => {
try {
getCarUpdates(
{
vin,
limit: pageSize,
offset: pageSize * pageIndex,
},
token
);
} catch (e) {
setMessage(e.message);
}
// eslint-disable-next-line
}, [pageIndex, pageSize, token]);
const handleChangePageIndex = (event, newIndex) => {
setPageIndex(newIndex);
};
const handleChangePageSize = (event) => {
setPageSize(parseInt(event.target.value, 10));
setPageIndex(0);
};
return (
<div className={classes.paper} style={{ height: 700, width: "100%" }}>
<Typography component="h1" variant="h5">
{vin} Updates
</Typography>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell align="center">ID</TableCell>
<TableCell align="center">Update</TableCell>
<TableCell align="center">Status</TableCell>
<TableCell align="center">Created</TableCell>
<TableCell align="center">Updated</TableCell>
</TableRow>
</TableHead>
<TableBody>
{carUpdates.map((row) => (
<TableRow key={row.id}>
<TableCell align="center">{row.id}</TableCell>
<TableCell align="center">{`${row.updatepackage.package_name} ${row.updatepackage.version}`}</TableCell>
<TableCell align="center">{row.status}</TableCell>
<TableCell align="center">
{LocalDateTimeString(row.created)}
</TableCell>
<TableCell align="center">
{LocalDateTimeString(row.updated)}
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
colSpan={5}
count={totalCarUpdates}
rowsPerPage={pageSize}
page={pageIndex}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true,
}}
onChangePage={handleChangePageIndex}
onChangeRowsPerPage={handleChangePageSize}
/>
</TableRow>
</TableFooter>
</Table>
</TableContainer>
</div>
);
};
const CarUpdates = () => (
<UpdatesProvider>
<MainForm />
</UpdatesProvider>
);
export default CarUpdates;

View File

@@ -0,0 +1,91 @@
import React, { useEffect, useState } from "react";
import {
Backdrop,
Modal,
Fade,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
} from "@material-ui/core";
import useStyles from "../../useStyles";
import { useUpdatesContext } from "../../Contexts/UpdatesContext";
import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import { LocalDateTimeString } from "../../../utils/dates";
export default function CarStatusModal(props) {
const classes = useStyles();
const [updates, setUpdates] = useState([]);
const { setMessage } = useStatusContext();
const { getVINUpdates } = useUpdatesContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
useEffect(() => {
(async () => {
try {
if (!props.vin) return;
const result = await getVINUpdates(props.vin, token);
if (result.error) {
throw new Error(`Get VIN updates error. ${result.message}`);
} else {
setUpdates(result.data);
}
} catch (e) {
setMessage(e.message);
}
})();
// eslint-disable-next-line
}, [props.vin]);
return (
<div>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
className={classes.modal}
open={props.vin !== null && props.vin !== undefined}
onClose={props.handleClose}
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
>
<Fade in={props.vin}>
<div className={classes.modaldialog}>
<h2 id="transition-modal-title">{props.vin} Updates</h2>
<Table>
<TableHead>
<TableRow>
<TableCell align="center">Date</TableCell>
<TableCell align="center">Update</TableCell>
<TableCell align="center">Status</TableCell>
<TableCell align="center">Updated</TableCell>
</TableRow>
</TableHead>
<TableBody>
{updates.map((update) => (
<TableRow key={update.id}>
<TableCell align="center">
{LocalDateTimeString(update.created)}
</TableCell>
<TableCell align="center">{`${update.updatepackage.package_name} ${update.updatepackage.version}`}</TableCell>
<TableCell align="center">{update.status}</TableCell>
<TableCell align="center">
{LocalDateTimeString(update.updated)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</Fade>
</Modal>
</div>
);
}

View File

@@ -81,6 +81,21 @@ export const UpdatesProvider = ({ children }) => {
return result;
};
const getVINUpdates = async (vin, token) => {
let result;
try {
setBusy(true);
result = await api.getVINUpdates(vin, token);
if (result.error)
throw new Error(`Get VIN updates error. ${result.message}`);
} finally {
setBusy(false);
}
return result;
};
return (
<UpdatesContext.Provider
value={{
@@ -93,6 +108,7 @@ export const UpdatesProvider = ({ children }) => {
updatePackage,
createCarUpdates,
getCarUpdates,
getVINUpdates,
}}
>
{children}
@@ -125,7 +141,7 @@ const validateCreateCarUpdates = (data) => {
throw new Error("Package id required");
}
if (!data.car_ids || data.car_ids.length === 0) {
if (!data.vins || data.vins.length === 0) {
throw new Error("Car ids required");
}
};

View File

@@ -7,8 +7,8 @@ import {
fireEvent,
waitFor,
} from "@testing-library/react";
import { UpdatesProvider, useUpdatesContext } from "../Contexts/UpdatesContext";
import { StatusProvider, useStatusContext } from "../Contexts/StatusContext";
import { UpdatesProvider, useUpdatesContext } from "./UpdatesContext";
import { StatusProvider, useStatusContext } from "./StatusContext";
import { TEST_AUTH_OBJECT } from "../../utils/testing";
describe("UpdatesContext", () => {
@@ -222,7 +222,7 @@ describe("UpdatesContext", () => {
data-testid="with-bad-data"
onClick={async () => {
result = await exec(
{ package_id: 1, car_ids: [] },
{ package_id: 1, vins: [] },
TEST_AUTH_OBJECT
);
}}
@@ -233,7 +233,7 @@ describe("UpdatesContext", () => {
result = await exec(
{
package_id: 1,
car_ids: [1, 2, 3],
vins: ["FISKER123", "FISKER124", "FISKER125"],
},
TEST_AUTH_OBJECT
);
@@ -284,7 +284,7 @@ describe("UpdatesContext", () => {
checkState("false", "", {
id: 1,
package_id: 1,
car_ids: [1, 2, 3],
vins: ["FISKER123", "FISKER124", "FISKER125"],
});
});
});

View File

@@ -0,0 +1,26 @@
import React from "react";
const UpdatesContext = React.createContext();
let busy = false;
let packages = [];
let totalPackages = 0;
let carUpdates = [];
let totalCarUpdates = 0;
export const UpdatesProvider = ({ children }) => {
return <div data-testid="mocked-updatesprovider">{children}</div>;
};
export const useUpdatesContext = () => ({
busy,
packages,
totalPackages,
carUpdates,
totalCarUpdates,
getPackages: jest.fn(() => packages),
updatePackage: jest.fn((data) => data),
createCarUpdates: jest.fn((data) => data),
getCarUpdates: jest.fn(() => carUpdates),
getVINUpdates: jest.fn(() => carUpdates),
});

View File

@@ -2,6 +2,7 @@ import React from "react";
let busy = false;
let vehicles = [];
let totalVehicles = 0;
let error = null;
export const VehicleProvider = ({ children }) => {
@@ -11,6 +12,7 @@ export const VehicleProvider = ({ children }) => {
export const useVehicleContext = () => ({
busy,
vehicles,
totalVehicles,
getVehicles: jest.fn(() => vehicles),
addVehicle: jest.fn(),
});

View File

@@ -2,25 +2,60 @@ import React, { Component } from "react";
import PropTypes from "prop-types";
import { Typography } from "@material-ui/core";
const reload = () => {
window.location.reload();
};
export default class ErrorBoundary extends Component {
state = {
error: "",
errorInfo: "",
hasError: false,
};
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
this.setState({ errorInfo });
}
render() {
if (this.state.hasError)
if (this.state.hasError) {
if (this.state.error && this.state.error.name === "ChunkLoadError") {
reload();
return;
}
return (
<Typography variant="h3" align="center">
Oops. An React JS Error Occured.
</Typography>
<div
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
}}
>
<Typography variant="h3" align="center">
Sorry, an error has occured and been logged
</Typography>
<Typography
variant="h5"
align="center"
style={{
cursor: "pointer",
textDecorationColor: "Blue",
textDecorationStyle: "solid",
textDecorationLine: "underline",
color: "Blue",
}}
onClick={reload}
>
Click to reload
</Typography>
</div>
);
}
return this.props.children;
}
}

View File

@@ -15,6 +15,7 @@ const UpdatePackagesForm = React.lazy(() => import("../UpdatePackages/List"));
const UpdatePackageEdit = React.lazy(() => import("../UpdatePackages/Edit"));
const CarUpdatesDeploy = React.lazy(() => import("../CarUpdates/Deploy"));
const CarUpdatesStatus = React.lazy(() => import("../CarUpdates/Status"));
const CarUpdates = React.lazy(() => import("../Cars/Status"));
const VehiclesList = React.lazy(() => import("../Cars/List"));
const SiteRoutes = () => {
@@ -92,6 +93,14 @@ const SiteRoutes = () => {
groups={groups}
roles={[Roles.CREATE]}
/>
<AuthRoute
path="/vehicle-status/:vin"
render={() => <CarUpdates />}
type={TYPES.PROTECTED}
token={token}
groups={groups}
roles={[Roles.READ, Roles.CREATE]}
/>
<PageNotFound />
</Switch>
</Suspense>

View File

@@ -3,12 +3,12 @@
exports[`Sign In Form Should render 1`] = `
<div>
<div
class="makeStyles-paper-1"
class="makeStyles-paper-3"
style="justify-content: center;"
>
<a
aria-disabled="false"
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-4 MuiButton-containedPrimary"
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary"
href="https://cognito.com/authorize?redirect=https://example.com/callback"
tabindex="0"
>

View File

@@ -6,7 +6,7 @@ exports[`File Upload Form Should render 1`] = `
data-testid="mocked-fileuploadprovider"
>
<div
class="makeStyles-paper-1"
class="makeStyles-paper-3"
>
<h1
class="MuiTypography-root MuiTypography-h5"
@@ -15,7 +15,7 @@ exports[`File Upload Form Should render 1`] = `
</h1>
<form
action="{onSubmit}"
class="makeStyles-form-3"
class="makeStyles-form-5"
novalidate=""
>
<div
@@ -51,10 +51,10 @@ exports[`File Upload Form Should render 1`] = `
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
class="PrivateNotchedOutline-root-24 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-23"
class="PrivateNotchedOutline-legendLabelled-26"
>
<span>
Package name
@@ -97,10 +97,10 @@ exports[`File Upload Form Should render 1`] = `
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
class="PrivateNotchedOutline-root-24 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-23"
class="PrivateNotchedOutline-legendLabelled-26"
>
<span>
Version
@@ -135,10 +135,10 @@ exports[`File Upload Form Should render 1`] = `
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
class="PrivateNotchedOutline-root-24 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-23"
class="PrivateNotchedOutline-legendLabelled-26"
>
<span>
Description
@@ -173,10 +173,10 @@ exports[`File Upload Form Should render 1`] = `
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
class="PrivateNotchedOutline-root-24 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-23"
class="PrivateNotchedOutline-legendLabelled-26"
>
<span>
Release Notes URL
@@ -217,7 +217,7 @@ exports[`File Upload Form Should render 1`] = `
</div>
</div>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-4 MuiButton-containedPrimary MuiButton-fullWidth"
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary MuiButton-fullWidth"
tabindex="0"
type="submit"
>

View File

@@ -5,6 +5,17 @@ const MENUITEM_PADDING_TOP = 8;
const DRAWER_WIDTH = 240;
const useStyles = makeStyles((theme) => ({
modal: {
display: "flex",
alignItems: "center",
justifyContent: "center",
},
modaldialog: {
backgroundColor: theme.palette.background.paper,
border: "2px solid #000",
boxShadow: theme.shadows[5],
padding: theme.spacing(2, 4, 3),
},
paper: {
marginTop: theme.spacing(8),
display: "flex",
@@ -105,6 +116,13 @@ const useStyles = makeStyles((theme) => ({
marginLeft: "auto",
marginRight: -12,
},
link: {
cursor: "pointer",
textDecorationColor: "Blue",
textDecorationStyle: "solid",
textDecorationLine: "underline",
color: "Blue",
},
}));
export default useStyles;