Merge branch 'develop'

This commit is contained in:
jwu-fisker
2022-06-07 07:48:36 -07:00
26 changed files with 17792 additions and 425 deletions

View File

@@ -2,4 +2,4 @@ REACT_APP_CERT_SERVICE_URL=https://dev-gw.cloud.fiskerinc.com/certificate
REACT_APP_AUTH_SERVICE_URL=https://dev-gw.cloud.fiskerinc.com/compute_auth
REACT_APP_UPLOAD_SERVICE_URL=https://dev-gw.cloud.fiskerinc.com/ota_update
REACT_APP_AUTH_CALLBACK_URL=https://dev-ota-admin.cloud.fiskerinc.com
REACT_APP_SUPERSET_URL=http://superset-dev.fiskercloud.internal
REACT_APP_SUPERSET_URL=https://dev-superset.cloud.fiskerinc.com

View File

@@ -2,4 +2,4 @@ REACT_APP_AUTH_SERVICE_URL=http://localhost/compute_auth
REACT_APP_CERT_SERVICE_URL=http://localhost/certificate
REACT_APP_UPLOAD_SERVICE_URL=http://localhost/ota_update
REACT_APP_AUTH_CALLBACK_URL=http://localhost:3000
REACT_APP_SUPERSET_URL=http://superset-dev.fiskercloud.internal
REACT_APP_SUPERSET_URL=https://dev-superset.cloud.fiskerinc.com

View File

@@ -2,4 +2,4 @@ REACT_APP_AUTH_SERVICE_URL=https://gw.cloud.fiskerinc.com/compute_auth
REACT_APP_CERT_SERVICE_URL=https://gw.cloud.fiskerinc.com/certificate
REACT_APP_UPLOAD_SERVICE_URL=https://gw.cloud.fiskerinc.com/ota_update
REACT_APP_AUTH_CALLBACK_URL=https://ota-admin.cloud.fiskerinc.com
REACT_APP_SUPERSET_URL=http://superset.fiskercloud.internal
REACT_APP_SUPERSET_URL=https://superset.cloud.fiskerinc.com

View File

@@ -2,4 +2,4 @@ REACT_APP_AUTH_SERVICE_URL=https://stg-gw.cloud.fiskerinc.com/compute_auth
REACT_APP_CERT_SERVICE_URL=https://stg-gw.cloud.fiskerinc.com/certificate
REACT_APP_UPLOAD_SERVICE_URL=https://stg-gw.cloud.fiskerinc.com/ota_update
REACT_APP_AUTH_CALLBACK_URL=https://stg-ota-admin.cloud.fiskerinc.com
REACT_APP_SUPERSET_URL=http://superset-stg.fiskercloud.internal
REACT_APP_SUPERSET_URL=https://stg-superset.cloud.fiskerinc.com

View File

@@ -1,5 +1,5 @@
REACT_APP_AUTH_SERVICE_URL=http://localhost/compute_auth
REACT_APP_UPLOAD_SERVICE_URL=http://localhost/ota_update
REACT_APP_AUTH_CALLBACK_URL=http://localhost:3000
REACT_APP_SUPERSET_URL=http://superset-dev.fisker.internal
REACT_APP_SUPERSET_URL=https://dev-superset.cloud.fiskerinc.com
REACT_APP_CERT_SERVICE_URL=http://localhost/certificate

View File

@@ -1,6 +1,10 @@
name: Node.js CI
on: [pull_request]
on:
push:
branches:
- develop
pull_request:
jobs:
build:
@@ -14,7 +18,7 @@ jobs:
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: "14"
node-version: "16"
cache: "npm"
- run: npm install
- run: npm run build --if-present

View File

@@ -6,7 +6,7 @@ Front-end web application for administrating services
Running locally
1. Install Node 14
1. Install Node 16
2. Run `npm install`
3. Copy .env.template to .env and edit the service urls for authentication and api services
4. Run `./run.sh` from the terminal

