Merge branch 'development' into main

This commit is contained in:
jwu-fisker
2021-08-13 09:33:19 -07:00
22 changed files with 640 additions and 705 deletions

View File

@@ -5,6 +5,7 @@ jest.mock("../Contexts/UserContext");
jest.mock("../Contexts/ManifestsContext"); jest.mock("../Contexts/ManifestsContext");
jest.mock("../Contexts/CarUpdatesContext"); jest.mock("../Contexts/CarUpdatesContext");
jest.mock("../../services/monitoring"); jest.mock("../../services/monitoring");
jest.mock("../../services/grafana");
import { render, screen, cleanup, waitFor, waitForElementToBeRemoved } from "@testing-library/react"; import { render, screen, cleanup, waitFor, waitForElementToBeRemoved } from "@testing-library/react";
import { setToken } from "../Contexts/UserContext"; import { setToken } from "../Contexts/UserContext";
@@ -57,11 +58,11 @@ describe("App", () => {
}); });
it("Route / unauthenticated", async () => { it("Route / unauthenticated", async () => {
await check("/", "span.MuiButton-label", "Sign In"); await sleepAndCheck("/", "span.MuiButton-label", "Sign In");
}); });
it("Route /home unauthenticated", async () => { it("Route /home unauthenticated", async () => {
await check("/home", "span.MuiButton-label", "Sign In"); await sleepAndCheck("/home", "span.MuiButton-label", "Sign In");
}); });
it("Route /vehicle-add unauthenticated", async () => { it("Route /vehicle-add unauthenticated", async () => {
@@ -85,7 +86,7 @@ describe("App", () => {
}); });
it("Route /datascope unauthenticated", async () => { it("Route /datascope unauthenticated", async () => {
await check("/datascope", "span.MuiButton-label", "Sign In"); await sleepAndCheck("/datascope", "span.MuiButton-label", "Sign In");
}); });
it("Route /datascope/battery unauthenticated", async () => { it("Route /datascope/battery unauthenticated", async () => {
@@ -110,12 +111,12 @@ describe("App", () => {
it("Route / authenticated", async () => { it("Route / authenticated", async () => {
setToken(TEST_AUTH_OBJECT); setToken(TEST_AUTH_OBJECT);
await sleepAndCheck("/", "h1", "Welcome John!"); await sleepAndCheck("/", "h6", "Home");
}); });
it("Route /home authenticated", async () => { it("Route /home authenticated", async () => {
setToken(TEST_AUTH_OBJECT); setToken(TEST_AUTH_OBJECT);
await sleepAndCheck("/home", "h1", "Welcome John!"); await sleepAndCheck("/home", "h6", "Home");
}); });
it("Route /vehicle-add authenticated", async () => { it("Route /vehicle-add authenticated", async () => {
@@ -154,7 +155,7 @@ describe("App", () => {
it("Route /datascope authenticated", async () => { it("Route /datascope authenticated", async () => {
setToken(TEST_AUTH_OBJECT); setToken(TEST_AUTH_OBJECT);
await check("/datascope", "h6", "Datascope"); await sleepAndCheck("/datascope", "h6", "Datascope");
}); });
it("Route /datascope/battery authenticated", async () => { it("Route /datascope/battery authenticated", async () => {

File diff suppressed because it is too large Load Diff

View File

@@ -105,7 +105,7 @@ const MainForm = () => {
}; };
return ( return (
<div className={classes.paper} style={{ height: 700, width: "100%" }}> <div className={`${classes.paper} ${classes.tableSize}`}>
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>
@@ -125,7 +125,7 @@ const MainForm = () => {
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
{row.status} {row.status}
{row.progress > 0 && ( {row.progress > -1 && (
<LinearProgress variant="determinate" value={row.progress} /> <LinearProgress variant="determinate" value={row.progress} />
)} )}
</TableCell> </TableCell>

View File

@@ -1,158 +0,0 @@
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
import { FormControl, InputLabel, Select } from "@material-ui/core";
import {
useVehicleContext,
VehicleProvider,
} from "../../Contexts/VehicleContext";
import { useUserContext } from "../../Contexts/UserContext";
import useStyles from "../../useStyles";
const Control = (props) => {
const classes = useStyles();
const { models, years, vehicles, getModels, getYears, getVehicles } =
useVehicleContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
const [model, setModel] = useState("");
const [year, setYear] = useState(-1);
const [trim, setTrim] = useState("");
const handleChangeModel = (event) => {
setModel(event.target.value);
};
const handleChangeYear = (event) => {
setYear(event.target.value);
};
const handleChangeTrim = (event) => {
setTrim(event.target.value);
};
useEffect(() => {
if (!token) return;
(async () => {
try {
await getModels(token);
await getYears(token);
} catch (e) {}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
useEffect(() => {
if (!models || models.length === 0) return;
setModel(models[0]);
}, [models]);
useEffect(() => {
if (!years || years.length === 0) return;
setYear(years[0]);
}, [years]);
useEffect(() => {
if (model === null || year === -1) return;
getVehicles({ model, year, trim }, token);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [model, year, trim]);
useEffect(() => {
if (!props.onSelection) return;
const vins = vehicles.map((item) => item.vin);
props.onSelection(vins);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [vehicles]);
return (
<div className={classes.paper}>
<div className={classes.form}>
<FormControl className={classes.formControlInline} variant="outlined">
<InputLabel htmlFor="car-model" style={{ backgroundColor: "White" }}>
Model
</InputLabel>
<Select
native
value={model}
onChange={handleChangeModel}
variant="outlined"
inputProps={{
name: "car-model",
id: "car-model",
}}
>
{models.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</Select>
</FormControl>
<FormControl className={classes.formControlInline} variant="outlined">
<InputLabel htmlFor="car-year" style={{ backgroundColor: "White" }}>
Year
</InputLabel>
<Select
native
value={year}
onChange={handleChangeYear}
variant="outlined"
inputProps={{
name: "car-year",
id: "car-year",
}}
>
{years.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</Select>
</FormControl>
<FormControl className={classes.formControlInline} variant="outlined">
<InputLabel htmlFor="car-trim" style={{ backgroundColor: "White" }}>
Trim
</InputLabel>
<Select
native
value={year}
onChange={handleChangeTrim}
variant="outlined"
inputProps={{
name: "car-trim",
id: "car-trim",
}}
>
{years.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</Select>
</FormControl>
<div className={classes.labelInline}>
{vehicles.length === 0
? "No Cars Selected"
: vehicles.length === 1
? "1 Car Selected"
: `${vehicles.length} Cars Selected`}
</div>
</div>
<div className={classes.form}></div>
</div>
);
};
const CarSelection = (props) => (
<VehicleProvider>
<Control {...props} />
</VehicleProvider>
);
CarSelection.propTypes = {
onSelection: PropTypes.func,
};
export default CarSelection;

View File

@@ -114,7 +114,7 @@ const CarSelectionTable = (props) => {
}, [pageIndex, pageSize, orderBy, order, search, token]); }, [pageIndex, pageSize, orderBy, order, search, token]);
return ( return (
<div className={classes.paper} style={{ height: 700, width: "100%" }}> <div className={`${classes.paper} ${classes.tableSize}`}>
<Table> <Table>
<TableHeaderSortable <TableHeaderSortable
classes={classes} classes={classes}

View File

@@ -75,9 +75,13 @@ const SendCommand = ({ vins }) => {
}, []); }, []);
return ( return (
<div className={classes.form}> <div className={`${classes.form}`} style={{ marginTop: 20 }}>
<FormControl className={classes.formControlInline} variant="outlined"> <FormControl
<InputLabel htmlFor="send-command" style={{ backgroundColor: "White" }}> className={classes.formControlInline}
variant="outlined"
size="small"
>
<InputLabel htmlFor="send-command" className={classes.whiteBackground}>
Command Command
</InputLabel> </InputLabel>
<Select <Select
@@ -97,10 +101,14 @@ const SendCommand = ({ vins }) => {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl className={classes.formControlInline} variant="outlined"> <FormControl
className={classes.formControlInline}
variant="outlined"
size="small"
>
<InputLabel <InputLabel
htmlFor="send-parameter" htmlFor="send-parameter"
style={{ backgroundColor: "White" }} className={classes.whiteBackground}
> >
Parameter Parameter
</InputLabel> </InputLabel>
@@ -127,6 +135,7 @@ const SendCommand = ({ vins }) => {
aria-label="send command" aria-label="send command"
component="span" component="span"
onClick={clickHandler} onClick={clickHandler}
size="small"
disabled={busy || vins.length === 0} disabled={busy || vins.length === 0}
> >
<SendIcon fontSize="large" /> <SendIcon fontSize="large" />

View File

@@ -54,19 +54,21 @@ const MainForm = () => {
}, []); }, []);
return ( return (
<div className={classes.paper} style={{ height: 700, width: "100%" }}> <div className={`${classes.paper} ${classes.tableSize}`}>
<Grid container className={classes.root} spacing={2}> <Grid container className={classes.root} spacing={2}>
<Grid item md={6}> <Grid item md={4} className={classes.textJustifyAlign}>
<Link to="/vehicle-add" className={classes.labelInline}> <Link to="/vehicle-add">
<AddCircleIcon fontSize="large" /> <AddCircleIcon fontSize="large" />
</Link> </Link>
<SearchField classes={classes} onSearch={handleSearch} />
<div <div
className={classes.labelInline} className={classes.labelInline}
>{`${selected.length} Selected`}</div> >{`${selected.length} Selected`}</div>
</Grid> </Grid>
<Grid item md={6} style={{ textAlign: "right" }}> <Grid item md={4} className={classes.textCenterAlign}>
<SendCommand vins={selected} style={{ display: "flex" }} /> <SearchField classes={classes} onSearch={handleSearch} />
</Grid>
<Grid item md={4} className={classes.textRightAlign}>
<SendCommand vins={selected} />
</Grid> </Grid>
</Grid> </Grid>
<CarSelectionTable <CarSelectionTable

View File

@@ -1,7 +1,9 @@
import React, { useContext, useState } from "react"; import React, { useContext, useState } from "react";
import api from "../../services/updates"; import api from "../../services/updates";
import { validateStatusMessage } from "../../utils/statusMessage";
const FINAL_UPDATE_STATES = ["package_install_complete"];
const CarUpdatesContext = React.createContext(); const CarUpdatesContext = React.createContext();
const validateDeployCarUpdates = (data) => { const validateDeployCarUpdates = (data) => {
@@ -49,6 +51,8 @@ export const CarUpdatesProvider = ({ children }) => {
result = await api.getCarUpdates(search, token); result = await api.getCarUpdates(search, token);
if (result.error) if (result.error)
throw new Error(`Get car updates error. ${result.message}`); throw new Error(`Get car updates error. ${result.message}`);
result.data.forEach((item) => (item.progress = 0));
console.log(result.data);
setCarUpdates(result.data); setCarUpdates(result.data);
if (search && search.offset === 0 && result.total) { if (search && search.offset === 0 && result.total) {
setTotalCarUpdates(result.total); setTotalCarUpdates(result.total);
@@ -75,30 +79,48 @@ export const CarUpdatesProvider = ({ children }) => {
return result; return result;
}; };
const getDownloadProgress = (status) => {
if (status.package_total > 0)
return Math.floor((100 * status.package_current) / status.package_total);
return 0;
};
const getInstallProgress = (status) => {
if (status.total_files > 0)
return Math.floor((100 * status.installed) / status.total_files);
return 0;
};
const applyProgressStatus = (item, status) => { const applyProgressStatus = (item, status) => {
if (status.msg === "package_download_complete") { if (validateStatusMessage(status)) {
delete item.progress; if (status.msg === "downloading") {
item.status = "downloaded"; item.progress = getDownloadProgress(status);
} else if (status.msg === "downloading" && status.package_total > 0) { item.status = `downloading ${item.progress}%`;
let progress = Math.floor( return;
(100 * status.package_current) / status.package_total } else if (status.msg === "package_download_complete") {
); item.progress = 100;
if (progress > 99) progress = 0; item.status = "downloaded";
item.progress = progress; return;
item.status = `downloading ${progress}%`; } else if (status.msg === "installing") {
} else if (status.error > 0 || status.msg === "download_error") { item.progress = getInstallProgress(status);
item.status = "download error"; item.status = `installing ${item.progress}%`;
} else { return;
item.status = "downloading"; } else if (status.msg === "package_install_complete") {
item.progress = 100;
item.status = "installed";
return;
}
} }
delete item.progress;
item.status = status.msg;
}; };
const applyProgressStatuses = (statuses) => { const applyProgressStatuses = (statuses) => {
let items = JSON.parse(JSON.stringify(carUpdates)); let items = JSON.parse(JSON.stringify(carUpdates));
statuses.forEach((status) => { statuses.forEach((status) => {
let item = items.find((item) => status.id === item.id); let item = items.find((item) => status.car_update_id === item.id);
if (!item || status.id === 0) return; if (!item || status.car_update_id === 0) return;
applyProgressStatus(item, status); applyProgressStatus(item, status);
}); });
@@ -113,7 +135,7 @@ export const CarUpdatesProvider = ({ children }) => {
try { try {
setBusy(true); setBusy(true);
const carupdateids = carUpdates.reduce((accum, update) => { const carupdateids = carUpdates.reduce((accum, update) => {
if (update.status !== "downloaded") accum.push(update.id); if (update.status !== "installed") accum.push(update.id);
return accum; return accum;
}, []); }, []);
if (carupdateids.length === 0) return; if (carupdateids.length === 0) return;
@@ -138,7 +160,7 @@ export const CarUpdatesProvider = ({ children }) => {
return 1000; return 1000;
} }
for (let i = 0, len = carUpdates.length; i < len; i++) { for (let i = 0, len = carUpdates.length; i < len; i++) {
if (carUpdates[i].status.indexOf("downloading") > -1) return 1000; if (FINAL_UPDATE_STATES.indexOf(carUpdates[i].status) === -1) return 1000;
} }
return 10000; return 10000;
}; };

View File

@@ -1,15 +1,11 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { Typography } from "@material-ui/core";
import useStyles from "../useStyles"; import useStyles from "../useStyles";
import { useUserContext } from "../Contexts/UserContext";
import { useStatusContext } from "../Contexts/StatusContext"; import { useStatusContext } from "../Contexts/StatusContext";
import VehicleMap from "../VehicleMap"; import VehicleMap from "../VehicleMap";
import { getName } from "../../utils/jwt";
const Home = () => { const Home = () => {
const classes = useStyles(); const classes = useStyles();
const { token } = useUserContext();
const { setTitle, setSitePath } = useStatusContext(); const { setTitle, setSitePath } = useStatusContext();
@@ -21,9 +17,6 @@ const Home = () => {
return ( return (
<div className={classes.paper}> <div className={classes.paper}>
<Typography className={classes.homePageTitle} component="h1" variant="h5">
Welcome {getName(token)}!
</Typography>
<VehicleMap /> <VehicleMap />
</div> </div>
); );

View File

@@ -52,7 +52,9 @@ export default function MenuDrawer({ children }) {
paper: classes.drawerPaper, paper: classes.drawerPaper,
}} }}
> >
<div className={classes.drawerHeader}> <div
className={`${classes.drawerHeader} ${classes.drawerHeaderLogo}`}
>
<img <img
src={logo} src={logo}
alt="Fisker Admin Portal" alt="Fisker Admin Portal"
@@ -69,9 +71,7 @@ export default function MenuDrawer({ children }) {
})} })}
> >
<div className={classes.drawerHeader} /> <div className={classes.drawerHeader} />
<Container component="main" maxWidth="lg"> <Container component="main">{children}</Container>
{children}
</Container>
</main> </main>
</div> </div>
); );

View File

@@ -182,11 +182,10 @@ exports[`SideMenu Authenticated 1`] = `
<li> <li>
<a <a
aria-disabled="false" aria-disabled="false"
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button MuiTypography-colorPrimary" class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiListItem-root makeStyles-menuExternalLink-53 MuiListItem-gutters MuiListItem-button MuiTypography-colorPrimary"
href="https://grafana.fiskerdps.com" href="https://grafana.fiskerdps.com"
rel="noopener" rel="noopener"
role="button" role="button"
style="text-decoration: inherit;"
tabindex="0" tabindex="0"
target="_blank" target="_blank"
> >

View File

@@ -5,31 +5,20 @@ import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText"; import ListItemText from "@material-ui/core/ListItemText";
import { Link } from "@material-ui/core"; import { Link } from "@material-ui/core";
import useStyles from "./useStyles";
function ListItemExternalLink(props) { function ListItemExternalLink(props) {
const { icon, primary, url } = props; const { icon, primary, url } = props;
const style = { const classes = useStyles();
textDecoration: "inherit",
color: "inherit",
"&:link": {
textDecoration: "inherit",
color: "inherit",
cursor: "auto",
},
"&:visited": {
textDecoration: "inherit",
color: "inherit",
cursor: "auto",
},
};
return ( return (
<ListItem <ListItem
button button
component={Link} component={Link}
style={style}
href={url} href={url}
rel="noopener" rel="noopener"
target="_blank" target="_blank"
className={classes.menuExternalLink}
> >
{icon ? <ListItemIcon>{icon}</ListItemIcon> : null} {icon ? <ListItemIcon>{icon}</ListItemIcon> : null}
<ListItemText primary={primary} /> <ListItemText primary={primary} />

View File

@@ -126,17 +126,18 @@ const MainForm = () => {
<form className={classes.form} noValidate action="{onSubmit}"> <form className={classes.form} noValidate action="{onSubmit}">
<Typography variant="body2">Created {createDate}.</Typography> <Typography variant="body2">Created {createDate}.</Typography>
<Grid container className={classes.root} spacing={2}> <Grid container className={classes.root} spacing={2}>
<Grid item md={10}> <Grid item md={2}>
<SearchField classes={classes} onSearch={handleSearch} />
<div <div
className={classes.labelInline} className={classes.labelInline}
>{`${selected.length} Selected`}</div> >{`${selected.length} Selected`}</div>
</Grid> </Grid>
<Grid item md={2} style={{ textAlign: "right" }}> <Grid item md={8} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch} />
</Grid>
<Grid item md={2} className={classes.textRightAlign}>
<Button <Button
type="submit" type="submit"
disabled={busy || selected.length === 0} disabled={busy || selected.length === 0}
fullWidth
variant="contained" variant="contained"
color="primary" color="primary"
className={classes.formControl} className={classes.formControl}

View File

@@ -1,13 +1,13 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { import {
Grid,
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableFooter, TableFooter,
TablePagination, TablePagination,
TableRow, TableRow,
Toolbar,
Tooltip, Tooltip,
} from "@material-ui/core"; } from "@material-ui/core";
import AddCircleIcon from "@material-ui/icons/AddCircle"; import AddCircleIcon from "@material-ui/icons/AddCircle";
@@ -184,13 +184,18 @@ const MainForm = () => {
}; };
return ( return (
<div className={classes.paper} style={{ height: 700, width: "100%" }}> <div className={`${classes.paper} ${classes.tableSize}`}>
<Toolbar className={classes.tableToolbar}> <Grid container className={classes.root} spacing={2}>
<Link to="/package-create" className={classes.labelInline}> <Grid item md={4} className={classes.textJustifyAlign}>
<AddCircleIcon fontSize="large" /> <Link to="/package-create" className={classes.labelInline}>
</Link> <AddCircleIcon fontSize="large" />
<SearchField classes={classes} onSearch={handleSearch} /> </Link>
</Toolbar> </Grid>
<Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch} />
</Grid>
<Grid item md={4} className={classes.textRightAlign}></Grid>
</Grid>
<Table> <Table>
<TableHeaderSortable <TableHeaderSortable
classes={classes} classes={classes}

View File

@@ -119,7 +119,7 @@ const MainForm = () => {
}; };
return ( return (
<div className={classes.paper} style={{ height: 700, width: "100%" }}> <div className={`${classes.paper} ${classes.tableSize}`}>
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>
@@ -139,7 +139,7 @@ const MainForm = () => {
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
{row.status} {row.status}
{row.progress > 0 && ( {row.progress > -1 && (
<LinearProgress variant="determinate" value={row.progress} /> <LinearProgress variant="determinate" value={row.progress} />
)} )}
</TableCell> </TableCell>

View File

@@ -3,8 +3,7 @@
exports[`Sign In Form Should render 1`] = ` exports[`Sign In Form Should render 1`] = `
<div> <div>
<div <div
class="makeStyles-paper-3" class="makeStyles-paper-3 makeStyles-textJustifyAlign-48"
style="justify-content: center;"
> >
<a <a
aria-disabled="false" aria-disabled="false"

View File

@@ -21,7 +21,7 @@ export default function SignInForm() {
}, []); }, []);
return ( return (
<div className={classes.paper} style={{ justifyContent: "center" }}> <div className={`${classes.paper} ${classes.textJustifyAlign}`}>
<Button <Button
type="submit" type="submit"
variant="contained" variant="contained"

View File

@@ -45,7 +45,7 @@ const useStyles = makeStyles((theme) => ({
}, },
labelInline: { labelInline: {
fontSize: "1.25em", fontSize: "1.25em",
margin: theme.spacing(2, 1, 1), margin: theme.spacing(4, 1, 1),
display: "inline-flex", display: "inline-flex",
boxSizing: "border-box", boxSizing: "border-box",
verticalAlign: "bottom", verticalAlign: "bottom",
@@ -77,6 +77,8 @@ const useStyles = makeStyles((theme) => ({
easing: theme.transitions.easing.sharp, easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen, duration: theme.transitions.duration.leavingScreen,
}), }),
color: "black",
backgroundColor: "white",
}, },
appBarShift: { appBarShift: {
width: `calc(100% - ${DRAWER_WIDTH}px)`, width: `calc(100% - ${DRAWER_WIDTH}px)`,
@@ -107,6 +109,9 @@ const useStyles = makeStyles((theme) => ({
...theme.mixins.toolbar, ...theme.mixins.toolbar,
justifyContent: "flex-start", justifyContent: "flex-start",
}, },
drawerHeaderLogo: {
backgroundColor: "#3f51b5",
},
content: { content: {
flexGrow: 1, flexGrow: 1,
padding: theme.spacing(3), padding: theme.spacing(3),
@@ -231,6 +236,32 @@ const useStyles = makeStyles((theme) => ({
datascopeContainerValue: { datascopeContainerValue: {
margin: 0, margin: 0,
}, },
textJustifyAlign: { textAlign: "justifyContent" },
textCenterAlign: { textAlign: "center" },
textRightAlign: { textAlign: "right" },
fullWidth: { width: "100%" },
pageCenter: {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
},
menuExternalLink: {
textDecoration: "inherit",
color: "inherit",
"&:link": {
textDecoration: "inherit",
color: "inherit",
cursor: "auto",
},
"&:visited": {
textDecoration: "inherit",
color: "inherit",
cursor: "auto",
},
},
tableSize: { height: 700, width: "100%" },
whiteBackground: { backgroundColor: "White" },
})); }));
export default useStyles; export default useStyles;

View File

@@ -1,19 +1,6 @@
const grafanaAPI = { const grafanaAPI = {
getCarsCount: async () => fetch(`${API_ENDPOINT}/?query=SELECT%20countDistinct(vin)%20as%20count%0AFROM%20default.vehicle_data%20FORMAT%20JSON`, { getCarsCount: async () => 500,
method: "GET", getSignalsCount: async () => 1234567890,
headers: Object.assign({ "Content-Type": "application/json" }),
})
.then(fetchRespHandler)
.then(result => result.data[0].count)
.catch(error => console.log(error)),
getSignalsCount: async () => fetch(`${API_ENDPOINT}/?query=SELECT%20count()%20as%20count%0AFROM%20default.vehicle_signal%20FORMAT%20JSON`, {
method: "GET",
headers: Object.assign({ "Content-Type": "application/json" }),
})
.then(fetchRespHandler)
.then(result => result.data[0].count)
.catch(error => console.log(error)),
}; };
export default vehiclesAPI; export default grafanaAPI;

View File

@@ -1,6 +1,23 @@
import { fetchRespHandler } from "../utils/http"
const API_ENDPOINT = "https://grafana.fiskerdps.com/api/datasources/proxy/2"
const grafanaAPI = { const grafanaAPI = {
getCarsCount: async () => 500, getCarsCount: async () => fetch(`${API_ENDPOINT}/?query=SELECT%20countDistinct(vin)%20as%20count%0AFROM%20default.vehicle_data%20FORMAT%20JSON`, {
getSignalsCount: async () => 1234567890, method: "GET",
headers: Object.assign({ "Content-Type": "application/json" }),
})
.then(fetchRespHandler)
.then(result => result.data[0].count)
.catch(error => console.log(error)),
getSignalsCount: async () => fetch(`${API_ENDPOINT}/?query=SELECT%20count()%20as%20count%0AFROM%20default.vehicle_signal%20FORMAT%20JSON`, {
method: "GET",
headers: Object.assign({ "Content-Type": "application/json" }),
})
.then(fetchRespHandler)
.then(result => result.data[0].count)
.catch(error => console.log(error)),
}; };
export default grafanaAPI; export default grafanaAPI;

View File

@@ -0,0 +1,23 @@
const DOWNLOAD_STATUSES = ["download_start", "downloading", "download_complete", "download_error", "package_download_complete"];
const INSTALL_STATUSES = ["install_start", "installing", "install_complete", "install_error", "pacakge_install_complete"];
export const isDownloadStatusMessage = (status) => {
return DOWNLOAD_STATUSES.indexOf(status.msg) > -1;
};
export const isInstallStatusMessage = (status) => {
return INSTALL_STATUSES.indexOf(status.msg) > -1;
};
export const validateStatusMessage = (status) => {
if (isDownloadStatusMessage(status)) {
if (!(status.package_current > -1 && status.package_total > -1)) return false;
if (status.package_current > status.package_total) return false;
return true;
} else if (isInstallStatusMessage(status)) {
if (!(status.installed > -1 && status.total_files > -1)) return false;
if (status.installed > status.total_files) return false;
return true;
}
return false;
};

View File

@@ -0,0 +1,48 @@
import { isDownloadStatusMessage, isInstallStatusMessage, validateStatusMessage} from "./statusMessage";
describe("validatons", () => {
it("isDownloadStatusMessage", () => {
expect(isDownloadStatusMessage({ msg: "download_start"})).toEqual(true);
expect(isDownloadStatusMessage({ msg: "downloading"})).toEqual(true);
expect(isDownloadStatusMessage({ msg: "download_complete"})).toEqual(true);
expect(isDownloadStatusMessage({ msg: "download_error"})).toEqual(true);
expect(isDownloadStatusMessage({ msg: "package_download_complete"})).toEqual(true);
expect(isDownloadStatusMessage({ msg: "installing"})).toEqual(false);
expect(isDownloadStatusMessage({ msg: "install_start"})).toEqual(false);
expect(isDownloadStatusMessage({ msg: "install_complete"})).toEqual(false);
expect(isDownloadStatusMessage({ msg: "install_error"})).toEqual(false);
expect(isDownloadStatusMessage({ msg: "pacakge_install_complete"})).toEqual(false);
});
it("isInstallStatusMessage", () => {
expect(isInstallStatusMessage({ msg: "download_start"})).toEqual(false);
expect(isInstallStatusMessage({ msg: "downloading"})).toEqual(false);
expect(isInstallStatusMessage({ msg: "download_complete"})).toEqual(false);
expect(isInstallStatusMessage({ msg: "download_error"})).toEqual(false);
expect(isInstallStatusMessage({ msg: "package_download_complete"})).toEqual(false);
expect(isInstallStatusMessage({ msg: "installing"})).toEqual(true);
expect(isInstallStatusMessage({ msg: "install_start"})).toEqual(true);
expect(isInstallStatusMessage({ msg: "install_complete"})).toEqual(true);
expect(isInstallStatusMessage({ msg: "install_error"})).toEqual(true);
expect(isInstallStatusMessage({ msg: "pacakge_install_complete"})).toEqual(true);
});
it("validateStatusMessage", () => {
expect(validateStatusMessage({ msg: "download_start", package_current: 0, package_total: 0})).toEqual(true);
expect(validateStatusMessage({ msg: "downloading", package_current: 0, package_total: 0})).toEqual(true);
expect(validateStatusMessage({ msg: "download_complete", package_current: 0, package_total: 0})).toEqual(true);
expect(validateStatusMessage({ msg: "download_error", package_current: 0, package_total: 0})).toEqual(true);
expect(validateStatusMessage({ msg: "package_download_complete", package_current: 0, package_total: 0})).toEqual(true);
expect(validateStatusMessage({ msg: "installing", installed: 0, total_files: 0 })).toEqual(true);
expect(validateStatusMessage({ msg: "install_start", installed: 0, total_files: 0 })).toEqual(true);
expect(validateStatusMessage({ msg: "install_complete", installed: 0, total_files: 0 })).toEqual(true);
expect(validateStatusMessage({ msg: "install_error", installed: 0, total_files: 0 })).toEqual(true);
expect(validateStatusMessage({ msg: "pacakge_install_complete", installed: 0, total_files: 0 })).toEqual(true);
expect(validateStatusMessage({ msg: "downloading", package_current: -1, package_total: -1})).toEqual(false);
expect(validateStatusMessage({ msg: "installing", installed: -1, total_files: -1})).toEqual(false);
expect(validateStatusMessage({ msg: "download_start", package_current: 100, package_total: 50})).toEqual(false);
expect(validateStatusMessage({ msg: "install_start", installed: 10, total_files: 9})).toEqual(false);
});
})