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_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69
REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8 REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8
REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f 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_ECCKEY_ENV=
REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":49.8327,"lng":9.8816,"zoom":4.5} REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":49.8327,"lng":9.8816,"zoom":4.5}
REACT_APP_ENABLE_DEBUGMASK=1 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_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69
REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8 REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8
REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f 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_ECCKEY_ENV=
REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5} REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5}
REACT_APP_ENABLE_DEBUGMASK=1 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_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69
REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8 REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8
REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f 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_ECCKEY_ENV=stage,prod
REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5} REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5}
REACT_APP_ENABLE_DEBUGMASK=1 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_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69
REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8 REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8
REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f 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_ECCKEY_ENV=dev,stage,prod
REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5} REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5}
REACT_APP_ENABLE_DEBUGMASK=1 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_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69
REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8 REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8
REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f 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_ECCKEY_ENV=stage
REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5} REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5}
REACT_APP_ENABLE_DEBUGMASK=1 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_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69
REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8 REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8
REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f 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_ECCKEY_ENV=prod
REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5} REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5}
REACT_APP_ENABLE_DEBUGMASK=1 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_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69
REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8 REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8
REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f 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_ECCKEY_ENV=dev,stage,prod
REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5} 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-dom": "^5.3.0",
"react-router-hash-link": "^2.4.3", "react-router-hash-link": "^2.4.3",
"react-scripts": "5.0.0", "react-scripts": "5.0.0",
"semver-compare": "^1.0.0",
"usehooks-ts": "^2.7.1", "usehooks-ts": "^2.7.1",
"web-vitals": "^2.1.4", "web-vitals": "^2.1.4",
"webpack": "^5.74.0" "webpack": "^5.74.0"
@@ -15130,6 +15131,11 @@
"node": ">=10" "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": { "node_modules/send": {
"version": "0.17.2", "version": "0.17.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
@@ -28020,6 +28026,11 @@
"lru-cache": "^6.0.0" "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": { "send": {
"version": "0.17.2", "version": "0.17.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", "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-dom": "^5.3.0",
"react-router-hash-link": "^2.4.3", "react-router-hash-link": "^2.4.3",
"react-scripts": "5.0.0", "react-scripts": "5.0.0",
"semver-compare": "^1.0.0",
"usehooks-ts": "^2.7.1", "usehooks-ts": "^2.7.1",
"web-vitals": "^2.1.4", "web-vitals": "^2.1.4",
"webpack": "^5.74.0" "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" class="MuiButtonBase-root MuiToggleButton-root Mui-selected MuiToggleButton-sizeMedium MuiToggleButton-standard MuiToggleButtonGroup-grouped MuiToggleButtonGroup-groupedHorizontal css-ueukts-MuiButtonBase-root-MuiToggleButton-root"
tabindex="0" tabindex="0"
type="button" type="button"
value="true" value="software"
> >
Active Software
<span <span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root" 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" class="MuiButtonBase-root MuiToggleButton-root MuiToggleButton-sizeMedium MuiToggleButton-standard MuiToggleButtonGroup-grouped MuiToggleButtonGroup-groupedHorizontal css-ueukts-MuiButtonBase-root-MuiToggleButton-root"
tabindex="0" tabindex="0"
type="button" type="button"
value="false" value="archived"
> >
Archived Archived
<span <span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root" class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/> />
</button> </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> </div>
<div <div
class="MuiGrid-root makeStyles-textRightAlign-0 MuiGrid-item MuiGrid-grid-md-4" 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> </div>
<table <table
class="MuiTable-root" class="MuiTable-root"
@@ -6295,6 +6344,40 @@ exports[`App Route /packages authenticated 1`] = `
<tr <tr
class="MuiTableRow-root MuiTableRow-head" 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 <th
aria-sort="ascending" aria-sort="ascending"
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter" class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
@@ -6370,6 +6453,29 @@ exports[`App Route /packages authenticated 1`] = `
</svg> </svg>
</span> </span>
</th> </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 <th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter" class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col" scope="col"
@@ -6403,7 +6509,7 @@ exports[`App Route /packages authenticated 1`] = `
role="button" role="button"
tabindex="0" tabindex="0"
> >
Type Update
<svg <svg
aria-hidden="true" aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc" class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
@@ -6476,6 +6582,38 @@ exports[`App Route /packages authenticated 1`] = `
<tr <tr
class="MuiTableRow-root" 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 <td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter" class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
> >
@@ -6494,6 +6632,9 @@ exports[`App Route /packages authenticated 1`] = `
<td <td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter" class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
/> />
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
/>
<td <td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter" class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
> >
@@ -6548,24 +6689,6 @@ exports[`App Route /packages authenticated 1`] = `
/> />
</svg> </svg>
</a> </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> </td>
</tr> </tr>
</tbody> </tbody>
@@ -6577,7 +6700,7 @@ exports[`App Route /packages authenticated 1`] = `
> >
<td <td
class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root" class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root"
colspan="8" colspan="10"
> >
<div <div
class="MuiToolbar-root MuiToolbar-regular MuiTablePagination-toolbar MuiToolbar-gutters" class="MuiToolbar-root MuiToolbar-regular MuiTablePagination-toolbar MuiToolbar-gutters"
@@ -6696,6 +6819,7 @@ exports[`App Route /packages authenticated 1`] = `
</tfoot> </tfoot>
</table> </table>
<div /> <div />
<div />
</div> </div>
</div> </div>
</main> </main>
@@ -11427,6 +11551,13 @@ exports[`App Route /vehicle-status authenticated 1`] = `
: :
false false
</p> </p>
<p>
<b>
DLT Logging Enabled
</b>
:
false
</p>
</div> </div>
<div <div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12" 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>
<div <div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12" 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 <div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12" 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 false
</p> </p>
<p>
<b>
DLT Logging Enabled
</b>
:
false
</p>
</div> </div>
<div <div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12" class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"
@@ -160,63 +167,7 @@ exports[`VehicleDetailsTab Render 1`] = `
</div> </div>
<div <div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12" 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 <div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12" class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"
> >

View File

@@ -115,38 +115,41 @@ const MainForm = ({ vin }) => {
<b>Info Source</b>: {vehicle.info_source} <b>Info Source</b>: {vehicle.info_source}
</p> </p>
<p> <p>
<b>Tags</b>: {vehicle.tags ? vehicle.tags.join(", ") : "none" } <b>Tags</b>: {vehicle.tags ? vehicle.tags.join(", ") : "none"}
</p> </p>
</Grid> </Grid>
<Grid item md={12} className={classes.textCenterAlign}> <Grid item md={12} className={classes.textCenterAlign}>
{vehicle.log_level != null && ( {vehicle.log_level != null && (
<p> <p>
<b>Log Level</b>: {vehicle.log_level} <b>Log Level</b>: {vehicle.log_level}
</p> </p>
)} )}
{vehicle.canbus && ( {vehicle.canbus && (
<> <>
<p> <p>
<b>CANBus Enabled</b>: {vehicle.canbus.enabled.toString()} <b>CANBus Enabled</b>: {vehicle.canbus.enabled.toString()}
</p> </p>
<p> <p>
<b>Max Memory Buffer Size</b>: {vehicle.canbus.max_mem_buffer_size ?? "Default"} <b>Max Memory Buffer Size</b>: {vehicle.canbus.max_mem_buffer_size ?? "Default"}
</p> </p>
<p> <p>
<b>Data Logger Enabled</b>: {vehicle.canbus.data_logger_enabled.toString()} <b>Data Logger Enabled</b>: {vehicle.canbus.data_logger_enabled.toString()}
</p> </p>
<p> <p>
<b>Max Disk Buffer Size</b>: {vehicle.canbus.max_disk_buffer_size ?? "Default"} <b>Max Disk Buffer Size</b>: {vehicle.canbus.max_disk_buffer_size ?? "Default"}
</p> </p>
<p> <p>
<b>Filters</b>: {vehicle.canbus.filters ? vehicle.canbus.filters.length : 0} <b>Filters</b>: {vehicle.canbus.filters ? vehicle.canbus.filters.length : 0}
</p> </p>
<p> <p>
<b>DTC Enabled</b>: { (vehicle.canbus.dtc_enabled || false).toString() } <b>DTC Enabled</b>: {(vehicle.canbus.dtc_enabled || false).toString()}
</p> </p>
</> <p>
)} <b>DLT Logging Enabled</b>: {(vehicle.dlt_enabled || false).toString()}
</Grid> </p>
</>
)}
</Grid>
{showDebugMask && ( {showDebugMask && (
<Grid item md={12} className={classes.textCenterAlign}> <Grid item md={12} className={classes.textCenterAlign}>
<p> <p>
@@ -156,19 +159,19 @@ const MainForm = ({ vin }) => {
)} )}
<Grid item md={12} className={classes.textCenterAlign}> <Grid item md={12} className={classes.textCenterAlign}>
<RoleWrap <RoleWrap
groups={groups} groups={groups}
providers={providers} providers={providers}
rolesPerProvider={Permissions.FiskerCreate} rolesPerProvider={Permissions.FiskerUpdateDeploy}
> >
<FormControlLabel <FormControlLabel
label="Force Config Update" label="Force Config Update"
control={ control={
<Checkbox <Checkbox
checked={forced} checked={forced}
onChange={onForcedChange} onChange={onForcedChange}
/>
}
/> />
}
/>
<Tooltip key={`push-config-${vin}`} title={`Push Config Update to "${vin}"`}> <Tooltip key={`push-config-${vin}`} title={`Push Config Update to "${vin}"`}>
<Link to="#" onClick={() => setShowUploadConfigModal(true)} > <Link to="#" onClick={() => setShowUploadConfigModal(true)} >
<UploadIcon aria-label={`Push Config Update to "${vin}"`} fontSize="large" /> <UploadIcon aria-label={`Push Config Update to "${vin}"`} fontSize="large" />

View File

@@ -2,24 +2,25 @@ jest.mock("../../../Contexts/VehicleContext");
jest.mock("../../../Contexts/StatusContext"); jest.mock("../../../Contexts/StatusContext");
jest.mock("../../../Contexts/UserContext"); 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 { BrowserRouter } from "react-router-dom";
import routeData from "react-router"; import routeData from "react-router";
import { VehicleProvider } from "../../../Contexts/VehicleContext"; import { VehicleProvider } from "../../../Contexts/VehicleContext";
import { StatusProvider } from "../../../Contexts/StatusContext"; import { StatusProvider } from "../../../Contexts/StatusContext";
import { UserProvider, setToken } from "../../../Contexts/UserContext"; 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 MainForm from "./index";
import addSnapshotSerializer from "../../../../utils/snapshot"; import addSnapshotSerializer from "../../../../utils/snapshot";
import * as Roles from "../../../../utils/roles";
const renderVehicleDetailsTab = async () => { const renderVehicleDetailsTab = async () => {
const { container } = render( const { container } = render(
<VehicleProvider> <VehicleProvider>
<StatusProvider> <StatusProvider>
<UserProvider> <UserProvider >
<BrowserRouter> <BrowserRouter>
<MainForm vin="TESTVIN1234567890"/> <MainForm vin="TESTVIN1234567890" />
</BrowserRouter> </BrowserRouter>
</UserProvider> </UserProvider>
</StatusProvider> </StatusProvider>
@@ -46,4 +47,23 @@ describe("VehicleDetailsTab", () => {
const container = await renderVehicleDetailsTab(); const container = await renderVehicleDetailsTab();
expect(container).toMatchSnapshot(); 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 false
</p> </p>
<p>
<b>
DLT Logging Enabled
</b>
:
false
</p>
</div> </div>
<div <div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12" class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"
@@ -168,63 +175,7 @@ exports[`DetailsTab Render 1`] = `
</div> </div>
<div <div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12" 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 <div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12" 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 77.7 km/h
</p> </p>
</div> </div>
<div
class="makeStyles-popupSection-0"
>
<p>
<b>
Parked
</b>
:
Yes
</p>
</div>
</div> </div>
<div <div
style="width: 100vh;" style="width: 100vh;"

View File

@@ -331,63 +331,7 @@ exports[`CarStatus Render 1`] = `
</div> </div>
<div <div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12" 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 <div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12" 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 ECUsTab from "./ECUsTab";
import FleetsTab from "./FleetsTab"; import FleetsTab from "./FleetsTab";
import RemoteCommandsTab from "./RemoteCommandsTab"; import RemoteCommandsTab from "./RemoteCommandsTab";
import RemoteDiagnosticCommandsTab from "./RemoteDiagnosticCommands";
import TRexLogsTab from "./TRexLogsTab"; import TRexLogsTab from "./TRexLogsTab";
const tabHashes = ["details", "updates", "filters"]; const tabHashes = ["details", "updates", "filters"];
@@ -63,6 +64,11 @@ const TabViews = [
component: RemoteCommandsTab, component: RemoteCommandsTab,
rolesPerProvider: Permissions.FiskerMagnaCreate, rolesPerProvider: Permissions.FiskerMagnaCreate,
}, },
{
label: "Remote Diagnostic Commands",
component: RemoteDiagnosticCommandsTab,
rolesPerProvider: Permissions.CarDiagnostic,
},
{ {
label: "Fleets", label: "Fleets",
component: FleetsTab, component: FleetsTab,

View File

@@ -1006,6 +1006,43 @@ exports[`VehicleUpdate Render 1`] = `
DTC Enabled DTC Enabled
</span> </span>
</label> </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 <div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth" class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
> >

View File

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

View File

@@ -1,5 +1,6 @@
import React, { useContext, useState } from "react"; import React, { useContext, useState } from "react";
import api from "../../services/fleetsAPI"; import api from "../../services/fleetsAPI";
import vehiclesAPI from "../../services/vehiclesAPI";
import { validateCANID, validateFilter, validateVIN } from "../../utils/validationSupplier"; import { validateCANID, validateFilter, validateVIN } from "../../utils/validationSupplier";
const FleetContext = React.createContext(); const FleetContext = React.createContext();
@@ -112,7 +113,22 @@ export const FleetProvider = ({ children }) => {
throw new Error(`Get fleet vehicles error. ${result.message}`); 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) { if (result.total) {
setTotalFleetVehicles(result.total); setTotalFleetVehicles(result.total);
} }

View File

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

View File

@@ -71,7 +71,7 @@ export const VehicleProvider = ({ children }) => {
const result = await api.addTags(vins, tags, token) const result = await api.addTags(vins, tags, token)
if (result.error) if (result.error)
throw new Error(`Add tags error. ${result.message}`); throw new Error(`Add tags error. ${result.message}`);
} finally { } finally {
setBusy(false) setBusy(false)
} }
@@ -105,7 +105,7 @@ export const VehicleProvider = ({ children }) => {
setBusy(true); setBusy(true);
const result = await api.getLocations(token); const result = await api.getLocations(token);
if (result.error) if (result.error)
throw new Error(`Get locations vehicle paths error. ${result.message}`); throw new Error(`Get locations error. ${result.message}`);
return result; return result;
} finally { } finally {
setBusy(false); 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) => { const updateVehicle = async (vin, v, token) => {
try { try {
setBusy(true); setBusy(true);
@@ -313,6 +325,7 @@ export const VehicleProvider = ({ children }) => {
getVehicle, getVehicle,
getVehicles, getVehicles,
sendCommand, sendCommand,
sendDiagnosticCommand,
updateVehicle, updateVehicle,
getFleets, getFleets,
getVersionLog, getVersionLog,

View File

@@ -62,7 +62,26 @@ export const useFleetContext = () => ({
fleetVehicles, fleetVehicles,
totalFleetVehicles, 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(), addFleetVehicles: jest.fn(),
deleteFleetVehicle: jest.fn(), deleteFleetVehicle: jest.fn(),

View File

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

View File

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

View File

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

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 UNKNOWN = "unknown";
const LOCKED = "Locked"; const LOCKED = "Locked";
const UNLOCKED = "Unlocked"; const UNLOCKED = "Unlocked";
const PARKED = "Yes";
const NOT_PARKED = "Not Parked";
const appendUnits = (value, units) => { const appendUnits = (value, units) => {
if (value || value === 0) return `${value}${units}`; if (value || value === 0) return `${value}${units}`;
@@ -32,7 +34,7 @@ const windowState = (value) => {
const DigitalTwin = (props) => { const DigitalTwin = (props) => {
const classes = useStyles(); 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 ( return (
<div> <div>
@@ -133,6 +135,11 @@ const DigitalTwin = (props) => {
{keyValueTemplate("Vehicle Speed", appendUnits(vehicle_speed?.speed, " km/h"))} {keyValueTemplate("Vehicle Speed", appendUnits(vehicle_speed?.speed, " km/h"))}
</div> </div>
)} )}
{gear && (
<div className={classes.popupSection}>
{keyValueTemplate("Parked", gear.in_park ? PARKED : NOT_PARKED)}
</div>
)}
</div> </div>
); );
}; };

View File

@@ -143,6 +143,48 @@ exports[`FleetDetailsTab Render 1`] = `
</svg> </svg>
</a> </a>
</div> </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 /> <div />
</div> </div>

View File

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

View File

@@ -137,62 +137,7 @@ exports[`FleetVehiclesTable Render 1`] = `
</thead> </thead>
<tbody <tbody
class="MuiTableBody-root" 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 <tfoot
class="MuiTableFooter-root" class="MuiTableFooter-root"
> >

View File

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

View File

@@ -151,6 +151,48 @@ exports[`DetailsTab Render 1`] = `
</svg> </svg>
</a> </a>
</div> </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 /> <div />
</div> </div>

View File

@@ -136,62 +136,7 @@ exports[`VehiclesTab Render 1`] = `
</thead> </thead>
<tbody <tbody
class="MuiTableBody-root" 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 <tfoot
class="MuiTableFooter-root" class="MuiTableFooter-root"
> >

View File

@@ -239,6 +239,48 @@ exports[`FleetStatus Render 1`] = `
</svg> </svg>
</a> </a>
</div> </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 /> <div />
</div> </div>

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,22 @@
import { import {
addQueryParams, errorHandler, fetchRespHandler, getAuthHeaderOptions addQueryParams, errorHandler, fetchRespHandler, getAuthHeaderOptions
} from "../utils/http"; } from "../utils/http";
const API_ENDPOINT = process.env.REACT_APP_OTA_SERVICE_URL; const API_ENDPOINT = process.env.REACT_APP_OTA_SERVICE_URL;
const manifestsAPI = { 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) => deleteManifest: async (manifest_id, token) =>
fetch(`${API_ENDPOINT}/manifest?id=${manifest_id}`, { fetch(`${API_ENDPOINT}/manifest?id=${manifest_id}`, {
method: "DELETE", method: "DELETE",
@@ -79,8 +91,8 @@ const manifestsAPI = {
.then(fetchRespHandler) .then(fetchRespHandler)
.catch(errorHandler), .catch(errorHandler),
migrateManifest: async (manifest_id, token) => migrateManifest: async (manifest_id, token) =>
fetch(`${API_ENDPOINT}/manifestmigrate/${manifest_id}`,{ fetch(`${API_ENDPOINT}/manifestmigrate/${manifest_id}`, {
method: "POST", method: "POST",
headers: Object.assign( headers: Object.assign(
{ "Content-Type": "application/json" }, { "Content-Type": "application/json" },

View File

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

View File

@@ -1,2 +1,4 @@
export const TYPE_MANIFEST_SOFTWARE = 1; export const TYPE_MANIFEST_SOFTWARE = 1;
export const TYPE_MANIFEST_CONFIG = 2; 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, DELETE: process.env.REACT_APP_ROLE_DELETE,
CERTIFICATES: process.env.REACT_APP_ROLE_GENERATE_CERTIFICATE, CERTIFICATES: process.env.REACT_APP_ROLE_GENERATE_CERTIFICATE,
APPROVESUPPLIERS: process.env.REACT_APP_ROLE_SUPPLIER_APPROVER, APPROVESUPPLIERS: process.env.REACT_APP_ROLE_SUPPLIER_APPROVER,
UPDATEDEPLOY: process.env.REACT_APP_ROLE_UPDATE_DEPLOY,
MANUFACTURE: process.env.REACT_APP_ROLE_MANUFACTURE, MANUFACTURE: process.env.REACT_APP_ROLE_MANUFACTURE,
MAGNAGROUP: process.env.REACT_APP_MAGNA_GROUP_ID, 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 = { export const Providers = {
@@ -81,6 +83,9 @@ export const Permissions = {
[Providers.FISKER_QA]: [Roles.MANUFACTURE], [Providers.FISKER_QA]: [Roles.MANUFACTURE],
[Providers.MAGNA]: [Roles.MAGNAGROUP], [Providers.MAGNA]: [Roles.MAGNAGROUP],
}, },
FiskerUpdateDeploy: {
[Providers.FISKER]: [Roles.UPDATEDEPLOY],
},
Magna: { Magna: {
[Providers.FISKER_QA]: [Roles.MANUFACTURE], [Providers.FISKER_QA]: [Roles.MANUFACTURE],
[Providers.MAGNA]: [Roles.MAGNAGROUP], [Providers.MAGNA]: [Roles.MAGNAGROUP],
@@ -97,5 +102,9 @@ export const Permissions = {
}, },
ManifestMigration: { ManifestMigration: {
[Providers.FISKER]: [Roles.MANIFEST_MIGRATION] [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); ).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", () => { it("Check Magna permission", () => {
expect( expect(
hasRole([Roles.MAGNAGROUP], Permissions.Magna, [Providers.MAGNA]) hasRole([Roles.MAGNAGROUP], Permissions.Magna, [Providers.MAGNA])

View File

@@ -1,36 +1,67 @@
export default class TaskRunner { export default class TaskRunner {
constructor(concurrencyLimit = 1) { constructor(concurrencyLimit = 1, total) {
this.queue = []; this._queue = [];
this.running = 0; this._index = 0;
this.concurrencyLimit = concurrencyLimit; 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() { execute() {
if (this.running >= this.concurrencyLimit || this.queue.length === 0) { if (this._running >= this._concurrencyLimit || this._queue.length === 0) {
return; return;
} }
const task = this.queue.shift(); const task = this._queue.shift();
this.running += 1; this._running += 1;
task(); task(this._index);
this._index += 1;
} }
async push(fn) { async push(fn) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const task = async () => { const task = async (index) => {
try { try {
const result = await fn(); const response = await fn();
resolve(result); if (this._responses) {
this._responses[index] = response;
}
resolve(response);
} catch (error) { } catch (error) {
reject(error); reject(error);
} finally { } finally {
this.running -= 1; this._running -= 1;
this.#progress();
this.execute(); this.execute();
} }
} }
this.queue.push(task); this._queue.push(task);
this.execute(); 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)); await new Promise(resolve => setTimeout(resolve, ms));
return id; 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 asyncFn1 = () => mockPromise(1, 200);
const asyncFn2 = () => mockPromise(2, 100); const asyncFn2 = () => mockPromise(2, 100);
@@ -12,19 +16,19 @@ const asyncFn3 = () => mockPromise(3, 50);
describe("TaskRunner", () => { describe("TaskRunner", () => {
it("runs task added to queue, when space available", () => { it("runs task added to queue, when space available", () => {
const taskRunner = new TaskRunner(2); const taskRunner = new TaskRunner(2);
expect(taskRunner.running).toEqual(0); expect(taskRunner._running).toEqual(0);
taskRunner.push(() => mockPromise(1, 300)); taskRunner.push(() => mockPromise(1, 300));
expect(taskRunner.running).toEqual(1); expect(taskRunner._running).toEqual(1);
}); });
it("keeps task in queue when at concurrency limit", () => { it("keeps task in queue when at concurrency limit", () => {
const taskRunner = new TaskRunner(2); const taskRunner = new TaskRunner(2);
expect(taskRunner.running).toEqual(0); expect(taskRunner._running).toEqual(0);
taskRunner.push(() => mockPromise(1, 100)); taskRunner.push(() => mockPromise(1, 100));
taskRunner.push(() => mockPromise(2, 25)); taskRunner.push(() => mockPromise(2, 25));
taskRunner.push(() => mockPromise(3, 10)); taskRunner.push(() => mockPromise(3, 10));
expect(taskRunner.running).toEqual(2); expect(taskRunner._running).toEqual(2);
expect(taskRunner.queue.length).toEqual(1); expect(taskRunner._queue.length).toEqual(1);
}); });
it("runs queued tasks as space becomes available", async () => { it("runs queued tasks as space becomes available", async () => {
@@ -32,9 +36,9 @@ describe("TaskRunner", () => {
taskRunner.push(() => mockPromise(1, 600)); taskRunner.push(() => mockPromise(1, 600));
taskRunner.push(() => mockPromise(2, 300)); taskRunner.push(() => mockPromise(2, 300));
taskRunner.push(() => mockPromise(3, 100)); taskRunner.push(() => mockPromise(3, 100));
expect(taskRunner.queue.length).toEqual(1); expect(taskRunner._queue.length).toEqual(1);
await new Promise(r => setTimeout(r, 301)); 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 () => { it("runs tasks in order", async () => {
@@ -52,7 +56,44 @@ describe("TaskRunner", () => {
.then((id) => { .then((id) => {
actual.push(id); actual.push(id);
}); });
await new Promise(resolve => setTimeout(resolve, 500)); await new Promise(resolve => setTimeout(resolve, 500));
expect(actual).toEqual([2, 3, 1]); 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.");
});
});
});