Merge branch 'release/0.0.3'
This commit is contained in:
52
package-lock.json
generated
52
package-lock.json
generated
@@ -27,6 +27,8 @@
|
|||||||
"date-fns": "^2.29.2",
|
"date-fns": "^2.29.2",
|
||||||
"email-validator": "^2.0.4",
|
"email-validator": "^2.0.4",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"filesaver": "^0.0.13",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"leaflet": "^1.8.0",
|
"leaflet": "^1.8.0",
|
||||||
"material-ui-dropzone": "^3.5.0",
|
"material-ui-dropzone": "^3.5.0",
|
||||||
@@ -6941,9 +6943,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001387",
|
"version": "1.0.30001458",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001387.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001458.tgz",
|
||||||
"integrity": "sha512-fKDH0F1KOJvR+mWSOvhj8lVRr/Q/mc5u5nabU2vi1/sgvlSqEsE8dOq0Hy/BqVbDkCYQPRRHB1WRjW6PGB/7PA==",
|
"integrity": "sha512-lQ1VlUUq5q9ro9X+5gOEyH7i3vm+AYVT1WDCVB69XOZ17KZRhnZ9J0Sqz7wTHQaLBJccNCHq8/Ww5LlOIZbB0w==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -9424,6 +9426,11 @@
|
|||||||
"webpack": "^4.0.0 || ^5.0.0"
|
"webpack": "^4.0.0 || ^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/file-saver": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
||||||
|
},
|
||||||
"node_modules/file-selector": {
|
"node_modules/file-selector": {
|
||||||
"version": "0.1.19",
|
"version": "0.1.19",
|
||||||
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.19.tgz",
|
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.19.tgz",
|
||||||
@@ -9443,6 +9450,15 @@
|
|||||||
"minimatch": "^3.0.4"
|
"minimatch": "^3.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/filesaver": {
|
||||||
|
"version": "0.0.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/filesaver/-/filesaver-0.0.13.tgz",
|
||||||
|
"integrity": "sha512-ay2iShYJKmzKRPk89cgb14foqtCXcJIe5i+qdlSPAouKfBv7F2VZ0lxk9GjpcODe9p2YrXfi3Q+4CRn7ZDmleQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"mkdirp": "^0.5.0",
|
||||||
|
"safename": "0.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/filesize": {
|
"node_modules/filesize": {
|
||||||
"version": "8.0.7",
|
"version": "8.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz",
|
||||||
@@ -15612,6 +15628,11 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safename": {
|
||||||
|
"version": "0.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/safename/-/safename-0.0.4.tgz",
|
||||||
|
"integrity": "sha512-+n4TsvESZKTXbHxOTSyQ0Q1JCXRb6MohgrqC2fbdALzTNQP/IhPOnCNRA4JPtagQq+6DD5ZsQ3lKMy57BYvwJA=="
|
||||||
|
},
|
||||||
"node_modules/safer-buffer": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
@@ -22881,9 +22902,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"caniuse-lite": {
|
"caniuse-lite": {
|
||||||
"version": "1.0.30001387",
|
"version": "1.0.30001458",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001387.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001458.tgz",
|
||||||
"integrity": "sha512-fKDH0F1KOJvR+mWSOvhj8lVRr/Q/mc5u5nabU2vi1/sgvlSqEsE8dOq0Hy/BqVbDkCYQPRRHB1WRjW6PGB/7PA=="
|
"integrity": "sha512-lQ1VlUUq5q9ro9X+5gOEyH7i3vm+AYVT1WDCVB69XOZ17KZRhnZ9J0Sqz7wTHQaLBJccNCHq8/Ww5LlOIZbB0w=="
|
||||||
},
|
},
|
||||||
"case-sensitive-paths-webpack-plugin": {
|
"case-sensitive-paths-webpack-plugin": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
@@ -24710,6 +24731,11 @@
|
|||||||
"schema-utils": "^3.0.0"
|
"schema-utils": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"file-saver": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
||||||
|
},
|
||||||
"file-selector": {
|
"file-selector": {
|
||||||
"version": "0.1.19",
|
"version": "0.1.19",
|
||||||
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.19.tgz",
|
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.19.tgz",
|
||||||
@@ -24726,6 +24752,15 @@
|
|||||||
"minimatch": "^3.0.4"
|
"minimatch": "^3.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"filesaver": {
|
||||||
|
"version": "0.0.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/filesaver/-/filesaver-0.0.13.tgz",
|
||||||
|
"integrity": "sha512-ay2iShYJKmzKRPk89cgb14foqtCXcJIe5i+qdlSPAouKfBv7F2VZ0lxk9GjpcODe9p2YrXfi3Q+4CRn7ZDmleQ==",
|
||||||
|
"requires": {
|
||||||
|
"mkdirp": "^0.5.0",
|
||||||
|
"safename": "0.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"filesize": {
|
"filesize": {
|
||||||
"version": "8.0.7",
|
"version": "8.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz",
|
||||||
@@ -29080,6 +29115,11 @@
|
|||||||
"is-regex": "^1.1.4"
|
"is-regex": "^1.1.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"safename": {
|
||||||
|
"version": "0.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/safename/-/safename-0.0.4.tgz",
|
||||||
|
"integrity": "sha512-+n4TsvESZKTXbHxOTSyQ0Q1JCXRb6MohgrqC2fbdALzTNQP/IhPOnCNRA4JPtagQq+6DD5ZsQ3lKMy57BYvwJA=="
|
||||||
|
},
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
"date-fns": "^2.29.2",
|
"date-fns": "^2.29.2",
|
||||||
"email-validator": "^2.0.4",
|
"email-validator": "^2.0.4",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"filesaver": "^0.0.13",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"leaflet": "^1.8.0",
|
"leaflet": "^1.8.0",
|
||||||
"material-ui-dropzone": "^3.5.0",
|
"material-ui-dropzone": "^3.5.0",
|
||||||
|
|||||||
@@ -9851,7 +9851,7 @@ exports[`App Route /vehicle-status authenticated 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="MuiTab-wrapper"
|
class="MuiTab-wrapper"
|
||||||
>
|
>
|
||||||
ECUs
|
T.Rex logs
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="MuiTouchRipple-root"
|
class="MuiTouchRipple-root"
|
||||||
@@ -9869,7 +9869,7 @@ exports[`App Route /vehicle-status authenticated 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="MuiTab-wrapper"
|
class="MuiTab-wrapper"
|
||||||
>
|
>
|
||||||
Remote Commands
|
ECUs
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="MuiTouchRipple-root"
|
class="MuiTouchRipple-root"
|
||||||
@@ -9883,6 +9883,24 @@ exports[`App Route /vehicle-status authenticated 1`] = `
|
|||||||
role="tab"
|
role="tab"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
type="button"
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiTab-wrapper"
|
||||||
|
>
|
||||||
|
Remote Commands
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-controls="tabpanel-8"
|
||||||
|
aria-selected="false"
|
||||||
|
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit"
|
||||||
|
id="tab-8"
|
||||||
|
role="tab"
|
||||||
|
tabindex="-1"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="MuiTab-wrapper"
|
class="MuiTab-wrapper"
|
||||||
@@ -9893,6 +9911,24 @@ exports[`App Route /vehicle-status authenticated 1`] = `
|
|||||||
class="MuiTouchRipple-root"
|
class="MuiTouchRipple-root"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
aria-controls="tabpanel-9"
|
||||||
|
aria-selected="false"
|
||||||
|
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit"
|
||||||
|
id="tab-9"
|
||||||
|
role="tab"
|
||||||
|
tabindex="-1"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiTab-wrapper"
|
||||||
|
>
|
||||||
|
CAN Signal Export
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="PrivateTabIndicator-root-0 PrivateTabIndicator-colorSecondary-0 MuiTabs-indicator"
|
class="PrivateTabIndicator-root-0 PrivateTabIndicator-colorSecondary-0 MuiTabs-indicator"
|
||||||
@@ -10124,6 +10160,18 @@ exports[`App Route /vehicle-status authenticated 1`] = `
|
|||||||
id="tabpanel-7"
|
id="tabpanel-7"
|
||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
aria-labelledby="tab-8"
|
||||||
|
hidden=""
|
||||||
|
id="tabpanel-8"
|
||||||
|
role="tabpanel"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-labelledby="tab-9"
|
||||||
|
hidden=""
|
||||||
|
id="tabpanel-9"
|
||||||
|
role="tabpanel"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -0,0 +1,374 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render Render 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
data-testid="mocked-statusprovider"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-testid="mocked-userprovider"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="makeStyles-paper-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiGrid-root MuiGrid-container MuiGrid-spacing-xs-3 MuiGrid-justify-content-xs-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiGrid-root MuiGrid-container MuiGrid-justify-content-xs-space-between"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-6 MuiGrid-grid-md-3"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiFormLabel-filled Mui-required Mui-required"
|
||||||
|
data-shrink="true"
|
||||||
|
for="date-picker-inline"
|
||||||
|
id="date-picker-inline-label"
|
||||||
|
>
|
||||||
|
Date From
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||||
|
>
|
||||||
|
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl MuiInputBase-adornedEnd"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-invalid="false"
|
||||||
|
class="MuiInputBase-input MuiInput-input MuiInputBase-inputAdornedEnd"
|
||||||
|
id="date-picker-inline"
|
||||||
|
required=""
|
||||||
|
type="text"
|
||||||
|
value="03/31/2023"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="MuiInputAdornment-root MuiInputAdornment-positionEnd"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label="change date"
|
||||||
|
class="MuiButtonBase-root MuiIconButton-root"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiIconButton-label"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSvgIcon-root"
|
||||||
|
focusable="false"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M17 12h-5v5h5v-5zM16 1v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2h-1V1h-2zm3 18H5V8h14v11z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M0 0h24v24H0z"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-6 MuiGrid-grid-md-3"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiFormLabel-filled Mui-required Mui-required"
|
||||||
|
data-shrink="true"
|
||||||
|
for="time-picker"
|
||||||
|
id="time-picker-label"
|
||||||
|
>
|
||||||
|
Time From
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||||
|
>
|
||||||
|
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl MuiInputBase-adornedEnd"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-invalid="false"
|
||||||
|
class="MuiInputBase-input MuiInput-input MuiInputBase-inputAdornedEnd"
|
||||||
|
id="time-picker"
|
||||||
|
required=""
|
||||||
|
type="text"
|
||||||
|
value="06:30 AM"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="MuiInputAdornment-root MuiInputAdornment-positionEnd"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label="change time"
|
||||||
|
class="MuiButtonBase-root MuiIconButton-root"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiIconButton-label"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSvgIcon-root"
|
||||||
|
focusable="false"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M17 12h-5v5h5v-5zM16 1v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2h-1V1h-2zm3 18H5V8h14v11z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M0 0h24v24H0z"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-6 MuiGrid-grid-md-3"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiFormLabel-filled Mui-required Mui-required"
|
||||||
|
data-shrink="true"
|
||||||
|
for="date-picker-inline"
|
||||||
|
id="date-picker-inline-label"
|
||||||
|
>
|
||||||
|
Date To
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||||
|
>
|
||||||
|
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl MuiInputBase-adornedEnd"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-invalid="false"
|
||||||
|
class="MuiInputBase-input MuiInput-input MuiInputBase-inputAdornedEnd"
|
||||||
|
id="date-picker-inline"
|
||||||
|
required=""
|
||||||
|
type="text"
|
||||||
|
value="04/01/2023"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="MuiInputAdornment-root MuiInputAdornment-positionEnd"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label="change date"
|
||||||
|
class="MuiButtonBase-root MuiIconButton-root"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiIconButton-label"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSvgIcon-root"
|
||||||
|
focusable="false"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M17 12h-5v5h5v-5zM16 1v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2h-1V1h-2zm3 18H5V8h14v11z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M0 0h24v24H0z"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-6 MuiGrid-grid-md-3"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiFormLabel-filled Mui-required Mui-required"
|
||||||
|
data-shrink="true"
|
||||||
|
for="time-picker"
|
||||||
|
id="time-picker-label"
|
||||||
|
>
|
||||||
|
Time To
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||||
|
>
|
||||||
|
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl MuiInputBase-adornedEnd"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-invalid="false"
|
||||||
|
class="MuiInputBase-input MuiInput-input MuiInputBase-inputAdornedEnd"
|
||||||
|
id="time-picker"
|
||||||
|
required=""
|
||||||
|
type="text"
|
||||||
|
value="06:30 AM"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="MuiInputAdornment-root MuiInputAdornment-positionEnd"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label="change time"
|
||||||
|
class="MuiButtonBase-root MuiIconButton-root"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiIconButton-label"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSvgIcon-root"
|
||||||
|
focusable="false"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M17 12h-5v5h5v-5zM16 1v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2h-1V1h-2zm3 18H5V8h14v11z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M0 0h24v24H0z"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiFormControl-root MuiFormControl-fullWidth"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated Mui-required Mui-required"
|
||||||
|
data-shrink="false"
|
||||||
|
id="select-can-signals-label"
|
||||||
|
>
|
||||||
|
Select CAN signals
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||||
|
>
|
||||||
|
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-fullWidth MuiInput-fullWidth MuiInputBase-formControl MuiInput-formControl"
|
||||||
|
inputvariant="outlined"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-labelledby="select-can-signals-label select-can-signals"
|
||||||
|
class="MuiSelect-root MuiSelect-select MuiSelect-selectMenu MuiInputBase-input MuiInput-input"
|
||||||
|
id="select-can-signals"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSelect-nativeInput"
|
||||||
|
required=""
|
||||||
|
tabindex="-1"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSvgIcon-root MuiSelect-icon"
|
||||||
|
focusable="false"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7 10l5 5 5-5z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary Mui-disabled MuiButton-fullWidth Mui-disabled"
|
||||||
|
disabled=""
|
||||||
|
tabindex="-1"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiButton-label"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
197
src/components/CANSelfServe/SelfServe/index.jsx
Normal file
197
src/components/CANSelfServe/SelfServe/index.jsx
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import DateFnsUtils from '@date-io/date-fns';
|
||||||
|
import { Button, Checkbox, Chip, CircularProgress, FormControl, Grid, InputLabel, ListItemText, MenuItem, Select } from "@material-ui/core";
|
||||||
|
import { KeyboardDatePicker, KeyboardTimePicker, MuiPickersUtilsProvider } from '@material-ui/pickers';
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { logger } from "../../../services/monitoring";
|
||||||
|
import { CANSignalsExportProvider, useCANSignalsExportContext } from "../../Contexts/CANSignalsExportContext";
|
||||||
|
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||||
|
import { useUserContext } from "../../Contexts/UserContext";
|
||||||
|
import useStyles from "../../useStyles";
|
||||||
|
|
||||||
|
const MainForm = ({ id }) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const { setMessage } = useStatusContext();
|
||||||
|
const { busy, canSignals, getCANSignalList, getDynamicColumnCANSignals } = useCANSignalsExportContext();
|
||||||
|
|
||||||
|
const [selectedStartDate, setSelectedStartDate] = useState(new Date(Date.now() - 24 * 60 * 60 * 1000));
|
||||||
|
const [selectedEndDate, setSelectedEndDate] = useState(new Date());
|
||||||
|
const [selectedCanSignals, setSelectedCanSignals] = useState([]);
|
||||||
|
|
||||||
|
|
||||||
|
const {
|
||||||
|
token: {
|
||||||
|
idToken: { jwtToken: token },
|
||||||
|
},
|
||||||
|
} = useUserContext();
|
||||||
|
|
||||||
|
const handleSubmit = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
let timestamp_start = Date.parse(selectedStartDate.toUTCString()) / 1000
|
||||||
|
let timestamp_end = Date.parse(selectedEndDate.toUTCString()) / 1000
|
||||||
|
try {
|
||||||
|
await getDynamicColumnCANSignals(id, timestamp_start, timestamp_end, selectedCanSignals, token)
|
||||||
|
} catch(e){
|
||||||
|
setMessage(e.message);
|
||||||
|
logger.error(e.stack)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSubmitDisabled = !selectedStartDate || !selectedEndDate || selectedCanSignals.length === 0;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
if (!token) return;
|
||||||
|
await getCANSignalList(token);
|
||||||
|
} catch (e) {
|
||||||
|
setMessage(e.message);
|
||||||
|
logger.warn(e.stack);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
const handleDateChange = (value, dateType) => {
|
||||||
|
const newDate = new Date(value);
|
||||||
|
const oldDate = dateType === "start" ? selectedStartDate || new Date() : selectedEndDate || new Date();
|
||||||
|
newDate.setHours(oldDate.getHours());
|
||||||
|
newDate.setMinutes(oldDate.getMinutes());
|
||||||
|
newDate.setSeconds(oldDate.getSeconds());
|
||||||
|
if (dateType === "start") {
|
||||||
|
setSelectedStartDate(newDate);
|
||||||
|
} else {
|
||||||
|
setSelectedEndDate(newDate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimeFromChange = (value) => {
|
||||||
|
setSelectedStartDate(value);
|
||||||
|
};
|
||||||
|
const handleTimeToChange = (value) => {
|
||||||
|
setSelectedEndDate(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectedItemsChange = (event) => {
|
||||||
|
setSelectedCanSignals(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.paper}>
|
||||||
|
<Grid container spacing={3} justifyContent="center">
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<MuiPickersUtilsProvider utils={DateFnsUtils}>
|
||||||
|
<Grid container justifyContent="space-between">
|
||||||
|
<Grid item xs={6} md={3}>
|
||||||
|
<KeyboardDatePicker
|
||||||
|
required
|
||||||
|
disableToolbar
|
||||||
|
variant="inline"
|
||||||
|
format="MM/dd/yyyy"
|
||||||
|
margin="normal"
|
||||||
|
id="date-picker-inline"
|
||||||
|
label="Date From"
|
||||||
|
value={selectedStartDate}
|
||||||
|
onChange={(value) => handleDateChange(value, "start")}
|
||||||
|
KeyboardButtonProps={{
|
||||||
|
'aria-label': 'change date',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} md={3}>
|
||||||
|
<KeyboardTimePicker
|
||||||
|
required
|
||||||
|
margin="normal"
|
||||||
|
variant="inline"
|
||||||
|
id="time-picker"
|
||||||
|
label="Time From"
|
||||||
|
value={selectedStartDate}
|
||||||
|
onChange={handleTimeFromChange}
|
||||||
|
KeyboardButtonProps={{
|
||||||
|
'aria-label': 'change time',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} md={3}>
|
||||||
|
<KeyboardDatePicker
|
||||||
|
required
|
||||||
|
disableToolbar
|
||||||
|
variant="inline"
|
||||||
|
format="MM/dd/yyyy"
|
||||||
|
margin="normal"
|
||||||
|
id="date-picker-inline"
|
||||||
|
label="Date To"
|
||||||
|
value={selectedEndDate}
|
||||||
|
onChange={(value) => handleDateChange(value, "end")}
|
||||||
|
KeyboardButtonProps={{
|
||||||
|
'aria-label': 'change date',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} md={3}>
|
||||||
|
<KeyboardTimePicker
|
||||||
|
required
|
||||||
|
margin="normal"
|
||||||
|
id="time-picker"
|
||||||
|
variant="inline"
|
||||||
|
label="Time To"
|
||||||
|
value={selectedEndDate}
|
||||||
|
onChange={handleTimeToChange}
|
||||||
|
KeyboardButtonProps={{
|
||||||
|
'aria-label': 'change time',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</MuiPickersUtilsProvider>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<FormControl fullWidth required>
|
||||||
|
<InputLabel id="select-can-signals-label">Select CAN signals</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="select-can-signals-label"
|
||||||
|
id="select-can-signals"
|
||||||
|
multiple
|
||||||
|
value={selectedCanSignals}
|
||||||
|
onChange={handleSelectedItemsChange}
|
||||||
|
fullWidth
|
||||||
|
inputvariant="outlined"
|
||||||
|
renderValue={(selected) => (
|
||||||
|
<div className={classes.chips}>
|
||||||
|
{selected.map((value) => (
|
||||||
|
<Chip key={value} label={value} className={classes.chip} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{canSignals.map((signal) => (
|
||||||
|
<MenuItem key={signal.signal_name} value={signal.signal_name}>
|
||||||
|
<Checkbox checked={selectedCanSignals.indexOf(signal.signal_name) > -1} />
|
||||||
|
<ListItemText primary={signal.signal_name} />
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={isSubmitDisabled || busy}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{busy ? <CircularProgress size={24} /> : "Submit"}
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CANSignalExport = (props) => (
|
||||||
|
<CANSignalsExportProvider>
|
||||||
|
<MainForm {...props} />
|
||||||
|
</CANSignalsExportProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default CANSignalExport;
|
||||||
42
src/components/CANSelfServe/SelfServe/index.test.jsx
Normal file
42
src/components/CANSelfServe/SelfServe/index.test.jsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
jest.mock("../../Contexts/StatusContext");
|
||||||
|
jest.mock("../../Contexts/UserContext");
|
||||||
|
jest.mock("../../../services/CANSignalAPI");
|
||||||
|
jest.useFakeTimers();
|
||||||
|
jest.setSystemTime(new Date(2023, 3, 1, 6, 30, 45, 100));
|
||||||
|
|
||||||
|
import { render, waitFor } from "@testing-library/react";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import addSnapshotSerializer from "../../../utils/snapshot";
|
||||||
|
import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing";
|
||||||
|
|
||||||
|
import { StatusProvider } from "../../Contexts/StatusContext";
|
||||||
|
import { setToken, UserProvider } from "../../Contexts/UserContext";
|
||||||
|
import CANSignalExport from "./index";
|
||||||
|
|
||||||
|
const renderCANSignalExport = async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<StatusProvider>
|
||||||
|
<UserProvider>
|
||||||
|
<BrowserRouter>
|
||||||
|
<CANSignalExport id="TESTVIN1234567890" />
|
||||||
|
</BrowserRouter>
|
||||||
|
</UserProvider>
|
||||||
|
</StatusProvider>
|
||||||
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
/* render */
|
||||||
|
});
|
||||||
|
return container;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Render", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
addSnapshotSerializer(expect);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Render", async () => {
|
||||||
|
setToken(TEST_AUTH_OBJECT_FISKER);
|
||||||
|
const container = await renderCANSignalExport();
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
21
src/components/CANSelfServe/SelfServeTab.jsx
Normal file
21
src/components/CANSelfServe/SelfServeTab.jsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Typography } from "@material-ui/core";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import React from "react";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
|
||||||
|
import useStyles from "../useStyles";
|
||||||
|
import SelfServe from "./SelfServe";
|
||||||
|
|
||||||
|
const SelfServeTab = () => {
|
||||||
|
const { vin } = useParams();
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx(classes.paper, classes.tableSize)}>
|
||||||
|
<Typography variant="h6">Can Signals Self Serve</Typography>
|
||||||
|
<SelfServe id={vin} classes={classes} />
|
||||||
|
</div >
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelfServeTab;
|
||||||
36
src/components/Cars/Status/TRexLogsTab.jsx
Normal file
36
src/components/Cars/Status/TRexLogsTab.jsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Typography } from "@material-ui/core";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import React from "react";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
|
||||||
|
import { useUserContext } from "../../Contexts/UserContext";
|
||||||
|
import { VehicleProvider } from "../../Contexts/VehicleContext";
|
||||||
|
import TRexLogsTable from "../../Controls/TRexLogs";
|
||||||
|
import useStyles from "../../useStyles";
|
||||||
|
|
||||||
|
const MainForm = () => {
|
||||||
|
const { vin } = useParams();
|
||||||
|
const classes = useStyles();
|
||||||
|
const {
|
||||||
|
token: {
|
||||||
|
idToken: { jwtToken: token },
|
||||||
|
},
|
||||||
|
} = useUserContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx(classes.paper, classes.tableSize)}>
|
||||||
|
<Typography variant="h6">
|
||||||
|
T.Rex Logs
|
||||||
|
</Typography>
|
||||||
|
<TRexLogsTable vin={vin} token={token} classes={classes} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TRexLogsTab = () => (
|
||||||
|
<VehicleProvider>
|
||||||
|
<MainForm />
|
||||||
|
</VehicleProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default TRexLogsTab;
|
||||||
@@ -123,7 +123,7 @@ exports[`DigitalTwinTab Render 1`] = `
|
|||||||
sunroof
|
sunroof
|
||||||
</b>
|
</b>
|
||||||
:
|
:
|
||||||
closed
|
closed (0)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ exports[`CarStatus Render 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="MuiTab-wrapper"
|
class="MuiTab-wrapper"
|
||||||
>
|
>
|
||||||
ECUs
|
T.Rex logs
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="MuiTouchRipple-root"
|
class="MuiTouchRipple-root"
|
||||||
@@ -153,7 +153,7 @@ exports[`CarStatus Render 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="MuiTab-wrapper"
|
class="MuiTab-wrapper"
|
||||||
>
|
>
|
||||||
Remote Commands
|
ECUs
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="MuiTouchRipple-root"
|
class="MuiTouchRipple-root"
|
||||||
@@ -167,6 +167,24 @@ exports[`CarStatus Render 1`] = `
|
|||||||
role="tab"
|
role="tab"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
type="button"
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiTab-wrapper"
|
||||||
|
>
|
||||||
|
Remote Commands
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-controls="tabpanel-8"
|
||||||
|
aria-selected="false"
|
||||||
|
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit"
|
||||||
|
id="tab-8"
|
||||||
|
role="tab"
|
||||||
|
tabindex="-1"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="MuiTab-wrapper"
|
class="MuiTab-wrapper"
|
||||||
@@ -177,6 +195,24 @@ exports[`CarStatus Render 1`] = `
|
|||||||
class="MuiTouchRipple-root"
|
class="MuiTouchRipple-root"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
aria-controls="tabpanel-9"
|
||||||
|
aria-selected="false"
|
||||||
|
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit"
|
||||||
|
id="tab-9"
|
||||||
|
role="tab"
|
||||||
|
tabindex="-1"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiTab-wrapper"
|
||||||
|
>
|
||||||
|
CAN Signal Export
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="PrivateTabIndicator-root-0 PrivateTabIndicator-colorSecondary-0 MuiTabs-indicator"
|
class="PrivateTabIndicator-root-0 PrivateTabIndicator-colorSecondary-0 MuiTabs-indicator"
|
||||||
@@ -352,6 +388,18 @@ exports[`CarStatus Render 1`] = `
|
|||||||
id="tabpanel-7"
|
id="tabpanel-7"
|
||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
aria-labelledby="tab-8"
|
||||||
|
hidden=""
|
||||||
|
id="tabpanel-8"
|
||||||
|
role="tabpanel"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-labelledby="tab-9"
|
||||||
|
hidden=""
|
||||||
|
id="tabpanel-9"
|
||||||
|
role="tabpanel"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useParams } from "react-router";
|
|||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
import { hasRole, Permissions } from "../../../utils/roles";
|
import { hasRole, Permissions } from "../../../utils/roles";
|
||||||
|
import SelfServeTab from "../../CANSelfServe/SelfServeTab";
|
||||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||||
import { useUserContext } from "../../Contexts/UserContext";
|
import { useUserContext } from "../../Contexts/UserContext";
|
||||||
import TabPanel from "../../Controls/TabPanel";
|
import TabPanel from "../../Controls/TabPanel";
|
||||||
@@ -17,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 TRexLogsTab from "./TRexLogsTab";
|
||||||
|
|
||||||
const tabHashes = ["details", "updates", "filters"];
|
const tabHashes = ["details", "updates", "filters"];
|
||||||
|
|
||||||
@@ -42,6 +44,10 @@ const TabViews = [
|
|||||||
label: "CAN Signals",
|
label: "CAN Signals",
|
||||||
component: CANSignalsTab,
|
component: CANSignalsTab,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "T.Rex logs",
|
||||||
|
component: TRexLogsTab,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "ECUs",
|
label: "ECUs",
|
||||||
component: ECUsTab,
|
component: ECUsTab,
|
||||||
@@ -55,6 +61,10 @@ const TabViews = [
|
|||||||
component: FleetsTab,
|
component: FleetsTab,
|
||||||
rolesPerProvider: Permissions.FiskerRead,
|
rolesPerProvider: Permissions.FiskerRead,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "CAN Signal Export",
|
||||||
|
component: SelfServeTab
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const filterTabs = (data, groups, providers) => {
|
const filterTabs = (data, groups, providers) => {
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ jest.mock("@material-ui/core/utils/unstable_useId", () =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
import { render, waitFor } from "@testing-library/react";
|
import { render, waitFor } from "@testing-library/react";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
|
||||||
import routeData from "react-router";
|
import routeData from "react-router";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
|
||||||
|
import addSnapshotSerializer from "../../../utils/snapshot";
|
||||||
|
import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing";
|
||||||
import { CANFiltersProvider } from "../../Contexts/CANFiltersContext";
|
import { CANFiltersProvider } from "../../Contexts/CANFiltersContext";
|
||||||
import { StatusProvider } from "../../Contexts/StatusContext";
|
import { StatusProvider } from "../../Contexts/StatusContext";
|
||||||
import { UserProvider, setToken } from "../../Contexts/UserContext";
|
import { setToken, UserProvider } from "../../Contexts/UserContext";
|
||||||
import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing";
|
|
||||||
import CarStatus from "./index";
|
import CarStatus from "./index";
|
||||||
import addSnapshotSerializer from "../../../utils/snapshot";
|
|
||||||
|
|
||||||
const renderCarStatus = async () => {
|
const renderCarStatus = async () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useContext, useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import api from "../../services/CANFiltersAPI";
|
import api from "../../services/CANFiltersAPI";
|
||||||
import { validateCANID, validateFilter } from "../../utils/validationSupplier";
|
import { validateCANID, validateFilter, validateVIN } from "../../utils/validationSupplier";
|
||||||
|
|
||||||
const CANFiltersContext = React.createContext();
|
const CANFiltersContext = React.createContext();
|
||||||
|
|
||||||
@@ -100,10 +100,4 @@ export const CANFiltersProvider = ({ children }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateVIN = (vin) => {
|
|
||||||
if (vin == null || vin.length !== 17) {
|
|
||||||
throw new Error("Invalid VIN");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useCANFiltersContext = () => useContext(CANFiltersContext);
|
export const useCANFiltersContext = () => useContext(CANFiltersContext);
|
||||||
|
|||||||
73
src/components/Contexts/CANSignalsExportContext.jsx
Normal file
73
src/components/Contexts/CANSignalsExportContext.jsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import React, { useContext, useState } from "react";
|
||||||
|
import api from "../../services/CANSignalAPI";
|
||||||
|
import { validateVIN } from "../../utils/validationSupplier";
|
||||||
|
|
||||||
|
const CANSignalsExportContext = React.createContext();
|
||||||
|
|
||||||
|
export const CANSignalsExportProvider = ({ children }) => {
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
|
const [canSignals, setCanSignals] = useState([]);
|
||||||
|
|
||||||
|
const getCANSignalList = async (token) => {
|
||||||
|
try {
|
||||||
|
setBusy(true);
|
||||||
|
const result = await api.getCanSignalList(token);
|
||||||
|
if (result.error) {
|
||||||
|
throw new Error(`Get can signal list error. ${result.message}`);
|
||||||
|
}
|
||||||
|
setCanSignals(result.data ?? []);
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDynamicColumnCANSignals = async (vin, timestart, timeend, cansingals, token) => {
|
||||||
|
try {
|
||||||
|
setBusy(true)
|
||||||
|
if (!vin) return;
|
||||||
|
|
||||||
|
validateVIN(vin);
|
||||||
|
if (timestart > timeend) throw new Error("Start time cannot be after end time");
|
||||||
|
const result = await api.getCanSignalsVin(vin, timestart, timeend, cansingals, token);
|
||||||
|
if (result.error || !result.ok)
|
||||||
|
throw new Error(`Get CAN signals error. ${result.message}`);
|
||||||
|
|
||||||
|
const blob = await result.blob();
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const csvData = reader.result;
|
||||||
|
const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = window.URL.createObjectURL(blob);
|
||||||
|
link.download = 'CAN_signals.csv';
|
||||||
|
link.click();
|
||||||
|
};
|
||||||
|
reader.readAsText(blob);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(e)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CANSignalsExportContext.Provider
|
||||||
|
value={{
|
||||||
|
busy,
|
||||||
|
canSignals,
|
||||||
|
getCANSignalList,
|
||||||
|
getDynamicColumnCANSignals
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CANSignalsExportContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCANSignalsExportContext = () => useContext(CANSignalsExportContext);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { useContext, useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
|
|
||||||
import { logger } from "../../services/monitoring";
|
import { logger } from "../../services/monitoring";
|
||||||
import api from "../../services/vehiclesAPI";
|
import api from "../../services/vehiclesAPI";
|
||||||
|
import { validateVIN } from "../../utils/validationSupplier";
|
||||||
|
|
||||||
const VehicleContext = React.createContext();
|
const VehicleContext = React.createContext();
|
||||||
|
|
||||||
@@ -285,10 +287,4 @@ const validateVehicle = (v) => {
|
|||||||
validateVIN(v.vin);
|
validateVIN(v.vin);
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateVIN = (vin) => {
|
|
||||||
if (vin == null || vin.length !== 17) {
|
|
||||||
throw new Error("Invalid VIN");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useVehicleContext = () => useContext(VehicleContext);
|
export const useVehicleContext = () => useContext(VehicleContext);
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
let busy = false;
|
||||||
|
let canSignals = [
|
||||||
|
{
|
||||||
|
signal_name: "123",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal_name: "456",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal_name: "789",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CANSignalsExportProvider = ({ children }) => {
|
||||||
|
return <div data-testid="mocked-cansignalsprovider">{children}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCANSignalsExportContext= () => ({
|
||||||
|
busy,
|
||||||
|
canSignals,
|
||||||
|
getCANSignalList: jest.fn(),
|
||||||
|
getDynamicColumnCANSignals: jest.fn(),
|
||||||
|
});
|
||||||
329
src/components/Controls/TRexLogs/index.jsx
Normal file
329
src/components/Controls/TRexLogs/index.jsx
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableFooter,
|
||||||
|
TablePagination,
|
||||||
|
TableRow
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
|
import FormControl from '@mui/material/FormControl';
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
import FormGroup from '@mui/material/FormGroup';
|
||||||
|
import FormLabel from '@mui/material/FormLabel';
|
||||||
|
import LinearProgress from '@mui/material/LinearProgress';
|
||||||
|
|
||||||
|
import {
|
||||||
|
KeyboardDatePicker, MuiPickersUtilsProvider
|
||||||
|
} from '@material-ui/pickers';
|
||||||
|
import clsx from "clsx";
|
||||||
|
import api from "../../../services/vehiclesAPI";
|
||||||
|
|
||||||
|
import DateFnsUtils from '@date-io/date-fns';
|
||||||
|
import { TableHead } from "@mui/material";
|
||||||
|
import { logger } from "../../../services/monitoring";
|
||||||
|
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||||
|
|
||||||
|
const tableColumns = [
|
||||||
|
{
|
||||||
|
id: "level",
|
||||||
|
label: "Level",
|
||||||
|
width: "5%",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "trex_timestamp",
|
||||||
|
label: "T.Rex Timestamp",
|
||||||
|
width: "10%",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cloud_timestamp",
|
||||||
|
label: "Cloud Timestamp",
|
||||||
|
width: "10%",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "line_number",
|
||||||
|
label: "Line Number",
|
||||||
|
width: "5%",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "filename",
|
||||||
|
label: "Filename",
|
||||||
|
width: "10%",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "msg",
|
||||||
|
label: "Message",
|
||||||
|
width: "60%",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const logLevelCheckBoxes = [
|
||||||
|
{
|
||||||
|
"id": "trace",
|
||||||
|
"label": "Trace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "debug",
|
||||||
|
"label": "Debug"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "info",
|
||||||
|
"label": "Info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "warning",
|
||||||
|
"label": "Warning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "error",
|
||||||
|
"label": "Error"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "critical",
|
||||||
|
"label": "Critical"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const transformLogs = (logs) =>
|
||||||
|
logs.map((log) => {
|
||||||
|
const { level, timestamp, received_timestamp, line_number, filename, msg } = log;
|
||||||
|
//const trex_time = new Date(timestamp)
|
||||||
|
//const cloud_time = new Date(received_timestamp)
|
||||||
|
return {
|
||||||
|
level: level,
|
||||||
|
trex_timestamp: timestamp,
|
||||||
|
cloud_timestamp: received_timestamp,
|
||||||
|
line_number: line_number,
|
||||||
|
filename: filename,
|
||||||
|
msg: msg
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.flat()
|
||||||
|
|
||||||
|
const fromatDateForRequest = (date) => {
|
||||||
|
const getYear = date.toLocaleString("default", { year: "numeric" });
|
||||||
|
const getMonth = date.toLocaleString("default", { month: "2-digit" });
|
||||||
|
const getDay = date.toLocaleString("default", { day: "2-digit" });
|
||||||
|
return getYear + "-" + getMonth + "-" + getDay
|
||||||
|
};
|
||||||
|
|
||||||
|
//read at least 50000 bytes per one API request
|
||||||
|
const ONE_READ_SIZE = 50000
|
||||||
|
const DEFAULT_PAGE_SIZE = 25
|
||||||
|
|
||||||
|
const TRexLogsTable = ({ vin, token, classes }) => {
|
||||||
|
const [allLogsFetched, setAllLogsFetched] = useState(false)
|
||||||
|
const [blobSize, setBlobSize] = useState(0)
|
||||||
|
const [currentOffset, setCurrentOffset] = useState(0)
|
||||||
|
const [currectLogLevels, setCurrentLogLevels] = useState({
|
||||||
|
trace: true,
|
||||||
|
debug: true,
|
||||||
|
info: true,
|
||||||
|
warning: true,
|
||||||
|
error: true,
|
||||||
|
critical: true
|
||||||
|
})
|
||||||
|
const [logs, setLogs] = useState([]);
|
||||||
|
const [pageIndex, setPageIndex] = useState(0);
|
||||||
|
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
|
||||||
|
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const { setMessage } = useStatusContext();
|
||||||
|
|
||||||
|
let controller = new AbortController()
|
||||||
|
const readBlob = async (offset, count) => {
|
||||||
|
return await api.getTRexLogs(vin, fromatDateForRequest(selectedDate), offset, count, "UP", token, controller)
|
||||||
|
}
|
||||||
|
const getDesiredSize = () => {
|
||||||
|
return pageSize * pageIndex + pageSize
|
||||||
|
}
|
||||||
|
const getReadPercentage = () => {
|
||||||
|
return (currentOffset * 100 / blobSize).toFixed(2);
|
||||||
|
}
|
||||||
|
const getFilteredLogs = (logs) => {
|
||||||
|
return logs.filter(log => currectLogLevels[log.level] === true)
|
||||||
|
}
|
||||||
|
const fetchAllLogs = async () => {
|
||||||
|
let fetched = []
|
||||||
|
let offset = currentOffset
|
||||||
|
let readSize = ONE_READ_SIZE
|
||||||
|
for (; ;) {
|
||||||
|
const result = await readBlob(offset, readSize)
|
||||||
|
if (result.error) {
|
||||||
|
fetched.error = result.error
|
||||||
|
break
|
||||||
|
}
|
||||||
|
setBlobSize(result.blobSize)
|
||||||
|
readSize *= 2
|
||||||
|
offset = result.RealOffset + result.bytesRead
|
||||||
|
setCurrentOffset(offset)
|
||||||
|
fetched = transformLogs(result.data).concat(fetched)
|
||||||
|
setLogs(fetched)
|
||||||
|
if (offset >= result.blobSize) {
|
||||||
|
setMessage(`All log for ${fromatDateForRequest(selectedDate)} fetched`)
|
||||||
|
setAllLogsFetched(true)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fetched.length === 0) {
|
||||||
|
if (logs.length !== 0) {
|
||||||
|
setMessage(`No more T.Rex logs for ${fromatDateForRequest(selectedDate)}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setTotal(0)
|
||||||
|
const msg = `Cannot fetch logs for ${fromatDateForRequest(selectedDate)}`
|
||||||
|
setMessage(msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setCurrentOffset(offset)
|
||||||
|
return fetched
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
if (!vin || !token) return;
|
||||||
|
const desiredSize = getDesiredSize()
|
||||||
|
if (desiredSize < logs.length || allLogsFetched) {
|
||||||
|
setTotal(getFilteredLogs(logs).length)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let fetched = await fetchAllLogs()
|
||||||
|
if (!fetched || fetched.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setTotal(getFilteredLogs(fetched).length);
|
||||||
|
} catch (e) {
|
||||||
|
setMessage(e.message);
|
||||||
|
logger.warn(e.stack);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => controller?.abort()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [vin, token, pageIndex, pageSize, selectedDate, currectLogLevels]);
|
||||||
|
|
||||||
|
const handleChangePageSize = (event) => {
|
||||||
|
setPageSize(parseInt(event.target.value, 10));
|
||||||
|
setTotal(getFilteredLogs(logs).length)
|
||||||
|
setPageIndex(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNewDate = (newValue) => {
|
||||||
|
setPageIndex(0);
|
||||||
|
setCurrentOffset(0)
|
||||||
|
setLogs([])
|
||||||
|
setAllLogsFetched(false)
|
||||||
|
setBlobSize(0)
|
||||||
|
setSelectedDate(newValue)
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNewFilter = (event) => {
|
||||||
|
setPageIndex(0)
|
||||||
|
setCurrentLogLevels({
|
||||||
|
...currectLogLevels,
|
||||||
|
[event.target.defaultValue]: event.target.checked,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className={clsx(classes.paper, classes.tableSize)}>
|
||||||
|
<Table
|
||||||
|
style={{ tableLayout: "fixed", width: "100%" }}
|
||||||
|
>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell align="center">
|
||||||
|
<FormControl component="fieldset">
|
||||||
|
<FormLabel component="legend">Log levels</FormLabel>
|
||||||
|
<FormGroup aria-label="position" row>
|
||||||
|
{logLevelCheckBoxes.map((box) => (
|
||||||
|
<FormControlLabel
|
||||||
|
value={box.id}
|
||||||
|
control={<Checkbox defaultChecked disabled={!allLogsFetched} />}
|
||||||
|
label={box.label}
|
||||||
|
labelPlacement="bottom"
|
||||||
|
onChange={handleNewFilter}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</FormGroup>
|
||||||
|
</FormControl>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell align="center">
|
||||||
|
<MuiPickersUtilsProvider utils={DateFnsUtils}>
|
||||||
|
<KeyboardDatePicker
|
||||||
|
disableToolbar
|
||||||
|
variant="inline"
|
||||||
|
format="yyyy/MM/dd"
|
||||||
|
margin="normal"
|
||||||
|
id="date-picker-inline"
|
||||||
|
label="Choose date"
|
||||||
|
value={selectedDate}
|
||||||
|
onChange={handleNewDate}
|
||||||
|
KeyboardButtonProps={{
|
||||||
|
'aria-label': 'change date',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</MuiPickersUtilsProvider>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{
|
||||||
|
blobSize === 0 ? `No logs for ${fromatDateForRequest(selectedDate)}` :
|
||||||
|
`Read ${getReadPercentage()}% of logs`
|
||||||
|
}
|
||||||
|
{
|
||||||
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
align="center"
|
||||||
|
value={getReadPercentage()} />
|
||||||
|
}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
style={{ tableLayout: "fixed", width: "100%" }}
|
||||||
|
>
|
||||||
|
<TableHead classes={classes}>
|
||||||
|
<TableRow>
|
||||||
|
{tableColumns.map((column) => (
|
||||||
|
<TableCell style={{ width: column.width }} align="center" key={column.label || "none"} className={classes.tableHeader} >{column.label}</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead >
|
||||||
|
<TableBody>
|
||||||
|
{getFilteredLogs(logs).slice(-getDesiredSize(), (pageIndex === 0 ? undefined : -(pageSize * pageIndex))).map((log, i) => (
|
||||||
|
<TableRow key={log.trex_timestamp + log.cloud_timestamp} >
|
||||||
|
<TableCell align="center">{log.level}</TableCell>
|
||||||
|
<TableCell align="center" style={{ wordBreak: "break-all" }}>{log.trex_timestamp}</TableCell>
|
||||||
|
<TableCell align="center" style={{ wordBreak: "break-all" }}>{log.cloud_timestamp}</TableCell>
|
||||||
|
<TableCell align="center">{log.line_number}</TableCell>
|
||||||
|
<TableCell align="center" style={{ wordBreak: "break-all" }}>{log.filename}</TableCell>
|
||||||
|
<TableCell align="left" style={{ wordBreak: "break-all" }}>{log.msg}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
<TableFooter>
|
||||||
|
<TableRow>
|
||||||
|
<TablePagination
|
||||||
|
rowsPerPageOptions={[25, 100, 1000, 10000]}
|
||||||
|
colSpan={6}
|
||||||
|
count={total}
|
||||||
|
rowsPerPage={pageSize}
|
||||||
|
page={!pageIndex || pageIndex <= 0 ? 0 : pageIndex}
|
||||||
|
SelectProps={{
|
||||||
|
inputProps: { "aria-label": "rows per page" },
|
||||||
|
native: true,
|
||||||
|
}}
|
||||||
|
onPageChange={(_, newValue) => setPageIndex(newValue)}
|
||||||
|
onRowsPerPageChange={handleChangePageSize}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
</TableFooter>
|
||||||
|
</Table>
|
||||||
|
</div >
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TRexLogsTable;
|
||||||
@@ -13,6 +13,14 @@ const openCloseState = (value) => (value ? "open" : "closed");
|
|||||||
const mapOpenCloseState = (value) =>
|
const mapOpenCloseState = (value) =>
|
||||||
keyValueTemplate(value[0], openCloseState(value[1]));
|
keyValueTemplate(value[0], openCloseState(value[1]));
|
||||||
|
|
||||||
|
const windowState = (value) => {
|
||||||
|
if (value[1] === 0 || value[1] > 100) {
|
||||||
|
return keyValueTemplate(value[0], `closed (${value[1]})`);
|
||||||
|
} else {
|
||||||
|
return keyValueTemplate(value[0], `${value[1]}% open`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 } = props;
|
const { battery, doors, location, trex_version, ip, updated, windows, misc_windows, sunroof, dbc_version, door_locks } = props;
|
||||||
@@ -45,12 +53,7 @@ const DigitalTwin = (props) => {
|
|||||||
<div className={classes.popupSection}>
|
<div className={classes.popupSection}>
|
||||||
<h3>Windows</h3>
|
<h3>Windows</h3>
|
||||||
{Object.entries(windows).map((value) => {
|
{Object.entries(windows).map((value) => {
|
||||||
if (value[1] === 0) {
|
return windowState(value);
|
||||||
return keyValueTemplate(value[0], "closed");
|
|
||||||
} else {
|
|
||||||
const percentOpen = Math.min(value[1], 100);
|
|
||||||
return keyValueTemplate(value[0], `${percentOpen}% open`);
|
|
||||||
}
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -58,11 +61,7 @@ const DigitalTwin = (props) => {
|
|||||||
<div className={classes.popupSection}>
|
<div className={classes.popupSection}>
|
||||||
<h3>Misc Windows</h3>
|
<h3>Misc Windows</h3>
|
||||||
{Object.entries(misc_windows).map((value) => {
|
{Object.entries(misc_windows).map((value) => {
|
||||||
if (value[1] === 0 || value[1] > 100) {
|
return windowState(value);
|
||||||
return keyValueTemplate(value[0], `closed ${value[1]}%`);
|
|
||||||
} else {
|
|
||||||
return keyValueTemplate(value[0], `${value[1]}% open`);
|
|
||||||
}
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -70,12 +69,7 @@ const DigitalTwin = (props) => {
|
|||||||
<div className={classes.popupSection}>
|
<div className={classes.popupSection}>
|
||||||
<h3>Sunroof</h3>
|
<h3>Sunroof</h3>
|
||||||
{Object.entries(sunroof).map((value) => {
|
{Object.entries(sunroof).map((value) => {
|
||||||
if (value[1] === 0) {
|
return windowState(value);
|
||||||
return keyValueTemplate(value[0], "closed");
|
|
||||||
} else {
|
|
||||||
const percentOpen = Math.min(value[1], 100);
|
|
||||||
return keyValueTemplate(value[0], `${percentOpen}% open`);
|
|
||||||
}
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,3 +1,168 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`SMS Send Component Render 1`] = `undefined`;
|
exports[`SMS Send Component Render 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
data-testid="mocked-userprovider"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-testid="mocked-smsprovider"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="makeStyles-paper-0"
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
action="{onSubmit}"
|
||||||
|
class="makeStyles-form-0"
|
||||||
|
novalidate=""
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||||
|
data-shrink="false"
|
||||||
|
for="iccid"
|
||||||
|
id="iccid-label"
|
||||||
|
>
|
||||||
|
ICCID
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||||
|
>
|
||||||
|
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-invalid="false"
|
||||||
|
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||||
|
id="iccid"
|
||||||
|
maxlength="50"
|
||||||
|
minlength="15"
|
||||||
|
name="iccid"
|
||||||
|
required=""
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<fieldset
|
||||||
|
aria-hidden="true"
|
||||||
|
class="PrivateNotchedOutline-root-0 MuiOutlinedInput-notchedOutline"
|
||||||
|
>
|
||||||
|
<legend
|
||||||
|
class="PrivateNotchedOutline-legendLabelled-0"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
ICCID
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</legend>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||||
|
data-shrink="false"
|
||||||
|
for="message"
|
||||||
|
id="message-label"
|
||||||
|
>
|
||||||
|
Message
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||||
|
>
|
||||||
|
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-invalid="false"
|
||||||
|
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||||
|
id="message"
|
||||||
|
maxlength="320"
|
||||||
|
name="message"
|
||||||
|
required=""
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<fieldset
|
||||||
|
aria-hidden="true"
|
||||||
|
class="PrivateNotchedOutline-root-0 MuiOutlinedInput-notchedOutline"
|
||||||
|
>
|
||||||
|
<legend
|
||||||
|
class="PrivateNotchedOutline-legendLabelled-0"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Message
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</legend>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label
|
||||||
|
class="MuiFormControlLabel-root"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-disabled="false"
|
||||||
|
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-0 MuiCheckbox-root MuiCheckbox-colorPrimary MuiIconButton-colorPrimary"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiIconButton-label"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="PrivateSwitchBase-input-0"
|
||||||
|
data-indeterminate="false"
|
||||||
|
type="checkbox"
|
||||||
|
value="isAwaited"
|
||||||
|
/>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
Await delivery
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-0 MuiButton-containedPrimary MuiButton-fullWidth"
|
||||||
|
tabindex="0"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiButton-label"
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useSMSContext, SMSProvider } from "../../Contexts/SMSContext";
|
import { logger } from "../../../services/monitoring";
|
||||||
|
import { SMSProvider, useSMSContext } from "../../Contexts/SMSContext";
|
||||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||||
import { useUserContext } from "../../Contexts/UserContext";
|
import { useUserContext } from "../../Contexts/UserContext";
|
||||||
import { logger } from "../../../services/monitoring";
|
|
||||||
import SendForm from "./SendForm";
|
import SendForm from "./SendForm";
|
||||||
import ViewResult from "./ViewResult";
|
import ViewResult from "./ViewResult";
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import {TEST_AUTH_OBJECT_FISKER, TEST_TOKEN_FISKER} from "../../../utils/testing";
|
import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing";
|
||||||
|
|
||||||
jest.mock("../../Contexts/SMSContext");
|
jest.mock("../../Contexts/SMSContext");
|
||||||
jest.mock("../../Contexts/UserContext");
|
jest.mock("../../Contexts/UserContext");
|
||||||
|
|
||||||
|
|
||||||
import SMSSend from "./index";
|
import { render, waitFor } from "@testing-library/react";
|
||||||
import {BrowserRouter} from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import {UserProvider, setToken} from "../../Contexts/UserContext";
|
|
||||||
import {StatusProvider} from "../../Contexts/StatusContext";
|
|
||||||
import addSnapshotSerializer from "../../../utils/snapshot";
|
import addSnapshotSerializer from "../../../utils/snapshot";
|
||||||
import {render, waitFor} from "@testing-library/react";
|
import { StatusProvider } from "../../Contexts/StatusContext";
|
||||||
|
import { setToken, UserProvider } from "../../Contexts/UserContext";
|
||||||
|
import SMSSend from "./index";
|
||||||
|
|
||||||
|
|
||||||
const renderSendSMS = async () => {
|
const renderSendSMS = async () => {
|
||||||
@@ -38,7 +38,7 @@ describe("SMS Send Component", () => {
|
|||||||
|
|
||||||
it("Render", async () => {
|
it("Render", async () => {
|
||||||
// setToken(TEST_AUTH_OBJECT_FISKER);
|
// setToken(TEST_AUTH_OBJECT_FISKER);
|
||||||
const {container} = await renderSendSMS();
|
const container = await renderSendSMS();
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -285,6 +285,10 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
color: "blue",
|
color: "blue",
|
||||||
textDecoration: "underline",
|
textDecoration: "underline",
|
||||||
},
|
},
|
||||||
|
tableHeader: {
|
||||||
|
textDecorationStyle: "solid",
|
||||||
|
fontWeight:500,
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default useStyles;
|
export default useStyles;
|
||||||
|
|||||||
32
src/services/CANSignalAPI.js
Normal file
32
src/services/CANSignalAPI.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import {
|
||||||
|
addQueryParams, errorHandler, fetchRespHandler, getAuthHeaderOptions
|
||||||
|
} from "../utils/http";
|
||||||
|
|
||||||
|
const API_ENDPOINT = process.env.REACT_APP_OTA_SERVICE_URL;
|
||||||
|
|
||||||
|
const canSignalAPI = {
|
||||||
|
|
||||||
|
getCanSignalsVin: async (vin, timestamp_start, timestamp_end, can_signals, token) =>
|
||||||
|
fetch(addQueryParams(`${API_ENDPOINT}/can_signals_export`, { vin, timestamp_start, timestamp_end, can_signals }), {
|
||||||
|
method: "GET",
|
||||||
|
headers: Object.assign(
|
||||||
|
{ "Content-Type": "application/json" },
|
||||||
|
getAuthHeaderOptions(token)
|
||||||
|
),
|
||||||
|
responseType: "blob"
|
||||||
|
})
|
||||||
|
.catch(errorHandler),
|
||||||
|
|
||||||
|
|
||||||
|
getCanSignalList: async (token) =>
|
||||||
|
fetch(addQueryParams(`${API_ENDPOINT}/can_signals_list`), {
|
||||||
|
method: "GET",
|
||||||
|
headers: Object.assign(
|
||||||
|
{ "Content-Type": "application/json" },
|
||||||
|
getAuthHeaderOptions(token)
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.then(fetchRespHandler)
|
||||||
|
.catch(errorHandler),
|
||||||
|
};
|
||||||
|
export default canSignalAPI;
|
||||||
16
src/services/__mocks__/CANSignalAPI.js
Normal file
16
src/services/__mocks__/CANSignalAPI.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const canSignalList = [
|
||||||
|
{ signal_name: "123"},
|
||||||
|
{ signal_name: "456"},
|
||||||
|
{ signal_name: "789"}
|
||||||
|
];
|
||||||
|
|
||||||
|
const canSignalAPI = {
|
||||||
|
getCanSignalsVin: async (vin, timestamp_start, timestamp_end, can_signals, token) => {
|
||||||
|
return { data: "fake data" };
|
||||||
|
},
|
||||||
|
getCanSignalList: async (token) => {
|
||||||
|
return canSignalList;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default canSignalAPI;
|
||||||
@@ -58,6 +58,38 @@ const signals = {data:[
|
|||||||
},
|
},
|
||||||
]};
|
]};
|
||||||
|
|
||||||
|
const trexLogs = {
|
||||||
|
RealOffset: 0,
|
||||||
|
blobSize: 62072819,
|
||||||
|
bytesRead: 29874,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
"level": "info",
|
||||||
|
"timestamp": "2023-Feb-15 23:44:36.934746",
|
||||||
|
"line_number": 948,
|
||||||
|
"filename": "ota_service.cpp",
|
||||||
|
"msg": "ota::cleanup() Deleting directory \"/fisker/ota/1534\"",
|
||||||
|
"received_timestamp": "2023-Feb-15 23:44:37.203102197"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": "debug",
|
||||||
|
"timestamp": "2023-Feb-15 23:44:36.970715",
|
||||||
|
"line_number": 992,
|
||||||
|
"filename": "ota_service.cpp",
|
||||||
|
"msg": "ota::end() manifest process aborted with error state",
|
||||||
|
"received_timestamp": "2023-Feb-15 23:44:37.203197597"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": "debug",
|
||||||
|
"timestamp": "2023-Feb-15 23:44:36.971745",
|
||||||
|
"line_number": 91,
|
||||||
|
"filename": "vom_impl.cpp",
|
||||||
|
"msg": "vom::send_ota_result_fb() Sending 0x4F9->tbox_ota_res_fb (==OTA_error)",
|
||||||
|
"received_timestamp": "2023-Feb-15 23:44:37.203249997"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
const vehiclesAPI = {
|
const vehiclesAPI = {
|
||||||
addVehicle: async (vehicle) => {
|
addVehicle: async (vehicle) => {
|
||||||
data.push(vehicle);
|
data.push(vehicle);
|
||||||
@@ -117,6 +149,9 @@ const vehiclesAPI = {
|
|||||||
getCANSignals: async (vin, vehicle) => {
|
getCANSignals: async (vin, vehicle) => {
|
||||||
return signals;
|
return signals;
|
||||||
},
|
},
|
||||||
|
getTRexLogs: async (vin, date, offset, count, direction, token) => {
|
||||||
|
return trexLogs;
|
||||||
|
},
|
||||||
getVersionLog: async (vin) => ({
|
getVersionLog: async (vin) => ({
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -173,6 +173,18 @@ const vehiclesAPI = {
|
|||||||
.then(fetchRespHandler)
|
.then(fetchRespHandler)
|
||||||
.catch(errorHandler),
|
.catch(errorHandler),
|
||||||
|
|
||||||
|
getTRexLogs: async (vin, date, offset, count, direction, token, controller) =>
|
||||||
|
fetch(`${API_ENDPOINT}/vehicle/${vin}/trex-logs?date=${date}&offset=${offset}&count=${count}&direction=${direction}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: Object.assign(
|
||||||
|
{ "Content-Type": "application/json" },
|
||||||
|
getAuthHeaderOptions(token)
|
||||||
|
),
|
||||||
|
signal: controller.signal
|
||||||
|
})
|
||||||
|
.then(fetchRespHandler)
|
||||||
|
.catch(errorHandler),
|
||||||
|
|
||||||
getVersionLog: async ({vin, ...search}, token) => {
|
getVersionLog: async ({vin, ...search}, token) => {
|
||||||
const u = addQueryParams(`${API_ENDPOINT}/vehicle/${vin}/version/logs`, search);
|
const u = addQueryParams(`${API_ENDPOINT}/vehicle/${vin}/version/logs`, search);
|
||||||
return fetch(u, {
|
return fetch(u, {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export const addQueryParams = (url, params) => {
|
|||||||
const u = new URL(url);
|
const u = new URL(url);
|
||||||
|
|
||||||
Object.keys(params).forEach((key) => u.searchParams.append(key, params[key]));
|
Object.keys(params).forEach((key) => u.searchParams.append(key, params[key]));
|
||||||
|
|
||||||
return u.toString();
|
return u.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
const invalidLocation = 99999;
|
||||||
|
|
||||||
export const ValidateLocationData = (location) => {
|
export const ValidateLocationData = (location) => {
|
||||||
if (Math.abs(location.latitude) > 90 || Math.abs(location.longitude) > 180) {
|
if (Math.abs(location.latitude) > 90 || Math.abs(location.longitude) > 180) {
|
||||||
return false;
|
return false;
|
||||||
@@ -9,6 +11,7 @@ export const ValidateLocationData = (location) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ValidateLocationByParam = (parameter, value) => {
|
export const ValidateLocationByParam = (parameter, value) => {
|
||||||
|
if (invalidLocation === value) return false;
|
||||||
switch (parameter) {
|
switch (parameter) {
|
||||||
case "latitude":
|
case "latitude":
|
||||||
return Math.abs(value) <= 90;
|
return Math.abs(value) <= 90;
|
||||||
|
|||||||
Reference in New Issue
Block a user