CEC-381 Fix install progress (#77)

* Fix install progress

* Remove unused components and inline styles

* Update test

* errors are not the final update state

* Remove max width for main container

* Progress starts at 0
This commit is contained in:
John Wu
2021-08-12 17:51:40 -07:00
committed by GitHub
parent 2b95bab38b
commit f273e25cf8
18 changed files with 489 additions and 612 deletions

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

@@ -81,7 +81,7 @@ const SendCommand = ({ vins }) => {
variant="outlined" variant="outlined"
size="small" size="small"
> >
<InputLabel htmlFor="send-command" style={{ backgroundColor: "White" }}> <InputLabel htmlFor="send-command" className={classes.whiteBackground}>
Command Command
</InputLabel> </InputLabel>
<Select <Select
@@ -108,7 +108,7 @@ const SendCommand = ({ vins }) => {
> >
<InputLabel <InputLabel
htmlFor="send-parameter" htmlFor="send-parameter"
style={{ backgroundColor: "White" }} className={classes.whiteBackground}
> >
Parameter Parameter
</InputLabel> </InputLabel>

View File

@@ -54,9 +54,9 @@ 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={4} style={{ textAlign: "justify" }}> <Grid item md={4} className={classes.textJustifyAlign}>
<Link to="/vehicle-add"> <Link to="/vehicle-add">
<AddCircleIcon fontSize="large" /> <AddCircleIcon fontSize="large" />
</Link> </Link>
@@ -64,11 +64,11 @@ const MainForm = () => {
className={classes.labelInline} className={classes.labelInline}
>{`${selected.length} Selected`}</div> >{`${selected.length} Selected`}</div>
</Grid> </Grid>
<Grid item md={4} style={{ textAlign: "center" }}> <Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch} /> <SearchField classes={classes} onSearch={handleSearch} />
</Grid> </Grid>
<Grid item md={4} style={{ textAlign: "right" }}> <Grid item md={4} className={classes.textRightAlign}>
<SendCommand vins={selected} style={{ display: "flex" }} /> <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,47 +79,40 @@ 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 ( if (validateStatusMessage(status)) {
status.msg === "package_download_start" || if (status.msg === "downloading") {
status.msg === "download_start" item.progress = getDownloadProgress(status);
) { item.status = `downloading ${item.progress}%`;
item.progress = 1; return;
item.status = "downloading"; } else if (status.msg === "package_download_complete") {
} else if (status.msg === "package_download_complete") { item.progress = 100;
item.status = "downloaded"; item.status = "downloaded";
} else if ( return;
status.msg === "downloading" && } else if (status.msg === "installing") {
status.package_current && item.progress = getInstallProgress(status);
status.package_total && item.status = `installing ${item.progress}%`;
status.package_total > 0 return;
) { } else if (status.msg === "package_install_complete") {
let progress = Math.floor( item.progress = 100;
(100 * status.package_current) / status.package_total item.status = "installed";
); return;
if (progress > 99) progress = 0; }
item.progress = progress;
item.status = `downloading ${progress}%`;
} else if (status.error > 0 || status.msg === "download_error") {
item.status = "download error";
} else if (status.msg === "package_install_start") {
item.progress = 1;
item.status = "installing";
} else if (
status.msg === "installing" &&
status.installed &&
status.total_files &&
status.total_files > 0
) {
let progress = Math.floor((100 * status.installed) / status.total_files);
item.progress = progress;
item.status = `installing ${progress}%`;
} else if (status.msg === "package_install_complete") {
item.status = "installed";
} else {
delete item.progress;
item.status = status.msg;
} }
delete item.progress;
item.status = status.msg;
}; };
const applyProgressStatuses = (statuses) => { const applyProgressStatuses = (statuses) => {
@@ -163,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

@@ -71,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

@@ -131,10 +131,10 @@ const MainForm = () => {
className={classes.labelInline} className={classes.labelInline}
>{`${selected.length} Selected`}</div> >{`${selected.length} Selected`}</div>
</Grid> </Grid>
<Grid item md={8} style={{ textAlign: "center" }}> <Grid item md={8} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch} /> <SearchField classes={classes} onSearch={handleSearch} />
</Grid> </Grid>
<Grid item md={2} style={{ textAlign: "right" }}> <Grid item md={2} className={classes.textRightAlign}>
<Button <Button
type="submit" type="submit"
disabled={busy || selected.length === 0} disabled={busy || selected.length === 0}

View File

@@ -184,17 +184,17 @@ 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={4} style={{ textAlign: "justify" }}> <Grid item md={4} className={classes.textJustifyAlign}>
<Link to="/package-create" className={classes.labelInline}> <Link to="/package-create" className={classes.labelInline}>
<AddCircleIcon fontSize="large" /> <AddCircleIcon fontSize="large" />
</Link> </Link>
</Grid> </Grid>
<Grid item md={4} style={{ textAlign: "center" }}> <Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch} /> <SearchField classes={classes} onSearch={handleSearch} />
</Grid> </Grid>
<Grid item md={4} style={{ textAlign: "right" }}></Grid> <Grid item md={4} className={classes.textRightAlign}></Grid>
</Grid> </Grid>
<Table> <Table>
<TableHeaderSortable <TableHeaderSortable

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

@@ -236,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

@@ -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);
});
})