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

View File

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

View File

@@ -54,9 +54,9 @@ const MainForm = () => {
}, []);
return (
<div className={classes.paper} style={{ height: 700, width: "100%" }}>
<div className={`${classes.paper} ${classes.tableSize}`}>
<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">
<AddCircleIcon fontSize="large" />
</Link>
@@ -64,11 +64,11 @@ const MainForm = () => {
className={classes.labelInline}
>{`${selected.length} Selected`}</div>
</Grid>
<Grid item md={4} style={{ textAlign: "center" }}>
<Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch} />
</Grid>
<Grid item md={4} style={{ textAlign: "right" }}>
<SendCommand vins={selected} style={{ display: "flex" }} />
<Grid item md={4} className={classes.textRightAlign}>
<SendCommand vins={selected} />
</Grid>
</Grid>
<CarSelectionTable

View File

@@ -1,7 +1,9 @@
import React, { useContext, useState } from "react";
import api from "../../services/updates";
import { validateStatusMessage } from "../../utils/statusMessage";
const FINAL_UPDATE_STATES = ["package_install_complete"];
const CarUpdatesContext = React.createContext();
const validateDeployCarUpdates = (data) => {
@@ -49,6 +51,8 @@ export const CarUpdatesProvider = ({ children }) => {
result = await api.getCarUpdates(search, token);
if (result.error)
throw new Error(`Get car updates error. ${result.message}`);
result.data.forEach((item) => (item.progress = 0));
console.log(result.data);
setCarUpdates(result.data);
if (search && search.offset === 0 && result.total) {
setTotalCarUpdates(result.total);
@@ -75,47 +79,40 @@ export const CarUpdatesProvider = ({ children }) => {
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) => {
if (
status.msg === "package_download_start" ||
status.msg === "download_start"
) {
item.progress = 1;
item.status = "downloading";
} else if (status.msg === "package_download_complete") {
item.status = "downloaded";
} else if (
status.msg === "downloading" &&
status.package_current &&
status.package_total &&
status.package_total > 0
) {
let progress = Math.floor(
(100 * status.package_current) / status.package_total
);
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;
if (validateStatusMessage(status)) {
if (status.msg === "downloading") {
item.progress = getDownloadProgress(status);
item.status = `downloading ${item.progress}%`;
return;
} else if (status.msg === "package_download_complete") {
item.progress = 100;
item.status = "downloaded";
return;
} else if (status.msg === "installing") {
item.progress = getInstallProgress(status);
item.status = `installing ${item.progress}%`;
return;
} else if (status.msg === "package_install_complete") {
item.progress = 100;
item.status = "installed";
return;
}
}
delete item.progress;
item.status = status.msg;
};
const applyProgressStatuses = (statuses) => {
@@ -163,7 +160,7 @@ export const CarUpdatesProvider = ({ children }) => {
return 1000;
}
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;
};

View File

@@ -71,9 +71,7 @@ export default function MenuDrawer({ children }) {
})}
>
<div className={classes.drawerHeader} />
<Container component="main" maxWidth="lg">
{children}
</Container>
<Container component="main">{children}</Container>
</main>
</div>
);

View File

@@ -182,11 +182,10 @@ exports[`SideMenu Authenticated 1`] = `
<li>
<a
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"
rel="noopener"
role="button"
style="text-decoration: inherit;"
tabindex="0"
target="_blank"
>

View File

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

View File

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

View File

@@ -184,17 +184,17 @@ const MainForm = () => {
};
return (
<div className={classes.paper} style={{ height: 700, width: "100%" }}>
<div className={`${classes.paper} ${classes.tableSize}`}>
<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}>
<AddCircleIcon fontSize="large" />
</Link>
</Grid>
<Grid item md={4} style={{ textAlign: "center" }}>
<Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch} />
</Grid>
<Grid item md={4} style={{ textAlign: "right" }}></Grid>
<Grid item md={4} className={classes.textRightAlign}></Grid>
</Grid>
<Table>
<TableHeaderSortable

View File

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

View File

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

View File

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

View File

@@ -236,6 +236,32 @@ const useStyles = makeStyles((theme) => ({
datascopeContainerValue: {
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;

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