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:
John Wu
2021-04-30 15:55:27 -07:00
committed by GitHub
parent bf81903ecd
commit e51450426e
19 changed files with 2138 additions and 2005 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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={() => {

View File

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

View File

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

View File

@@ -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],
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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