17453
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -213,7 +213,7 @@ exports[`App Route / authenticated 1`] = `
<a
aria-disabled="false"
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiListItem-root makeStyles-menuExternalLink-0000 MuiListItem-gutters MuiListItem-button MuiTypography-colorPrimary"
href="http://superset-dev.fiskercloud.internal/r/3"
href="https://dev-superset.cloud.fiskerinc.com/r/3"
rel="noopener"
role="button"
tabindex="0"
@@ -711,7 +711,7 @@ exports[`App Route /home authenticated 1`] = `
<a
aria-disabled="false"
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiListItem-root makeStyles-menuExternalLink-0000 MuiListItem-gutters MuiListItem-button MuiTypography-colorPrimary"
href="http://superset-dev.fiskercloud.internal/r/3"
href="https://dev-superset.cloud.fiskerinc.com/r/3"
rel="noopener"
role="button"
tabindex="0"
@@ -1245,7 +1245,7 @@ exports[`App Route /package-create authenticated 1`] = `
<a
aria-disabled="false"
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiListItem-root makeStyles-menuExternalLink-0000 MuiListItem-gutters MuiListItem-button MuiTypography-colorPrimary"
href="http://superset-dev.fiskercloud.internal/r/3"
href="https://dev-superset.cloud.fiskerinc.com/r/3"
rel="noopener"
role="button"
tabindex="0"
@@ -2742,7 +2742,7 @@ exports[`App Route /package-deploy authenticated 1`] = `
<a
aria-disabled="false"
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiListItem-root makeStyles-menuExternalLink-0000 MuiListItem-gutters MuiListItem-button MuiTypography-colorPrimary"
href="http://superset-dev.fiskercloud.internal/r/3"
href="https://dev-superset.cloud.fiskerinc.com/r/3"
rel="noopener"
role="button"
tabindex="0"
@@ -3614,7 +3614,7 @@ exports[`App Route /package-status authenticated 1`] = `
<a
aria-disabled="false"
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiListItem-root makeStyles-menuExternalLink-0000 MuiListItem-gutters MuiListItem-button MuiTypography-colorPrimary"
href="http://superset-dev.fiskercloud.internal/r/3"
href="https://dev-superset.cloud.fiskerinc.com/r/3"
rel="noopener"
role="button"
tabindex="0"
@@ -4222,7 +4222,7 @@ exports[`App Route /packages authenticated 1`] = `
<a
aria-disabled="false"
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiListItem-root makeStyles-menuExternalLink-0000 MuiListItem-gutters MuiListItem-button MuiTypography-colorPrimary"
href="http://superset-dev.fiskercloud.internal/r/3"
href="https://dev-superset.cloud.fiskerinc.com/r/3"
rel="noopener"
role="button"
tabindex="0"
@@ -5032,7 +5032,7 @@ exports[`App Route /page-not-found authenticated 1`] = `
<a
aria-disabled="false"
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiListItem-root makeStyles-menuExternalLink-0000 MuiListItem-gutters MuiListItem-button MuiTypography-colorPrimary"
href="http://superset-dev.fiskercloud.internal/r/3"
href="https://dev-superset.cloud.fiskerinc.com/r/3"
rel="noopener"
role="button"
tabindex="0"
@@ -5453,7 +5453,7 @@ exports[`App Route /tools/certificates/add authenticated 1`] = `
<a
aria-disabled="false"
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiListItem-root makeStyles-menuExternalLink-0000 MuiListItem-gutters MuiListItem-button MuiTypography-colorPrimary"
href="http://superset-dev.fiskercloud.internal/r/3"
href="https://dev-superset.cloud.fiskerinc.com/r/3"
rel="noopener"
role="button"
tabindex="0"
@@ -6112,7 +6112,7 @@ exports[`App Route /vehicle-add authenticated 1`] = `
<a
aria-disabled="false"
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiListItem-root makeStyles-menuExternalLink-0000 MuiListItem-gutters MuiListItem-button MuiTypography-colorPrimary"
href="http://superset-dev.fiskercloud.internal/r/3"
href="https://dev-superset.cloud.fiskerinc.com/r/3"
rel="noopener"
role="button"
tabindex="0"
@@ -7246,7 +7246,7 @@ exports[`App Route /vehicle-status authenticated 1`] = `
<a
aria-disabled="false"
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiListItem-root makeStyles-menuExternalLink-0000 MuiListItem-gutters MuiListItem-button MuiTypography-colorPrimary"
href="http://superset-dev.fiskercloud.internal/r/3"
href="https://dev-superset.cloud.fiskerinc.com/r/3"
rel="noopener"
role="button"
tabindex="0"
@@ -7911,7 +7911,7 @@ exports[`App Route /vehicles authenticated 1`] = `
<a
aria-disabled="false"
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiListItem-root makeStyles-menuExternalLink-0000 MuiListItem-gutters MuiListItem-button MuiTypography-colorPrimary"
href="http://superset-dev.fiskercloud.internal/r/3"
href="https://dev-superset.cloud.fiskerinc.com/r/3"
rel="noopener"
role="button"
tabindex="0"

View File

@@ -1,64 +1,30 @@
import React from "react";
import { useParams } from "react-router";
import clsx from "clsx";
import { Button, Grid, Typography } from "@material-ui/core";
import { Typography } from "@material-ui/core";
import CarECUsTable from "../../Controls/CarECUsTable";
import CarUpdatesTable from "../../Controls/CarUpdatesTable";
import { logger } from "../../../services/monitoring";
import {
VehicleProvider,
useVehicleContext,
} from "../../Contexts/VehicleContext";
import { VehicleProvider } from "../../Contexts/VehicleContext";
import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
const MainForm = () => {
const { vin } = useParams();
const classes = useStyles();
const { setMessage } = useStatusContext();
const { busy, sendCommand } = useVehicleContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
const updateHandler = async (e) => {
try {
await sendCommand([vin], "ecu", "", token);
setMessage(`Sent command to ${vin}`);
} catch (error) {
setMessage(error.message);
logger.error(error.stack);
}
};
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Typography variant="h6">Car Updates</Typography>
<CarUpdatesTable vin={vin} token={token} classes={classes} />
<Grid container className={classes.root} spacing={2}>
<Grid item md={4} className={classes.textJustifyAlign}></Grid>
<Grid item md={4} className={classes.textCenterAlign}>
<Typography variant="h6" className={classes.labelInline}>
Car ECUs
</Typography>
</Grid>
<Grid item md={4} className={classes.textRightAlign}>
<Button
type="submit"
disabled={busy}
variant="contained"
color="primary"
className={clsx(classes.formControl, classes.textField)}
onClick={updateHandler}
>
{busy ? "Sending..." : "Refresh"}
</Button>
</Grid>
</Grid>
<CarECUsTable vin={vin} token={token} classes={classes} />
</div>
);

View File

@@ -278,40 +278,11 @@ exports[`CarUpdatesTab Render 1`] = `
</tr>
</tfoot>
</table>
<div
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
>
<div
class="MuiGrid-root makeStyles-textJustifyAlign-47 MuiGrid-item MuiGrid-grid-md-4"
/>
<div
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-4"
>
<h6
class="MuiTypography-root makeStyles-labelInline-9 MuiTypography-h6"
>
Car ECUs
</h6>
</div>
<div
class="MuiGrid-root makeStyles-textRightAlign-49 MuiGrid-item MuiGrid-grid-md-4"
>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-formControl-7 makeStyles-textField-29 MuiButton-containedPrimary"
tabindex="0"
type="submit"
>
<span
class="MuiButton-label"
>
Refresh
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
</div>
<div
class="makeStyles-paper-3 makeStyles-tableSize-53"
>

