Merge Development to Main (#36)
* Fix sign up form bug * Add run.sh to run setup and run web app * Output node version * Update readme with run.sh * Fix file upload form to handle ota_update service * Enable file upload form Enable error boundary to catch React errors (#7) Fix warning for link noreferrer Include authorization header with file upload * Remove default localhost settings (#8) * Remove default localhost settings Replace with deployment settings * Fix for upload data format * Fix test data for last commit * Fix json link format and remove localhost default settings (#10) * Remove default localhost settings Replace with deployment settings * Fix for upload data format * Fix test data for last commit * Fix link data format * Fix link json again (#12) Use id token instead of access token * nginx things * Web Worker Sign Out and Use Go API (#13) * Calculate checksum and send with file upload * Limit file upload and display rejected file error * Add sign in timeout * Check auth token structure before setting Clean up * Use web worker timer to sign out Remove checksum Point to Go ota update * Remove checksum dependency * Use compute auth service and fix static code analyzer warnings (#15) * Clean up formatting * Use new compute_auth service Implment SSO Implement token refresh Clean up unit tests * Fix unit tests * Fix auth test Fix warnings * Update default settings for compute_auth * Change main UI layout and add VINs to add and upload forms (#16) * Add new upload update package form Add new add vehicle form Add new side menu layout Add new toolbar layout Update and add unit tests * Enable add get and add vehicles * Integration issues with ota_update service * Update get vehicle JSON format * Fix related unit test Add release notes field * Add StatusContext to display error and status messages * Handle api error json (#18) * Handle api error json * Fix get vehicles error handling Update .env.template * Fix signout refresh (#20) * Merge to main (#17) * Fix sign up form bug * Add run.sh to run setup and run web app * Output node version * Update readme with run.sh * Fix file upload form to handle ota_update service * Enable file upload form Enable error boundary to catch React errors (#7) Fix warning for link noreferrer Include authorization header with file upload * Remove default localhost settings (#8) * Remove default localhost settings Replace with deployment settings * Fix for upload data format * Fix test data for last commit * Fix json link format and remove localhost default settings (#10) * Remove default localhost settings Replace with deployment settings * Fix for upload data format * Fix test data for last commit * Fix link data format * Fix link json again (#12) Use id token instead of access token * nginx things * Web Worker Sign Out and Use Go API (#13) * Calculate checksum and send with file upload * Limit file upload and display rejected file error * Add sign in timeout * Check auth token structure before setting Clean up * Use web worker timer to sign out Remove checksum Point to Go ota update * Remove checksum dependency * Use compute auth service and fix static code analyzer warnings (#15) * Clean up formatting * Use new compute_auth service Implment SSO Implement token refresh Clean up unit tests * Fix unit tests * Fix auth test Fix warnings * Update default settings for compute_auth * Change main UI layout and add VINs to add and upload forms (#16) * Add new upload update package form Add new add vehicle form Add new side menu layout Add new toolbar layout Update and add unit tests * Enable add get and add vehicles * Integration issues with ota_update service * Update get vehicle JSON format * Fix related unit test Add release notes field * Add StatusContext to display error and status messages * Handle api error json (#18) * Handle api error json * Fix get vehicles error handling Update .env.template Co-authored-by: Rafi Greenberg <rgreenberg@fiskerinc.com> * Fix sign out and refresh * Check for bad json Co-authored-by: Rafi Greenberg <rgreenberg@fiskerinc.com> * Add role checks (#21) * Add role checks * Remove moved Roles enum * Add package updates, car updates, and vehicle screens (#25) * Car table update (#27) * Add Datadog RUM (#28) * fix run.sh * Add updates by car screen and modal popup (#29) * CEC-180 Cache Control (#30) * Set cache expire to 1 day Add snapshot tests for new screens * Fix table pagniation random ids for snapshot tests * Auto reload on chunk load error * OTA Admin Portal => Admin Portal * CEC-179 Car download progress (#32) * Display download progress * Change default * Fix * Fix * Update readme * Update readme and defaults Fix Dockerfile * CEC-179 Car update progress build fix (#33) * Display download progress * Change default * Fix * Fix * Update readme * Update readme and defaults Fix Dockerfile * Fix build * Undo Docker changes (#34) Co-authored-by: Rafi Greenberg <rgreenberg@fiskerinc.com> Co-authored-by: Roger Standridge <rstandridge@fiskerinc.com>
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
REACT_APP_AUTH_SERVICE_URL = https://dev-auth.fiskerdps.com
|
REACT_APP_AUTH_SERVICE_URL = http://localhost/compute_auth
|
||||||
REACT_APP_UPLOAD_SERVICE_URL = https://gw-dev.fiskerdps.com
|
REACT_APP_UPLOAD_SERVICE_URL = http://localhost/ota_update
|
||||||
REACT_APP_AUTH_CALLBACK_URL = https://dev-ota-admin.fiskerdps.com/
|
REACT_APP_AUTH_CALLBACK_URL = http://localhost:3000
|
||||||
17
README.md
17
README.md
@@ -1,16 +1,23 @@
|
|||||||
# Fisker Admin Portal
|
# Fisker Admin Portal
|
||||||
|
|
||||||
Front-end web application for administarting OTA services
|
Front-end web application for administrating services
|
||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
|
|
||||||
Run `./run.sh` from the terminal or
|
Running locally
|
||||||
|
|
||||||
1. Install Node 12
|
1. Install Node 12
|
||||||
2. Run `npm install`
|
2. Run `npm install`
|
||||||
3. Setup environment variables listed in .env.template
|
3. Copy .env.template to .env and edit the service urls for authentication and api services
|
||||||
4. Or copy .env.template to .env
|
4. Run `./run.sh` from the terminal
|
||||||
5. Edit .env with the service urls for authentication and api services
|
5. Access portal at localhost:3000
|
||||||
|
|
||||||
|
Running Docker container
|
||||||
|
|
||||||
|
1. Copy .env.template to .env and edit the service urls for authentication and api services
|
||||||
|
2. Build the image `docker build -t fiskerinc/portal .`
|
||||||
|
3. Start the container `docker run -p 3000:80 fiskerinc/portal`
|
||||||
|
4. Access portal at localhost:3000
|
||||||
|
|
||||||
## Available Scripts
|
## Available Scripts
|
||||||
|
|
||||||
|
|||||||
@@ -59,12 +59,6 @@ describe("App", () => {
|
|||||||
await check("/package-upload", "span.MuiButton-label", "Sign In");
|
await check("/package-upload", "span.MuiButton-label", "Sign In");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Route /package-upload unauthenticated", async () => {
|
|
||||||
const container = await renderRoute("/package-upload");
|
|
||||||
expect(container.querySelector("span.MuiButton-label").innerHTML).toEqual("Sign In");
|
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Route /vehicle-add unauthenticated", async () => {
|
it("Route /vehicle-add unauthenticated", async () => {
|
||||||
await check("/vehicle-add", "span.MuiButton-label", "Sign In");
|
await check("/vehicle-add", "span.MuiButton-label", "Sign In");
|
||||||
});
|
});
|
||||||
@@ -105,7 +99,7 @@ describe("App", () => {
|
|||||||
|
|
||||||
it("Route /package-upload authenticated", async () => {
|
it("Route /package-upload authenticated", async () => {
|
||||||
setToken(TEST_AUTH_OBJECT);
|
setToken(TEST_AUTH_OBJECT);
|
||||||
await check("/package-upload", "h1", "Upload Update Package");
|
await check("/package-upload", "h1", "Create Update Package");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Route /vehicle-add authenticated", async () => {
|
it("Route /vehicle-add authenticated", async () => {
|
||||||
@@ -115,17 +109,17 @@ describe("App", () => {
|
|||||||
|
|
||||||
it("Route /updates authenticated", async () => {
|
it("Route /updates authenticated", async () => {
|
||||||
setToken(TEST_AUTH_OBJECT);
|
setToken(TEST_AUTH_OBJECT);
|
||||||
await check("/updates", "h1", "Updates");
|
await check("/updates", "h1", "Update Packages");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Route /update authenticated", async () => {
|
it("Route /update authenticated", async () => {
|
||||||
setToken(TEST_AUTH_OBJECT);
|
setToken(TEST_AUTH_OBJECT);
|
||||||
await check("/update/1", "h1", "Update Package 1");
|
await check("/update/1", "h1", "Edit Update Package 1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Route /carupdate-deploy authenticated", async () => {
|
it("Route /carupdate-deploy authenticated", async () => {
|
||||||
setToken(TEST_AUTH_OBJECT);
|
setToken(TEST_AUTH_OBJECT);
|
||||||
await check("/carupdate-deploy/1", "h1", "[1] ");
|
await check("/carupdate-deploy/1", "h1", "Deploy [1]");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Route /carupdate-status authenticated", async () => {
|
it("Route /carupdate-status authenticated", async () => {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useParams } from "react-router";
|
import { useParams, Redirect } from "react-router";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Chip,
|
Chip,
|
||||||
@@ -44,6 +44,7 @@ const MainForm = () => {
|
|||||||
const [releaseNotesLink, setReleaseNotesLink] = useState("");
|
const [releaseNotesLink, setReleaseNotesLink] = useState("");
|
||||||
const [createDate, setCreateDate] = useState("");
|
const [createDate, setCreateDate] = useState("");
|
||||||
const [selectedVehicles, setSelectedVehicles] = useState([]);
|
const [selectedVehicles, setSelectedVehicles] = useState([]);
|
||||||
|
const [redirect, setRedirect] = useState("");
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const handleVehiclesChange = (event) => {
|
const handleVehiclesChange = (event) => {
|
||||||
@@ -60,7 +61,7 @@ const MainForm = () => {
|
|||||||
setMessage(
|
setMessage(
|
||||||
`Deployed ${packageName} ${version} to ${selectedVehicles.length} cars`
|
`Deployed ${packageName} ${version} to ${selectedVehicles.length} cars`
|
||||||
);
|
);
|
||||||
setSelectedVehicles([]);
|
setRedirect(`/carupdate-status/${packageid}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setMessage(e.message);
|
setMessage(e.message);
|
||||||
}
|
}
|
||||||
@@ -95,10 +96,14 @@ const MainForm = () => {
|
|||||||
setCreateDate(tsLocalDateTimeString(data.timestamp));
|
setCreateDate(tsLocalDateTimeString(data.timestamp));
|
||||||
}, [packages]);
|
}, [packages]);
|
||||||
|
|
||||||
|
if (redirect.length > 0) {
|
||||||
|
return <Redirect to={redirect} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.paper}>
|
<div className={classes.paper}>
|
||||||
<Typography component="h1" variant="h5">
|
<Typography component="h1" variant="h5">
|
||||||
{`[${packageid}] ${packageName} ${version}`}
|
Deploy {`${packageName} ${version} [${packageid}]`}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form className={classes.form} noValidate action="{onSubmit}">
|
<form className={classes.form} noValidate action="{onSubmit}">
|
||||||
<TextField
|
<TextField
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import {
|
import {
|
||||||
|
LinearProgress,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
@@ -25,16 +26,17 @@ import VehicleStatus from "../../Cars/StatusModal";
|
|||||||
const MainForm = () => {
|
const MainForm = () => {
|
||||||
const { packageid } = useParams();
|
const { packageid } = useParams();
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(25);
|
||||||
const [pageIndex, setPageIndex] = useState(0);
|
const [pageIndex, setPageIndex] = useState(0);
|
||||||
const [viewVIN, setViewVIN] = useState(null);
|
const [viewVIN, setViewVIN] = useState(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getCarUpdates,
|
getCarUpdates,
|
||||||
carUpdates,
|
carUpdates,
|
||||||
totalCarUpdates,
|
totalCarUpdates,
|
||||||
getPackages,
|
getPackages,
|
||||||
packages,
|
packages,
|
||||||
|
startMonitor,
|
||||||
|
stopMonitor,
|
||||||
} = useUpdatesContext();
|
} = useUpdatesContext();
|
||||||
const { setMessage } = useStatusContext();
|
const { setMessage } = useStatusContext();
|
||||||
const {
|
const {
|
||||||
@@ -54,6 +56,7 @@ const MainForm = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
|
stopMonitor();
|
||||||
getCarUpdates(
|
getCarUpdates(
|
||||||
{
|
{
|
||||||
packageid,
|
packageid,
|
||||||
@@ -68,6 +71,19 @@ const MainForm = () => {
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, [pageIndex, pageSize, token]);
|
}, [pageIndex, pageSize, token]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
if (carUpdates.length === 0) return;
|
||||||
|
startMonitor(token);
|
||||||
|
} catch (e) {
|
||||||
|
setMessage(e.message);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
stopMonitor();
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [carUpdates]);
|
||||||
|
|
||||||
const handleChangePageIndex = (event, newIndex) => {
|
const handleChangePageIndex = (event, newIndex) => {
|
||||||
setPageIndex(newIndex);
|
setPageIndex(newIndex);
|
||||||
};
|
};
|
||||||
@@ -113,7 +129,15 @@ const MainForm = () => {
|
|||||||
{row.vin}
|
{row.vin}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="center">{row.status}</TableCell>
|
<TableCell align="center">
|
||||||
|
{row.status}
|
||||||
|
{row.progress > 0 && (
|
||||||
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={row.progress}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
{LocalDateTimeString(row.created)}
|
{LocalDateTimeString(row.created)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { LocalDateTimeString } from "../../../utils/dates";
|
|||||||
|
|
||||||
const MainForm = () => {
|
const MainForm = () => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [pageSize, setPageSize] = useState(5);
|
const [pageSize, setPageSize] = useState(25);
|
||||||
const [pageIndex, setPageIndex] = useState(0);
|
const [pageIndex, setPageIndex] = useState(0);
|
||||||
const { getVehicles, vehicles, totalVehicles } = useVehicleContext();
|
const { getVehicles, vehicles, totalVehicles } = useVehicleContext();
|
||||||
const { setMessage } = useStatusContext();
|
const { setMessage } = useStatusContext();
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export const FileUploadProvider = ({ children }) => {
|
|||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const [status, setStatus] = useState(null);
|
const [status, setStatus] = useState(null);
|
||||||
const [cancelUpload, setCancelUpload] = useState(null);
|
const [cancelUpload, setCancelUpload] = useState(null);
|
||||||
const [linkURL, setLinkURL] = useState(null);
|
|
||||||
const [files, setFiles] = useState(null);
|
const [files, setFiles] = useState(null);
|
||||||
|
|
||||||
const done = () => {
|
const done = () => {
|
||||||
@@ -45,6 +44,14 @@ export const FileUploadProvider = ({ children }) => {
|
|||||||
if (!accessToken || accessToken.length === 0) {
|
if (!accessToken || accessToken.length === 0) {
|
||||||
throw new Error("Access token required");
|
throw new Error("Access token required");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!formData.description || formData.description.length === 0) {
|
||||||
|
throw new Error("Package update description required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.releasenotes || formData.releasenotes.length === 0) {
|
||||||
|
throw new Error("Package update release notes link required");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const upload = async (formData, accessToken, uploadFiles) => {
|
const upload = async (formData, accessToken, uploadFiles) => {
|
||||||
@@ -55,7 +62,6 @@ export const FileUploadProvider = ({ children }) => {
|
|||||||
const filename = file.name;
|
const filename = file.name;
|
||||||
|
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
setLinkURL(null);
|
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
setStatus(`Uploading ${filename}`);
|
setStatus(`Uploading ${filename}`);
|
||||||
setCancelUpload(getCancelToken());
|
setCancelUpload(getCancelToken());
|
||||||
@@ -70,11 +76,11 @@ export const FileUploadProvider = ({ children }) => {
|
|||||||
if (data.message) {
|
if (data.message) {
|
||||||
throw new Error(`${data.error}. ${data.message}`);
|
throw new Error(`${data.error}. ${data.message}`);
|
||||||
}
|
}
|
||||||
const url = data && data.link ? data.link : "No URL available";
|
|
||||||
setLinkURL(url);
|
|
||||||
setStatus(`Uploaded ${filename}`);
|
setStatus(`Uploaded ${filename}`);
|
||||||
setCancelUpload(null);
|
setCancelUpload(null);
|
||||||
setProgress(100);
|
setProgress(100);
|
||||||
|
|
||||||
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
setStatus(`Error occured: ${e.message}`);
|
setStatus(`Error occured: ${e.message}`);
|
||||||
@@ -88,7 +94,6 @@ export const FileUploadProvider = ({ children }) => {
|
|||||||
uploading,
|
uploading,
|
||||||
progress,
|
progress,
|
||||||
status,
|
status,
|
||||||
linkURL,
|
|
||||||
files,
|
files,
|
||||||
upload,
|
upload,
|
||||||
cancel,
|
cancel,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
fireEvent,
|
fireEvent,
|
||||||
waitFor,
|
waitFor,
|
||||||
} from "@testing-library/react";
|
} from "@testing-library/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
import { setUploadFileDelay } from "../../services/uploadFile";
|
import { setUploadFileDelay } from "../../services/uploadFile";
|
||||||
import {
|
import {
|
||||||
@@ -30,21 +31,26 @@ describe("FileUploadContext", () => {
|
|||||||
progress,
|
progress,
|
||||||
uploading,
|
uploading,
|
||||||
status,
|
status,
|
||||||
linkURL,
|
|
||||||
upload,
|
upload,
|
||||||
cancel,
|
cancel,
|
||||||
} = useFileUploadContext();
|
} = useFileUploadContext();
|
||||||
const { message, setMessage } = useStatusContext();
|
const { message, setMessage } = useStatusContext();
|
||||||
|
const [link, setLink] = useState(null);
|
||||||
const TEST_FILE = [{ name: "test.jpg", size: 0 }];
|
const TEST_FILE = [{ name: "test.jpg", size: 0 }];
|
||||||
const TEST_ACCESSTOKEN = "ACCESSTOKEN";
|
const TEST_ACCESSTOKEN = "ACCESSTOKEN";
|
||||||
const TEST_FORMDATA = {
|
const TEST_FORMDATA = {
|
||||||
packagename: "TEST",
|
packagename: "TEST",
|
||||||
version: "VERSION",
|
version: "VERSION",
|
||||||
vehicles: ["VIN"],
|
vehicles: ["VIN"],
|
||||||
|
description: "TEST DESC",
|
||||||
|
releasenotes: "http://releasenotes.com",
|
||||||
};
|
};
|
||||||
const exec = async (form, token, file) => {
|
const exec = async (form, token, file) => {
|
||||||
try {
|
try {
|
||||||
await upload(form, token, file);
|
const data = await upload(form, token, file);
|
||||||
|
if (data.link) {
|
||||||
|
setLink(data.link);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setMessage(e.message);
|
setMessage(e.message);
|
||||||
}
|
}
|
||||||
@@ -56,7 +62,7 @@ describe("FileUploadContext", () => {
|
|||||||
<div data-testid="progress">{progress.toString()}</div>
|
<div data-testid="progress">{progress.toString()}</div>
|
||||||
<div data-testid="status">{status}</div>
|
<div data-testid="status">{status}</div>
|
||||||
<div data-testid="message">{message}</div>
|
<div data-testid="message">{message}</div>
|
||||||
<div data-testid="linkURL">{linkURL}</div>
|
<div data-testid="linkURL">{link}</div>
|
||||||
<button
|
<button
|
||||||
data-testid="uploadNoFile"
|
data-testid="uploadNoFile"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export const UpdatesProvider = ({ children }) => {
|
|||||||
const [carUpdates, setCarUpdates] = useState([]);
|
const [carUpdates, setCarUpdates] = useState([]);
|
||||||
const [totalPackages, setTotalPackages] = useState(0);
|
const [totalPackages, setTotalPackages] = useState(0);
|
||||||
const [totalCarUpdates, setTotalCarUpdates] = useState(0);
|
const [totalCarUpdates, setTotalCarUpdates] = useState(0);
|
||||||
|
const [delayCount, setDelayCount] = useState(0);
|
||||||
|
let progressTimer = 0;
|
||||||
|
|
||||||
const getPackages = async (search, token) => {
|
const getPackages = async (search, token) => {
|
||||||
let result;
|
let result;
|
||||||
@@ -96,6 +98,86 @@ export const UpdatesProvider = ({ children }) => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const applyProgressStatus = (item, status) => {
|
||||||
|
if (status.msg === "DONE") {
|
||||||
|
delete item.progress;
|
||||||
|
item.status = "downloaded";
|
||||||
|
} else if (status.msg === "downloading" && status.total > 0) {
|
||||||
|
let progress = Math.floor((100 * status.bytes) / status.total);
|
||||||
|
if (progress > 99) progress = 0;
|
||||||
|
item.progress = progress;
|
||||||
|
item.status = `downloading ${progress}%`;
|
||||||
|
} else if (status.error > 0) {
|
||||||
|
item.status = "download error";
|
||||||
|
} else {
|
||||||
|
item.status = "downloading";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyProgressStatuses = (statuses) => {
|
||||||
|
let items = JSON.parse(JSON.stringify(carUpdates));
|
||||||
|
|
||||||
|
statuses.forEach((status) => {
|
||||||
|
let item = items.find((item) => status.id === item.id);
|
||||||
|
if (!item || status.id === 0) return;
|
||||||
|
applyProgressStatus(item, status);
|
||||||
|
});
|
||||||
|
|
||||||
|
setCarUpdates(items);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateStatusProgress = async (token) => {
|
||||||
|
stopMonitor();
|
||||||
|
|
||||||
|
if (!token || carUpdates.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setBusy(true);
|
||||||
|
const carupdateids = carUpdates.reduce((accum, update) => {
|
||||||
|
if (update.status !== "downloaded") accum.push(update.id);
|
||||||
|
return accum;
|
||||||
|
}, []);
|
||||||
|
if (carupdateids.length === 0) return;
|
||||||
|
|
||||||
|
const result = await api.getCarUpdateProgress(
|
||||||
|
carupdateids.join(","),
|
||||||
|
token
|
||||||
|
);
|
||||||
|
if (result.error)
|
||||||
|
throw new Error(`Get update progress error. ${result.message}`);
|
||||||
|
|
||||||
|
applyProgressStatuses(result.statuses);
|
||||||
|
} catch (e) {
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDelay = () => {
|
||||||
|
if (delayCount < 3) {
|
||||||
|
setDelayCount(delayCount + 1);
|
||||||
|
return 1000;
|
||||||
|
}
|
||||||
|
for (let i = 0, len = carUpdates.length; i < len; i++) {
|
||||||
|
if (carUpdates[i].status.indexOf("downloading") > -1) return 1000;
|
||||||
|
}
|
||||||
|
return 10000;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startMonitor = async (token) => {
|
||||||
|
const delay = getDelay();
|
||||||
|
stopMonitor();
|
||||||
|
progressTimer = setTimeout(() => {
|
||||||
|
updateStatusProgress(token);
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopMonitor = async () => {
|
||||||
|
if (progressTimer === 0) return;
|
||||||
|
clearTimeout(progressTimer);
|
||||||
|
progressTimer = 0;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UpdatesContext.Provider
|
<UpdatesContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -109,6 +191,8 @@ export const UpdatesProvider = ({ children }) => {
|
|||||||
createCarUpdates,
|
createCarUpdates,
|
||||||
getCarUpdates,
|
getCarUpdates,
|
||||||
getVINUpdates,
|
getVINUpdates,
|
||||||
|
startMonitor,
|
||||||
|
stopMonitor,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -23,4 +23,6 @@ export const useUpdatesContext = () => ({
|
|||||||
createCarUpdates: jest.fn((data) => data),
|
createCarUpdates: jest.fn((data) => data),
|
||||||
getCarUpdates: jest.fn(() => carUpdates),
|
getCarUpdates: jest.fn(() => carUpdates),
|
||||||
getVINUpdates: jest.fn(() => carUpdates),
|
getVINUpdates: jest.fn(() => carUpdates),
|
||||||
|
startMonitor: jest.fn(),
|
||||||
|
stopMonitor: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ const menuData = [
|
|||||||
roles: [],
|
roles: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "View Updates",
|
label: "View Packages",
|
||||||
to: "/updates",
|
to: "/updates",
|
||||||
roles: [Roles.CREATE, Roles.READ],
|
roles: [Roles.CREATE, Roles.READ],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Create Updates",
|
label: "Create Packages",
|
||||||
to: "/package-upload",
|
to: "/package-upload",
|
||||||
roles: [Roles.CREATE],
|
roles: [Roles.CREATE],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ exports[`SideMenu Authenticated 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
>
|
>
|
||||||
View Updates
|
View Packages
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
@@ -66,7 +66,7 @@ exports[`SideMenu Authenticated 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
>
|
>
|
||||||
Create Updates
|
Create Packages
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ exports[`File Upload Form Should render 1`] = `
|
|||||||
<h1
|
<h1
|
||||||
class="MuiTypography-root MuiTypography-h5"
|
class="MuiTypography-root MuiTypography-h5"
|
||||||
>
|
>
|
||||||
Upload Update Package
|
Create Update Package
|
||||||
</h1>
|
</h1>
|
||||||
<form
|
<form
|
||||||
action="{onSubmit}"
|
action="{onSubmit}"
|
||||||
@@ -114,12 +114,19 @@ exports[`File Upload Form Should render 1`] = `
|
|||||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined"
|
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||||
data-shrink="false"
|
data-shrink="false"
|
||||||
for="description"
|
for="description"
|
||||||
id="description-label"
|
id="description-label"
|
||||||
>
|
>
|
||||||
Description
|
Description
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||||
|
>
|
||||||
|
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl MuiInputBase-multiline MuiOutlinedInput-multiline"
|
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl MuiInputBase-multiline MuiOutlinedInput-multiline"
|
||||||
@@ -131,6 +138,7 @@ exports[`File Upload Form Should render 1`] = `
|
|||||||
maxlength="5120"
|
maxlength="5120"
|
||||||
name="description"
|
name="description"
|
||||||
placeholder="Package description"
|
placeholder="Package description"
|
||||||
|
required=""
|
||||||
rows="4"
|
rows="4"
|
||||||
/>
|
/>
|
||||||
<fieldset
|
<fieldset
|
||||||
@@ -142,6 +150,7 @@ exports[`File Upload Form Should render 1`] = `
|
|||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
Description
|
Description
|
||||||
|
*
|
||||||
</span>
|
</span>
|
||||||
</legend>
|
</legend>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -151,12 +160,19 @@ exports[`File Upload Form Should render 1`] = `
|
|||||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined"
|
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||||
data-shrink="false"
|
data-shrink="false"
|
||||||
for="releasenotes"
|
for="releasenotes"
|
||||||
id="releasenotes-label"
|
id="releasenotes-label"
|
||||||
>
|
>
|
||||||
Release Notes URL
|
Release Notes URL
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||||
|
>
|
||||||
|
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||||
@@ -168,6 +184,7 @@ exports[`File Upload Form Should render 1`] = `
|
|||||||
maxlength="1024"
|
maxlength="1024"
|
||||||
name="releasenotes"
|
name="releasenotes"
|
||||||
placeholder="Release Notes URL"
|
placeholder="Release Notes URL"
|
||||||
|
required=""
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
@@ -180,6 +197,7 @@ exports[`File Upload Form Should render 1`] = `
|
|||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
Release Notes URL
|
Release Notes URL
|
||||||
|
*
|
||||||
</span>
|
</span>
|
||||||
</legend>
|
</legend>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useRef } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
import { Button, TextField, Typography } from "@material-ui/core";
|
import { Button, TextField, Typography } from "@material-ui/core";
|
||||||
import { DropzoneArea } from "material-ui-dropzone";
|
import { DropzoneArea } from "material-ui-dropzone";
|
||||||
import { useUserContext } from "../../Contexts/UserContext";
|
import { useUserContext } from "../../Contexts/UserContext";
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from "../../Contexts/FileUploadContext";
|
} from "../../Contexts/FileUploadContext";
|
||||||
import ModalProgressBar from "../../ModalProgressBar";
|
import ModalProgressBar from "../../ModalProgressBar";
|
||||||
import useStyles from "../../useStyles";
|
import useStyles from "../../useStyles";
|
||||||
|
import { Redirect } from "react-router";
|
||||||
|
|
||||||
const FileUploadZone = ({ classes, token }) => {
|
const FileUploadZone = ({ classes, token }) => {
|
||||||
const { setFiles } = useFileUploadContext();
|
const { setFiles } = useFileUploadContext();
|
||||||
@@ -39,9 +40,10 @@ const FileUploadZone = ({ classes, token }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MainForm = () => {
|
const MainForm = () => {
|
||||||
const { uploading, upload, files } = useFileUploadContext();
|
const { uploading, upload, files, cancel } = useFileUploadContext();
|
||||||
const { token } = useUserContext();
|
const { token } = useUserContext();
|
||||||
const { setMessage } = useStatusContext();
|
const { setMessage } = useStatusContext();
|
||||||
|
const [redirect, setRedirect] = useState(null);
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const packagenameEl = useRef(null);
|
const packagenameEl = useRef(null);
|
||||||
const versionEl = useRef(null);
|
const versionEl = useRef(null);
|
||||||
@@ -59,17 +61,26 @@ const MainForm = () => {
|
|||||||
description: descEl.current.value,
|
description: descEl.current.value,
|
||||||
releasenotes: releasenotesEl.current.value,
|
releasenotes: releasenotesEl.current.value,
|
||||||
};
|
};
|
||||||
|
const result = await upload(formData, authToken, files);
|
||||||
|
|
||||||
await upload(formData, authToken, files);
|
if (!result || result.error) return;
|
||||||
|
|
||||||
|
cancel();
|
||||||
|
setMessage(`Package uploaded`);
|
||||||
|
setRedirect(`/carupdate-deploy/${result.id}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setMessage(e.message);
|
setMessage(e.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (redirect && redirect.length > 0) {
|
||||||
|
return <Redirect to={redirect} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.paper}>
|
<div className={classes.paper}>
|
||||||
<Typography component="h1" variant="h5">
|
<Typography component="h1" variant="h5">
|
||||||
Upload Update Package
|
Create Update Package
|
||||||
</Typography>
|
</Typography>
|
||||||
<form className={classes.form} noValidate action="{onSubmit}">
|
<form className={classes.form} noValidate action="{onSubmit}">
|
||||||
<TextField
|
<TextField
|
||||||
@@ -107,6 +118,7 @@ const MainForm = () => {
|
|||||||
inputProps={{
|
inputProps={{
|
||||||
maxLength: "5120",
|
maxLength: "5120",
|
||||||
}}
|
}}
|
||||||
|
required
|
||||||
fullWidth
|
fullWidth
|
||||||
multiline
|
multiline
|
||||||
rows={4}
|
rows={4}
|
||||||
@@ -122,6 +134,7 @@ const MainForm = () => {
|
|||||||
inputProps={{
|
inputProps={{
|
||||||
maxLength: "1024",
|
maxLength: "1024",
|
||||||
}}
|
}}
|
||||||
|
required
|
||||||
fullWidth
|
fullWidth
|
||||||
placeholder="Release Notes URL"
|
placeholder="Release Notes URL"
|
||||||
inputRef={releasenotesEl}
|
inputRef={releasenotesEl}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ const MainForm = () => {
|
|||||||
return (
|
return (
|
||||||
<div className={classes.paper}>
|
<div className={classes.paper}>
|
||||||
<Typography component="h1" variant="h5">
|
<Typography component="h1" variant="h5">
|
||||||
Update Package {id}
|
Edit Update Package {id}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form className={classes.form} noValidate action="{onSubmit}">
|
<form className={classes.form} noValidate action="{onSubmit}">
|
||||||
<TextField
|
<TextField
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { Roles, hasRole } from "../../../utils/roles";
|
|||||||
|
|
||||||
const UpdatePackagesList = () => {
|
const UpdatePackagesList = () => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [pageSize, setPageSize] = useState(5);
|
const [pageSize, setPageSize] = useState(25);
|
||||||
const [pageIndex, setPageIndex] = useState(0);
|
const [pageIndex, setPageIndex] = useState(0);
|
||||||
const { getPackages, packages, totalPackages } = useUpdatesContext();
|
const { getPackages, packages, totalPackages } = useUpdatesContext();
|
||||||
const {
|
const {
|
||||||
@@ -98,7 +98,7 @@ const UpdatePackagesList = () => {
|
|||||||
return (
|
return (
|
||||||
<div className={classes.paper} style={{ height: 700, width: "100%" }}>
|
<div className={classes.paper} style={{ height: 700, width: "100%" }}>
|
||||||
<Typography component="h1" variant="h5">
|
<Typography component="h1" variant="h5">
|
||||||
Updates
|
Update Packages
|
||||||
</Typography>
|
</Typography>
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ const updatesAPI = {
|
|||||||
getVINUpdates: async (vin, token) => {
|
getVINUpdates: async (vin, token) => {
|
||||||
return { data: [] };
|
return { data: [] };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getCarUpdateProgress: async (carupdateids, token) => {
|
||||||
|
return { statuses: [] };
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default updatesAPI;
|
export default updatesAPI;
|
||||||
|
|||||||
@@ -45,6 +45,15 @@ const updatesAPI = {
|
|||||||
})
|
})
|
||||||
.then(fetchRespHandler);
|
.then(fetchRespHandler);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getCarUpdateProgress: async (carupdateids, token) => {
|
||||||
|
var u = `${API_ENDPOINT}/carupdatesstatuses?carupdateids=${carupdateids}`;
|
||||||
|
return fetch(u, {
|
||||||
|
method: "GET",
|
||||||
|
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
||||||
|
})
|
||||||
|
.then(fetchRespHandler);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default updatesAPI;
|
export default updatesAPI;
|
||||||
|
|||||||
Reference in New Issue
Block a user