114
.github/workflows/deploy-on-demand.yml
vendored
Normal file
114
.github/workflows/deploy-on-demand.yml
vendored
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
name: OTA Portal Deploy - On Demand
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
description: "Environment"
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- dev
|
||||||
|
- stage
|
||||||
|
- preprod
|
||||||
|
|
||||||
|
env:
|
||||||
|
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||||
|
SLACK_CHANNEL: "#cloud-builds"
|
||||||
|
SLACK_FOOTER: ""
|
||||||
|
SLACK_USERNAME: GitHub Actions
|
||||||
|
SLACK_ICON: "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"
|
||||||
|
TAG: ${{ github.sha }}
|
||||||
|
PROJECT: ota-admin-portal
|
||||||
|
REGISTRY: fiskercloud.azurecr.io
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
build-env: ${{ steps.set-env.outputs.ENVIRONMENT }}
|
||||||
|
steps:
|
||||||
|
- name: Slack Notification
|
||||||
|
uses: rtCamp/action-slack-notify@v2
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Azure Login
|
||||||
|
uses: azure/login@v1
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
||||||
|
|
||||||
|
- name: Login to ACR
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
password: ${{ secrets.AZURE_CLIENT_SECRET }}
|
||||||
|
|
||||||
|
- name: Set Env
|
||||||
|
env:
|
||||||
|
ENV: ${{ inputs.environment }}
|
||||||
|
id: set-env
|
||||||
|
run: |
|
||||||
|
case ${ENV} in
|
||||||
|
dev)
|
||||||
|
ENVIRONMENT=dev;;
|
||||||
|
stage)
|
||||||
|
ENVIRONMENT=stg;;
|
||||||
|
preprod)
|
||||||
|
ENVIRONMENT=prd;;
|
||||||
|
*)
|
||||||
|
ENVIRONMENT=dev;;
|
||||||
|
esac
|
||||||
|
echo "ENVIRONMENT=${ENVIRONMENT}" >> $GITHUB_ENV
|
||||||
|
echo "ENVIRONMENT=${ENVIRONMENT}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
build-args: ENVIRONMENT=${{ env.ENVIRONMENT }}
|
||||||
|
push: true
|
||||||
|
tags: ${{ env.REGISTRY }}/${{ env.PROJECT }}:${{ env.TAG }}-${{ env.ENVIRONMENT }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
runs-on: [self-hosted, azure]
|
||||||
|
env:
|
||||||
|
ENVIRONMENT: ${{ needs.build.outputs.build-env }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: rtCamp/action-slack-notify@v2
|
||||||
|
env:
|
||||||
|
MSG_MINIMAL: true
|
||||||
|
SLACK_MESSAGE: "Deploying ${{ env.PROJECT }} to ${{ inputs.environment }}... :partydeploy:"
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
run: |-
|
||||||
|
helm upgrade \
|
||||||
|
--kube-context $ENVIRONMENT \
|
||||||
|
--set image.registry=$REGISTRY \
|
||||||
|
--set image.name=$PROJECT \
|
||||||
|
--set image.tag=$TAG-$ENVIRONMENT \
|
||||||
|
--wait -i -f k8s/values-$ENVIRONMENT.yaml $PROJECT k8s/
|
||||||
|
|
||||||
|
- name: Notify deploy
|
||||||
|
uses: rtCamp/action-slack-notify@v2
|
||||||
|
env:
|
||||||
|
MSG_MINIMAL: true
|
||||||
|
SLACK_MESSAGE: "Successfully deployed ${{ env.PROJECT }} to ${{ inputs.environment }}! :gopher_party:"
|
||||||
|
|
||||||
|
- name: Notify if failure
|
||||||
|
if: ${{ failure() }}
|
||||||
|
uses: rtCamp/action-slack-notify@v2
|
||||||
|
env:
|
||||||
|
SLACK_COLOR: ${{ job.status }}
|
||||||
|
SLACK_MESSAGE: "Failed to deploy ${{ env.PROJECT }} to ${{ inputs.environment }}! :this-is-fine:"
|
||||||
@@ -338,58 +338,12 @@ exports[`Render Render 1`] = `
|
|||||||
<div
|
<div
|
||||||
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12"
|
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12"
|
||||||
>
|
>
|
||||||
<div
|
<ul
|
||||||
class="MuiFormControl-root MuiFormControl-fullWidth"
|
class="MuiList-root MuiList-padding"
|
||||||
>
|
/>
|
||||||
<label
|
<ul
|
||||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated Mui-required Mui-required"
|
class="makeStyles-chipList-0"
|
||||||
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>
|
||||||
<div
|
<div
|
||||||
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12"
|
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import DateFnsUtils from '@date-io/date-fns';
|
import DateFnsUtils from '@date-io/date-fns';
|
||||||
import { Button, Checkbox, Chip, CircularProgress, FormControl, FormControlLabel, Grid, InputLabel, ListItemText, MenuItem, Select } from "@material-ui/core";
|
import { Button, Checkbox, CircularProgress, FormControlLabel, Grid } from "@material-ui/core";
|
||||||
import { KeyboardDatePicker, KeyboardTimePicker, MuiPickersUtilsProvider } from '@material-ui/pickers';
|
import { KeyboardDatePicker, KeyboardTimePicker, MuiPickersUtilsProvider } from '@material-ui/pickers';
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { logger } from "../../../services/monitoring";
|
import { logger } from "../../../services/monitoring";
|
||||||
@@ -7,6 +7,7 @@ import { CANSignalsExportProvider, useCANSignalsExportContext } from "../../Cont
|
|||||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||||
import { useUserContext } from "../../Contexts/UserContext";
|
import { useUserContext } from "../../Contexts/UserContext";
|
||||||
import useStyles from "../../useStyles";
|
import useStyles from "../../useStyles";
|
||||||
|
import { TrieSelect } from '../../Controls/TrieSelect';
|
||||||
|
|
||||||
const MainForm = ({ id }) => {
|
const MainForm = ({ id }) => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
@@ -72,27 +73,19 @@ const MainForm = ({ id }) => {
|
|||||||
setSelectedEndDate(value);
|
setSelectedEndDate(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectedItemsChange = (event) => {
|
|
||||||
const { value } = event.target;
|
|
||||||
if (value.some(item => item === "Select All")) {
|
|
||||||
setSelectAllCanSignals(true);
|
|
||||||
if (selectedCanSignals.length === canSignals.length) {
|
|
||||||
setSelectedCanSignals([]);
|
|
||||||
} else {
|
|
||||||
setSelectedCanSignals(canSignals.map(signal => signal.signal_name));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setSelectAllCanSignals(false);
|
|
||||||
setSelectedCanSignals(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const displayTimeAsGMT = (date) => {
|
const displayTimeAsGMT = (date) => {
|
||||||
return gmtTimezone
|
return gmtTimezone
|
||||||
? date.toLocaleString("en-US", {timeZone: "Etc/GMT"})
|
? date.toLocaleString("en-US", { timeZone: "Etc/GMT" })
|
||||||
: date;
|
: date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (canSignals.length === selectedCanSignals.length) {
|
||||||
|
setSelectAllCanSignals(true);
|
||||||
|
} else {
|
||||||
|
setSelectAllCanSignals(false);
|
||||||
|
}
|
||||||
|
}, [canSignals, selectedCanSignals, setSelectAllCanSignals]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.paper}>
|
<div className={classes.paper}>
|
||||||
@@ -175,36 +168,12 @@ const MainForm = ({ id }) => {
|
|||||||
</MuiPickersUtilsProvider>
|
</MuiPickersUtilsProvider>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<FormControl fullWidth required>
|
<TrieSelect
|
||||||
<InputLabel id="select-can-signals-label">Select CAN signals</InputLabel>
|
label="All CAN Signals"
|
||||||
<Select
|
classification="Signals"
|
||||||
labelId="select-can-signals-label"
|
options={canSignals.map((signal => signal.signal_name))}
|
||||||
id="select-can-signals"
|
onChange={setSelectedCanSignals}
|
||||||
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>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<MenuItem value="Select All">
|
|
||||||
<Checkbox checked={selectedCanSignals.length === canSignals.length} />
|
|
||||||
<ListItemText primary="Select All" />
|
|
||||||
</MenuItem>
|
|
||||||
{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>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -112,10 +112,10 @@ export const VehicleProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLocationsVehiclePaths = async (token, vinsParam) => {
|
const getLocationsVehiclePaths = async (token, param, vins) => {
|
||||||
try {
|
try {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
const result = await api.getLocationsVehiclePaths(token, vinsParam);
|
const result = await api.getLocationsVehiclePaths(token, param, vins);
|
||||||
if (result.error)
|
if (result.error)
|
||||||
throw new Error(`Get locations vehicle paths error. ${result.message}`);
|
throw new Error(`Get locations vehicle paths error. ${result.message}`);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ const SendDiagnosticCommand = ({ vin, token, classes }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (!vin) return;
|
if (!vin) return;
|
||||||
const result = await getECUs({ vin }, token)
|
const unique = true;
|
||||||
|
const result = await getECUs({ vin, unique }, token)
|
||||||
sortECUs(result.data)
|
sortECUs(result.data)
|
||||||
result.data.push({ ecu: "TBOX" })
|
result.data.push({ ecu: "TBOX" })
|
||||||
setCurrentECU(result.data[0].ecu)
|
setCurrentECU(result.data[0].ecu)
|
||||||
@@ -70,7 +71,7 @@ const SendDiagnosticCommand = ({ vin, token, classes }) => {
|
|||||||
|
|
||||||
const TREX_MIN_VER = "1.1.141";
|
const TREX_MIN_VER = "1.1.141";
|
||||||
const isRemoteResetSupported = () => {
|
const isRemoteResetSupported = () => {
|
||||||
return !carState?.trex_version ? true : cmp(carState.trex_version, TREX_MIN_VER) >= 0;
|
return !carState?.trex_version ? true : cmp(carState.trex_version, TREX_MIN_VER) >= 0 || carState.trex_version.includes("dev");
|
||||||
};
|
};
|
||||||
|
|
||||||
const clickHandler = async (_) => {
|
const clickHandler = async (_) => {
|
||||||
|
|||||||
183
src/components/Controls/TrieSelect/TrieSelect.jsx
Normal file
183
src/components/Controls/TrieSelect/TrieSelect.jsx
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Checkbox,
|
||||||
|
Chip,
|
||||||
|
Collapse,
|
||||||
|
Divider,
|
||||||
|
FormControlLabel,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemSecondaryAction,
|
||||||
|
ListItemText,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import ExpandLess from '@material-ui/icons/ExpandLess';
|
||||||
|
import ExpandMore from '@material-ui/icons/ExpandMore';
|
||||||
|
import { Trie } from "./trie";
|
||||||
|
import { TrieSelectProvider, useTrieSelect } from "./TrieSelectContext";
|
||||||
|
import useStyles from "../../useStyles";
|
||||||
|
|
||||||
|
export const TrieSelect = ({
|
||||||
|
label,
|
||||||
|
classification,
|
||||||
|
onChange = () => { },
|
||||||
|
options = [],
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TrieSelectProvider onChange={onChange}>
|
||||||
|
<TrieSelectList
|
||||||
|
label={label}
|
||||||
|
classification={classification}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
</TrieSelectProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TrieSelectList = ({
|
||||||
|
label,
|
||||||
|
classification,
|
||||||
|
options,
|
||||||
|
}) => {
|
||||||
|
const { selected, remove } = useTrieSelect();
|
||||||
|
const classes = useStyles();
|
||||||
|
const trie = new Trie(options);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<List>
|
||||||
|
<TrieSelectLevel
|
||||||
|
node={trie.getRoot()}
|
||||||
|
classification={classification}
|
||||||
|
label={label}
|
||||||
|
/>
|
||||||
|
</List>
|
||||||
|
<ul className={classes.chipList}>
|
||||||
|
{selected.map((signal) => {
|
||||||
|
return (
|
||||||
|
<li key={signal}>
|
||||||
|
<Chip label={signal} onDelete={() => remove([signal])} />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const TrieSelectLevel = ({
|
||||||
|
prefix = "",
|
||||||
|
node,
|
||||||
|
children,
|
||||||
|
classification,
|
||||||
|
label,
|
||||||
|
level = -1,
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const { selected, add, remove } = useTrieSelect();
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const completeChildren = Object.values(node.children).filter(child => child.isComplete);
|
||||||
|
const hasCompleteChildren = completeChildren.length > 0;
|
||||||
|
const descendantCount = `${node.count} ${classification}`;
|
||||||
|
const isParentOfMultiple = completeChildren.length > 1;
|
||||||
|
const isAdoptiveParentOfMultiple = completeChildren.length <= 1 && node.count > 1;
|
||||||
|
|
||||||
|
const handleExpand = () => {
|
||||||
|
setOpen(open => !open);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheck = (names = [], checked) => {
|
||||||
|
if (checked) {
|
||||||
|
remove(names);
|
||||||
|
} else {
|
||||||
|
add(names)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getWords = (node, prepend = prefix, result = new Set()) => {
|
||||||
|
if (prepend === prefix && node.isComplete) result.add(prepend);
|
||||||
|
|
||||||
|
for (const child of Object.values(node.children)) {
|
||||||
|
const word = (prepend ? prepend + "_" : "") + child.data;
|
||||||
|
if (child.isComplete) {
|
||||||
|
result.add(word);
|
||||||
|
}
|
||||||
|
if (child.children) {
|
||||||
|
getWords(child, word, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const indent = (level) => `${12 * level}px`;
|
||||||
|
|
||||||
|
const listItems = (level) => (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
{Object.values(node.children).map((child) => {
|
||||||
|
const fullName = (prefix ? prefix + "_" : "") + child.data;
|
||||||
|
const isChecked = selected.includes(fullName);
|
||||||
|
return (
|
||||||
|
<TrieSelectLevel
|
||||||
|
prefix={fullName}
|
||||||
|
node={child}
|
||||||
|
key={fullName}
|
||||||
|
classification={classification}
|
||||||
|
level={level}
|
||||||
|
>
|
||||||
|
{child.isComplete && (
|
||||||
|
<ListItem className={classes.whiteBackground}>
|
||||||
|
<Box sx={{ paddingLeft: indent(level), display: "flex", alignItems: "center" }}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Checkbox checked={isChecked} onClick={() => handleCheck([fullName], isChecked)} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={fullName} />
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
</TrieSelectLevel>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isParentOfMultiple || isAdoptiveParentOfMultiple) {
|
||||||
|
const allDescendants = getWords(node, prefix);
|
||||||
|
const isSelectAll = allDescendants.every(descendant => selected.includes(descendant));
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ListItem onClick={handleExpand} divider className={classes.defaultBackground}>
|
||||||
|
<Box sx={{ paddingLeft: indent(level) }}>
|
||||||
|
<ListItemText primary={prefix === "" ? label : prefix} secondary={descendantCount} />
|
||||||
|
</Box>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", gap: "16px" }}>
|
||||||
|
<FormControlLabel
|
||||||
|
label="Select All"
|
||||||
|
labelPlacement="start"
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={isSelectAll}
|
||||||
|
onClick={() => handleCheck(allDescendants, isSelectAll)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{open ? (<ExpandLess />) : (<ExpandMore />)}
|
||||||
|
</Box>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
<Collapse in={open}>
|
||||||
|
<List>
|
||||||
|
{listItems(level + 1)}
|
||||||
|
</List>
|
||||||
|
{hasCompleteChildren && <Divider />}
|
||||||
|
</Collapse>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return listItems(level);
|
||||||
|
}
|
||||||
54
src/components/Controls/TrieSelect/TrieSelect.test.tsx
Normal file
54
src/components/Controls/TrieSelect/TrieSelect.test.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { render, waitFor } from "@testing-library/react";
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
|
import { TrieSelect } from ".";
|
||||||
|
import addSnapshotSerializer from "../../../utils/snapshot";
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
"ace",
|
||||||
|
"ace_bev_one",
|
||||||
|
"ace_bev_two",
|
||||||
|
"ace_bev_three",
|
||||||
|
"bev",
|
||||||
|
"bev_chaz_deep",
|
||||||
|
"bev_chaz_deep_one",
|
||||||
|
"bev_chaz_deep_two",
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("TrieSelect", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
addSnapshotSerializer(expect);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Render", async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<TrieSelect
|
||||||
|
label={"The button label"}
|
||||||
|
classification="Signal"
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
/* render */
|
||||||
|
});
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("properly passes payload to callback", async () => {
|
||||||
|
const mockCallback = jest.fn();
|
||||||
|
|
||||||
|
const { getAllByText } = render(
|
||||||
|
<TrieSelect
|
||||||
|
label={"The input label"}
|
||||||
|
classification="Signal"
|
||||||
|
options={options}
|
||||||
|
onChange={mockCallback}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectAll = getAllByText("Select All")[0];
|
||||||
|
userEvent.click(selectAll);
|
||||||
|
expect(mockCallback).toHaveBeenCalledWith(options);
|
||||||
|
});
|
||||||
|
});
|
||||||
38
src/components/Controls/TrieSelect/TrieSelectContext.js
Normal file
38
src/components/Controls/TrieSelect/TrieSelectContext.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { createContext, useState, useContext, useEffect } from "react";
|
||||||
|
|
||||||
|
const TrieSelectContext = createContext();
|
||||||
|
|
||||||
|
export const useTrieSelect = () => {
|
||||||
|
const context = useContext(TrieSelectContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useTrieSelect must be used within a TrieSelectProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TrieSelectProvider = ({ children, onChange }) => {
|
||||||
|
const [selected, setSelected] = useState(new Set());
|
||||||
|
|
||||||
|
const add = (ids) => setSelected(prev => new Set([...prev, ...ids]));
|
||||||
|
const remove = (ids) => setSelected(prev => {
|
||||||
|
for (const id of ids) {
|
||||||
|
prev.delete(id);
|
||||||
|
}
|
||||||
|
return new Set(prev);
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChange(Array.from(selected));
|
||||||
|
}, [onChange, selected]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TrieSelectContext.Provider value={{
|
||||||
|
selected: Array.from(selected),
|
||||||
|
add,
|
||||||
|
remove
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</TrieSelectContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
1
src/components/Controls/TrieSelect/index.js
Normal file
1
src/components/Controls/TrieSelect/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./TrieSelect";
|
||||||
62
src/components/Controls/TrieSelect/trie.js
Normal file
62
src/components/Controls/TrieSelect/trie.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
class Node {
|
||||||
|
#count
|
||||||
|
|
||||||
|
constructor(data, isComplete = false) {
|
||||||
|
this.data = data;
|
||||||
|
this.children = {};
|
||||||
|
this.isComplete = isComplete;
|
||||||
|
this.#count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get count() {
|
||||||
|
return this.#count;
|
||||||
|
}
|
||||||
|
|
||||||
|
incrementCount() {
|
||||||
|
this.#count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Trie {
|
||||||
|
constructor(words = []) {
|
||||||
|
this.root = new Node();
|
||||||
|
this.populate(words);
|
||||||
|
}
|
||||||
|
|
||||||
|
populate(words) {
|
||||||
|
for (const word of words) {
|
||||||
|
this.add(word);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add(parts, node = this.root) {
|
||||||
|
if (typeof parts === "string") {
|
||||||
|
parts = parts.split("_");
|
||||||
|
}
|
||||||
|
|
||||||
|
node.incrementCount();
|
||||||
|
|
||||||
|
if (parts.length === 0) {
|
||||||
|
node.isComplete = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const part = parts.shift();
|
||||||
|
|
||||||
|
if (node.children[part]) {
|
||||||
|
this.add(parts, node.children[part]);
|
||||||
|
} else {
|
||||||
|
const newNode = new Node(part);
|
||||||
|
node.children[part] = newNode;
|
||||||
|
this.add(parts, newNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoot() {
|
||||||
|
return this.root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Trie,
|
||||||
|
};
|
||||||
24
src/components/Controls/TrieSelect/trie.test.js
Normal file
24
src/components/Controls/TrieSelect/trie.test.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Trie } from "./trie";
|
||||||
|
|
||||||
|
describe("Trie", () => {
|
||||||
|
it("adds words from instantiation", () => {
|
||||||
|
const trie = new Trie(["AAA_BBB_CCC"]);
|
||||||
|
const { children: { AAA: root } } = trie.getRoot();
|
||||||
|
|
||||||
|
expect(root.data).toBe("AAA");
|
||||||
|
expect(root.children["BBB"].data).toBe("BBB");
|
||||||
|
expect(root.children["BBB"].isComplete).toBe(false);
|
||||||
|
expect(root.children["BBB"].children["CCC"].data).toBe("CCC");
|
||||||
|
expect(root.children["BBB"].children["CCC"].isComplete).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds words with add method", () => {
|
||||||
|
const trie = new Trie();
|
||||||
|
trie.add("AAA_BBB_CCC");
|
||||||
|
const { children: { AAA: root } } = trie.getRoot();
|
||||||
|
|
||||||
|
expect(root.data).toBe("AAA");
|
||||||
|
expect(root.children["BBB"].data).toBe("BBB");
|
||||||
|
expect(root.children["BBB"].children["CCC"].data).toBe("CCC");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -44,16 +44,9 @@ const ComponentVehiclePathsMap = (props) => {
|
|||||||
|
|
||||||
const retrieveAndStoreLocations = (accessToken) => {
|
const retrieveAndStoreLocations = (accessToken) => {
|
||||||
let vinsToShowOnMap = [...props.vinsToShowOnMapColors.keys()];
|
let vinsToShowOnMap = [...props.vinsToShowOnMapColors.keys()];
|
||||||
let vinsParam = ""
|
let param = "lookback_hours=" + props.lookbackHours
|
||||||
for (let vinToShowOnMap of vinsToShowOnMap) {
|
|
||||||
vinsParam += "vins="
|
|
||||||
vinsParam += vinToShowOnMap
|
|
||||||
vinsParam += "&"
|
|
||||||
}
|
|
||||||
vinsParam += "lookback_hours="
|
|
||||||
vinsParam += props.lookbackHours
|
|
||||||
|
|
||||||
return getLocationsVehiclePaths(accessToken, vinsParam)
|
return getLocationsVehiclePaths(accessToken, param, vinsToShowOnMap)
|
||||||
.then(async (result) => {
|
.then(async (result) => {
|
||||||
let resultArray = Object.entries(result)
|
let resultArray = Object.entries(result)
|
||||||
const points = []
|
const points = []
|
||||||
|
|||||||
@@ -274,6 +274,7 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
},
|
},
|
||||||
whiteBackground: { backgroundColor: "White" },
|
whiteBackground: { backgroundColor: "White" },
|
||||||
|
defaultBackground: { backgroundColor: "#fafafa" },
|
||||||
progressIcon: { width: 40, height: 40 },
|
progressIcon: { width: 40, height: 40 },
|
||||||
progressSuccess: { color: "green" },
|
progressSuccess: { color: "green" },
|
||||||
progressError: { color: "red" },
|
progressError: { color: "red" },
|
||||||
@@ -313,6 +314,13 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "12px",
|
gap: "12px",
|
||||||
},
|
},
|
||||||
|
chipList: {
|
||||||
|
display: "flex",
|
||||||
|
gap: "4px 8px",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
listStyleType: "none",
|
||||||
|
paddingLeft: 0,
|
||||||
|
},
|
||||||
flex: {
|
flex: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -89,13 +89,14 @@ const vehiclesAPI = {
|
|||||||
.then(fetchRespHandler)
|
.then(fetchRespHandler)
|
||||||
.catch(errorHandler),
|
.catch(errorHandler),
|
||||||
|
|
||||||
getLocationsVehiclePaths: async (token, vinsParam) =>
|
getLocationsVehiclePaths: async (token, param, vins) =>
|
||||||
fetch(`${API_ENDPOINT}/vehicle_paths?${vinsParam}`, {
|
fetch(`${API_ENDPOINT}/vehicle_paths?${param}`, {
|
||||||
method: "GET",
|
method: "POST",
|
||||||
headers: Object.assign(
|
headers: Object.assign(
|
||||||
{ "Content-Type": "application/json" },
|
{ "Content-Type": "application/json" },
|
||||||
getAuthHeaderOptions(token)
|
getAuthHeaderOptions(token)
|
||||||
),
|
),
|
||||||
|
body: JSON.stringify({ vins: vins }),
|
||||||
})
|
})
|
||||||
.then(fetchRespHandler)
|
.then(fetchRespHandler)
|
||||||
.catch(errorHandler),
|
.catch(errorHandler),
|
||||||
|
|||||||
Reference in New Issue
Block a user