CEC-3514 create can self serve page (#288)

* first push

* fix snapshot

* remove unused vars

* update snap

* remove some console logs

* Remove snapshot

* Update

* CEC-3770 Update cert expire text (#282)

* CEC-3577: fetch T.Rex log from the cloud (#283)

* CEC-3577: fetch T.Rex log from the cloud

* tabs?

* CSS

* smells

* fix smells and warnings

* suggestions

* CEC-3577 Style tweak (#284)

* CEC-3577: trex logs (#285)

* CEC-3577: trex logs

add filtering
add progress bar for log fetching
always fetch all the logs
request canceling

* don't hide progress

* CEC-3751, CEC-3478 misc window status and invalid location value (#287)

* CEC-3751 misc window status
CEC-3478 invalid location value

* Fix snapshot
Update browser list

* merge develop update snap

* resolve comments

* add date and time picker seperately, use checkbox for dropdown

* added verification for date and fixed time picker

* fix snap

* resolve comments

* removed small bug

* tweak layout

* added snap shot test for can signals

* small change

* Fix test

* fix sms snap

* change function name

* mock can signals api

* resolved comments

* fix ci

* Clean up

---------

Co-authored-by: jwu-fisker <jwu@fiskerinc.com>
Co-authored-by: John Wu <76966357+jwu-fisker@users.noreply.github.com>
Co-authored-by: Eduard Voronkin <116690094+eduardvoronkin@users.noreply.github.com>
This commit is contained in:
das31
2023-03-10 00:13:27 -05:00
committed by GitHub
parent 7b6a2bfa11
commit 324e3d2b91
21 changed files with 1059 additions and 39 deletions

40
package-lock.json generated
View File

@@ -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",
@@ -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",
@@ -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",

View File

@@ -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",

View File

@@ -9911,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"
@@ -10148,6 +10166,12 @@ exports[`App Route /vehicle-status authenticated 1`] = `
id="tabpanel-8" id="tabpanel-8"
role="tabpanel" role="tabpanel"
/> />
<div
aria-labelledby="tab-9"
hidden=""
id="tabpanel-9"
role="tabpanel"
/>
</div> </div>
</main> </main>
</main> </main>

View File

@@ -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>
`;

View 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;

View 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();
});
});

View 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;

View File

@@ -195,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"
@@ -376,6 +394,12 @@ exports[`CarStatus Render 1`] = `
id="tabpanel-8" id="tabpanel-8"
role="tabpanel" role="tabpanel"
/> />
<div
aria-labelledby="tab-9"
hidden=""
id="tabpanel-9"
role="tabpanel"
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -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,7 +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" import TRexLogsTab from "./TRexLogsTab";
const tabHashes = ["details", "updates", "filters"]; const tabHashes = ["details", "updates", "filters"];
@@ -60,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) => {

View File

@@ -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(

View File

@@ -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);

View 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);

View File

@@ -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);

View File

@@ -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(),
});

View File

@@ -8,12 +8,12 @@ import {
TablePagination, TablePagination,
TableRow TableRow
} from "@material-ui/core"; } from "@material-ui/core";
import LinearProgress from '@mui/material/LinearProgress';
import Checkbox from '@mui/material/Checkbox'; import Checkbox from '@mui/material/Checkbox';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormControl from '@mui/material/FormControl'; 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 FormLabel from '@mui/material/FormLabel';
import LinearProgress from '@mui/material/LinearProgress';
import { import {
KeyboardDatePicker, MuiPickersUtilsProvider KeyboardDatePicker, MuiPickersUtilsProvider
@@ -134,7 +134,6 @@ const TRexLogsTable = ({ vin, token, classes }) => {
let controller = new AbortController() let controller = new AbortController()
const readBlob = async (offset, count) => { const readBlob = async (offset, count) => {
console.log(`reading from offset: ${offset}`)
return await api.getTRexLogs(vin, fromatDateForRequest(selectedDate), offset, count, "UP", token, controller) return await api.getTRexLogs(vin, fromatDateForRequest(selectedDate), offset, count, "UP", token, controller)
} }
const getDesiredSize = () => { const getDesiredSize = () => {
@@ -156,10 +155,8 @@ const TRexLogsTable = ({ vin, token, classes }) => {
fetched.error = result.error fetched.error = result.error
break break
} }
console.log(`ret.RealOffset ${result.RealOffset}\nret.bytesRead ${result.bytesRead}\ndesired offset ${offset}\ndesired read size ${readSize}\nblobsize: ${result.blobSize}`)
setBlobSize(result.blobSize) setBlobSize(result.blobSize)
readSize *= 2 readSize *= 2
console.log(`new read size: ${readSize}`)
offset = result.RealOffset + result.bytesRead offset = result.RealOffset + result.bytesRead
setCurrentOffset(offset) setCurrentOffset(offset)
fetched = transformLogs(result.data).concat(fetched) fetched = transformLogs(result.data).concat(fetched)
@@ -179,7 +176,6 @@ const TRexLogsTable = ({ vin, token, classes }) => {
setTotal(0) setTotal(0)
const msg = `Cannot fetch logs for ${fromatDateForRequest(selectedDate)}` const msg = `Cannot fetch logs for ${fromatDateForRequest(selectedDate)}`
setMessage(msg) setMessage(msg)
console.log(`${msg}, Cloud error:\n${fetched.error}`);
return return
} }
setCurrentOffset(offset) setCurrentOffset(offset)
@@ -191,9 +187,7 @@ const TRexLogsTable = ({ vin, token, classes }) => {
try { try {
if (!vin || !token) return; if (!vin || !token) return;
const desiredSize = getDesiredSize() const desiredSize = getDesiredSize()
console.log(`desired size: ${desiredSize}, actual size: ${logs.length}`)
if (desiredSize < logs.length || allLogsFetched) { if (desiredSize < logs.length || allLogsFetched) {
console.log(`enough logs in cache`)
setTotal(getFilteredLogs(logs).length) setTotal(getFilteredLogs(logs).length)
return return
} }
@@ -228,7 +222,6 @@ const TRexLogsTable = ({ vin, token, classes }) => {
const handleNewFilter = (event) => { const handleNewFilter = (event) => {
setPageIndex(0) setPageIndex(0)
console.log(event)
setCurrentLogLevels({ setCurrentLogLevels({
...currectLogLevels, ...currectLogLevels,
[event.target.defaultValue]: event.target.checked, [event.target.defaultValue]: event.target.checked,

View File

@@ -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>
`;

View File

@@ -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";

View File

@@ -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 {BrowserRouter} from "react-router-dom";
import {UserProvider, setToken} from "../../Contexts/UserContext";
import {StatusProvider} from "../../Contexts/StatusContext";
import addSnapshotSerializer from "../../../utils/snapshot";
import { render, waitFor } from "@testing-library/react"; import { render, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import addSnapshotSerializer from "../../../utils/snapshot";
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();
}); });
}); });

View 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;

View 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;

View File

@@ -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();
}; };