Merge branch 'release/0.9.0'

This commit is contained in:
jwu-fisker
2023-07-06 11:55:06 -07:00
54 changed files with 1377 additions and 538 deletions

View File

@@ -15,6 +15,8 @@ REACT_APP_ROLE_GENERATE_CERTIFICATE=9af2d8c0-c26d-4d6d-bbd1-ac53cbd37ebc
REACT_APP_ROLE_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69
REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8
REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f
REACT_APP_ROLE_CAR_DIAGNOSTIC=2914e67f-fb85-4b78-b79d-656f4f37faa1
REACT_APP_ROLE_UPDATE_DEPLOY=e4af2c4c-6c5e-4784-9097-7c18e776d7b6
REACT_APP_ECCKEY_ENV=
REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":49.8327,"lng":9.8816,"zoom":4.5}
REACT_APP_ENABLE_DEBUGMASK=1

View File

@@ -15,6 +15,8 @@ REACT_APP_ROLE_GENERATE_CERTIFICATE=9af2d8c0-c26d-4d6d-bbd1-ac53cbd37ebc
REACT_APP_ROLE_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69
REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8
REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f
REACT_APP_ROLE_CAR_DIAGNOSTIC=2914e67f-fb85-4b78-b79d-656f4f37faa1
REACT_APP_ROLE_UPDATE_DEPLOY=e4af2c4c-6c5e-4784-9097-7c18e776d7b6
REACT_APP_ECCKEY_ENV=
REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5}
REACT_APP_ENABLE_DEBUGMASK=1

View File

@@ -15,6 +15,8 @@ REACT_APP_ROLE_GENERATE_CERTIFICATE=746f34b0-9ba0-4b5d-8d84-0256a9c8e390
REACT_APP_ROLE_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69
REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8
REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f
REACT_APP_ROLE_CAR_DIAGNOSTIC=2914e67f-fb85-4b78-b79d-656f4f37faa1
REACT_APP_ROLE_UPDATE_DEPLOY=3590ec3f-1c05-428b-81a4-40b00baf83de
REACT_APP_ECCKEY_ENV=stage,prod
REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5}
REACT_APP_ENABLE_DEBUGMASK=1

View File

@@ -15,6 +15,8 @@ REACT_APP_ROLE_GENERATE_CERTIFICATE=746f34b0-9ba0-4b5d-8d84-0256a9c8e390
REACT_APP_ROLE_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69
REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8
REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f
REACT_APP_ROLE_UPDATE_DEPLOY=3590ec3f-1c05-428b-81a4-40b00baf83de
REACT_APP_ROLE_CAR_DIAGNOSTIC=2914e67f-fb85-4b78-b79d-656f4f37faa1
REACT_APP_ECCKEY_ENV=dev,stage,prod
REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5}
REACT_APP_ENABLE_DEBUGMASK=1

View File

@@ -15,6 +15,8 @@ REACT_APP_ROLE_GENERATE_CERTIFICATE=746f34b0-9ba0-4b5d-8d84-0256a9c8e390
REACT_APP_ROLE_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69
REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8
REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f
REACT_APP_ROLE_CAR_DIAGNOSTIC=2914e67f-fb85-4b78-b79d-656f4f37faa1
REACT_APP_ROLE_UPDATE_DEPLOY=e4af2c4c-6c5e-4784-9097-7c18e776d7b6
REACT_APP_ECCKEY_ENV=stage
REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5}
REACT_APP_ENABLE_DEBUGMASK=1

View File

@@ -15,6 +15,8 @@ REACT_APP_ROLE_GENERATE_CERTIFICATE=746f34b0-9ba0-4b5d-8d84-0256a9c8e390
REACT_APP_ROLE_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69
REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8
REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f
REACT_APP_ROLE_CAR_DIAGNOSTIC=2914e67f-fb85-4b78-b79d-656f4f37faa1
REACT_APP_ROLE_UPDATE_DEPLOY=3590ec3f-1c05-428b-81a4-40b00baf83de
REACT_APP_ECCKEY_ENV=prod
REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5}
REACT_APP_ENABLE_DEBUGMASK=1

View File

@@ -15,5 +15,7 @@ REACT_APP_ROLE_GENERATE_CERTIFICATE=746f34b0-9ba0-4b5d-8d84-0256a9c8e390
REACT_APP_ROLE_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69
REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8
REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f
REACT_APP_ROLE_CAR_DIAGNOSTIC=2914e67f-fb85-4b78-b79d-656f4f37faa1
REACT_APP_ROLE_UPDATE_DEPLOY=3590ec3f-1c05-428b-81a4-40b00baf83de
REACT_APP_ECCKEY_ENV=dev,stage,prod
REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5}

11
package-lock.json generated
View File