View File

@@ -1,4 +1,4 @@
jest.mock("../../services/CANFiltersAPI")
jest.mock("../../services/CANFiltersAPI");
import {
render,
@@ -83,9 +83,7 @@ describe("CANFiltersContext", () => {
<button data-testid="addFilterNoCANID" onClick={() => add({})} />
<button
data-testid="addFilter"
onClick={() =>
add({ can_id: "123", interval: 1000 })
}
onClick={() => add({ can_id: "123", interval: 1000 })}
/>
</>
);
@@ -149,13 +147,17 @@ describe("CANFiltersContext", () => {
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="updateFilterNull" onClick={() => update(null)} />
<button data-testid="updateFilterNoCANID" onClick={() => update({})} />
<button
data-testid="updateFilterNull"
onClick={() => update(null)}
/>
<button
data-testid="updateFilterNoCANID"
onClick={() => update({})}
/>
<button
data-testid="updateFilter"
onClick={() =>
update({ can_id: "123", interval: 1000 })
}
onClick={() => update({ can_id: "123", interval: 1000 })}
/>
</>
);
@@ -182,7 +184,10 @@ describe("CANFiltersContext", () => {
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("Cannot read property 'can_id' of null", "false");
checkBaseResults(
"Cannot read properties of null (reading 'can_id')",
"false"
);
});
it("updateFilterNoCANID", async () => {
@@ -219,14 +224,15 @@ describe("CANFiltersContext", () => {
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="deleteFilterNull" onClick={() => deleteF(null)} />
<button data-testid="deleteFilterNonexistent" onClick={() => deleteF(-1)} />
<button
data-testid="deleteFilter"
onClick={() =>
deleteF(123)
}
data-testid="deleteFilterNull"
onClick={() => deleteF(null)}
/>
<button
data-testid="deleteFilterNonexistent"
onClick={() => deleteF(-1)}
/>
<button data-testid="deleteFilter" onClick={() => deleteF(123)} />
</>
);
};
@@ -276,14 +282,14 @@ describe("CANFiltersContext", () => {
const expectedFiltersData = [
{
can_id: "123",
interval: 1000
interval: 1000,
},
{
can_id: "456",
interval: 0
interval: 0,
},
{
can_id: "789-1000",
interval: 5
interval: 5,
},
];

View File

@@ -1,4 +1,4 @@
jest.mock("../../services/fleetsAPI")
jest.mock("../../services/fleetsAPI");
import {
render,
@@ -23,12 +23,12 @@ const checkFleetsResults = (error, busy, fleets) => {
const checkFleetVehicleResults = (error, busy, vehicles) => {
checkBaseResults(error, busy);
expect(screen.getByTestId("fleet-vehicles").innerHTML).toEqual(vehicles);
}
};
const checkFleetCANFilterResults = (error, busy, filters) => {
checkBaseResults(error, busy);
expect(screen.getByTestId("fleet-filters").innerHTML).toEqual(filters);
}
};
const checkBaseResults = (error, busy) => {
expect(screen.getByTestId("error").innerHTML).toEqual(error);
@@ -46,10 +46,7 @@ describe("FleetContext", () => {
<div data-testid="error">{error}</div>
<div data-testid="busy">{busy.toString()}</div>
<div data-testid="fleets">{JSON.stringify(fleets)}</div>
<button
data-testid="getFleets"
onClick={() => getFleets()}
/>
<button data-testid="getFleets" onClick={() => getFleets()} />
</>
);
};
@@ -140,7 +137,11 @@ describe("FleetContext", () => {
<button
data-testid="addFleet"
onClick={() =>
add({ name: "EU-WEST", log_level: "warn", canbus: { enabled: false } })
add({
name: "EU-WEST",
log_level: "warn",
canbus: { enabled: false },
})
}
/>
</>
@@ -205,12 +206,22 @@ describe("FleetContext", () => {
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="updateFleetNull" onClick={() => update(null)} />
<button data-testid="updateFleetNoName" onClick={() => update({})} />
<button
data-testid="updateFleetNull"
onClick={() => update(null)}
/>
<button
data-testid="updateFleetNoName"
onClick={() => update({})}
/>
<button
data-testid="updateFleet"
onClick={() =>
update({ name: "EU-WEST", log_level: "warn", canbus: { enabled: false } })
update({
name: "EU-WEST",
log_level: "warn",
canbus: { enabled: false },
})
}
/>
</>
@@ -275,13 +286,17 @@ describe("FleetContext", () => {
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="deleteFleetNull" onClick={() => deleteF(null)} />
<button data-testid="deleteFleetNonexistent" onClick={() => deleteF("INVALID")} />
<button
data-testid="deleteFleetNull"
onClick={() => deleteF(null)}
/>
<button
data-testid="deleteFleetNonexistent"
onClick={() => deleteF("INVALID")}
/>
<button
data-testid="deleteFleet"
onClick={() =>
deleteF("US-WEST")
}
onClick={() => deleteF("US-WEST")}
/>
</>
);
@@ -331,13 +346,16 @@ describe("FleetContext", () => {
describe("getFleetVehicles", () => {
beforeEach(() => {
const TestComp = () => {
const { busy, error, fleetVehicles, getFleetVehicles } = useFleetContext();
const { busy, error, fleetVehicles, getFleetVehicles } =
useFleetContext();
return (
<>
<div data-testid="error">{error}</div>
<div data-testid="busy">{busy.toString()}</div>
<div data-testid="fleet-vehicles">{JSON.stringify(fleetVehicles)}</div>
<div data-testid="fleet-vehicles">
{JSON.stringify(fleetVehicles)}
</div>
<button
data-testid="getFleetVehicles"
onClick={() => getFleetVehicles("US-WEST")}
@@ -365,7 +383,11 @@ describe("FleetContext", () => {
await waitFor(() =>
expect(screen.getByTestId("fleet-vehicles").innerHTML).not.toBe("[]")
);
checkFleetVehicleResults("", "false", JSON.stringify(expectedFleetVehiclesData));
checkFleetVehicleResults(
"",
"false",
JSON.stringify(expectedFleetVehiclesData)
);
});
});
@@ -386,13 +408,17 @@ describe("FleetContext", () => {
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="addFleetVehicleNull" onClick={() => add(null)} />
<button data-testid="addFleetVehicleNoName" onClick={() => add({})} />
<button
data-testid="addFleetVehicleNull"
onClick={() => add(null)}
/>
<button
data-testid="addFleetVehicleNoName"
onClick={() => add({})}
/>
<button
data-testid="addFleetVehicle"
onClick={() =>
add("US-TEST", { vin: "TESTVIN1234567890" })
}
onClick={() => add("US-TEST", { vin: "TESTVIN1234567890" })}
/>
</>
);
@@ -456,13 +482,17 @@ describe("FleetContext", () => {
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="deleteFleetVehicleNull" onClick={() => deleteFV("US-WEST", null)} />
<button data-testid="deleteFleetVehicleInvalid" onClick={() => deleteFV("US-WEST", "INVALID")} />
<button
data-testid="deleteFleetVehicleNull"
onClick={() => deleteFV("US-WEST", null)}
/>
<button
data-testid="deleteFleetVehicleInvalid"
onClick={() => deleteFV("US-WEST", "INVALID")}
/>
<button
data-testid="deleteFleetVehicle"
onClick={() =>
deleteFV("US-WEST", { vin: "USWESTVIN12345678" })
}
onClick={() => deleteFV("US-WEST", { vin: "USWESTVIN12345678" })}
/>
</>
);
@@ -489,7 +519,10 @@ describe("FleetContext", () => {
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("Cannot read property 'vin' of null", "false");
checkBaseResults(
"Cannot read properties of null (reading 'vin')",
"false"
);
});
it("deleteFleetVehicleNonexistent", async () => {
@@ -512,13 +545,16 @@ describe("FleetContext", () => {
describe("getFleetCANFilters", () => {
beforeEach(() => {
const TestComp = () => {
const { busy, error, fleetCANFilters, getFleetCANFilters } = useFleetContext();
const { busy, error, fleetCANFilters, getFleetCANFilters } =
useFleetContext();
return (
<>
<div data-testid="error">{error}</div>
<div data-testid="busy">{busy.toString()}</div>
<div data-testid="fleet-filters">{JSON.stringify(fleetCANFilters)}</div>
<div data-testid="fleet-filters">
{JSON.stringify(fleetCANFilters)}
</div>
<button
data-testid="getFleetCANFilters"
onClick={() => getFleetCANFilters("US-TEST")}
@@ -546,7 +582,11 @@ describe("FleetContext", () => {
await waitFor(() =>
expect(screen.getByTestId("fleet-filters").innerHTML).not.toBe("[]")
);
checkFleetCANFilterResults("", "false", JSON.stringify(expectedFleetCANFiltersData));
checkFleetCANFilterResults(
"",
"false",
JSON.stringify(expectedFleetCANFiltersData)
);
});
});
@@ -567,13 +607,17 @@ describe("FleetContext", () => {
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="addFleetCANFilterNull" onClick={() => add(null)} />
<button data-testid="addFleetCANFilterNoName" onClick={() => add({})} />
<button
data-testid="addFleetCANFilterNull"
onClick={() => add(null)}
/>
<button
data-testid="addFleetCANFilterNoName"
onClick={() => add({})}
/>
<button
data-testid="addFleetCANFilter"
onClick={() =>
add("US-TEST", { can_id: "111", interval: 222 })
}
onClick={() => add("US-TEST", { can_id: "111", interval: 222 })}
/>
</>
);
@@ -637,13 +681,17 @@ describe("FleetContext", () => {
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="deleteFleetCANFilterNull" onClick={() => deleteFF("US-WEST", null)} />
<button data-testid="deleteFleetCANFilterInvalid" onClick={() => deleteFF("US-WEST", "INVALID")} />
<button
data-testid="deleteFleetCANFilterNull"
onClick={() => deleteFF("US-WEST", null)}
/>
<button
data-testid="deleteFleetCANFilterInvalid"
onClick={() => deleteFF("US-WEST", "INVALID")}
/>
<button
data-testid="deleteFleetCANFilter"
onClick={() =>
deleteFF("US-WEST", "123-456")
}
onClick={() => deleteFF("US-WEST", "123-456")}
/>
</>
);
@@ -694,59 +742,80 @@ describe("FleetContext", () => {
const expectedFilters = [
{
can_id: "123-456",
interval: 789
interval: 789,
},
{
can_id: "1",
interval: 1000
interval: 1000,
},
{
can_id: "1000",
interval: 1
}
]
interval: 1,
},
];
const expectedFleetData = {
name: "US-WEST",
log_level: "info",
canbus: { enabled: true, data_logger_enabled: true, max_mem_buffer_size: 1, max_disk_buffer_size: 2, filters: expectedFilters },
vehicles: ["USWESTVIN12345678", "USWESTVIN12345679", "USWESTVIN12345670"]
}
canbus: {
enabled: true,
data_logger_enabled: true,
max_mem_buffer_size: 1,
max_disk_buffer_size: 2,
filters: expectedFilters,
},
vehicles: ["USWESTVIN12345678", "USWESTVIN12345679", "USWESTVIN12345670"],
};
const expectedFleetsData = [
{
name: "US-WEST",
log_level: "info",
canbus: { enabled: true, data_logger_enabled: true, max_mem_buffer_size: 1, max_disk_buffer_size: 2, filters: expectedFilters },
vehicles: ["USWESTVIN12345678", "USWESTVIN12345679", "USWESTVIN12345670"]
canbus: {
enabled: true,
data_logger_enabled: true,
max_mem_buffer_size: 1,
max_disk_buffer_size: 2,
filters: expectedFilters,
},
vehicles: ["USWESTVIN12345678", "USWESTVIN12345679", "USWESTVIN12345670"],
},
{
name: "US-CENTRAL",
log_level: "warn",
canbus: { enabled: false, data_logger_enabled: false, max_mem_buffer_size: 0, max_disk_buffer_size: 0 },
vehicles: ["USCENTVIN12345678", "USCENTVIN12345679", "USCENTVIN12345670"]
canbus: {
enabled: false,
data_logger_enabled: false,
max_mem_buffer_size: 0,
max_disk_buffer_size: 0,
},
vehicles: ["USCENTVIN12345678", "USCENTVIN12345679", "USCENTVIN12345670"],
},
{
name: "US-EAST",
log_level: "error",
canbus: { enabled: true },
vehicles: ["USEASTVIN12345678", "USEASTVIN12345679", "USEASTVIN12345670"]
vehicles: ["USEASTVIN12345678", "USEASTVIN12345679", "USEASTVIN12345670"],
},
];
const expectedFleetVehiclesData = ["USWESTVIN12345678", "USWESTVIN12345679", "USWESTVIN12345670"];
const expectedFleetVehiclesData = [
"USWESTVIN12345678",
"USWESTVIN12345679",
"USWESTVIN12345670",
];
const expectedFleetCANFiltersData = [
{
can_id: "123-456",
interval: 789
interval: 789,
},
{
can_id: "1",
interval: 1000
interval: 1000,
},
{
can_id: "1000",
interval: 1
}
interval: 1,
},
];

View File

@@ -127,12 +127,12 @@ export const VehicleProvider = ({ children }) => {
const result = await api.getVehicle(vin, token);
if (result.error) throw new Error(`Get vehicle error. ${result.message}`);
setVehicle(result);
setVehicle(result ?? []);
return result;
} finally {
setBusy(false);
}
}
};
const getVehicles = async (search, token) => {
try {
@@ -143,7 +143,7 @@ export const VehicleProvider = ({ children }) => {
throw new Error(`Get vehicles error. ${result.message}`);
}
await addConnections(result.data, token);
setVehicles(result.data);
setVehicles(result.data ?? []);
if (result.total) {
setTotalVehicles(result.total);
}
@@ -188,7 +188,7 @@ export const VehicleProvider = ({ children }) => {
} finally {
setBusy(false);
}
}
};
const deleteVehicle = async (vin, token) => {
try {
@@ -202,7 +202,7 @@ export const VehicleProvider = ({ children }) => {
} finally {
setBusy(false);
}
}
};
return (
<VehicleContext.Provider
@@ -224,7 +224,7 @@ export const VehicleProvider = ({ children }) => {
getVehicle,
getVehicles,
sendCommand,
updateVehicle
updateVehicle,
}}
>
{children}
@@ -246,5 +246,4 @@ const validateVIN = (vin) => {
}
};
export const useVehicleContext = () => useContext(VehicleContext);

View File

@@ -48,7 +48,15 @@ const tableColumns = [
];
const CarSelectionTable = (props) => {
const { token, classes, search, multiSelect, selected, onSelect, onSelectAll } = props;
const {
token,
classes,
search,
multiSelect,
selected,
onSelect,
onSelectAll,
} = props;
const [pageSize, setPageSize] = useState(10);
const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("vin");
@@ -126,19 +134,23 @@ const CarSelectionTable = (props) => {
multiSelect={multiSelect}
onSelectAll={handleSelectAll}
selectCount={selected ? selected.length : 0}
rowCount={vehicles.length}
rowCount={vehicles ? vehicles.length : 0}
/>
<TableBody>
{vehicles.map((row) => {
const isSelected = selected ? selected.indexOf(row.vin) !== -1 : false;
const isSelected = selected
? selected.indexOf(row.vin) !== -1
: false;
return (
<TableRow key={row.vin}>
{multiSelect && (<TableCell padding="checkbox">
{multiSelect && (
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
onChange={(event) => handleSelect(event, row.vin)}
/>
</TableCell>)}
</TableCell>
)}
<TableCell align="center">
<ConnectedIcon
connected={row.connected}

View File

@@ -8,22 +8,26 @@ import s from "./Statuses";
const AwaitStatus = -1;
const ErrorStatus = -100;
const CompleteStatus = 100;
const PHASES = [
{
label: "Pending",
events: [s.Pending],
progress: () => 100,
progress: () => CompleteStatus,
},
{
label: "Received",
events: [s.ManifestAccepted, s.ManifestReceived, s.ManifestRejected],
progress: () => 100,
progress: (msg) => {
if (msg === s.ManifestRejected) return ErrorStatus;
return CompleteStatus;
},
},
{
label: "Precondition",
events: [s.PreconditionAwait, s.PreconditionSuceeded],
progress: () => 100,
progress: () => CompleteStatus,
},
{
label: "Download",
@@ -55,14 +59,14 @@ const PHASES = [
],
progress: (msg, progress) => {
if (msg === s.InstallFailed) return ErrorStatus;
if (msg === s.PackageInstallCompleted) return 100;
if (msg === s.PackageInstallCompleted) return CompleteStatus;
return progress;
},
},
{
label: "Updated",
events: [s.ManifestSucceeded],
progress: (_msg, _progress) => 100,
progress: (_msg, _progress) => CompleteStatus,
},
];
@@ -84,7 +88,7 @@ const Progress = ({ value, classes }) => {
const getProgress = (index, phase, progress) => {
if (index === phase) return progress;
if (index < phase) return 100;
if (index < phase) return CompleteStatus;
return -1;
};

View File

@@ -156,7 +156,7 @@ exports[`SideMenu Authenticated 1`] = `
<a
aria-disabled="false"
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiListItem-root makeStyles-menuExternalLink-52 MuiListItem-gutters MuiListItem-button MuiTypography-colorPrimary"
href="http://superset-dev.fiskercloud.internal/r/3"
href="https://dev-superset.cloud.fiskerinc.com/r/3"
rel="noopener"
role="button"
tabindex="0"

View File

@@ -246,16 +246,6 @@ const useStyles = makeStyles((theme) => ({
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" },

View File

@@ -1,4 +1,5 @@
import {
errorHandler,
getAuthHeaderOptions,
fetchRespHandler,
addQueryParams,
@@ -14,8 +15,10 @@ const canFiltersAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
body: JSON.stringify(filter)
}).then(fetchRespHandler),
body: JSON.stringify(filter),
})
.then(fetchRespHandler)
.catch(errorHandler),
getFilters: async (vin, search, token) =>
fetch(addQueryParams(`${API_ENDPOINT}/vehicle/${vin}/filters`, search), {
@@ -24,7 +27,9 @@ const canFiltersAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler),
})
.then(fetchRespHandler)
.catch(errorHandler),
updateFilter: async (vin, canID, filter, token) =>
fetch(`${API_ENDPOINT}/vehicle/${vin}/filter/${canID}`, {
@@ -33,8 +38,10 @@ const canFiltersAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
body: JSON.stringify(filter)
}).then(fetchRespHandler),
body: JSON.stringify(filter),
})
.then(fetchRespHandler)
.catch(errorHandler),
deleteFilter: async (vin, canID, token) =>
fetch(`${API_ENDPOINT}/vehicle/${vin}/filter/${canID}`, {
@@ -42,8 +49,10 @@ const canFiltersAPI = {
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
)
}).then(fetchRespHandler),
),
})
.then(fetchRespHandler)
.catch(errorHandler),
};
export default canFiltersAPI;

View File

@@ -1,4 +1,4 @@
import { fetchRespHandler } from "../utils/http";
import { errorHandler, fetchRespHandler } from "../utils/http";
const AUTH_URL = process.env.REACT_APP_AUTH_SERVICE_URL;
const CALLBACK_URL = process.env.REACT_APP_AUTH_CALLBACK_URL;
@@ -16,7 +16,9 @@ const auth = {
code,
redirect: CALLBACK_URL,
}),
}).then(fetchRespHandler),
})
.then(fetchRespHandler)
.catch(errorHandler),
verify: (idToken) =>
fetch(`${AUTH_URL}/verify`, {
@@ -25,7 +27,9 @@ const auth = {
"Content-Type": "application/json",
},
body: JSON.stringify({ token: idToken }),
}).then(fetchRespHandler),
})
.then(fetchRespHandler)
.catch(errorHandler),
refresh: (refreshToken) =>
fetch(`${AUTH_URL}/refresh`, {
@@ -34,7 +38,9 @@ const auth = {
"Content-Type": "application/json",
},
body: JSON.stringify({ refresh_token: refreshToken }),
}).then(fetchRespHandler),
})
.then(fetchRespHandler)
.catch(errorHandler),
};
export default auth;

View File

@@ -1,4 +1,8 @@
import { getAuthHeaderOptions, fetchRespHandler } from "../utils/http";
import {
errorHandler,
getAuthHeaderOptions,
fetchRespHandler,
} from "../utils/http";
const API_ENDPOINT = process.env.REACT_APP_CERT_SERVICE_URL;
@@ -11,7 +15,9 @@ const certificatesAPI = {
getAuthHeaderOptions(token)
),
body: JSON.stringify(data),
}).then(fetchRespHandler),
})
.then(fetchRespHandler)
.catch(errorHandler),
};
export default certificatesAPI;

View File

@@ -1,4 +1,5 @@
import {
errorHandler,
getAuthHeaderOptions,
fetchRespHandler,
addQueryParams,
@@ -14,8 +15,10 @@ const fleetsAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
body: JSON.stringify(fleet)
}).then(fetchRespHandler),
body: JSON.stringify(fleet),
})
.then(fetchRespHandler)
.catch(errorHandler),
getFleet: async (name, token) =>
fetch(`${API_ENDPOINT}/fleet/${name}`, {
@@ -23,8 +26,10 @@ const fleetsAPI = {
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
)
}).then(fetchRespHandler),
),
})
.then(fetchRespHandler)
.catch(errorHandler),
getFleets: async (search, token) =>
fetch(addQueryParams(`${API_ENDPOINT}/fleets`, search), {
@@ -33,7 +38,9 @@ const fleetsAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler),
})
.then(fetchRespHandler)
.catch(errorHandler),
updateFleet: async (name, fleet, token) =>
fetch(`${API_ENDPOINT}/fleet/${name}`, {
@@ -42,8 +49,10 @@ const fleetsAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
body: JSON.stringify(fleet)
}).then(fetchRespHandler),
body: JSON.stringify(fleet),
})
.then(fetchRespHandler)
.catch(errorHandler),
deleteFleet: async (name, token) =>
fetch(`${API_ENDPOINT}/fleet/${name}`, {
@@ -51,8 +60,10 @@ const fleetsAPI = {
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
)
}).then(fetchRespHandler),
),
})
.then(fetchRespHandler)
.catch(errorHandler),
getFleetVehicles: async (name, search, token) =>
fetch(addQueryParams(`${API_ENDPOINT}/fleet/${name}/vehicles`, search), {
@@ -60,8 +71,10 @@ const fleetsAPI = {
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
)
}).then(fetchRespHandler),
),
})
.then(fetchRespHandler)
.catch(errorHandler),
addFleetVehicle: async (name, vehicle, token) =>
fetch(`${API_ENDPOINT}/fleet/${name}/vehicle`, {
@@ -70,8 +83,10 @@ const fleetsAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
body: JSON.stringify(vehicle)
}).then(fetchRespHandler),
body: JSON.stringify(vehicle),
})
.then(fetchRespHandler)
.catch(errorHandler),
deleteFleetVehicle: async (name, vehicle, token) =>
fetch(`${API_ENDPOINT}/fleet/${name}/vehicle/${vehicle.vin}`, {
@@ -79,8 +94,10 @@ const fleetsAPI = {
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
)
}).then(fetchRespHandler),
),
})
.then(fetchRespHandler)
.catch(errorHandler),
getFleetCANFilters: async (name, search, token) =>
fetch(addQueryParams(`${API_ENDPOINT}/fleet/${name}/filters`, search), {
@@ -88,8 +105,10 @@ const fleetsAPI = {
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
)
}).then(fetchRespHandler),
),
})
.then(fetchRespHandler)
.catch(errorHandler),
addFleetCANFilter: async (name, filter, token) =>
fetch(`${API_ENDPOINT}/fleet/${name}/filter`, {
@@ -98,8 +117,10 @@ const fleetsAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
body: JSON.stringify(filter)
}).then(fetchRespHandler),
body: JSON.stringify(filter),
})
.then(fetchRespHandler)
.catch(errorHandler),
updateFleetCANFilter: async (name, can_id, filter, token) =>
fetch(`${API_ENDPOINT}/fleet/${name}/filter/${can_id}`, {
@@ -108,8 +129,10 @@ const fleetsAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
body: JSON.stringify(filter)
}).then(fetchRespHandler),
body: JSON.stringify(filter),
})
.then(fetchRespHandler)
.catch(errorHandler),
deleteFleetCANFilter: async (name, can_id, token) =>
fetch(`${API_ENDPOINT}/fleet/${name}/filter/${can_id}`, {
@@ -117,8 +140,10 @@ const fleetsAPI = {
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
)
}).then(fetchRespHandler),
),
})
.then(fetchRespHandler)
.catch(errorHandler),
};
export default fleetsAPI;

View File

@@ -1,4 +1,5 @@
import {
errorHandler,
getAuthHeaderOptions,
fetchRespHandler,
addQueryParams,
@@ -14,7 +15,22 @@ const manifestsAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler),
})
.then(fetchRespHandler)
.catch(errorHandler),
getManifest: async (id, token) => {
const u = addQueryParams(`${API_ENDPOINT}/manifest`, { id });
return fetch(u, {
method: "GET",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
})
.then(fetchRespHandler)
.catch(errorHandler);
},
getManifest: async (id, token) => {
const u = addQueryParams(`${API_ENDPOINT}/manifest`, { id });
@@ -35,7 +51,9 @@ const manifestsAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler);
})
.then(fetchRespHandler)
.catch(errorHandler);
},
createManifest: async (data, token) =>
@@ -46,7 +64,9 @@ const manifestsAPI = {
getAuthHeaderOptions(token)
),
body: JSON.stringify(data),
}).then(fetchRespHandler),
})
.then(fetchRespHandler)
.catch(errorHandler),
createManifestECU: async (data, token) =>
fetch(`${API_ENDPOINT}/manifestecu`, {
@@ -56,7 +76,9 @@ const manifestsAPI = {
getAuthHeaderOptions(token)
),
body: JSON.stringify(data),
}).then(fetchRespHandler),
})
.then(fetchRespHandler)
.catch(errorHandler),
};
export default manifestsAPI;

View File

@@ -1,4 +1,5 @@
import {
errorHandler,
getAuthHeaderOptions,
fetchRespHandler,
addQueryParams,
@@ -15,7 +16,9 @@ const updatesAPI = {
getAuthHeaderOptions(token)
),
body: JSON.stringify(data),
}).then(fetchRespHandler),
})
.then(fetchRespHandler)
.catch(errorHandler),
getCarUpdateLog: async (query, token) => {
const u = addQueryParams(`${API_ENDPOINT}/carupdateslog`, query);
@@ -25,7 +28,9 @@ const updatesAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler);
})
.then(fetchRespHandler)
.catch(errorHandler);
},
getCarUpdateProgress: async (carupdateids, token) => {
@@ -36,7 +41,9 @@ const updatesAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler);
})
.then(fetchRespHandler)
.catch(errorHandler);
},
getCarUpdates: async (search, token) => {
@@ -47,7 +54,9 @@ const updatesAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler);
})
.then(fetchRespHandler)
.catch(errorHandler);
},
getVINUpdates: async (vin, token) => {
@@ -58,7 +67,9 @@ const updatesAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler);
})
.then(fetchRespHandler)
.catch(errorHandler);
},
};

View File

@@ -1,4 +1,5 @@
import {
errorHandler,
getAuthHeaderOptions,
fetchRespHandler,
addQueryParams,
@@ -15,7 +16,20 @@ const vehiclesAPI = {
getAuthHeaderOptions(token)
),
body: JSON.stringify(vehicle),
}).then(fetchRespHandler),
})
.then(fetchRespHandler)
.catch(errorHandler),
deleteVehicle: async (vin, token) =>
fetch(`${API_ENDPOINT}/vehicle/${vin}`, {
method: "DELETE",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
})
.then(fetchRespHandler)
.catch(errorHandler),
deleteVehicle: async (vin, token) =>
fetch(`${API_ENDPOINT}/vehicle/${vin}`, {
@@ -34,7 +48,9 @@ const vehiclesAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler);
})
.then(fetchRespHandler)
.catch(errorHandler);
},
getECUs: async (search, token) => {
@@ -45,7 +61,9 @@ const vehiclesAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler);
})
.then(fetchRespHandler)
.catch(errorHandler);
},
getModels: async (token) =>
@@ -55,7 +73,9 @@ const vehiclesAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler),
})
.then(fetchRespHandler)
.catch(errorHandler),
getLocations: async (token) =>
fetch(`${API_ENDPOINT}/carslocations`, {
@@ -64,7 +84,9 @@ const vehiclesAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler),
})
.then(fetchRespHandler)
.catch(errorHandler),
getState: async (token, vin) =>
fetch(`${API_ENDPOINT}/carstate?vin=${vin}`, {
@@ -73,7 +95,20 @@ const vehiclesAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler),
})
.then(fetchRespHandler)
.catch(errorHandler),
getVehicle: async (vin, token) =>
fetch(`${API_ENDPOINT}/vehicle/${vin}`, {
method: "GET",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
})
.then(fetchRespHandler)
.catch(errorHandler),
getVehicle: async (vin, token) =>
fetch(`${API_ENDPOINT}/vehicle/${vin}`, {
@@ -92,7 +127,9 @@ const vehiclesAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler);
})
.then(fetchRespHandler)
.catch(errorHandler);
},
getYears: async (token) =>
@@ -102,7 +139,9 @@ const vehiclesAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler),
})
.then(fetchRespHandler)
.catch(errorHandler),
sendCommand: async (vins, command, parameters, token) =>
fetch(`${API_ENDPOINT}/vehiclecommand`, {
@@ -116,7 +155,9 @@ const vehiclesAPI = {
command,
parameters,
}),
}).then(fetchRespHandler),
})
.then(fetchRespHandler)
.catch(errorHandler),
updateVehicle: async (vin, vehicle, token) =>
fetch(`${API_ENDPOINT}/vehicle/${vin}`, {
@@ -125,8 +166,10 @@ const vehiclesAPI = {
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
body: JSON.stringify(vehicle)
}).then(fetchRespHandler),
body: JSON.stringify(vehicle),
})
.then(fetchRespHandler)
.catch(errorHandler),
};
export default vehiclesAPI;

View File

@@ -1,13 +1,28 @@
import { logger } from "../services/monitoring";
export const getAuthHeaderOptions = (token) => ({
"Authorization": `Bearer ${token}`,
});
export const addQueryParams = (url, params) => {
if (!params) return url;
const u = new URL(url);
Object.keys(params).forEach((key) => u.searchParams.append(key, params[key]));
return u.toString();
};
export const errorHandler = (e) => {
logger.error(e.stack);
return {
error: e.name,
message: e.message,
};
};
export const fetchRespHandler = (response) => {
if (response.ok) return response.json();
return response.text()
return response
.text()
.then((text) => {
if (response.status >= 500) logger.error(text);
return JSON.parse(text);
@@ -18,15 +33,9 @@ export const fetchRespHandler = (response) => {
error: response.statusText,
message: `${response.status} ${response.statusText}`,
};
})
}
});
};
export const addQueryParams = (url, params) => {
if (!params) return url;
const u = new URL(url);
Object.keys(params).forEach(key => u.searchParams.append(key, params[key]))
return u.toString();
}
export const getAuthHeaderOptions = (token) => ({
Authorization: `Bearer ${token}`,
});