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
|
||||
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=""
|
||||
<ul
|
||||
class="MuiList-root MuiList-padding"
|
||||
/>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSelect-icon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M7 10l5 5 5-5z"
|
||||
<ul
|
||||
class="makeStyles-chipList-0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 React, { useEffect, useState } from "react";
|
||||
import { logger } from "../../../services/monitoring";
|
||||
@@ -7,6 +7,7 @@ import { CANSignalsExportProvider, useCANSignalsExportContext } from "../../Cont
|
||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||
import { useUserContext } from "../../Contexts/UserContext";
|
||||
import useStyles from "../../useStyles";
|
||||
import { TrieSelect } from '../../Controls/TrieSelect';
|
||||
|
||||
const MainForm = ({ id }) => {
|
||||
const classes = useStyles();
|
||||
@@ -72,27 +73,19 @@ const MainForm = ({ id }) => {
|
||||
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) => {
|
||||
return gmtTimezone
|
||||
? date.toLocaleString("en-US", { timeZone: "Etc/GMT" })
|
||||
: date;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (canSignals.length === selectedCanSignals.length) {
|
||||
setSelectAllCanSignals(true);
|
||||
} else {
|
||||
setSelectAllCanSignals(false);
|
||||
}
|
||||
}, [canSignals, selectedCanSignals, setSelectAllCanSignals]);
|
||||
|
||||
return (
|
||||
<div className={classes.paper}>
|
||||
@@ -175,36 +168,12 @@ const MainForm = ({ id }) => {
|
||||
</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>
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
<TrieSelect
|
||||
label="All CAN Signals"
|
||||
classification="Signals"
|
||||
options={canSignals.map((signal => signal.signal_name))}
|
||||
onChange={setSelectedCanSignals}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Button
|
||||
|
||||
@@ -112,10 +112,10 @@ export const VehicleProvider = ({ children }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getLocationsVehiclePaths = async (token, vinsParam) => {
|
||||
const getLocationsVehiclePaths = async (token, param, vins) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
const result = await api.getLocationsVehiclePaths(token, vinsParam);
|
||||
const result = await api.getLocationsVehiclePaths(token, param, vins);
|
||||
if (result.error)
|
||||
throw new Error(`Get locations vehicle paths error. ${result.message}`);
|
||||
return result;
|
||||
|
||||
@@ -42,7 +42,8 @@ const SendDiagnosticCommand = ({ vin, token, classes }) => {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!vin) return;
|
||||
const result = await getECUs({ vin }, token)
|
||||
const unique = true;
|
||||
const result = await getECUs({ vin, unique }, token)
|
||||
sortECUs(result.data)
|
||||
result.data.push({ ecu: "TBOX" })
|
||||
setCurrentECU(result.data[0].ecu)
|
||||
@@ -70,7 +71,7 @@ const SendDiagnosticCommand = ({ vin, token, classes }) => {
|
||||
|
||||
const TREX_MIN_VER = "1.1.141";
|
||||
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 (_) => {
|
||||
|
||||
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) => {
|
||||
let vinsToShowOnMap = [...props.vinsToShowOnMapColors.keys()];
|
||||
let vinsParam = ""
|
||||
for (let vinToShowOnMap of vinsToShowOnMap) {
|
||||
vinsParam += "vins="
|
||||
vinsParam += vinToShowOnMap
|
||||
vinsParam += "&"
|
||||
}
|
||||
vinsParam += "lookback_hours="
|
||||
vinsParam += props.lookbackHours
|
||||
let param = "lookback_hours=" + props.lookbackHours
|
||||
|
||||
return getLocationsVehiclePaths(accessToken, vinsParam)
|
||||
return getLocationsVehiclePaths(accessToken, param, vinsToShowOnMap)
|
||||
.then(async (result) => {
|
||||
let resultArray = Object.entries(result)
|
||||
const points = []
|
||||
|
||||
@@ -274,6 +274,7 @@ const useStyles = makeStyles((theme) => ({
|
||||
maxWidth: "100%",
|
||||
},
|
||||
whiteBackground: { backgroundColor: "White" },
|
||||
defaultBackground: { backgroundColor: "#fafafa" },
|
||||
progressIcon: { width: 40, height: 40 },
|
||||
progressSuccess: { color: "green" },
|
||||
progressError: { color: "red" },
|
||||
@@ -313,6 +314,13 @@ const useStyles = makeStyles((theme) => ({
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
},
|
||||
chipList: {
|
||||
display: "flex",
|
||||
gap: "4px 8px",
|
||||
flexWrap: "wrap",
|
||||
listStyleType: "none",
|
||||
paddingLeft: 0,
|
||||
},
|
||||
flex: {
|
||||
display: "flex",
|
||||
},
|
||||
|
||||
@@ -89,13 +89,14 @@ const vehiclesAPI = {
|
||||
.then(fetchRespHandler)
|
||||
.catch(errorHandler),
|
||||
|
||||
getLocationsVehiclePaths: async (token, vinsParam) =>
|
||||
fetch(`${API_ENDPOINT}/vehicle_paths?${vinsParam}`, {
|
||||
method: "GET",
|
||||
getLocationsVehiclePaths: async (token, param, vins) =>
|
||||
fetch(`${API_ENDPOINT}/vehicle_paths?${param}`, {
|
||||
method: "POST",
|
||||
headers: Object.assign(
|
||||
{ "Content-Type": "application/json" },
|
||||
getAuthHeaderOptions(token)
|
||||
),
|
||||
body: JSON.stringify({ vins: vins }),
|
||||
})
|
||||
.then(fetchRespHandler)
|
||||
.catch(errorHandler),
|
||||
|
||||
Reference in New Issue
Block a user