@@ -41,6 +41,7 @@
"react-router-dom": "^5.3.0",
"react-router-hash-link": "^2.4.3",
"react-scripts": "5.0.0",
"semver-compare": "^1.0.0",
"usehooks-ts": "^2.7.1",
"web-vitals": "^2.1.4",
"webpack": "^5.74.0"
@@ -15130,6 +15131,11 @@
"node": ">=10"
}
},
"node_modules/semver-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="
},
"node_modules/send": {
"version": "0.17.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
@@ -28020,6 +28026,11 @@
"lru-cache": "^6.0.0"
}
},
"semver-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="
},
"send": {
"version": "0.17.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",

View File

@@ -36,6 +36,7 @@
"react-router-dom": "^5.3.0",
"react-router-hash-link": "^2.4.3",
"react-scripts": "5.0.0",
"semver-compare": "^1.0.0",
"usehooks-ts": "^2.7.1",
"web-vitals": "^2.1.4",
"webpack": "^5.74.0"

View File

@@ -6261,9 +6261,9 @@ exports[`App Route /packages authenticated 1`] = `
class="MuiButtonBase-root MuiToggleButton-root Mui-selected MuiToggleButton-sizeMedium MuiToggleButton-standard MuiToggleButtonGroup-grouped MuiToggleButtonGroup-groupedHorizontal css-ueukts-MuiButtonBase-root-MuiToggleButton-root"
tabindex="0"
type="button"
value="true"
value="software"
>
Active
Software
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
@@ -6273,18 +6273,67 @@ exports[`App Route /packages authenticated 1`] = `
class="MuiButtonBase-root MuiToggleButton-root MuiToggleButton-sizeMedium MuiToggleButton-standard MuiToggleButtonGroup-grouped MuiToggleButtonGroup-groupedHorizontal css-ueukts-MuiButtonBase-root-MuiToggleButton-root"
tabindex="0"
type="button"
value="false"
value="archived"
>
Archived
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
<button
aria-pressed="false"
class="MuiButtonBase-root MuiToggleButton-root MuiToggleButton-sizeMedium MuiToggleButton-standard MuiToggleButtonGroup-grouped MuiToggleButtonGroup-groupedHorizontal css-ueukts-MuiButtonBase-root-MuiToggleButton-root"
tabindex="0"
type="button"
value="all"
>
All
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
</div>
</div>
<div
class="MuiGrid-root makeStyles-textRightAlign-0 MuiGrid-item MuiGrid-grid-md-4"
>
<div
aria-label="choose action"
class="MuiButtonGroup-root MuiButtonGroup-contained css-zqcytf-MuiButtonGroup-root"
role="group"
>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium Mui-disabled MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-sghohy-MuiButtonBase-root-MuiButton-root"
disabled=""
tabindex="-1"
type="button"
>
Archive
</button>
<button
aria-haspopup="menu"
aria-label="select action"
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-11qr2p8-MuiButtonBase-root-MuiButton-root"
tabindex="0"
type="button"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-i4bv87-MuiSvgIcon-root"
data-testid="ArrowDropDownIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="m7 10 5 5 5-5z"
/>
</svg>
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
</div>
</div>
</div>
<table
class="MuiTable-root"
@@ -6295,6 +6344,40 @@ exports[`App Route /packages authenticated 1`] = `
<tr
class="MuiTableRow-root MuiTableRow-head"
>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-paddingCheckbox"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-0 MuiCheckbox-root MuiCheckbox-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
aria-label="select all desserts"
class="PrivateSwitchBase-input-0"
data-indeterminate="false"
type="checkbox"
value=""
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
</th>
<th
aria-sort="ascending"
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
@@ -6370,6 +6453,29 @@ exports[`App Route /packages authenticated 1`] = `
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Type
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
@@ -6403,7 +6509,7 @@ exports[`App Route /packages authenticated 1`] = `
role="button"
tabindex="0"
>
Type
Update
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
@@ -6476,6 +6582,38 @@ exports[`App Route /packages authenticated 1`] = `
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-paddingCheckbox"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-0 MuiCheckbox-root MuiCheckbox-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-0"
data-indeterminate="false"
type="checkbox"
value=""
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
@@ -6494,6 +6632,9 @@ exports[`App Route /packages authenticated 1`] = `
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
/>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
/>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
@@ -6548,24 +6689,6 @@ exports[`App Route /packages authenticated 1`] = `
/>
</svg>
</a>
<a
class=""
href="/package-deploy/1"
style="margin: 5px;"
title="Deploy \\"Test Manifest 1.0\\""
>
<svg
aria-hidden="true"
aria-label="Deploy Test Manifest 1.0"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"
/>
</svg>
</a>
</td>
</tr>
</tbody>
@@ -6577,7 +6700,7 @@ exports[`App Route /packages authenticated 1`] = `
>
<td
class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root"
colspan="8"
colspan="10"
>
<div
class="MuiToolbar-root MuiToolbar-regular MuiTablePagination-toolbar MuiToolbar-gutters"
@@ -6696,6 +6819,7 @@ exports[`App Route /packages authenticated 1`] = `
</tfoot>
</table>
<div />
<div />
</div>
</div>
</main>
@@ -11427,6 +11551,13 @@ exports[`App Route /vehicle-status authenticated 1`] = `
:
false
</p>
<p>
<b>
DLT Logging Enabled
</b>
:
false
</p>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"
@@ -11440,63 +11571,7 @@ exports[`App Route /vehicle-status authenticated 1`] = `
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"
>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-0 MuiCheckbox-root MuiCheckbox-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-0"
data-indeterminate="false"
type="checkbox"
value=""
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Force Config Update
</span>
</label>
<a
class=""
href="/vehicle-status/FISKER123"
title="Push Config Update to \\"FISKER123\\""
>
<svg
aria-hidden="true"
aria-label="Push Config Update to \\"FISKER123\\""
class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge css-tzssek-MuiSvgIcon-root"
data-testid="UploadIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M5 20h14v-2H5v2zm0-10h4v6h6v-6h4l-7-7-7 7z"
/>
</svg>
</a>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"
>

View File

@@ -0,0 +1,84 @@
import { useEffect, useState } from "react";
import TransformModal from "../TransformModal";
import DropDownButton from "../Controls/DropDownButton";
import { useUserContext } from "../Contexts/UserContext";
import { useStatusContext } from "../Contexts/StatusContext";
import useAddTags from "./useAddTags";
import useUpdateConfig from "./useUpdateConfig";
const transformArrayToCSV = (arr) => arr.join(", ");
export default function BulkActions({
vins = [],
}) {
const [vinCSV, setVinCSV] = useState(transformArrayToCSV(vins));
const [active, setActive] = useState(null);
const actions = [
{
name: "Update Configs",
disabled: vins.length === 0,
trigger: () => setActive("updateConfig"),
},
{
name: "Add Tags",
disabled: vins.length === 0,
trigger: () => setActive("addTags"),
},
];
const updateConfig = useUpdateConfig();
const addTags = useAddTags();
const { setMessage } = useStatusContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
const handleUpdateConfig = () => {
updateConfig.submit(vins, token)
.then(() => {
setMessage(`${vins.length} vehicles updated.`);
})
.catch((error) => {
setMessage(error.message);
});
}
const handleAddTags = () => {
addTags.submit(vins, token)
.then(() => setMessage(`Added ${addTags.data.tags.value.length} tags to ${vins.length} vehicles.`))
.catch((error) => setMessage(error.message));
}
const handleClose = () => setActive(null);
useEffect(() => {
setVinCSV(transformArrayToCSV(vins));
}, [vins]);
return (
<>
<DropDownButton actions={actions} payload={[vins]} />
<TransformModal
title="Update Config"
body={`You are updating the config for the following VINs: ${vinCSV}.`}
close={handleClose}
open={active === "updateConfig"}
data={updateConfig.data}
setData={updateConfig.setData}
submit={handleUpdateConfig}
/>
<TransformModal
title="Add Tags"
body={`You are adding tags for the following VINs: ${vinCSV}.`}
close={handleClose}
open={active === "addTags"}
data={addTags.data}
setData={addTags.setData}
submit={handleAddTags}
/>
</>
);
}

View File

@@ -0,0 +1,22 @@
import { useState } from "react";
import vehiclesAPI from "../../services/vehiclesAPI";
export default function useAddTags() {
const [tags, setTags] = useState({
tags: {
label: "Tags",
type: "list.string",
value: [],
},
});
const submit = async (vins, token) => {
return vehiclesAPI.addTags(vins, tags.tags.value, token);
}
return {
data: tags,
setData: setTags,
submit,
};
}

View File

@@ -0,0 +1,40 @@
import { useState } from "react";
import TaskRunner from "../../utils/taskRunner";
import vehiclesAPI from "../../services/vehiclesAPI";
export default function useUpdateConfig() {
const [config, setConfig] = useState({
force: {
label: "Force Push",
type: "boolean",
value: false,
},
});
const submit = async (vins, token) => {
return new Promise((resolve, reject) => {
const taskRunner = new TaskRunner(5);
const task = (vin, isLast) => {
return async () => vehiclesAPI.updateConfig(vin, config.force.value, token)
.then((response) => {
if (isLast) {
if (response.error) {
reject(response);
}
resolve(response)
}
})
.catch((error) => reject(error));
}
vins.forEach((vin, index) => taskRunner.push(task(vin, index === vins.length - 1)));
});
}
return {
data: config,
setData: setConfig,
submit,
};
}

View File

@@ -147,6 +147,13 @@ exports[`VehicleDetailsTab Render 1`] = `
:
false
</p>
<p>
<b>
DLT Logging Enabled
</b>
:
false
</p>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"
@@ -160,63 +167,7 @@ exports[`VehicleDetailsTab Render 1`] = `
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"
>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-0 MuiCheckbox-root MuiCheckbox-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-0"
data-indeterminate="false"
type="checkbox"
value=""
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Force Config Update
</span>
</label>
<a
class=""
href="/"
title="Push Config Update to \\"TESTVIN1234567890\\""
>
<svg
aria-hidden="true"
aria-label="Push Config Update to \\"TESTVIN1234567890\\""
class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge css-tzssek-MuiSvgIcon-root"
data-testid="UploadIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M5 20h14v-2H5v2zm0-10h4v6h6v-6h4l-7-7-7 7z"
/>
</svg>
</a>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"
>

View File

@@ -115,7 +115,7 @@ const MainForm = ({ vin }) => {
<b>Info Source</b>: {vehicle.info_source}
</p>
<p>
<b>Tags</b>: {vehicle.tags ? vehicle.tags.join(", ") : "none" }
<b>Tags</b>: {vehicle.tags ? vehicle.tags.join(", ") : "none"}
</p>
</Grid>
<Grid item md={12} className={classes.textCenterAlign}>
@@ -142,7 +142,10 @@ const MainForm = ({ vin }) => {
<b>Filters</b>: {vehicle.canbus.filters ? vehicle.canbus.filters.length : 0}
</p>
<p>
<b>DTC Enabled</b>: { (vehicle.canbus.dtc_enabled || false).toString() }
<b>DTC Enabled</b>: {(vehicle.canbus.dtc_enabled || false).toString()}
</p>
<p>
<b>DLT Logging Enabled</b>: {(vehicle.dlt_enabled || false).toString()}
</p>
</>
)}
@@ -158,7 +161,7 @@ const MainForm = ({ vin }) => {
<RoleWrap
groups={groups}
providers={providers}
rolesPerProvider={Permissions.FiskerCreate}
rolesPerProvider={Permissions.FiskerUpdateDeploy}
>
<FormControlLabel
label="Force Config Update"

View File

@@ -2,24 +2,25 @@ jest.mock("../../../Contexts/VehicleContext");
jest.mock("../../../Contexts/StatusContext");
jest.mock("../../../Contexts/UserContext");
import { render, waitFor } from "@testing-library/react";
import { render, screen, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import routeData from "react-router";
import { VehicleProvider } from "../../../Contexts/VehicleContext";
import { StatusProvider } from "../../../Contexts/StatusContext";
import { UserProvider, setToken } from "../../../Contexts/UserContext";
import { TEST_AUTH_OBJECT_FISKER }from "../../../../utils/testing";
import { TEST_AUTH_OBJECT_FISKER } from "../../../../utils/testing";
import MainForm from "./index";
import addSnapshotSerializer from "../../../../utils/snapshot";
import * as Roles from "../../../../utils/roles";
const renderVehicleDetailsTab = async () => {
const { container } = render(
<VehicleProvider>
<StatusProvider>
<UserProvider>
<UserProvider >
<BrowserRouter>
<MainForm vin="TESTVIN1234567890"/>
<MainForm vin="TESTVIN1234567890" />
</BrowserRouter>
</UserProvider>
</StatusProvider>
@@ -46,4 +47,23 @@ describe("VehicleDetailsTab", () => {
const container = await renderVehicleDetailsTab();
expect(container).toMatchSnapshot();
});
it("renders update config control when required permission is present.", () => {
const hasRole = jest.spyOn(Roles, 'hasRole');
hasRole.mockReturnValue(true);
render(
<VehicleProvider>
<StatusProvider>
<UserProvider>
<BrowserRouter>
<MainForm vin="TESTVIN1234567890" />
</BrowserRouter>
</UserProvider>
</StatusProvider>
</VehicleProvider>
);
expect(screen.getByLabelText("Force Config Update")).toBeTruthy();
hasRole.mockRestore();
})
});

View File

@@ -0,0 +1,30 @@
import useStyles from "../../useStyles";
import clsx from "clsx";
import Typography from "@material-ui/core/Typography";
import SendDiagnosticCommand from "../../Controls/SendDiagnosticCommand";
import { useParams } from "react-router";
import { useUserContext } from "../../Contexts/UserContext";
import {VehicleProvider} from "../../Contexts/VehicleContext";
const RemoteDiagnosticCommandsTab = (props) => {
const { vin } = useParams();
const classes = useStyles();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Typography variant="h6">Vehicle Diagnostic Commands</Typography>
<VehicleProvider>
<SendDiagnosticCommand vin={vin} token={token} classes={classes}></SendDiagnosticCommand>
</VehicleProvider>
</div>
)
}
export default RemoteDiagnosticCommandsTab

View File

@@ -155,6 +155,13 @@ exports[`DetailsTab Render 1`] = `
:
false
</p>
<p>
<b>
DLT Logging Enabled
</b>
:
false
</p>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"
@@ -168,63 +175,7 @@ exports[`DetailsTab Render 1`] = `
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"
>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-0 MuiCheckbox-root MuiCheckbox-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-0"
data-indeterminate="false"
type="checkbox"
value=""
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Force Config Update
</span>
</label>
<a
class=""
href="/testroute/TESTVIN1234567890"
title="Push Config Update to \\"TESTVIN1234567890\\""
>
<svg
aria-hidden="true"
aria-label="Push Config Update to \\"TESTVIN1234567890\\""
class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge css-tzssek-MuiSvgIcon-root"
data-testid="UploadIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M5 20h14v-2H5v2zm0-10h4v6h6v-6h4l-7-7-7 7z"
/>
</svg>
</a>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"
>

View File

@@ -237,6 +237,17 @@ exports[`DigitalTwinTab Render 1`] = `
77.7 km/h
</p>
</div>
<div
class="makeStyles-popupSection-0"
>
<p>
<b>
Parked
</b>
:
Yes
</p>
</div>
</div>
<div
style="width: 100vh;"

View File

@@ -331,63 +331,7 @@ exports[`CarStatus Render 1`] = `
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"
>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-0 MuiCheckbox-root MuiCheckbox-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-0"
data-indeterminate="false"
type="checkbox"
value=""
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Force Config Update
</span>
</label>
<a
class=""
href="/"
title="Push Config Update to \\"TESTVIN1234567890\\""
>
<svg
aria-hidden="true"
aria-label="Push Config Update to \\"TESTVIN1234567890\\""
class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge css-tzssek-MuiSvgIcon-root"
data-testid="UploadIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M5 20h14v-2H5v2zm0-10h4v6h6v-6h4l-7-7-7 7z"
/>
</svg>
</a>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"
>

View File

@@ -18,6 +18,7 @@ import DigitalTwinTab from "./DigitalTwinTab";
import ECUsTab from "./ECUsTab";
import FleetsTab from "./FleetsTab";
import RemoteCommandsTab from "./RemoteCommandsTab";
import RemoteDiagnosticCommandsTab from "./RemoteDiagnosticCommands";
import TRexLogsTab from "./TRexLogsTab";
const tabHashes = ["details", "updates", "filters"];
@@ -63,6 +64,11 @@ const TabViews = [
component: RemoteCommandsTab,
rolesPerProvider: Permissions.FiskerMagnaCreate,
},
{
label: "Remote Diagnostic Commands",
component: RemoteDiagnosticCommandsTab,
rolesPerProvider: Permissions.CarDiagnostic,
},
{
label: "Fleets",
component: FleetsTab,

View File

@@ -1006,6 +1006,43 @@ exports[`VehicleUpdate Render 1`] = `
DTC Enabled
</span>
</label>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-0 MuiCheckbox-root MuiCheckbox-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-0"
data-indeterminate="false"
type="checkbox"
value=""
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
DLT Logging Enabled (supported from T.Rex 1.1.127)
</span>
</label>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>

View File

@@ -47,6 +47,7 @@ const MainForm = () => {
const [maxMemBufferSize, setMaxMemBufferSize] = useState(0);
const [maxDiskBufferSize, setMaxDiskBufferSize] = useState(0);
const [dtcEnabled, setDTCEnabled] = useState(true);
const [dltEnabled, setDLTEnabled] = useState(false);
const debugMaskEl = useRef(null);
const tagsEl = useRef(null);
@@ -99,6 +100,7 @@ const MainForm = () => {
setMaxDiskBufferSize(vehicle.canbus.max_disk_buffer_size ?? maxDiskBufferSize);
setDTCEnabled(vehicle.canbus.dtc_enabled ?? dtcEnabled);
}
setDLTEnabled(vehicle.dlt_enabled ?? dltEnabled);
if (showDebugMask) {
debugMaskEl.current.value = vehicle.debug_mask ?? ""
@@ -125,6 +127,10 @@ const MainForm = () => {
setDTCEnabled(event.target.checked);
}
const onDltEnabledChange = (event) => {
setDLTEnabled(event.target.checked);
}
const onMaxMemBufferSizeChange = (event) => {
setMaxMemBufferSize(event.target.value);
}
@@ -148,7 +154,7 @@ const MainForm = () => {
restraint: restraintEl.current.value,
body_type: bodyTypeEl.current.value,
log_level: selectedLogLevel,
tags: tagsEl.current.value.split(",").map(function (word) {
tags: tagsEl.current.value.split(",").map(function(word) {
return word.trim();
}),
canbus: {
@@ -158,6 +164,7 @@ const MainForm = () => {
max_disk_buffer_size: canbusEnabled && dataLoggerEnabled ? parseInt(maxDiskBufferSize) : 0,
dtc_enabled: dtcEnabled
},
dlt_enabled: dltEnabled,
debug_mask: debugMaskEl.current?.value
};
@@ -423,6 +430,12 @@ const MainForm = () => {
onChange={onDtcEnabledChange}
/>
} label="DTC Enabled" />
<FormControlLabel control={
<Checkbox
checked={dltEnabled}
onChange={onDltEnabledChange}
/>
} label="DLT Logging Enabled (supported from T.Rex 1.1.127)" />
{showDebugMask && (
<TextField
id="debug_mask"

View File

@@ -1,5 +1,6 @@
import React, { useContext, useState } from "react";
import api from "../../services/fleetsAPI";
import vehiclesAPI from "../../services/vehiclesAPI";
import { validateCANID, validateFilter, validateVIN } from "../../utils/validationSupplier";
const FleetContext = React.createContext();
@@ -112,7 +113,22 @@ export const FleetProvider = ({ children }) => {
throw new Error(`Get fleet vehicles error. ${result.message}`);
}
setFleetVehicles(result.data)
const connectionsResult = await vehiclesAPI.getConnections(result.data, token)
if (result.error) {
setFleetVehicles([])
throw new Error(`Get vehicles connections error. ${result.message}`);
}
var cars = []
result.data.forEach((vin) => {
cars.push({
vin: vin,
connected: connectionsResult[vin] || false,
connectedHMI: connectionsResult[`2:${vin}`] || false
})
})
setFleetVehicles(cars)
if (result.total) {
setTotalFleetVehicles(result.total);
}

View File

@@ -1,4 +1,5 @@
jest.mock("../../services/fleetsAPI");
jest.mock("../../services/vehiclesAPI");
import {
render,
@@ -800,9 +801,21 @@ const expectedFleetsData = [
];
const expectedFleetVehiclesData = [
"USWESTVIN12345678",
"USWESTVIN12345679",
"USWESTVIN12345670",
{
vin: "USWESTVIN12345678",
connected: true,
connectedHMI: false,
},
{
vin: "USWESTVIN12345679",
connected: true,
connectedHMI: false,
},
{
vin: "USWESTVIN12345670",
connected: true,
connectedHMI: false,
},
];
const expectedFleetCANFiltersData = [

View File

@@ -105,7 +105,7 @@ export const VehicleProvider = ({ children }) => {
setBusy(true);
const result = await api.getLocations(token);
if (result.error)
throw new Error(`Get locations vehicle paths error. ${result.message}`);
throw new Error(`Get locations error. ${result.message}`);
return result;
} finally {
setBusy(false);
@@ -202,6 +202,18 @@ export const VehicleProvider = ({ children }) => {
}
};
const sendDiagnosticCommand = async (vins, command, token) => {
try {
setBusy(true);
const result = await api.sendDiagnosticCommand(vins, command, token);
if (result.error)
throw new Error(`Send diagnostic command error. ${result.message}`);
return result;
} finally {
setBusy(false);
}
};
const updateVehicle = async (vin, v, token) => {
try {
setBusy(true);
@@ -313,6 +325,7 @@ export const VehicleProvider = ({ children }) => {
getVehicle,
getVehicles,
sendCommand,
sendDiagnosticCommand,
updateVehicle,
getFleets,
getVersionLog,

View File

@@ -62,7 +62,26 @@ export const useFleetContext = () => ({
fleetVehicles,
totalFleetVehicles,
getFleetVehicles: jest.fn(),
getFleetVehicles: jest.fn().mockImplementation((name, search, _token) => {
const result = [
{
vin: "USWESTVIN12345678",
connected: false,
connectedHMI: false
},
{
vin: "USWESTVIN12345679",
connected: true,
connectedHMI: true
},
{
vin: "USWESTVIN12345670",
connected: false,
connectedHMI: false
},
];
return Promise.resolve(result);
}),
addFleetVehicles: jest.fn(),
deleteFleetVehicle: jest.fn(),

View File

@@ -78,6 +78,9 @@ let vehicleState = {
vehicle_speed: {
speed: 77.7,
},
gear: {
in_park: true,
}
},
};
@@ -109,10 +112,14 @@ export const useVehicleContext = () => ({
addVehicle: jest.fn(),
getConnections: jest
.fn().mockImplementation((vins, _token) => {
const result = {};
vins.forEach((vin) => {
result[vin] = true;
});
const result = {
"USWESTVIN12345678": true,
"2:USWESTVIN12345678": false,
"USWESTVIN12345679": true,
"2:USWESTVIN12345679": false,
"USWESTVIN12345670": true,
"2:USWESTVIN12345670": false,
};
return Promise.resolve(result);
}),
getECUs: jest.fn(() => {
@@ -146,9 +153,9 @@ export const useVehicleContext = () => ({
.fn()
.mockResolvedValue({
// tests only pass without mocking the data here
// '3FAFP13P71R199267': [],
// '3FAFP13P31R199430': [[16.891136999999986, 26.832352999999955], [56.891136999999986, 66.832352999999955], [26.891136999999986, 36.832352999999955]],
// '3FAFP13P71R199060': [[36.891136999999986, 46.832352999999955], [76.891136999999986, 16.832352999999955]],
// '3FAFP13P61R199390': [],
}),
getModels: jest.fn(() => {
models = ["Ocean", "PEAR"];

View File

@@ -40,6 +40,10 @@ const DropDownButton = ({ actions = [], payload = [] }) => {
setOpen(false);
};
if (!actions.length) {
return <></>;
}
return (
<>
<ButtonGroup

View File

@@ -2,7 +2,7 @@ import React from "react";
import { hasRole } from "../../../utils/roles";
export const RoleWrap = (props) => {
const {groups, rolesPerProvider, providers} = props;
const { groups, rolesPerProvider, providers } = props;
const eitherComponent = props["eitherComponent"] || null;

View File

@@ -0,0 +1,135 @@
import clsx from "clsx";
import { Button, FormControl, InputLabel, Select, FormControlLabel, FormGroup } from "@material-ui/core";
import Checkbox from '@mui/material/Checkbox';
import React, { useEffect, useState } from "react";
import { useStatusContext } from "../../Contexts/StatusContext";
import { logger } from "../../../services/monitoring";
import cmp from "semver-compare";
import {
useVehicleContext
} from "../../Contexts/VehicleContext";
const commands = ["Reset"]
const ecus = ["TBOX"]
const SendDiagnosticCommand = ({ vin, token, classes }) => {
const { getState, sendDiagnosticCommand } = useVehicleContext();
const [carState, setCarState] = useState(null);
const { setMessage } = useStatusContext();
const [currentCommand, setCurrentCommand] = useState(commands[0].toLowerCase());
const [currentECUs] = useState([ecus[0]]);
const changeCommandHandler = (e) => {
setCurrentCommand(e.target.value);
};
//Update online/offline state
useEffect(() => {
if (!vin) return;
getCarState();
const interval = setInterval(getCarState, 5000);
return () => { clearInterval(interval); }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [vin]);
const getCarState = async () => {
try {
const result = await getState(token, vin);
setCarState(result.data);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
};
const isOnline = () => {
return carState && carState?.online;
};
const TREX_MIN_VER = "1.1.108";
const isTBOXResetSupported = () => {
return !carState?.trex_version ? true : cmp(carState.trex_version, TREX_MIN_VER) === 1;
};
const clickHandler = async (_) => {
try {
await sendDiagnosticCommand([vin], { body: { command: currentCommand, ecus: currentECUs } }, token);
setMessage(`Sent diagnostic command to ${vin}`);
} catch (error) {
setMessage(error.message);
logger.error(error.stack);
}
};
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<FormControl
className={classes.formControl}
variant="outlined"
size="small"
>
<InputLabel htmlFor="send-command" className={classes.whiteBackground}>
Diagnostic Command
</InputLabel>
<Select
native
variant="outlined"
inputProps={{
name: "send-command",
id: "send-command",
}}
onChange={changeCommandHandler}
>
{commands.map((command, index) => (
<option key={index} value={command.toLowerCase()}>
{command}
</option>
))}
</Select>
</FormControl>
<FormGroup>
{
ecus.map((ecu, idx) => {
return <FormControlLabel
control={<Checkbox key={idx} />}
label={ecu}
value={ecu}
checked={true} />
})
}
</FormGroup>
<Button
type="submit"
aria-label="send command"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={clickHandler}
disabled={!isOnline() || !isTBOXResetSupported()}
>
Send
</Button>
<div>
<b>
{isOnline() ? "ONLINE" : "OFFLINE"}
</b>
</div>
<div>
<b>
{!isTBOXResetSupported() ? `TBOX Reset supported from ${TREX_MIN_VER}, current version ${carState.trex_version}` : ""}
</b>
</div>
</div >
);
};
export default SendDiagnosticCommand;

View File

@@ -7,6 +7,8 @@ import useStyles from "../useStyles";
const UNKNOWN = "unknown";
const LOCKED = "Locked";
const UNLOCKED = "Unlocked";
const PARKED = "Yes";
const NOT_PARKED = "Not Parked";
const appendUnits = (value, units) => {
if (value || value === 0) return `${value}${units}`;
@@ -32,7 +34,7 @@ const windowState = (value) => {
const DigitalTwin = (props) => {
const classes = useStyles();
const { battery, doors, location, trex_version, ip, updated, windows, misc_windows, sunroof, dbc_version, door_locks, vcu0x260, charging_metrics, max_range, vehicle_speed } = props;
const { battery, doors, location, trex_version, ip, updated, windows, misc_windows, sunroof, dbc_version, door_locks, vcu0x260, charging_metrics, max_range, vehicle_speed, gear } = props;
return (
<div>
@@ -133,6 +135,11 @@ const DigitalTwin = (props) => {
{keyValueTemplate("Vehicle Speed", appendUnits(vehicle_speed?.speed, " km/h"))}
</div>
)}
{gear && (
<div className={classes.popupSection}>
{keyValueTemplate("Parked", gear.in_park ? PARKED : NOT_PARKED)}
</div>
)}
</div>
);
};

View File

@@ -143,6 +143,48 @@ exports[`FleetDetailsTab Render 1`] = `
</svg>
</a>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"
>
<div
aria-label="choose action"
class="MuiButtonGroup-root MuiButtonGroup-contained css-zqcytf-MuiButtonGroup-root"
role="group"
>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-sghohy-MuiButtonBase-root-MuiButton-root"
tabindex="0"
type="button"
>
Update Configs
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
<button
aria-haspopup="menu"
aria-label="select action"
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-11qr2p8-MuiButtonBase-root-MuiButton-root"
tabindex="0"
type="button"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-i4bv87-MuiSvgIcon-root"
data-testid="ArrowDropDownIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="m7 10 5 5 5-5z"
/>
</svg>
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
</div>
</div>
</div>
<div />
</div>

View File

@@ -15,6 +15,7 @@ import { FleetProvider, useFleetContext } from "../../../Contexts/FleetContext"
import useStyles from "../../../useStyles";
import { logger } from "../../../../services/monitoring";
import DeleteConfirmation from "../../../DeleteConfirmation";
import BulkActions from "../../../BulkActions";
const MainForm = ({ name }) => {
const classes = useStyles();
@@ -94,6 +95,9 @@ const MainForm = ({ name }) => {
</Link>
</Tooltip>
</Grid>
<Grid item md={12} className={classes.textCenterAlign}>
<BulkActions vins={fleet.vehicles} />
</Grid>
</Grid>
<DeleteConfirmation message={name} open={showDeleteModal} close={() => setShowDeleteModal(false)} deleteFunction={onDelete} />
</div >

View File

@@ -137,62 +137,7 @@ exports[`FleetVehiclesTable Render 1`] = `
</thead>
<tbody
class="MuiTableBody-root"
>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
href="/vehicle-status/USWESTVIN12345678"
>
USWESTVIN12345678
</a>
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
No actions
</td>
</tr>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
href="/vehicle-status/USWESTVIN12345679"
>
USWESTVIN12345679
</a>
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
No actions
</td>
</tr>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
href="/vehicle-status/USWESTVIN12345670"
>
USWESTVIN12345670
</a>
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
No actions
</td>
</tr>
</tbody>
/>
<tfoot
class="MuiTableFooter-root"
>

View File

@@ -26,6 +26,7 @@ import SearchField from "../../../../Controls/SearchField";
import DeleteConfirmation from "../../../../DeleteConfirmation";
import TableHeaderSortable from "../../../../Table/HeaderSortable";
import { useLocalStorage } from "../../../../useLocalStorage";
import ConnectedIcon from "../../../../Controls/ConnectedIcon";
import useStyles from "../../../../useStyles";
const tableColumns = [
@@ -190,13 +191,22 @@ const MainForm = ({ name }) => {
onSortRequest={handleSort}
/>
<TableBody>
{fleetVehicles.map((vin) => (
<TableRow key={vin}>
<TableCell align="center">
<Link to={`/vehicle-status/${vin}`}>{vin}</Link>
{fleetVehicles && fleetVehicles.map((car) => (
(car.vin && <TableRow key={"row" + car.vin}>
<TableCell key={"cell" + car.vin} align="center">
{(car.connected || car.connectedHMI) &&
<ConnectedIcon
key={"icon" + car.vin}
connected={car.connected}
connectedHMI={car.connectedHMI}
style={{ marginRight: 3 }}
/>
}
<Link key={"link" + car.vin} to={`/vehicle-status/${car.vin}`}>{car.vin}</Link>
</TableCell>
<TableCell align="center">{Actions(vin)}</TableCell>
<TableCell key={"cell2" + car.vin} align="center">{Actions(car.vin)}</TableCell>
</TableRow>
)
))}
</TableBody>
<TableFooter>

View File

@@ -151,6 +151,48 @@ exports[`DetailsTab Render 1`] = `
</svg>
</a>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"
>
<div
aria-label="choose action"
class="MuiButtonGroup-root MuiButtonGroup-contained css-zqcytf-MuiButtonGroup-root"
role="group"
>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-sghohy-MuiButtonBase-root-MuiButton-root"
tabindex="0"
type="button"
>
Update Configs
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
<button
aria-haspopup="menu"
aria-label="select action"
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-11qr2p8-MuiButtonBase-root-MuiButton-root"
tabindex="0"
type="button"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-i4bv87-MuiSvgIcon-root"
data-testid="ArrowDropDownIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="m7 10 5 5 5-5z"
/>
</svg>
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
</div>
</div>
</div>
<div />
</div>

View File

@@ -136,62 +136,7 @@ exports[`VehiclesTab Render 1`] = `
</thead>
<tbody
class="MuiTableBody-root"
>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
href="/vehicle-status/USWESTVIN12345678"
>
USWESTVIN12345678
</a>
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
No actions
</td>
</tr>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
href="/vehicle-status/USWESTVIN12345679"
>
USWESTVIN12345679
</a>
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
No actions
</td>
</tr>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
href="/vehicle-status/USWESTVIN12345670"
>
USWESTVIN12345670
</a>
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
No actions
</td>
</tr>
</tbody>
/>
<tfoot
class="MuiTableFooter-root"
>

View File

@@ -239,6 +239,48 @@ exports[`FleetStatus Render 1`] = `
</svg>
</a>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"
>
<div
aria-label="choose action"
class="MuiButtonGroup-root MuiButtonGroup-contained css-zqcytf-MuiButtonGroup-root"
role="group"
>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-sghohy-MuiButtonBase-root-MuiButton-root"
tabindex="0"
type="button"
>
Update Configs
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
<button
aria-haspopup="menu"
aria-label="select action"
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-11qr2p8-MuiButtonBase-root-MuiButton-root"
tabindex="0"
type="button"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-i4bv87-MuiSvgIcon-root"
data-testid="ArrowDropDownIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="m7 10 5 5 5-5z"
/>
</svg>
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
</div>
</div>
</div>
<div />
</div>

View File

@@ -1,4 +1,5 @@
import {
Checkbox,
Grid,
Table,
TableBody,
@@ -6,7 +7,7 @@ import {
TableFooter,
TablePagination,
TableRow,
Tooltip
Tooltip,
} from "@material-ui/core";
import DeleteIcon from "@material-ui/icons/Delete";
import SendIcon from "@material-ui/icons/Send";
@@ -22,14 +23,15 @@ import { Link } from "react-router-dom";
import EditIcon from "@material-ui/icons/Edit";
import { logger } from "../../../services/monitoring";
import { LocalDateTimeString } from "../../../utils/dates";
import { TYPE_MANIFEST_SOFTWARE } from "../../../utils/manifest_types";
import { hasRole, Permissions } from "../../../utils/roles";
import { TYPE_MANIFEST_AFTERSALES, TYPE_MANIFEST_CONFIG, TYPE_MANIFEST_SOFTWARE } from "../../../utils/manifest_types";
import { Permissions, hasRole } from "../../../utils/roles";
import {
ManifestsProvider,
useManifestsContext
} from "../../Contexts/ManifestsContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext";
import DropDownButton from "../../Controls/DropDownButton";
import ECUList from "../../Controls/ECUList";
import { RoleWrap } from "../../Controls/RoleWrap";
import SearchField from "../../Controls/SearchField";
@@ -37,6 +39,8 @@ import DeleteConfirmation from "../../DeleteConfirmation";
import TableHeaderSortable from "../../Table/HeaderSortable";
import { useLocalStorage } from "../../useLocalStorage";
import useStyles from "../../useStyles";
import { useUpdateManifest } from "../../../hooks";
import GeneralConfirmation from "../../GeneralConfirmation";
const tableColumns = [
{
@@ -51,13 +55,17 @@ const tableColumns = [
id: "version",
label: "Version",
},
{
id: "manifest_type",
label: "Type",
},
{
id: "sums",
label: "SUMS",
},
{
id: "type",
label: "Type",
label: "Update",
},
{
id: "created_at",
@@ -73,7 +81,7 @@ const tableColumns = [
},
];
const formatManifestType = (type) => {
const formatType = (type) => {
switch (type) {
case "forced":
return "Forced";
@@ -82,6 +90,21 @@ const formatManifestType = (type) => {
}
};
const formatManifestType = (manifestType) => {
switch (manifestType) {
case 1:
return "Software";
case 2:
return "Config";
case 3:
return "Magna";
case 4:
return "Aftersales";
default:
return manifestType;
}
}
const PAGE_SIZE = "MANIFEST_LIST_PAGE_SIZE";
const MainForm = () => {
@@ -91,13 +114,13 @@ const MainForm = () => {
const [orderBy, setOrderBy] = useState("id");
const [order, setOrder] = useState("asc");
const [search, setSearch] = useLocalStorage("DEPLOYMENT_SEARCH", "");
const [active, setActive] = useLocalStorage("DEPLOYMENT_ACTIVE", "true");
const [active, setActive] = useLocalStorage("DEPLOYMENT_TAB_TOGGLE", "software");
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [deleteId, setDeleteId] = useState("");
const [deleteRowName, setDeleteRowName] = useState("");
const [showArchiveModal, setShowArchiveModal] = useState(false);
const [archiveLabel, setArchiveLabel] = useState("Archive");
const { getManifests, deleteManifest, manifests, totalManifests } =
const { getManifests, manifests, totalManifests } =
useManifestsContext();
const { setMessage, setTitle, setSitePath } = useStatusContext();
const {
@@ -107,6 +130,13 @@ const MainForm = () => {
groups,
providers,
} = useUserContext();
const {
remove,
archive,
updateManifestIds,
setUpdateManifestIds,
setMakeActive,
} = useUpdateManifest(token);
const sortHandler = (event, property) => {
if (property === orderBy) {
@@ -131,6 +161,45 @@ const MainForm = () => {
(async () => {
try {
handleActiveChange(null, active);
switch (active) {
case "all":
await getManifests(
{
limit: pageSize,
offset: pageSize * pageIndex,
order: `${orderBy} ${order}`,
search,
},
token
);
break;
case "aftersales":
await getManifests(
{
limit: pageSize,
offset: pageSize * pageIndex,
order: `${orderBy} ${order}`,
manifest_type: TYPE_MANIFEST_AFTERSALES,
search,
active: "true",
},
token
);
break;
case "config":
await getManifests(
{
limit: pageSize,
offset: pageSize * pageIndex,
order: `${orderBy} ${order}`,
manifest_type: TYPE_MANIFEST_CONFIG,
search,
active: "true",
},
token
);
break;
case "software":
await getManifests(
{
limit: pageSize,
@@ -138,17 +207,38 @@ const MainForm = () => {
order: `${orderBy} ${order}`,
manifest_type: TYPE_MANIFEST_SOFTWARE,
search,
active,
active: "true",
},
token
);
break;
case "archived":
await getManifests(
{
limit: pageSize,
offset: pageSize * pageIndex,
order: `${orderBy} ${order}`,
manifest_type: TYPE_MANIFEST_SOFTWARE,
search,
active: "false",
},
token
);
break;
default:
break;
}
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pageIndex, pageSize, token, orderBy, order, search, active]);
}, [pageIndex, pageSize, token, orderBy, order, search, active, updateManifestIds]);
useEffect(() => {
setUpdateManifestIds([]);
}, [active, setUpdateManifestIds]);
const handleChangePageIndex = (_event, newIndex) => {
setPageIndex(newIndex);
@@ -166,19 +256,58 @@ const MainForm = () => {
const handleActiveChange = (event, newAlignment) => {
if (newAlignment !== null) {
setActive(newAlignment)
setActive(newAlignment);
setMakeActive(newAlignment === 'archived');
setArchiveLabel(() => {
if (newAlignment === "archived") {
return "Activate";
}
return "Archive";
});
}
}
const setDeletePopup = (id, row) => {
setDeleteId(id);
setDeleteRowName(`${row.name} ${row.version}`);
const handleSelectAll = () => {
setUpdateManifestIds((selected) => selected.length ? [] : manifests.map((manifest) => manifest.id));
};
const handleSelect = (event, manifest) => {
setUpdateManifestIds((selected) => {
if (event.target.checked && selected.find((id) => id === manifest.id)) {
return selected;
} else if (event.target.checked) {
return [...selected, manifest.id];
}
return selected.filter(({ id }) => id !== manifest.id);
});
};
const setDeletePopup = (row) => {
handleSelect({ target: { checked: true } }, row);
setShowDeleteModal(true);
};
const onDelete = async (manifest_id) => {
const onArchive = async () => {
try {
await deleteManifest(parseInt(manifest_id), token);
await archive()
.then(({ message }) => {
setUpdateManifestIds([]);
setMessage(message);
});
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
};
const onDelete = async () => {
try {
await remove()
.then(({ summary }) => {
setUpdateManifestIds([]);
setMessage(summary);
});
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
@@ -203,7 +332,7 @@ const MainForm = () => {
icon: <EditIcon aria-label={`Update ${row.name} ${row.version}`} />,
});
}
if (hasRole(groups, Permissions.FiskerMagnaCreate, providers)) {
if (hasRole(groups, Permissions.FiskerUpdateDeploy, providers)) {
actions.push({
tip: `Deploy "${row.name} ${row.version}"`,
link: `/package-deploy/${row.id}`,
@@ -232,7 +361,7 @@ const MainForm = () => {
return (
<span key={`delete-${action.id}-of-key`}>
<Tooltip key={`delete-${action.id}`} title={action.tip}>
<Link to="#" onClick={() => setDeletePopup(action.id, row)}>
<Link to="#" onClick={() => setDeletePopup(row)}>
{action.icon}
</Link>
</Tooltip>
@@ -259,12 +388,23 @@ const MainForm = () => {
aria-label="Active"
onChange={handleActiveChange}
>
<ToggleButton value={"true"}>Active</ToggleButton>
<ToggleButton value={"false"}>Archived</ToggleButton>
<ToggleButton value={"software"}>Software</ToggleButton>
<ToggleButton value={"archived"}>Archived</ToggleButton>
<ToggleButton value={"all"}>All</ToggleButton>
</ToggleButtonGroup>
</RoleWrap>
</Grid>
<Grid item md={4} className={classes.textRightAlign}></Grid>
<Grid item md={4} className={classes.textRightAlign}>
<DropDownButton
actions={[
{
name: archiveLabel,
trigger: () => setShowArchiveModal(true),
disabled: !updateManifestIds.length || active === "all",
}
]}
/>
</Grid>
</Grid>
<Table>
<TableHeaderSortable
@@ -273,10 +413,24 @@ const MainForm = () => {
order={order}
columnData={tableColumns}
onSortRequest={sortHandler}
multiSelect
onSelectAll={handleSelectAll}
selectCount={updateManifestIds ? updateManifestIds.length : 0}
rowCount={manifests ? manifests.length : 0}
/>
<TableBody>
{manifests.map((row) => (
{manifests.map((row) => {
const isSelected = updateManifestIds
? !!updateManifestIds.find((id) => id === row.id)
: false;
return (
<TableRow key={row.id}>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
onChange={(event) => handleSelect(event, row)}
/>
</TableCell>
<TableCell align="center">{row.id}</TableCell>
<TableCell align="center">
{row.name}
@@ -292,9 +446,12 @@ const MainForm = () => {
)}
</TableCell>
<TableCell align="center">{row.version}</TableCell>
<TableCell align="center">
{formatManifestType(row.manifest_type)}
</TableCell>
<TableCell align="center">{row.sums}</TableCell>
<TableCell align="center">
{formatManifestType(row.type)}
{formatType(row.type)}
</TableCell>
<TableCell align="center">
{LocalDateTimeString(row.created)}
@@ -304,13 +461,14 @@ const MainForm = () => {
</TableCell>
<TableCell align="center">{Actions(row)}</TableCell>
</TableRow>
))}
);
})}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 100]}
colSpan={8}
colSpan={tableColumns.length + 1}
count={totalManifests}
rowsPerPage={pageSize}
page={pageIndex}
@@ -325,10 +483,17 @@ const MainForm = () => {
</TableFooter>
</Table>
<DeleteConfirmation
message={deleteRowName}
message={`${updateManifestIds.length} records.`}
open={showDeleteModal}
close={() => setShowDeleteModal(false)}
deleteFunction={() => onDelete(deleteId)}
deleteFunction={() => onDelete()}
/>
<GeneralConfirmation
title={`${archiveLabel} Resource?`}
message={`${archiveLabel} ${updateManifestIds.length} records.`}
open={showArchiveModal}
close={() => setShowArchiveModal(false)}
actionFunction={() => onArchive()}
/>
</div>
);

View File

@@ -0,0 +1,35 @@
jest.mock("../../Contexts/ManifestsContext");
jest.mock("../../Contexts/UserContext");
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import { UserProvider, setToken } from "../../Contexts/UserContext";
import { StatusProvider } from "../../Contexts/StatusContext";
import ManifestList from ".";
import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing";
const Page = (
<UserProvider>
<BrowserRouter>
<StatusProvider>
<ManifestList />
</StatusProvider>
</BrowserRouter>
</UserProvider>
);
describe("Manifest List Component", () => {
beforeAll(() => {
setToken(TEST_AUTH_OBJECT_FISKER);
});
it("adjusts the active state on switch to archived tab", async () => {
render(Page);
const archiveActionEl = screen.getByText("Archive");
fireEvent.click(screen.getByText("Archived"));
expect(archiveActionEl.innerHTML).toBe("Activate");
});
});

View File

@@ -28,7 +28,7 @@ const TransformModal = ({
const handleChange = (key, value) => {
setData((data) => {
const {[key]: toChange, ...rest} = data;
const { [key]: toChange, ...rest } = data;
switch (data[key].type) {
case "boolean":
toChange.value = !toChange.value;

View File

@@ -7,7 +7,7 @@ import useStyles from "../useStyles";
import GrayMarkerIcon from "../../assets/gray-marker.png";
import GreenMarkerIcon from "../../assets/green-marker.png";
import { logger } from "../../services/monitoring";
import { ValidateLocationVehiclePathsData } from "../../utils/locations";
import { ValidateLocationData, ValidateLocationVehiclePathsData } from "../../utils/locations";
import { useUserContext } from "../Contexts/UserContext";
import { useVehicleContext, VehicleProvider } from "../Contexts/VehicleContext";
import { VehiclePopUp } from "../VehicleMap/popup";
@@ -54,23 +54,32 @@ const ComponentVehiclePathsMap = (props) => {
vinsParam += props.lookbackHours
return getLocationsVehiclePaths(accessToken, vinsParam)
.then((result) => {
.then(async (result) => {
let resultArray = Object.entries(result)
const points = []
// validate each location
for (let vinLocations of resultArray) {
// if there are points for the vin; skip if empty points array
if (vinLocations[0] && vinLocations[1] && vinLocations[1][0]) {
let path = []
path[0] = vinLocations[0]
path[1] = []
if (vinLocations[0]) {
let path = [];
path[0] = vinLocations[0];
path[1] = [];
if (vinLocations[1] && vinLocations[1][0]) {
for (let location of vinLocations[1]) {
if (ValidateLocationVehiclePathsData(location) !== false) {
path[1].push(location);
}
}
points.push(path)
} else {
await getState(token, vinLocations[0]).then((stateResult) => {
if (stateResult.data && stateResult.data.location) {
if (ValidateLocationData(stateResult.data.location) !== false) {
path[1].push([stateResult.data.location.latitude, stateResult.data.location.longitude]);
}
}
});
}
points.push(path);
}
}

1
src/hooks/index.js Normal file
View File

@@ -0,0 +1 @@
export { useUpdateManifest } from "./useUpdateManifest";

View File

@@ -0,0 +1,47 @@
import { useState } from "react";
import manifestsAPI from "../services/manifestsAPI";
import TaskRunner from "../utils/taskRunner";
export const useUpdateManifest = (token) => {
const [updateManifestIds, setUpdateManifestIds] = useState([]);
const [makeActive, setMakeActive] = useState(false);
const remove = async () => {
return new Promise((resolve) => {
const taskRunner = new TaskRunner(5, updateManifestIds.length);
let errorCount = 0;
const task = (id) => {
return async () => manifestsAPI.deleteManifest(id, token);
}
updateManifestIds.forEach((id) => taskRunner.push(task(id))
.then((response) => {
if (response.error) {
errorCount += 1;
}
})
);
taskRunner.onComplete().then((responses) => resolve({
summary: `${updateManifestIds.length - errorCount} out of ${updateManifestIds.length} manifests were deleted.`,
responses,
}));
});
}
const archive = async () => {
return manifestsAPI.archiveManifest({
ids: updateManifestIds,
active: makeActive,
}, token);
}
return {
updateManifestIds,
setUpdateManifestIds,
makeActive,
setMakeActive,
archive,
remove,
};
};

View File

@@ -5,6 +5,10 @@ const manifestsAPI = {
return data;
},
archiveManifest: async (data, token) => {
return { message: "Archived 1 update manifests" };
},
deleteManifest: async (manifest_id, token) => {
return { message: "OK" };
},

View File

@@ -112,6 +112,7 @@ const vehiclesAPI = {
vins.forEach((vin) => {
result[vin] = true;
result["2:" + vin] = false;
});
return result;
@@ -133,6 +134,7 @@ const vehiclesAPI = {
return {
'3FAFP13P31R199430': [[16.891136999999986, 26.832352999999955], [56.891136999999986, 66.832352999999955], [26.891136999999986, 36.832352999999955]],
'3FAFP13P71R199060': [[36.891136999999986, 46.832352999999955], [76.891136999999986, 16.832352999999955]],
'3FAFP13P61R199390': [],
};
},
getVehicle: async (vin) => {

View File

@@ -5,6 +5,18 @@ import {
const API_ENDPOINT = process.env.REACT_APP_OTA_SERVICE_URL;
const manifestsAPI = {
archiveManifest: async (data, token) =>
fetch(`${API_ENDPOINT}/vehicles/archive`, {
method: "PUT",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
body: JSON.stringify(data)
})
.then(fetchRespHandler)
.catch(errorHandler),
deleteManifest: async (manifest_id, token) =>
fetch(`${API_ENDPOINT}/manifest?id=${manifest_id}`, {
method: "DELETE",
@@ -80,7 +92,7 @@ const manifestsAPI = {
.catch(errorHandler),
migrateManifest: async (manifest_id, token) =>
fetch(`${API_ENDPOINT}/manifestmigrate/${manifest_id}`,{
fetch(`${API_ENDPOINT}/manifestmigrate/${manifest_id}`, {
method: "POST",
headers: Object.assign(
{ "Content-Type": "application/json" },

View File

@@ -19,7 +19,7 @@ const vehiclesAPI = {
addTags: async (vins, tags, token) =>
fetch(`${API_ENDPOINT}/tags`, {
method: "PUT",
method: "POST",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token),
@@ -173,6 +173,21 @@ const vehiclesAPI = {
.then(fetchRespHandler)
.catch(errorHandler),
sendDiagnosticCommand: async (vins, command, token) =>
fetch(`${API_ENDPOINT}/vehiclediagnosticcommand`, {
method: "POST",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
body: JSON.stringify({
vins,
...command,
}),
})
.then(fetchRespHandler)
.catch(errorHandler),
updateVehicle: async (vin, vehicle, token) =>
fetch(`${API_ENDPOINT}/vehicle/${vin}`, {
method: "PUT",

View File

@@ -1,2 +1,4 @@
export const TYPE_MANIFEST_SOFTWARE = 1;
export const TYPE_MANIFEST_CONFIG = 2;
export const TYPE_MANIFEST_MAGNA = 3;
export const TYPE_MANIFEST_AFTERSALES = 4;

View File

@@ -6,9 +6,11 @@ export const Roles = {
DELETE: process.env.REACT_APP_ROLE_DELETE,
CERTIFICATES: process.env.REACT_APP_ROLE_GENERATE_CERTIFICATE,
APPROVESUPPLIERS: process.env.REACT_APP_ROLE_SUPPLIER_APPROVER,
UPDATEDEPLOY: process.env.REACT_APP_ROLE_UPDATE_DEPLOY,
MANUFACTURE: process.env.REACT_APP_ROLE_MANUFACTURE,
MAGNAGROUP: process.env.REACT_APP_MAGNA_GROUP_ID,
MANIFEST_MIGRATION: process.env.REACT_APP_ROLE_MANIFEST_MIGRATION
MANIFEST_MIGRATION: process.env.REACT_APP_ROLE_MANIFEST_MIGRATION,
CAR_DIAGNOSTIC: process.env.REACT_APP_ROLE_CAR_DIAGNOSTIC
};
export const Providers = {
@@ -81,6 +83,9 @@ export const Permissions = {
[Providers.FISKER_QA]: [Roles.MANUFACTURE],
[Providers.MAGNA]: [Roles.MAGNAGROUP],
},
FiskerUpdateDeploy: {
[Providers.FISKER]: [Roles.UPDATEDEPLOY],
},
Magna: {
[Providers.FISKER_QA]: [Roles.MANUFACTURE],
[Providers.MAGNA]: [Roles.MAGNAGROUP],
@@ -97,5 +102,9 @@ export const Permissions = {
},
ManifestMigration: {
[Providers.FISKER]: [Roles.MANIFEST_MIGRATION]
},
CarDiagnostic: {
[Providers.FISKER]: [Roles.CAR_DIAGNOSTIC],
[Providers.FISKER_QA]: [Roles.CAR_DIAGNOSTIC],
}
};

View File

@@ -68,6 +68,15 @@ describe("Roles Helper", () => {
).toEqual(true);
});
it("Check FiskerUpdateDeploy permission", () => {
expect(
hasRole([Roles.UPDATEDEPLOY], Permissions.FiskerUpdateDeploy, [Providers.FISKER])
).toEqual(true);
expect(
hasRole([Roles.UPDATEDEPLOY], Permissions.FiskerUpdateDeploy, [Providers.MAGNA])
).toEqual(false);
});
it("Check Magna permission", () => {
expect(
hasRole([Roles.MAGNAGROUP], Permissions.Magna, [Providers.MAGNA])

View File

@@ -1,36 +1,67 @@
export default class TaskRunner {
constructor(concurrencyLimit = 1) {
this.queue = [];
this.running = 0;
this.concurrencyLimit = concurrencyLimit;
constructor(concurrencyLimit = 1, total) {
this._queue = [];
this._index = 0;
this._running = 0;
this._complete = 0;
this._concurrencyLimit = concurrencyLimit;
if (total) {
this._total = total;
this._responses = new Array(total);
}
this._onComplete = new Promise((resolve, reject) => {
this._onCompleteResolve = resolve;
this._onCompleteReject = reject;
});
}
execute() {
if (this.running >= this.concurrencyLimit || this.queue.length === 0) {
if (this._running >= this._concurrencyLimit || this._queue.length === 0) {
return;
}
const task = this.queue.shift();
this.running += 1;
task();
const task = this._queue.shift();
this._running += 1;
task(this._index);
this._index += 1;
}
async push(fn) {
return new Promise((resolve, reject) => {
const task = async () => {
const task = async (index) => {
try {
const result = await fn();
resolve(result);
const response = await fn();
if (this._responses) {
this._responses[index] = response;
}
resolve(response);
} catch (error) {
reject(error);
} finally {
this.running -= 1;
this._running -= 1;
this.#progress();
this.execute();
}
}
this.queue.push(task);
this._queue.push(task);
this.execute();
});
}
#progress() {
this._complete += 1;
if (this._complete === this._total) {
this._onCompleteResolve(this._responses);
}
}
async onComplete() {
if (!this._total) {
this._onCompleteReject(new Error("Total is required to determine onComplete."));
}
return this._onComplete;
}
}

View File

@@ -4,6 +4,10 @@ const mockPromise = async (id, ms) => {
await new Promise(resolve => setTimeout(resolve, ms));
return id;
}
const mockPromiseError = async (id, ms) => {
await new Promise(resolve => setTimeout(resolve, ms));
return new Error(`Task ${id} had an error`);
}
const asyncFn1 = () => mockPromise(1, 200);
const asyncFn2 = () => mockPromise(2, 100);
@@ -12,19 +16,19 @@ const asyncFn3 = () => mockPromise(3, 50);
describe("TaskRunner", () => {
it("runs task added to queue, when space available", () => {
const taskRunner = new TaskRunner(2);
expect(taskRunner.running).toEqual(0);
expect(taskRunner._running).toEqual(0);
taskRunner.push(() => mockPromise(1, 300));
expect(taskRunner.running).toEqual(1);
expect(taskRunner._running).toEqual(1);
});
it("keeps task in queue when at concurrency limit", () => {
const taskRunner = new TaskRunner(2);
expect(taskRunner.running).toEqual(0);
expect(taskRunner._running).toEqual(0);
taskRunner.push(() => mockPromise(1, 100));
taskRunner.push(() => mockPromise(2, 25));
taskRunner.push(() => mockPromise(3, 10));
expect(taskRunner.running).toEqual(2);
expect(taskRunner.queue.length).toEqual(1);
expect(taskRunner._running).toEqual(2);
expect(taskRunner._queue.length).toEqual(1);
});
it("runs queued tasks as space becomes available", async () => {
@@ -32,9 +36,9 @@ describe("TaskRunner", () => {
taskRunner.push(() => mockPromise(1, 600));
taskRunner.push(() => mockPromise(2, 300));
taskRunner.push(() => mockPromise(3, 100));
expect(taskRunner.queue.length).toEqual(1);
expect(taskRunner._queue.length).toEqual(1);
await new Promise(r => setTimeout(r, 301));
expect(taskRunner.queue.length).toEqual(0);
expect(taskRunner._queue.length).toEqual(0);
});
it("runs tasks in order", async () => {
@@ -55,4 +59,41 @@ describe("TaskRunner", () => {
await new Promise(resolve => setTimeout(resolve, 500));
expect(actual).toEqual([2, 3, 1]);
});
})
it("resolves a promise when all tasks are complete", async () => {
const taskRunner = new TaskRunner(2, 5);
taskRunner.push(() => mockPromise(1, 600));
taskRunner.push(() => mockPromise(2, 300));
taskRunner.push(() => mockPromise(3, 200));
taskRunner.push(() => mockPromise(4, 600));
taskRunner.push(() => mockPromise(5, 100));
await taskRunner.onComplete().then((actual) => {
expect(actual).toStrictEqual([1, 2, 3, 4, 5]);
});
});
it("resolves a promise when all tasks are complete, even if some fail", async () => {
const error = new Error(`Task 3 had an error`);
const taskRunner = new TaskRunner(2, 5);
taskRunner.push(() => mockPromise(1, 600));
taskRunner.push(() => mockPromise(2, 300));
taskRunner.push(() => mockPromiseError(3, 200));
taskRunner.push(() => mockPromise(4, 600));
taskRunner.push(() => mockPromise(5, 100));
await taskRunner.onComplete().then((actual) => {
expect(actual).toStrictEqual([1, 2, error, 4, 5]);
});
});
it("rejects a promise when the total number of tasks is unknown", async () => {
const taskRunner = new TaskRunner(2);
taskRunner.push(() => mockPromise(1, 600));
taskRunner.push(() => mockPromise(2, 300));
taskRunner.push(() => mockPromise(3, 200));
taskRunner.push(() => mockPromise(4, 600));
taskRunner.push(() => mockPromise(5, 100));
await taskRunner.onComplete().catch((error) => {
expect(error.message).toBe("Total is required to determine onComplete.");
});
});
});