Merge branch 'release/0.9.0'
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
2
.env.dev
2
.env.dev
@@ -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
|
||||
@@ -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
|
||||
2
.env.prd
2
.env.prd
@@ -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
|
||||
2
.env.stg
2
.env.stg
@@ -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
|
||||
@@ -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
11
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
84
src/components/BulkActions/index.jsx
Normal file
84
src/components/BulkActions/index.jsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
22
src/components/BulkActions/useAddTags.js
Normal file
22
src/components/BulkActions/useAddTags.js
Normal 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,
|
||||
};
|
||||
}
|
||||
40
src/components/BulkActions/useUpdateConfig.js
Normal file
40
src/components/BulkActions/useUpdateConfig.js
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
})
|
||||
});
|
||||
|
||||
30
src/components/Cars/Status/RemoteDiagnosticCommands.jsx
Normal file
30
src/components/Cars/Status/RemoteDiagnosticCommands.jsx
Normal 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
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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;"
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -40,6 +40,10 @@ const DropDownButton = ({ actions = [], payload = [] }) => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
if (!actions.length) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonGroup
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
135
src/components/Controls/SendDiagnosticCommand/index.jsx
Normal file
135
src/components/Controls/SendDiagnosticCommand/index.jsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 >
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
35
src/components/Manifest/List/index.test.jsx
Normal file
35
src/components/Manifest/List/index.test.jsx
Normal 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");
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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
1
src/hooks/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { useUpdateManifest } from "./useUpdateManifest";
|
||||
47
src/hooks/useUpdateManifest.js
Normal file
47
src/hooks/useUpdateManifest.js
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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" };
|
||||
},
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user