CEC-1402 Merge to production (#148)

* Fix template function (#105)

* CEC-638 Add EK test ECU (#106)

* CEC-638 Add EK ECU

* Update test

* CEC-638 Should be EKS (#107)

* Should be EKS

* Update snapshot

* CEC-624 Display update status info and ECU (#108)

* Diplay ECU name in update status (#110)

Optimize car update status progress control
Remove car update status page test
Replace with individual component tests

* Handle case ECU is not in message (#111)

* Refresh button label (#112)

* Update ECU refresh button label

* Update snapshot

* remove

* CEC-660 Fix release notes field (#113)

* CEC-775 Manifest details component (#114)

* CEC-775 Manifest details component

* Code smells

* Fix build warning

* CEC-1050 New manifest format (#117)

* CEC-1050 Manifest changes

* Fix delete bug

* Add approve update button

* Code smell

* Remove update approval

* CEC-464 can filters forms (#118)

* can filters forms and lists

* unit tests

* updating warnings and tests

* merge develop

* fixed snapshots

* update jest mocks

* updating tests

* CEC-1050 Self download indicator (#119)

* CEC-1160 Fix package warnings (#121)

* CEC-1160 Last dependabot fix (#122)

* CEC-1058 fleet forms (#123)

* working fleets page

* unit tests

* snapshots

* updating messages and snapshots

* updating extraneous snaps

* Update codeowners (#125)

* CEC-1167 ota admin portal (#127)

* Add test coverage script

* Remove unnecessary check

* CEC-1167 unit test and code coverage

* included sonar job

* updated the workflow

* updated sonar properties

* updated sonar properties

* updated sonar properties

* updated sonar properties

* updated sonar properties

* updated sonar properties

* updated sonar properties

* updated sonar properties

Co-authored-by: jwu-fisker <jwu@fiskerinc.com>

* CEC-1167 implementing ths coverage thresold (#128)

* CEC-1216 Remove unused components (#129)

* CEC-1216 Remove unused components

* Remove import

* CEC-1183/CEC-1201 fleet vehicles forms (#130)

* working fleet vehicles forms

* snapshots and api tests

* CEC-1182 fleet filter forms (#131)

* forms for fleet can filters

* unit tests for fleet filters

* removing warnings

* updating regex

* CEC-532 Display manifest file properties (#133)

* CEC-532 Display update file properties

* npm audit fix

* CEC-1317 npm update (#134)

* CEC-1320 Update for memory regions (#135)

* CEC-1320 Update for memory regions

* Clean up

* CEC-1256/CEC-1330 data logger for vehicles/fleets and details tabs for vehicles/fleets (#136)

* forms for fleet can filters

* unit tests for fleet filters

* removing warnings

* updating regex

* added fleet details page

* fleet pages

* smoothed out bugs

* fleets done

* working update, delete vehicles

* finished mocks, still need snapshots and context tests

* contexts done

* snapshot tests

* updating code smells

* smells

* CEC-1256/CEC-1330 fixing filters length function (#137)

* fixing filters length function

* adding filters testing

* code smell

* code smells

* bug

* CEC-1387 superset integration and removal of grafana (#138)

* replace grafana with superset

* updating snapshots

* CEC-1316 azure migration (#140)

* test portal azure

* :doh:

* runner

* WIP

* values

* letsencrypt + docker cache

* stg/prd

* portal things

* cleanup

* split build/deploy + temp stage deploy

* :doh:

* try this

* and prod

* this works for now, can improve later

* no need to specify azure anymore

Co-authored-by: Drew Taylor <69828061+drew-fisker@users.noreply.github.com>

* CEC-1369 Fix display of update error (#139)

* CEC-1369 Fix display of update error

* Update snapshot

* CEC-749 Generate cert UI (#141)

* Add Create Certificate page

* Tests

* Update permission check

* Use Azure

* CEC-1387 updating superset dns names (#142)

* updating superset dns names

* updating snapshots

* Fix (#143)

* CEC-749 Fix types (#144)

* Merge branch 'develop'

Co-authored-by: Drew Taylor <69828061+drew-fisker@users.noreply.github.com>
Co-authored-by: venkats09 <97122017+venkats09@users.noreply.github.com>
Co-authored-by: Rafi Greenberg <72412693+rafi-fisker@users.noreply.github.com>
This commit is contained in:
John Wu
2022-04-19 15:51:36 -07:00
committed by GitHub
parent 97b215ec35
commit d4134141a4
158 changed files with 23912 additions and 14284 deletions

View File

@@ -1,13 +1,5 @@
REACT_APP_AUTH_SERVICE_URL=https://gw-dev.fiskerdps.com/compute_auth
REACT_APP_UPLOAD_SERVICE_URL=https://gw-dev.fiskerdps.com/ota_update
REACT_APP_AUTH_CALLBACK_URL=https://dev-ota-admin.fiskerdps.com
REACT_APP_GRAFANA_BASE_URL=https://dev-grafana.fiskerdps.com
REACT_APP_GRAFANA_HOME_CHART_PATH=/d-solo/1VTVJ_qGk/dashboard?orgId=2&refresh=30s&panelId=12
REACT_APP_GRAFANA_VOLTAGE_CHART_PATH=/d-solo/LVI-aQGnz/diagnostics?orgId=2&var-VIN={vin}&var-Signal=BMS_CellVolt{cellNum}&panelId=2
REACT_APP_GRAFANA_CELLTEMP_CHART_PATH=/d-solo/LVI-aQGnz/diagnostics?orgId=2&var-VIN={vin}&var-Signal=BMS_CellT{cellNum}&panelId=2
REACT_APP_GRAFANA_BATTERYTEMP_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&refresh=1m&panelId=4
REACT_APP_GRAFANA_BATTERYCAP_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&refresh=1m&panelId=6
REACT_APP_GRAFANA_BATTERYPERCENT_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&panelId=12
REACT_APP_GRAFANA_BATTERY12VPERCENT_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&refresh=1m&panelId=2
REACT_APP_GRAFANA_BATTERY12VVOLTAGE_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&refresh=1m&panelId=9
REACT_APP_GRAFANA_API=https://dev-grafana.fiskerdps.com/api/datasources/proxy/2
REACT_APP_CERT_SERVICE_URL=https://dev-gw.cloud.fiskerinc.com/certificate
REACT_APP_AUTH_SERVICE_URL=https://dev-gw.cloud.fiskerinc.com/compute_auth
REACT_APP_UPLOAD_SERVICE_URL=https://dev-gw.cloud.fiskerinc.com/ota_update
REACT_APP_AUTH_CALLBACK_URL=https://dev-ota-admin.cloud.fiskerinc.com
REACT_APP_SUPERSET_URL=http://superset-dev.fiskercloud.internal

View File

@@ -1,13 +1,5 @@
REACT_APP_AUTH_SERVICE_URL=http://localhost/compute_auth
REACT_APP_CERT_SERVICE_URL=http://localhost/certificate
REACT_APP_UPLOAD_SERVICE_URL=http://localhost/ota_update
REACT_APP_AUTH_CALLBACK_URL=http://localhost:3000
REACT_APP_GRAFANA_BASE_URL=https://dev-grafana.fiskerdps.com
REACT_APP_GRAFANA_HOME_CHART_PATH=/d-solo/1VTVJ_qGk/dashboard?orgId=2&refresh=30s&panelId=12
REACT_APP_GRAFANA_VOLTAGE_CHART_PATH=/d-solo/LVI-aQGnz/diagnostics?orgId=2&var-VIN={vin}&var-Signal=BMS_CellVolt{cellNum}&panelId=2
REACT_APP_GRAFANA_CELLTEMP_CHART_PATH=/d-solo/LVI-aQGnz/diagnostics?orgId=2&var-VIN={vin}&var-Signal=BMS_CellT{cellNum}&panelId=2
REACT_APP_GRAFANA_BATTERYTEMP_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&refresh=1m&panelId=4
REACT_APP_GRAFANA_BATTERYCAP_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&refresh=1m&panelId=6
REACT_APP_GRAFANA_BATTERYPERCENT_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&panelId=12
REACT_APP_GRAFANA_BATTERY12VPERCENT_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&refresh=1m&panelId=2
REACT_APP_GRAFANA_BATTERY12VVOLTAGE_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&refresh=1m&panelId=9
REACT_APP_GRAFANA_API=https://dev-grafana.fiskerdps.com/api/datasources/proxy/2
REACT_APP_SUPERSET_URL=http://superset-dev.fiskercloud.internal

View File

@@ -1,13 +1,5 @@
REACT_APP_AUTH_SERVICE_URL=https://gw.fiskerdps.com/compute_auth
REACT_APP_UPLOAD_SERVICE_URL=https://gw.fiskerdps.com/ota_update
REACT_APP_AUTH_CALLBACK_URL=https://ota-admin.fiskerdps.com
REACT_APP_GRAFANA_BASE_URL=https://grafana.fiskerdps.com
REACT_APP_GRAFANA_HOME_CHART_PATH=/d-solo/1VTVJ_qGk/dashboard?orgId=2&refresh=30s&panelId=12
REACT_APP_GRAFANA_VOLTAGE_CHART_PATH=/d-solo/LVI-aQGnz/diagnostics?orgId=2&var-VIN={vin}&var-Signal=BMS_CellVolt{cellNum}&panelId=2
REACT_APP_GRAFANA_CELLTEMP_CHART_PATH=/d-solo/LVI-aQGnz/diagnostics?orgId=2&var-VIN={vin}&var-Signal=BMS_CellT{cellNum}&panelId=2
REACT_APP_GRAFANA_BATTERYTEMP_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&refresh=1m&panelId=4
REACT_APP_GRAFANA_BATTERYCAP_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&refresh=1m&panelId=6
REACT_APP_GRAFANA_BATTERYPERCENT_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&panelId=12
REACT_APP_GRAFANA_BATTERY12VPERCENT_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&refresh=1m&panelId=2
REACT_APP_GRAFANA_BATTERY12VVOLTAGE_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&refresh=1m&panelId=9
REACT_APP_GRAFANA_API=https://grafana.fiskerdps.com/api/datasources/proxy/1
REACT_APP_AUTH_SERVICE_URL=https://gw.cloud.fiskerinc.com/compute_auth
REACT_APP_CERT_SERVICE_URL=https://gw.cloud.fiskerinc.com/certificate
REACT_APP_UPLOAD_SERVICE_URL=https://gw.cloud.fiskerinc.com/ota_update
REACT_APP_AUTH_CALLBACK_URL=https://ota-admin.cloud.fiskerinc.com
REACT_APP_SUPERSET_URL=http://superset.fiskercloud.internal

View File

@@ -1,13 +1,5 @@
REACT_APP_AUTH_SERVICE_URL=https://gw-stg.fiskerdps.com/compute_auth
REACT_APP_UPLOAD_SERVICE_URL=https://gw-stg.fiskerdps.com/ota_update
REACT_APP_AUTH_CALLBACK_URL=https://stg-ota-admin.fiskerdps.com
REACT_APP_GRAFANA_BASE_URL=https://stg-grafana.fiskerdps.com
REACT_APP_GRAFANA_HOME_CHART_PATH=/d-solo/1VTVJ_qGk/dashboard?orgId=2&refresh=30s&panelId=12
REACT_APP_GRAFANA_VOLTAGE_CHART_PATH=/d-solo/LVI-aQGnz/diagnostics?orgId=2&var-VIN={vin}&var-Signal=BMS_CellVolt{cellNum}&panelId=2
REACT_APP_GRAFANA_CELLTEMP_CHART_PATH=/d-solo/LVI-aQGnz/diagnostics?orgId=2&var-VIN={vin}&var-Signal=BMS_CellT{cellNum}&panelId=2
REACT_APP_GRAFANA_BATTERYTEMP_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&refresh=1m&panelId=4
REACT_APP_GRAFANA_BATTERYCAP_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&refresh=1m&panelId=6
REACT_APP_GRAFANA_BATTERYPERCENT_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&panelId=12
REACT_APP_GRAFANA_BATTERY12VPERCENT_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&refresh=1m&panelId=2
REACT_APP_GRAFANA_BATTERY12VVOLTAGE_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&refresh=1m&panelId=9
REACT_APP_GRAFANA_API=https://stg-grafana.fiskerdps.com/api/datasources/proxy/1
REACT_APP_AUTH_SERVICE_URL=https://stg-gw.cloud.fiskerinc.com/compute_auth
REACT_APP_CERT_SERVICE_URL=https://stg-gw.cloud.fiskerinc.com/certificate
REACT_APP_UPLOAD_SERVICE_URL=https://stg-gw.cloud.fiskerinc.com/ota_update
REACT_APP_AUTH_CALLBACK_URL=https://stg-ota-admin.cloud.fiskerinc.com
REACT_APP_SUPERSET_URL=http://superset-stg.fiskercloud.internal

View File

@@ -1,13 +1,5 @@
REACT_APP_AUTH_SERVICE_URL=http://localhost/compute_auth
REACT_APP_UPLOAD_SERVICE_URL=http://localhost/ota_update
REACT_APP_AUTH_CALLBACK_URL=http://localhost:3000
REACT_APP_GRAFANA_BASE_URL=https://grafana.fiskerdps.com
REACT_APP_GRAFANA_HOME_CHART_PATH=/d-solo/1VTVJ_qGk/dashboard?orgId=2&refresh=30s&panelId=12
REACT_APP_GRAFANA_VOLTAGE_CHART_PATH=/d-solo/LVI-aQGnz/diagnostics?orgId=2&var-VIN={vin}&var-Signal=BMS_CellVolt{cellNum}&panelId=2
REACT_APP_GRAFANA_CELLTEMP_CHART_PATH=/d-solo/LVI-aQGnz/diagnostics?orgId=2&var-VIN={vin}&var-Signal=BMS_CellT{cellNum}&panelId=2
REACT_APP_GRAFANA_BATTERYTEMP_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&refresh=1m&panelId=4
REACT_APP_GRAFANA_BATTERYCAP_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&refresh=1m&panelId=6
REACT_APP_GRAFANA_BATTERYPERCENT_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&panelId=12
REACT_APP_GRAFANA_BATTERY12VPERCENT_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&refresh=1m&panelId=2
REACT_APP_GRAFANA_BATTERY12VVOLTAGE_CHART=/d-solo/jRKKo2gnz/battery?orgId=2&var-VIN={vin}&refresh=1m&panelId=9
REACT_APP_GRAFANA_API=https://grafana.fiskerdps.com/api/datasources/proxy/1
REACT_APP_SUPERSET_URL=http://superset-dev.fisker.internal
REACT_APP_CERT_SERVICE_URL=http://localhost/certificate

9
.github/CODEOWNERS vendored
View File

@@ -1,8 +1,7 @@
# default codeowners
* jwu@fiskerinc.com dtaylor@fiskerinc.com ggetsin@fiskerinc.com bbaker@fiskerinc.com
* @Fisker-Inc/cloud
# devops
.github rgreenberg@fiskerinc.com jwu@fiskerinc.com dtaylor@fiskerinc.com ggetsin@fiskerinc.com bbaker@fiskerinc.com
Jenkinsfile rgreenberg@fiskerinc.com jwu@fiskerinc.com dtaylor@fiskerinc.com ggetsin@fiskerinc.com bbaker@fiskerinc.com
k8s rgreenberg@fiskerinc.com jwu@fiskerinc.com dtaylor@fiskerinc.com ggetsin@fiskerinc.com bbaker@fiskerinc.com
Dockerfile rgreenberg@fiskerinc.com jwu@fiskerinc.com dtaylor@fiskerinc.com ggetsin@fiskerinc.com bbaker@fiskerinc.com
.github @Fisker-Inc/devops @Fisker-Inc/cloud
k8s @Fisker-Inc/devops @Fisker-Inc/cloud
Dockerfile @Fisker-Inc/devops @Fisker-Inc/cloud

View File

@@ -5,12 +5,7 @@ on:
- main
- "release/**"
- "hotfix/**"
jobs:
deploy:
name: Deploy
runs-on: self-hosted
env:
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_CHANNEL: "#cloud-builds"
SLACK_FOOTER: ""
@@ -18,21 +13,34 @@ jobs:
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.build-env }}
steps:
- name: Slack Notification
uses: rtCamp/action-slack-notify@v2
- name: Checkout
uses: actions/checkout@v2
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
- name: Azure Login
uses: azure/login@v1
with:
aws-region: us-west-2
- name: Create ECR Repo
run: aws ecr create-repository --region us-west-2 --repository-name ${PROJECT} || true
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Docker login
uses: azure/docker-login@v1
with:
login-server: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Set Env
id: set-env
run: |
case ${GITHUB_REF} in
refs/heads/develop)
@@ -47,26 +55,33 @@ jobs:
ENVIRONMENT=dev;;
esac
echo "ENVIRONMENT=${ENVIRONMENT}" >> $GITHUB_ENV
echo "::set-output name=build-env::${ENVIRONMENT}"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
build-args: ENVIRONMENT=${{ env.ENVIRONMENT }}
push: true
tags: ${{ steps.login-ecr.outputs.registry }}/${{ env.PROJECT }}:${{ env.TAG}}-${{ env.ENVIRONMENT }}
cache-from: type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ env.PROJECT }}:${{ env.TAG}}-${{ env.ENVIRONMENT }}
cache-to: type=inline
- name: Notify deploy
uses: rtCamp/action-slack-notify@v2
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:
- uses: rtCamp/action-slack-notify@v2
env:
MSG_MINIMAL: true
SLACK_MESSAGE: "Deploying to ${{ env.ENVIRONMENT }}... :partydeploy:"
SLACK_MESSAGE: "Deploying ${{ env.PROJECT }} to ${{ env.ENVIRONMENT }}... :partydeploy:"
- name: Deploy
id: deploy
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
run: |-
helm upgrade \
--kube-context $ENVIRONMENT \
@@ -80,7 +95,7 @@ jobs:
uses: rtCamp/action-slack-notify@v2
env:
MSG_MINIMAL: true
SLACK_MESSAGE: "Successfully deployed to ${{ env.ENVIRONMENT }}! :gopher_party:"
SLACK_MESSAGE: "Successfully deployed ${{ env.PROJECT }} to ${{ env.ENVIRONMENT }}! :gopher_party:"
- name: Notify if failure
if: ${{ failure() }}

View File

@@ -8,6 +8,9 @@ jobs:
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
@@ -15,6 +18,12 @@ jobs:
cache: "npm"
- run: npm install
- run: npm run build --if-present
- run: npm test
- run: npm test -- --coverage --coverageDirectory='coverage' --watchAll=false
env:
CI: true
- name: SonarCloud Scan
uses: sonarsource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

View File

@@ -1,3 +1,7 @@
{
"editor.formatOnSave": true
"editor.formatOnSave": true,
// spacing
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.detectIndentation": false
}

View File

@@ -1,21 +1,25 @@
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
labels:
app: {{ .Chart.Name }}
name: {{ .Chart.Name }}
spec:
ingressClassName: nginx
rules:
- host: {{ .Values.ingress.hostname }}
http:
paths:
- backend:
serviceName: {{ .Chart.Name }}
servicePort: 80
service:
name: {{ .Chart.Name }}
port:
number: 80
path: /
pathType: ImplementationSpecific
tls:
- hosts:
- {{ .Values.ingress.hostname }}
secretName: fiskerdps-cert
secretName: {{ .Chart.Name }}-tls

View File

@@ -1,5 +1,5 @@
ingress:
hostname: dev-ota-admin.fiskerdps.com
hostname: dev-ota-admin.cloud.fiskerinc.com
resources:
requests:

View File

@@ -1,5 +1,5 @@
ingress:
hostname: ota-admin.fiskerdps.com
hostname: ota-admin.cloud.fiskerinc.com
resources:
requests:
@@ -9,4 +9,4 @@ resources:
cpu: 250m
memory: 256Mi
replicas: 1
replicas: 3

View File

@@ -1,5 +1,5 @@
ingress:
hostname: stg-ota-admin.fiskerdps.com
hostname: stg-ota-admin.cloud.fiskerinc.com
resources:
requests:

15758
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,23 +3,26 @@
"version": "0.1.1",
"private": true,
"dependencies": {
"@datadog/browser-logs": "^3.7.0",
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2",
"@testing-library/jest-dom": "^5.15.0",
"@testing-library/react": "^12.1.2",
"@datadog/browser-logs": "^3.11.0",
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^12.1.4",
"@testing-library/user-event": "^13.5.0",
"axios": "^0.21.4",
"axios": "^0.26.1",
"clsx": "^1.1.1",
"env-cmd": "^10.1.0",
"leaflet": "^1.7.1",
"material-ui-dropzone": "^3.5.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-leaflet": "^3.2.2",
"react-leaflet": "^3.2.5",
"react-router-dom": "^5.3.0",
"react-scripts": "4.0.3",
"web-vitals": "^2.1.2"
"react-router-hash-link": "^2.4.3",
"react-scripts": "5.0.0",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "env-cmd -f .env.local react-scripts start",
@@ -28,8 +31,9 @@
"build:stg": "env-cmd -f .env.stg react-scripts build",
"build:prd": "env-cmd -f .env.prd react-scripts build",
"build:local": "env-cmd -f .env.local react-scripts build",
"test": "env-cmd -f .env.local react-scripts test",
"test": "env-cmd -f .env.local react-scripts test --no-cache",
"test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
"test:coverage": "npm test -- --coverage --watchAll=false --no-cache",
"eject": "react-scripts eject"
},
"eslintConfig": {
@@ -44,12 +48,27 @@
"not op_mini all"
],
"engines": {
"node": "12.20.1"
"node": "14.17.6"
},
"devDependencies": {
"react-test-renderer": "^17.0.2"
},
"jest": {
"globalSetup": "./testEnv.js"
"globalSetup": "./testEnv.js",
"collectCoverageFrom": [
"src/**/*.{js,jsx,ts,tsx}"
],
"coverageThreshold": {
"global": {
"branches": 39,
"functions": 47,
"lines": 50,
"statements": 49
}
},
"coverageReporters": [
"html",
"lcov"
]
}
}

6
sonar-project.properties Normal file
View File

@@ -0,0 +1,6 @@
sonar.projectKey=Fisker-Inc_ota-admin-portal
sonar.organization=fisker-inc
sonar.sourceEncoding=UTF-8
sonar.javascript.coveragePlugin=lcov
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.sources=src

View File

@@ -4,7 +4,6 @@ jest.mock("../Contexts/VehicleContext");
jest.mock("../Contexts/ManifestCreateContext");
jest.mock("../Contexts/ManifestsContext");
jest.mock("../Contexts/UserContext");
jest.mock("../../services/grafanaAPI");
jest.mock("../../services/monitoring");
jest.mock("../../services/vehiclesAPI");
@@ -44,16 +43,29 @@ const sleepAndCheck = async (path, selector, compare) => {
};
describe("App", () => {
const rxMakeStyles = /makeStyles-(\w+)-(\d+)/gi;
beforeAll(() => {
// Stablize Table Pagination control ids
expect.addSnapshotSerializer({
test: function (val) {
return val && typeof val === "string" && val.indexOf("mui-") >= 0;
return val && typeof val === "string" && val.indexOf("mui-") > -1;
},
print: function (val) {
let str = val;
str = str.replace(/mui-\d*/g, "mui-00000");
return `"${str}"`;
},
});
expect.addSnapshotSerializer({
test: (val) => {
return val && typeof val === "string" && val.search(rxMakeStyles) > -1;
},
print: function (val) {
let str = val;
str = str.replace(rxMakeStyles, "makeStyles-$1-0000");
return `"${str}"`;
},
});
@@ -72,14 +84,6 @@ describe("App", () => {
await sleepAndCheck("/home", "span.MuiButton-label", "Sign In");
});
it("Route /datascope unauthenticated", async () => {
await sleepAndCheck("/datascope", "span.MuiButton-label", "Sign In");
});
it("Route /datascope/battery unauthenticated", async () => {
await check("/datascope/battery", "span.MuiButton-label", "Sign In");
});
it("Route /packages unauthenticated", async () => {
await check("/packages", "span.MuiButton-label", "Sign In");
});
@@ -116,6 +120,10 @@ describe("App", () => {
);
});
it("Route /tools/certificates/add unauthenticated", async () => {
await check("/tools/certificates/add", "span.MuiButton-label", "Sign In");
});
it("Route /page-not-found unauthenticated", async () => {
await check("/page-not-found", "h1", "Page Not Found");
});
@@ -134,16 +142,6 @@ describe("App", () => {
await check("/page-not-found", "h1", "Page Not Found");
});
it("Route /datascope authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await sleepAndCheck("/datascope", "h6", "Datascope");
});
it("Route /datascope/battery authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/datascope/battery", "h6", "Battery");
});
it("Route /packages authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/packages", "h6", "Deployments");
@@ -178,4 +176,9 @@ describe("App", () => {
setToken(TEST_AUTH_OBJECT);
await check("/vehicle-status/FISKER123", "h6", "Vehicle FISKER123 Details");
});
it("Route /tools/certificates/add authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/tools/certificates/add", "h6", "Create Certificate");
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,177 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CANFiltersAdd Render 1`] = `
<div>
<div
data-testid="mocked-canfiltersprovider"
>
<div
data-testid="mocked-statusprovider"
>
<div
data-testid="mocked-userprovider"
>
<div
data-testid="mocked-canfiltersprovider"
>
<div
class="makeStyles-paper-3"
>
<form
action="{onSubmit}"
class="makeStyles-form-5"
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="vin"
id="vin-label"
>
VIN
<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="vin"
maxlength="255"
name="vin"
readonly=""
required=""
type="text"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64"
>
<span>
VIN
 *
</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="canId"
id="canId-label"
>
CAN ID
<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="canId"
maxlength="255"
name="canId"
required=""
type="text"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64"
>
<span>
CAN ID
 *
</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"
data-shrink="false"
for="interval"
id="interval-label"
>
Interval
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
>
<input
aria-invalid="false"
class="MuiInputBase-input MuiOutlinedInput-input"
id="interval"
maxlength="255"
name="interval"
type="text"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64"
>
<span>
Interval
</span>
</legend>
</fieldset>
</div>
</div>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary MuiButton-fullWidth"
tabindex="0"
type="submit"
>
<span
class="MuiButton-label"
>
Submit
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</form>
</div>
</div>
</div>
</div>
f
</div>
</div>
`;

View File

@@ -0,0 +1,127 @@
import React, { useEffect, useRef, useState } from "react";
import { Redirect } from "react-router";
import { useLocation } from "react-router-dom";
import { Button, TextField } from "@material-ui/core";
import {
CANFiltersProvider,
useCANFiltersContext,
} from "../../Contexts/CANFiltersContext";
import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import { logger } from "../../../services/monitoring";
const MainForm = () => {
const { addFilter, busy } = useCANFiltersContext();
const { token: { idToken: { jwtToken: token } } } = useUserContext();
const { setMessage, setTitle, setSitePath } = useStatusContext();
const [redirect, setRedirect] = useState(null);
const classes = useStyles();
const canIdEl = useRef(null);
const intervalEl = useRef(null);
const queries = new URLSearchParams(useLocation().search);
const vin = queries.get("vin") ?? ""
useEffect(() => {
setTitle("Create CAN Filter");
setSitePath([
{
label: `Vehicle ${vin}`,
link: "/vehicles",
},
{
label: "Create CAN Filter",
},
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onSubmit = async (event) => {
try {
event.preventDefault();
const formData = {
can_id: canIdEl.current.value,
interval: parseInt(intervalEl.current.value)
};
const result = await addFilter(vin, formData, token);
if (!result || result.error) return;
setMessage(`Added filter`);
setRedirect(`/vehicle-status/${vin}#filters`);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
};
if (redirect && redirect.length > 0) {
return <Redirect to={redirect} />;
}
return (
<div className={classes.paper}>
<form className={classes.form} noValidate action="{onSubmit}">
<TextField
id="vin"
name="vin"
label="VIN"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "255",
readOnly: true,
}}
value={vin}
required
fullWidth
/>
<TextField
id="canId"
name="canId"
label="CAN ID"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "255",
}}
required
fullWidth
inputRef={canIdEl}
/>
<TextField
id="interval"
name="interval"
label="Interval"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "255",
}}
fullWidth
inputRef={intervalEl}
/>
<Button
type="submit"
disabled={busy}
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={onSubmit}
>
Submit
</Button>
</form>
</div>
);
};
const CANFilterCreate = (props) => (
<CANFiltersProvider>
<MainForm {...props} />
</CANFiltersProvider>
);
export default CANFilterCreate;

View File

@@ -0,0 +1,36 @@
jest.mock("../../Contexts/CANFiltersContext");
jest.mock("../../Contexts/StatusContext");
jest.mock("../../Contexts/UserContext");
import { render, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import { CANFiltersProvider } from "../../Contexts/CANFiltersContext";
import { StatusProvider } from "../../Contexts/StatusContext";
import { UserProvider, setToken } from "../../Contexts/UserContext";
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
import MainForm from "./index"
const renderCANFiltersAdd = async () => {
const { container } = render(
<CANFiltersProvider>
<StatusProvider>
<UserProvider>
<BrowserRouter>
<MainForm />
</BrowserRouter>
</UserProvider>
</StatusProvider>f
</CANFiltersProvider>
);
await waitFor(() => { });
return container;
};
describe("CANFiltersAdd", () => {
it("Render", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderCANFiltersAdd();
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,453 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CANFiltersTable Render 1`] = `
<div>
<div
data-testid="mocked-canfiltersprovider"
>
<div
data-testid="mocked-statusprovider"
>
<div
data-testid="mocked-userprovider"
>
<div
data-testid="mocked-canfiltersprovider"
>
<div
class="makeStyles-paper-3 makeStyles-tableSize-53"
>
<div
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
>
<div
class="MuiGrid-root makeStyles-textJustifyAlign-47 MuiGrid-item MuiGrid-grid-md-4"
>
<a
class="makeStyles-labelInline-9"
href="/filter-add?vin=undefined"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"
/>
</svg>
</a>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-8"
>
<div
class="MuiFormControl-root makeStyles-margin-28 makeStyles-fullWidth-50"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated"
data-shrink="false"
for="search"
>
Search
</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="search"
type="text"
value=""
/>
<div
class="MuiInputAdornment-root MuiInputAdornment-positionEnd"
>
<button
aria-label="search"
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="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
</div>
</div>
</div>
</div>
<table
class="MuiTable-root"
>
<thead
class="MuiTableHead-root"
>
<tr
class="MuiTableRow-root MuiTableRow-head"
>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
CAN ID
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Interval (ms)
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
Actions
</th>
</tr>
</thead>
<tbody
class="MuiTableBody-root"
>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
123
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
1000
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
class=""
href="/filter-update?vin=undefined&can_id=123&interval=1000"
style="margin: 5px;"
title="Update \\"123\\""
>
<svg
aria-hidden="true"
aria-label="Update 123"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"123\\""
>
<svg
aria-hidden="true"
aria-label="Delete 123"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
456-789
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
2000
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
class=""
href="/filter-update?vin=undefined&can_id=456-789&interval=2000"
style="margin: 5px;"
title="Update \\"456-789\\""
>
<svg
aria-hidden="true"
aria-label="Update 456-789"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"456-789\\""
>
<svg
aria-hidden="true"
aria-label="Delete 456-789"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
1
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
0
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
class=""
href="/filter-update?vin=undefined&can_id=1&interval=0"
style="margin: 5px;"
title="Update \\"1\\""
>
<svg
aria-hidden="true"
aria-label="Update 1"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"1\\""
>
<svg
aria-hidden="true"
aria-label="Delete 1"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
</tbody>
<tfoot
class="MuiTableFooter-root"
>
<tr
class="MuiTableRow-root MuiTableRow-footer"
>
<td
class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root"
colspan="8"
>
<div
class="MuiToolbar-root MuiToolbar-regular MuiTablePagination-toolbar MuiToolbar-gutters"
>
<div
class="MuiTablePagination-spacer"
/>
<p
class="MuiTypography-root MuiTablePagination-caption MuiTypography-body2 MuiTypography-colorInherit"
>
Rows per page:
</p>
<div
class="MuiInputBase-root MuiTablePagination-input MuiTablePagination-selectRoot"
>
<select
aria-label="rows per page"
class="MuiSelect-root MuiSelect-select MuiTablePagination-select MuiInputBase-input"
>
<option
class="MuiTablePagination-menuItem"
value="5"
>
5
</option>
<option
class="MuiTablePagination-menuItem"
value="10"
>
10
</option>
<option
class="MuiTablePagination-menuItem"
value="25"
>
25
</option>
<option
class="MuiTablePagination-menuItem"
value="100"
>
100
</option>
</select>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSelect-icon MuiTablePagination-selectIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
</svg>
</div>
<p
class="MuiTypography-root MuiTablePagination-caption MuiTypography-body2 MuiTypography-colorInherit"
>
1-3 of 3
</p>
<div
class="MuiTablePagination-actions"
>
<button
aria-label="Previous page"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
title="Previous page"
type="button"
>
<span
class="MuiIconButton-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z"
/>
</svg>
</span>
</button>
<button
aria-label="Next page"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
title="Next page"
type="button"
>
<span
class="MuiIconButton-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z"
/>
</svg>
</span>
</button>
</div>
</div>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,210 @@
import React, { useEffect, useState } from "react";
import { Link } from 'react-router-dom';
import {
Grid,
Table,
TableBody,
TableCell,
TableFooter,
TablePagination,
TableRow,
Tooltip,
} from "@material-ui/core";
import AddCircleIcon from "@material-ui/icons/AddCircle";
import DeleteIcon from "@material-ui/icons/Delete";
import EditIcon from '@material-ui/icons/Edit';
import clsx from "clsx";
import {
CANFiltersProvider,
useCANFiltersContext,
} from "../../Contexts/CANFiltersContext";
import TableHeaderSortable from "../../Table/HeaderSortable";
import {
useUserContext
} from "../../Contexts/UserContext"
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import SearchField from "../../Controls/SearchField";
import { logger } from "../../../services/monitoring";
import { Roles, hasRole } from "../../../utils/roles";
const tableColumns = [
{
id: "can_id",
label: "CAN ID"
},
{
id: "interval",
label: "Interval (ms)"
},
{
id: "",
label: "Actions"
}
];
const MainForm = ({ vin }) => {
const classes = useStyles();
const [pageSize, setPageSize] = useState(10);
const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("id");
const [order, setOrder] = useState("desc");
const { getFilters, deleteFilter, filters, totalFilters } = useCANFiltersContext();
const { setMessage } = useStatusContext();
const { token: { idToken: { jwtToken: token } }, groups } = useUserContext();
useEffect(() => {
(async () => {
try {
if (!vin || !token) return;
await getFilters(
vin,
{
limit: pageSize,
offset: pageSize * pageIndex,
order: `${orderBy} ${order}`,
},
token
);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [vin, token, pageIndex, pageSize, orderBy, order]);
const handleChangePageIndex = (event, newIndex) => {
setPageIndex(newIndex);
};
const handleChangePageSize = (event) => {
setPageSize(parseInt(event.target.value, 10));
setPageIndex(0);
};
const handleSort = (event, property) => {
try {
if (property === orderBy) {
if (order === "asc") {
setOrder("desc");
} else {
setOrder("asc");
}
} else {
setOrderBy(property);
setOrder("asc");
}
} catch (e) {
logger.warn(e.stack);
}
};
const onDelete = async (can_id) => {
try {
await deleteFilter(vin, can_id, token);
setMessage(`Deleted ${can_id}`)
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
};
const Actions = (row) => {
let actions = [];
if (hasRole([Roles.CREATE], groups)) {
actions.push({
tip: `Update "${row.can_id}"`,
link: `/filter-update?vin=${vin}&can_id=${row.can_id}&interval=${row.interval}`,
icon: <EditIcon aria-label={`Update ${row.can_id}`} />
});
}
if (hasRole([Roles.DELETE], groups)) {
actions.push({
tip: `Delete "${row.can_id}"`,
id: row.can_id,
icon: <DeleteIcon aria-label={`Delete ${row.can_id}`} />
})
}
if (actions.length === 0) return ["No actions"];
return actions.map((action) => {
if (action.link != null) {
return (
<Tooltip key={action.link} title={action.tip}>
<Link to={action.link} style={{ margin: 5 }}>
{action.icon}
</Link>
</Tooltip>
);
} else {
return (
<Tooltip key={`delete-${action.id}`} title={action.tip}>
<Link to="#" onClick={() => onDelete(action.id)}>
{action.icon}
</Link>
</Tooltip>
);
}
});
};
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Grid container className={classes.root} spacing={2}>
<Grid item md={4} className={classes.textJustifyAlign}>
<Link to={`/filter-add?vin=${vin}`} className={classes.labelInline}>
<AddCircleIcon fontSize="large" />
</Link>
</Grid>
<Grid item md={8} className={classes.textCenterAlign}>
<SearchField classes={classes} />
</Grid>
</Grid>
<Table>
<TableHeaderSortable
classes={classes}
orderBy={orderBy}
order={order}
columnData={tableColumns}
onSortRequest={handleSort}
/>
<TableBody>
{filters.map((row) => (
<TableRow key={row.can_id}>
<TableCell align="center">{row.can_id}</TableCell>
<TableCell align="center">{row.interval}</TableCell>
<TableCell align="center">{Actions(row)}</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 100]}
colSpan={8}
count={totalFilters}
rowsPerPage={pageSize}
page={pageIndex}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true,
}}
onPageChange={handleChangePageIndex}
onRowsPerPageChange={handleChangePageSize}
/>
</TableRow>
</TableFooter>
</Table>
</div >
);
};
const CANFiltersTable = (props) => (
<CANFiltersProvider>
<MainForm {...props} />
</CANFiltersProvider>
);
export default CANFiltersTable;

View File

@@ -0,0 +1,39 @@
jest.mock("../../Contexts/CANFiltersContext");
jest.mock("../../Contexts/StatusContext");
jest.mock("../../Contexts/UserContext");
jest.mock('@material-ui/core/utils/unstable_useId', () =>
jest.fn().mockReturnValue('mui-test-id'),
);
import { render, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import { CANFiltersProvider } from "../../Contexts/CANFiltersContext";
import { StatusProvider } from "../../Contexts/StatusContext";
import { UserProvider, setToken } from "../../Contexts/UserContext";
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
import MainForm from "./index"
const renderCANFiltersTable = async () => {
const { container } = render(
<CANFiltersProvider>
<StatusProvider>
<UserProvider>
<BrowserRouter>
<MainForm />
</BrowserRouter>
</UserProvider>
</StatusProvider>
</CANFiltersProvider>
);
await waitFor(() => { });
return container;
};
describe("CANFiltersTable", () => {
it("Render", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderCANFiltersTable();
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,186 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CANFiltersUpdate Render 1`] = `
<div>
<div
data-testid="mocked-canfiltersprovider"
>
<div
data-testid="mocked-statusprovider"
>
<div
data-testid="mocked-userprovider"
>
<div
data-testid="mocked-canfiltersprovider"
>
<div
class="makeStyles-paper-3"
>
<form
action="{onSubmit}"
class="makeStyles-form-5"
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="vin"
id="vin-label"
>
VIN
<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="vin"
maxlength="255"
name="vin"
readonly=""
required=""
type="text"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64"
>
<span>
VIN
 *
</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="canId"
id="canId-label"
>
CAN ID
<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="canId"
maxlength="255"
name="canId"
readonly=""
required=""
type="text"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64"
>
<span>
CAN ID
 *
</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="interval"
id="interval-label"
>
Interval
<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="interval"
maxlength="255"
name="interval"
required=""
type="text"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64"
>
<span>
Interval
 *
</span>
</legend>
</fieldset>
</div>
</div>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary MuiButton-fullWidth"
tabindex="0"
type="submit"
>
<span
class="MuiButton-label"
>
Submit
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,131 @@
import React, { useEffect, useRef, useState } from "react";
import { Redirect } from "react-router";
import { useLocation } from "react-router-dom";
import { Button, TextField } from "@material-ui/core";
import {
CANFiltersProvider,
useCANFiltersContext,
} from "../../Contexts/CANFiltersContext";
import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import { logger } from "../../../services/monitoring";
const MainForm = () => {
const { updateFilter, busy } = useCANFiltersContext();
const { token: { idToken: { jwtToken: token } } } = useUserContext();
const { setMessage, setTitle, setSitePath } = useStatusContext();
const [redirect, setRedirect] = useState(null);
const classes = useStyles();
const intervalEl = useRef(null);
const queries = new URLSearchParams(useLocation().search);
const vin = queries.get("vin") ?? ""
const canID = queries.get("can_id") ?? ""
const interval = queries.get("interval") ?? ""
useEffect(() => {
setTitle("Update CAN Filter");
setSitePath([
{
label: `Vehicle ${vin}`,
link: "/vehicles",
},
{
label: "Update CAN Filter",
},
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onSubmit = async (event) => {
try {
event.preventDefault();
const formData = {
can_id: canID,
interval: parseInt(intervalEl.current.value)
};
const result = await updateFilter(vin, canID, formData, token);
if (!result || result.error) return;
setMessage(`Updated filter`);
setRedirect(`/vehicle-status/${vin}#filters`);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
};
if (redirect && redirect.length > 0) {
return <Redirect to={redirect} />;
}
return (
<div className={classes.paper}>
<form className={classes.form} noValidate action="{onSubmit}">
<TextField
id="vin"
name="vin"
label="VIN"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "255",
readOnly: true,
}}
value={vin}
required
fullWidth
/>
<TextField
id="canId"
name="canId"
label="CAN ID"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "255",
readOnly: true,
}}
value={canID}
required
fullWidth
/>
<TextField
id="interval"
name="interval"
label="Interval"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "255",
}}
defaultValue={interval}
required
fullWidth
inputRef={intervalEl}
/>
<Button
type="submit"
disabled={busy}
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={onSubmit}
>
Submit
</Button>
</form>
</div>
);
};
const CANFilterUpdate = (props) => (
<CANFiltersProvider>
<MainForm {...props} />
</CANFiltersProvider>
);
export default CANFilterUpdate;

View File

@@ -0,0 +1,36 @@
jest.mock("../../Contexts/CANFiltersContext");
jest.mock("../../Contexts/StatusContext");
jest.mock("../../Contexts/UserContext");
import { render, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import { CANFiltersProvider } from "../../Contexts/CANFiltersContext";
import { StatusProvider } from "../../Contexts/StatusContext";
import { UserProvider, setToken } from "../../Contexts/UserContext";
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
import MainForm from "./index"
const renderCANFiltersUpdate = async () => {
const { container } = render(
<CANFiltersProvider>
<StatusProvider>
<UserProvider>
<BrowserRouter>
<MainForm />
</BrowserRouter>
</UserProvider>
</StatusProvider>
</CANFiltersProvider>
);
await waitFor(() => { });
return container;
};
describe("CANFiltersUpdate", () => {
it("Render", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderCANFiltersUpdate();
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,730 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VehicleAddForm Render 1`] = `
<div>
<div
data-testid="mocked-vehicleprovider"
>
<div
data-testid="mocked-statusprovider"
>
<div
data-testid="mocked-userprovider"
>
<div
data-testid="mocked-vehicleprovider"
>
<div
class="makeStyles-paper-3"
>
<form
action="{onSubmit}"
class="makeStyles-form-5"
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="vin"
id="vin-label"
>
VIN
<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="vin"
maxlength="17"
name="vin"
required=""
type="text"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64"
>
<span>
VIN
 *
</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-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required"
data-shrink="true"
for="model"
id="model-label"
>
Model
<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="model"
maxlength="255"
name="model"
required=""
type="text"
value="Ocean"
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
>
<span>
Model
 *
</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-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required"
data-shrink="true"
for="year"
id="year-label"
>
Year
<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="year"
maxlength="4"
minlength="4"
name="year"
required=""
type="number"
value="2022"
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
>
<span>
Year
 *
</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-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required"
data-shrink="true"
for="trim"
id="trim-label"
>
Trim
<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="trim"
maxlength="4"
minlength="4"
name="trim"
required=""
type="text"
value="Base"
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
>
<span>
Trim
 *
</span>
</legend>
</fieldset>
</div>
</div>
<label
class="MuiFormLabel-root"
id="demo-row-radio-buttons-group-label"
>
Log Level
</label>
<div
aria-labelledby="demo-row-radio-buttons-group-label"
class="MuiFormGroup-root MuiFormGroup-row"
margin="normal"
role="radiogroup"
>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-69"
name="log-level-group"
type="radio"
value="trace"
/>
<div
class="PrivateRadioButtonIcon-root-70"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden="true"
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</div>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Trace
</span>
</label>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-69"
name="log-level-group"
type="radio"
value="debug"
/>
<div
class="PrivateRadioButtonIcon-root-70"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden="true"
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</div>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Debug
</span>
</label>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary PrivateSwitchBase-checked-67 Mui-checked MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
checked=""
class="PrivateSwitchBase-input-69"
name="log-level-group"
type="radio"
value="info"
/>
<div
class="PrivateRadioButtonIcon-root-70 PrivateRadioButtonIcon-checked-72"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden="true"
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</div>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Info
</span>
</label>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-69"
name="log-level-group"
type="radio"
value="warn"
/>
<div
class="PrivateRadioButtonIcon-root-70"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden="true"
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</div>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Warn
</span>
</label>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-69"
name="log-level-group"
type="radio"
value="error"
/>
<div
class="PrivateRadioButtonIcon-root-70"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden="true"
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</div>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Error
</span>
</label>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-69"
name="log-level-group"
type="radio"
value="critical"
/>
<div
class="PrivateRadioButtonIcon-root-70"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden="true"
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</div>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Critical
</span>
</label>
</div>
<label
class="MuiFormLabel-root"
id="demo-row-radio-buttons-group-label"
>
CAN Bus
</label>
<div
class="MuiFormGroup-root"
>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiCheckbox-root MuiCheckbox-colorSecondary PrivateSwitchBase-checked-67 Mui-checked MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
checked=""
class="PrivateSwitchBase-input-69"
data-indeterminate="false"
type="checkbox"
value=""
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
CAN Bus Enabled
</span>
</label>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required"
data-shrink="true"
for="max_mem_buffer_size"
id="max_mem_buffer_size-label"
>
Max Memory Buffer Size (0 uses default size)
<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="max_mem_buffer_size"
maxlength="12"
name="max_mem_buffer_size"
required=""
type="number"
value="0"
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
>
<span>
Max Memory Buffer Size (0 uses default size)
 *
</span>
</legend>
</fieldset>
</div>
</div>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiCheckbox-root MuiCheckbox-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-69"
data-indeterminate="false"
type="checkbox"
value=""
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Data Logger Enabled
</span>
</label>
</div>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined Mui-disabled Mui-disabled MuiFormLabel-filled Mui-required Mui-required"
data-shrink="true"
for="max_disk_buffer_size"
id="max_disk_buffer_size-label"
>
Max Disk Buffer Size (0 uses default size)
<span
aria-hidden="true"
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
>
*
</span>
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root Mui-disabled Mui-disabled MuiInputBase-fullWidth MuiInputBase-formControl"
>
<input
aria-invalid="false"
class="MuiInputBase-input MuiOutlinedInput-input Mui-disabled Mui-disabled"
disabled=""
id="max_disk_buffer_size"
maxlength="12"
name="max_disk_buffer_size"
required=""
type="number"
value="0"
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
>
<span>
Max Disk Buffer Size (0 uses default size)
 *
</span>
</legend>
</fieldset>
</div>
</div>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary MuiButton-fullWidth"
tabindex="0"
type="submit"
>
<span
class="MuiButton-label"
>
Submit
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -1,4 +1,15 @@
import React, { useEffect, useRef } from "react";
import React, { useEffect, useRef, useState } from "react";
import { Redirect } from "react-router";
import {
Button,
Checkbox,
FormControlLabel,
FormGroup,
FormLabel,
Radio,
RadioGroup,
TextField
} from "@material-ui/core";
import useStyles from "../../useStyles";
import {
@@ -7,7 +18,6 @@ import {
} from "../../Contexts/VehicleContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext";
import { Button, TextField } from "@material-ui/core";
import { logger } from "../../../services/monitoring";
const MainForm = () => {
@@ -19,10 +29,17 @@ const MainForm = () => {
},
} = useUserContext();
const classes = useStyles();
const [redirect, setRedirect] = useState(null);
const vinEl = useRef(null);
const modelEl = useRef(null);
const yearEl = useRef(null);
const trimEl = useRef(null);
const [selectedLogLevel, setSelectedLogLevel] = useState("info");
const [canbusEnabled, setCANBusEnabled] = useState(true);
const [dataLoggerEnabled, setDataLoggerEnabled] = useState(false);
const [maxMemBufferSize, setMaxMemBufferSize] = useState(0);
const [maxDiskBufferSize, setMaxDiskBufferSize] = useState(0);
useEffect(() => {
setTitle("Add Vehicle");
@@ -37,6 +54,27 @@ const MainForm = () => {
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onLogLevelChange = (event) => {
setSelectedLogLevel(event.target.value);
}
const onCANBusChange = (event) => {
setCANBusEnabled(event.target.checked);
}
const onDataLoggerChange = (event) => {
setDataLoggerEnabled(event.target.checked);
}
const onMaxMemBufferSizeChange = (event) => {
setMaxMemBufferSize(event.target.value);
}
const onMaxDiskBufferSizeChange = (event) => {
setMaxDiskBufferSize(event.target.value);
}
const onSubmit = async (event) => {
try {
event.preventDefault();
@@ -46,18 +84,29 @@ const MainForm = () => {
model: modelEl.current.value,
year: parseInt(yearEl.current.value),
trim: trimEl.current.value,
log_level: selectedLogLevel,
canbus: {
enabled: canbusEnabled,
data_logger_enabled: canbusEnabled ? dataLoggerEnabled : false,
max_mem_buffer_size: canbusEnabled ? parseInt(maxMemBufferSize) : 0,
max_disk_buffer_size: canbusEnabled && dataLoggerEnabled ? parseInt(maxDiskBufferSize) : 0
}
};
const result = await addVehicle(formData, token);
setMessage(`Added ${result.vin}`);
vinEl.current.value = "";
setRedirect(`/vehicles`);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
};
if (redirect && redirect.length > 0) {
return <Redirect to={redirect} />;
}
return (
<div className={classes.paper}>
<form className={classes.form} noValidate action="{onSubmit}">
@@ -119,6 +168,70 @@ const MainForm = () => {
fullWidth
inputRef={trimEl}
/>
<FormLabel id="demo-row-radio-buttons-group-label">Log Level</FormLabel>
<RadioGroup
row
aria-labelledby="demo-row-radio-buttons-group-label"
name="log-level-group"
value={selectedLogLevel}
onChange={onLogLevelChange}
margin="normal"
>
<FormControlLabel value="trace" control={<Radio />} label="Trace" />
<FormControlLabel value="debug" control={<Radio />} label="Debug" />
<FormControlLabel value="info" control={<Radio />} label="Info" />
<FormControlLabel value="warn" control={<Radio />} label="Warn" />
<FormControlLabel value="error" control={<Radio />} label="Error" />
<FormControlLabel value="critical" control={<Radio />} label="Critical" />
</RadioGroup>
<FormLabel id="demo-row-radio-buttons-group-label">CAN Bus</FormLabel>
<FormGroup>
<FormControlLabel control={
<Checkbox
checked={canbusEnabled}
onChange={onCANBusChange}
/>
} label="CAN Bus Enabled" />
<TextField
id="max_mem_buffer_size"
name="max_mem_buffer_size"
label='Max Memory Buffer Size (0 uses default size)'
value={maxMemBufferSize}
onChange={onMaxMemBufferSizeChange}
variant="outlined"
margin="normal"
inputProps={{
maxLength: "12",
}}
type="number"
disabled={!canbusEnabled}
required
fullWidth
/>
<FormControlLabel control={
<Checkbox
checked={dataLoggerEnabled}
onChange={onDataLoggerChange}
disabled={!canbusEnabled}
/>
} label="Data Logger Enabled" />
</FormGroup>
<TextField
id="max_disk_buffer_size"
name="max_disk_buffer_size"
label='Max Disk Buffer Size (0 uses default size)'
value={maxDiskBufferSize}
onChange={onMaxDiskBufferSizeChange}
variant="outlined"
margin="normal"
inputProps={{
maxLength: "12",
}}
type="number"
disabled={!dataLoggerEnabled}
required
fullWidth
/>
<Button
type="submit"
disabled={busy}

View File

@@ -0,0 +1,36 @@
jest.mock("../../Contexts/VehicleContext");
jest.mock("../../Contexts/StatusContext");
jest.mock("../../Contexts/UserContext");
import { render, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import { VehicleProvider } from "../../Contexts/VehicleContext";
import { StatusProvider } from "../../Contexts/StatusContext";
import { UserProvider, setToken } from "../../Contexts/UserContext";
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
import MainForm from "./index"
const renderVehicleAdd = async () => {
const { container } = render(
<VehicleProvider>
<StatusProvider>
<UserProvider>
<BrowserRouter>
<MainForm />
</BrowserRouter>
</UserProvider>
</StatusProvider>
</VehicleProvider>
);
await waitFor(() => { /* render */ });
return container;
};
describe("VehicleAddForm", () => {
it("Render", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderVehicleAdd();
expect(container).toMatchSnapshot();
});
});

View File

@@ -1,174 +0,0 @@
import React, { useEffect, useState } from "react";
import {
Table,
TableBody,
TableCell,
TableFooter,
TablePagination,
TableRow,
} from "@material-ui/core";
import clsx from "clsx";
import { LocalDateTimeString } from "../../../utils/dates";
import TableHeaderSortable from "../../Table/HeaderSortable";
import { useVehicleContext } from "../../Contexts/VehicleContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import { logger } from "../../../services/monitoring";
const tableColumns = [
{
id: "ecu",
label: "ECU",
},
{
id: "sw_version",
label: "SW Version",
},
{
id: "boot_loader_version",
label: "BL Version",
},
{
id: "hw_version",
label: "HW Version",
},
{
id: "vendor",
label: "Vendor",
},
{
id: "config",
label: "Config",
},
{
id: "fingerprint",
label: "Fingerprint",
},
{
id: "serial_number",
label: "Serial",
},
{
id: "created_at",
label: "Created",
},
{
id: "updated_at",
label: "Updated",
},
];
const CarECUs = ({ vin, token }) => {
const [ecus, setECUs] = useState([]);
const [total, setTotal] = useState(0);
const classes = useStyles();
const [pageSize, setPageSize] = useState(10);
const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("ecu");
const [order, setOrder] = useState("desc");
const { getECUs } = useVehicleContext();
const { setMessage } = useStatusContext();
useEffect(() => {
(async () => {
try {
if (!vin || !token) return;
const result = await getECUs(
{
vin,
limit: pageSize,
offset: pageSize * pageIndex,
order: `${orderBy} ${order}`,
},
token
);
setECUs(result.data);
if (result.total > -1) setTotal(result.total);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [vin, token, pageIndex, pageSize, orderBy, order]);
const handleChangePageIndex = (event, newIndex) => {
setPageIndex(newIndex);
};
const handleChangePageSize = (event) => {
setPageSize(parseInt(event.target.value, 10));
setPageIndex(0);
};
const handleSort = (event, property) => {
try {
if (property === orderBy) {
if (order === "asc") {
setOrder("desc");
} else {
setOrder("asc");
}
} else {
setOrderBy(property);
setOrder("asc");
}
} catch (e) {
logger.warn(e.stack);
}
};
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Table>
<TableHeaderSortable
classes={classes}
orderBy={orderBy}
order={order}
columnData={tableColumns}
onSortRequest={handleSort}
/>
<TableBody>
{ecus.map((row) => (
<TableRow key={row.ecu}>
<TableCell align="center">{row.ecu}</TableCell>
<TableCell align="center">{row.sw_version}</TableCell>
<TableCell align="center">{row.boot_loader_version}</TableCell>
<TableCell align="center">{row.hw_version}</TableCell>
<TableCell align="center">{row.vendor}</TableCell>
<TableCell align="center">{row.config}</TableCell>
<TableCell align="center">{row.fingerprint}</TableCell>
<TableCell align="center">{row.serial_number}</TableCell>
<TableCell align="center">
{LocalDateTimeString(row.created)}
</TableCell>
<TableCell align="center">
{LocalDateTimeString(row.updated)}
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 100]}
colSpan={10}
count={total}
rowsPerPage={pageSize}
page={pageIndex}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true,
}}
onPageChange={handleChangePageIndex}
onRowsPerPageChange={handleChangePageSize}
/>
</TableRow>
</TableFooter>
</Table>
</div>
);
};
export default CarECUs;

View File

@@ -1,159 +0,0 @@
import React, { useEffect, useState } from "react";
import {
Table,
TableBody,
TableCell,
TableFooter,
TablePagination,
TableRow,
} from "@material-ui/core";
import { LocalDateTimeString } from "../../../utils/dates";
import TableHeaderSortable from "../../Table/HeaderSortable";
import {
UpdatesProvider,
useUpdatesContext,
} from "../../Contexts/UpdatesContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import { logger } from "../../../services/monitoring";
const tableColumns = [
{
id: "id",
label: "ID",
},
{
id: "update_package_id",
label: "Name",
},
{
id: "status",
label: "Status",
},
{
id: "created_at",
label: "Created",
},
{
id: "updated_at",
label: "Updated",
},
];
const MainForm = ({ vin, token }) => {
const classes = useStyles();
const [pageSize, setPageSize] = useState(10);
const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("id");
const [order, setOrder] = useState("desc");
const { getCarUpdates, carUpdates, totalCarUpdates } = useUpdatesContext();
const { setMessage } = useStatusContext();
useEffect(() => {
(async () => {
try {
if (!vin || !token) return;
await getCarUpdates(
{
vin,
limit: pageSize,
offset: pageSize * pageIndex,
order: `${orderBy} ${order}`,
},
token
);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [vin, token, pageIndex, pageSize, orderBy, order]);
const handleChangePageIndex = (event, newIndex) => {
setPageIndex(newIndex);
};
const handleChangePageSize = (event) => {
setPageSize(parseInt(event.target.value, 10));
setPageIndex(0);
};
const handleSort = (event, property) => {
try {
if (property === orderBy) {
if (order === "asc") {
setOrder("desc");
} else {
setOrder("asc");
}
} else {
setOrderBy(property);
setOrder("asc");
}
} catch (e) {
logger.warn(e.stack);
}
};
const updateName = (row) => {
if (row.updatepackage)
return `${row.updatepackage.package_name} ${row.updatepackage.version}`;
if (row.updatemanifest)
return `${row.updatemanifest.name} ${row.updatemanifest.version}`;
return "None";
};
return (
<Table>
<TableHeaderSortable
classes={classes}
orderBy={orderBy}
order={order}
columnData={tableColumns}
onSortRequest={handleSort}
/>
<TableBody>
{carUpdates.map((row) => (
<TableRow key={row.id}>
<TableCell align="center">{row.id}</TableCell>
<TableCell align="center">{updateName(row)}</TableCell>
<TableCell align="center">{row.status}</TableCell>
<TableCell align="center">
{LocalDateTimeString(row.created)}
</TableCell>
<TableCell align="center">
{LocalDateTimeString(row.updated)}
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 100]}
colSpan={5}
count={totalCarUpdates}
rowsPerPage={pageSize}
page={pageIndex}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true,
}}
onPageChange={handleChangePageIndex}
onRowsPerPageChange={handleChangePageSize}
/>
</TableRow>
</TableFooter>
</Table>
);
};
const CarUpdates = (props) => (
<UpdatesProvider>
<MainForm {...props} />
</UpdatesProvider>
);
export default CarUpdates;

View File

@@ -0,0 +1,391 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VehicleTable Render 1`] = `
<div>
<div
data-testid="mocked-vehicleprovider"
>
<div
data-testid="mocked-statusprovider"
>
<div
data-testid="mocked-userprovider"
>
<div
data-testid="mocked-vehicleprovider"
>
<div
class="makeStyles-paper-3 makeStyles-tableSize-53"
>
<div
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
>
<div
class="MuiGrid-root makeStyles-textJustifyAlign-47 MuiGrid-item MuiGrid-grid-md-4"
>
<a
href="/vehicle-add"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"
/>
</svg>
</a>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-4"
>
<div
class="MuiFormControl-root makeStyles-margin-28 makeStyles-fullWidth-50"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated"
data-shrink="false"
for="search"
>
Search
</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="search"
type="text"
value=""
/>
<div
class="MuiInputAdornment-root MuiInputAdornment-positionEnd"
>
<button
aria-label="search"
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="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
</div>
</div>
</div>
<div
class="MuiGrid-root makeStyles-textRightAlign-49 MuiGrid-item MuiGrid-grid-md-4"
/>
</div>
<div
class="makeStyles-paper-3 makeStyles-tableSize-53"
>
<table
class="MuiTable-root"
>
<thead
class="MuiTableHead-root"
>
<tr
class="MuiTableRow-root MuiTableRow-head"
>
<th
aria-sort="ascending"
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root MuiTableSortLabel-active"
role="button"
tabindex="0"
>
VIN
<span
class="makeStyles-hiddenSortSpan-27"
>
sorted ascending
</span>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Model
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Year
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Trim
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Created
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Updated
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
</tr>
</thead>
<tbody
class="MuiTableBody-root"
/>
<tfoot
class="MuiTableFooter-root"
>
<tr
class="MuiTableRow-root MuiTableRow-footer"
>
<td
class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root"
colspan="7"
>
<div
class="MuiToolbar-root MuiToolbar-regular MuiTablePagination-toolbar MuiToolbar-gutters"
>
<div
class="MuiTablePagination-spacer"
/>
<p
class="MuiTypography-root MuiTablePagination-caption MuiTypography-body2 MuiTypography-colorInherit"
>
Rows per page:
</p>
<div
class="MuiInputBase-root MuiTablePagination-input MuiTablePagination-selectRoot"
>
<select
aria-label="rows per page"
class="MuiSelect-root MuiSelect-select MuiTablePagination-select MuiInputBase-input"
>
<option
class="MuiTablePagination-menuItem"
value="5"
>
5
</option>
<option
class="MuiTablePagination-menuItem"
value="10"
>
10
</option>
<option
class="MuiTablePagination-menuItem"
value="25"
>
25
</option>
<option
class="MuiTablePagination-menuItem"
value="100"
>
100
</option>
</select>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSelect-icon MuiTablePagination-selectIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
</svg>
</div>
<p
class="MuiTypography-root MuiTablePagination-caption MuiTypography-body2 MuiTypography-colorInherit"
>
0-0 of 0
</p>
<div
class="MuiTablePagination-actions"
>
<button
aria-label="Previous page"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
title="Previous page"
type="button"
>
<span
class="MuiIconButton-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z"
/>
</svg>
</span>
</button>
<button
aria-label="Next page"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
title="Next page"
type="button"
>
<span
class="MuiIconButton-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z"
/>
</svg>
</span>
</button>
</div>
</div>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -8,14 +8,11 @@ import { VehicleProvider } from "../../Contexts/VehicleContext";
import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import SendCommand from "../../Controls/SendCommand";
import SearchField from "../../Controls/SearchField";
import CarSelectionTable from "../../Controls/CarSelectionTable";
import { logger } from "../../../services/monitoring";
const MainForm = () => {
const classes = useStyles();
const [selected, setSelected] = useState([]);
const [search, setSearch] = useState("");
const { setTitle, setSitePath } = useStatusContext();
const {
@@ -25,29 +22,9 @@ const MainForm = () => {
} = useUserContext();
const handleSearch = (query) => {
setSelected([]);
setSearch(query);
};
const handleSelectAll = (cars) => {
setSelected(cars);
};
const handleSelect = (event, key) => {
try {
let newSelected;
if (event.target.checked) {
newSelected = [...selected];
newSelected.push(key);
} else {
newSelected = selected.filter((vin) => vin !== key);
}
setSelected(newSelected);
} catch (e) {
logger.warn(e.stack);
}
};
useEffect(() => {
setTitle("Vehicles");
setSitePath([]);
@@ -61,24 +38,17 @@ const MainForm = () => {
<Link to="/vehicle-add">
<AddCircleIcon fontSize="large" />
</Link>
<div
className={classes.labelInline}
>{`${selected.length} Selected`}</div>
</Grid>
<Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch} />
</Grid>
<Grid item md={4} className={classes.textRightAlign}>
<SendCommand vins={selected} />
</Grid>
<Grid item md={4} className={classes.textRightAlign} />
</Grid>
<CarSelectionTable
classes={classes}
token={token}
multiSelect={false}
search={{ search }}
selected={selected}
onSelect={handleSelect}
onSelectAll={handleSelectAll}
/>
</div>
);

View File

@@ -0,0 +1,39 @@
jest.mock("../../Contexts/VehicleContext");
jest.mock("../../Contexts/StatusContext");
jest.mock("../../Contexts/UserContext");
jest.mock('@material-ui/core/utils/unstable_useId', () =>
jest.fn().mockReturnValue('mui-test-id'),
);
import { render, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import { VehicleProvider } from "../../Contexts/VehicleContext";
import { StatusProvider } from "../../Contexts/StatusContext";
import { UserProvider, setToken } from "../../Contexts/UserContext";
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
import MainForm from "./index"
const renderVehicleTable = async () => {
const { container } = render(
<VehicleProvider>
<StatusProvider>
<UserProvider>
<BrowserRouter>
<MainForm />
</BrowserRouter>
</UserProvider>
</StatusProvider>
</VehicleProvider>
);
await waitFor(() => { /* render */ });
return container;
};
describe("VehicleTable", () => {
it("Render", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderVehicleTable();
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,21 @@
import React from "react";
import { useParams } from "react-router";
import clsx from "clsx";
import { Typography } from "@material-ui/core";
import CANFiltersTable from "../../CANFilter/Table";
import useStyles from "../../useStyles";
const CANFiltersTab = () => {
const { vin } = useParams();
const classes = useStyles();
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Typography variant="h6">CAN Filters</Typography>
<CANFiltersTable vin={vin} classes={classes} />
</div >
);
};
export default CANFiltersTab;

View File

@@ -0,0 +1,31 @@
jest.mock("../../Contexts/CANFiltersContext");
jest.mock("../../Contexts/StatusContext");
jest.mock("../../Contexts/UserContext");
jest.mock('@material-ui/core/utils/unstable_useId', () =>
jest.fn().mockReturnValue('mui-test-id'),
);
import { render, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import { setToken } from "../../Contexts/UserContext";
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
import CANFiltersTab from "./CANFiltersTab"
const renderCANFiltersTab = async () => {
const { container } = render(
<BrowserRouter>
<CANFiltersTab vin="TESTVIN1234567890" />
</BrowserRouter>
);
await waitFor(() => { /* render */ });
return container;
};
describe("CANFiltersTab", () => {
it("Render", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderCANFiltersTab();
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,73 @@
import React from "react";
import { useParams } from "react-router";
import clsx from "clsx";
import { Button, Grid, Typography } from "@material-ui/core";
import CarECUsTable from "../../Controls/CarECUsTable";
import CarUpdatesTable from "../../Controls/CarUpdatesTable";
import { logger } from "../../../services/monitoring";
import {
VehicleProvider,
useVehicleContext,
} from "../../Contexts/VehicleContext";
import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
const MainForm = () => {
const { vin } = useParams();
const classes = useStyles();
const { setMessage } = useStatusContext();
const { busy, sendCommand } = useVehicleContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
const updateHandler = async (e) => {
try {
await sendCommand([vin], "ecu", "", token);
setMessage(`Sent command to ${vin}`);
} catch (error) {
setMessage(error.message);
logger.error(error.stack);
}
};
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Typography variant="h6">Car Updates</Typography>
<CarUpdatesTable vin={vin} token={token} classes={classes} />
<Grid container className={classes.root} spacing={2}>
<Grid item md={4} className={classes.textJustifyAlign}></Grid>
<Grid item md={4} className={classes.textCenterAlign}>
<Typography variant="h6" className={classes.labelInline}>
Car ECUs
</Typography>
</Grid>
<Grid item md={4} className={classes.textRightAlign}>
<Button
type="submit"
disabled={busy}
variant="contained"
color="primary"
className={clsx(classes.formControl, classes.textField)}
onClick={updateHandler}
>
{busy ? "Sending..." : "Refresh"}
</Button>
</Grid>
</Grid>
<CarECUsTable vin={vin} token={token} classes={classes} />
</div >
);
};
const CarUpdatesTab = () => (
<VehicleProvider>
<MainForm />
</VehicleProvider>
);
export default CarUpdatesTab;

View File

@@ -0,0 +1,39 @@
jest.mock("../../Contexts/CANFiltersContext");
jest.mock("../../Contexts/StatusContext");
jest.mock("../../Contexts/UserContext");
jest.mock('@material-ui/core/utils/unstable_useId', () =>
jest.fn().mockReturnValue('mui-test-id'),
);
import { render, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import { CANFiltersProvider } from "../../Contexts/CANFiltersContext";
import { StatusProvider } from "../../Contexts/StatusContext";
import { UserProvider, setToken } from "../../Contexts/UserContext";
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
import MainForm from "./CarUpdatesTab"
const renderCarUpdatesTab = async () => {
const { container } = render(
<CANFiltersProvider>
<StatusProvider>
<UserProvider>
<BrowserRouter>
<MainForm vin="TESTVIN1234567890" />
</BrowserRouter>
</UserProvider>
</StatusProvider>
</CANFiltersProvider>
);
await waitFor(() => { /* render */ });
return container;
};
describe("CarUpdatesTab", () => {
it("Render", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderCarUpdatesTab();
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,128 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VehicleDetailsTab Render 1`] = `
<div>
<div
data-testid="mocked-vehicleprovider"
>
<div
data-testid="mocked-statusprovider"
>
<div
data-testid="mocked-userprovider"
>
<div
data-testid="mocked-vehicleprovider"
>
<div
class="makeStyles-paper-3 makeStyles-tableSize-53"
>
<div
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
>
<div
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-12"
>
<p>
<b>
VIN
</b>
:
</p>
<p>
<b>
Log Level
</b>
:
info
</p>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-12"
>
<b>
CANBus
</b>
<p>
<b>
Enabled
</b>
:
true
</p>
<p>
<b>
Max Memory Buffer Size
</b>
:
1
</p>
<p>
<b>
Enabled
</b>
:
true
</p>
<p>
<b>
Max Disk Buffer Size
</b>
:
2
</p>
<p>
<b>
Filters
</b>
:
3
</p>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-12"
>
<a
class=""
href="/vehicle-update?vin=undefined"
style="margin: 5px;"
title="Update \\"undefined\\""
>
<svg
aria-hidden="true"
aria-label="Update \\"undefined\\""
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"undefined\\""
>
<svg
aria-hidden="true"
aria-label="Delete \\"undefined\\""
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,95 @@
import React, { useEffect, useState } from "react";
import { Redirect } from "react-router";
import { Link } from 'react-router-dom';
import {
Grid,
Tooltip,
} from "@material-ui/core";
import EditIcon from "@material-ui/icons/Edit"
import DeleteIcon from "@material-ui/icons/Delete";
import clsx from "clsx";
import { useUserContext } from "../../../Contexts/UserContext"
import { useStatusContext } from "../../../Contexts/StatusContext";
import useStyles from "../../../useStyles";
import { logger } from "../../../../services/monitoring";
import { useVehicleContext, VehicleProvider } from "../../../Contexts/VehicleContext";
const MainForm = ({ vin }) => {
const classes = useStyles();
const { setMessage } = useStatusContext();
const { vehicle, getVehicle, deleteVehicle } = useVehicleContext();
const [redirect, setRedirect] = useState(null);
const { token: { idToken: { jwtToken: token } } } = useUserContext();
useEffect(() => {
(async () => {
try {
if (!vin || !token) return;
await getVehicle(vin, token);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
const onDelete = async () => {
try {
await deleteVehicle(vin, token);
setMessage(`Deleted ${vin}`)
setRedirect(`/vehicles`);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
};
if (redirect && redirect.length > 0) {
return <Redirect to={redirect} />;
}
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Grid container className={classes.root} spacing={2}>
<Grid item md={12} className={classes.textCenterAlign}>
<p><b>VIN</b>: {vin}</p>
{vehicle.log_level != null && (
<p><b>Log Level</b>: {vehicle.log_level}</p>
)}
</Grid>
{vehicle.canbus && (
<Grid item md={12} className={classes.textCenterAlign}>
<b>CANBus</b>
<p><b>Enabled</b>: {vehicle.canbus.enabled.toString()}</p>
<p><b>Max Memory Buffer Size</b>: {vehicle.canbus.max_mem_buffer_size ?? "Default"}</p>
<p><b>Enabled</b>: {vehicle.canbus.data_logger_enabled.toString()}</p>
<p><b>Max Disk Buffer Size</b>: {vehicle.canbus.max_disk_buffer_size ?? "Default"}</p>
<p><b>Filters</b>: {vehicle.canbus.filters ? vehicle.canbus.filters.length : 0}</p>
</Grid>
)}
<Grid item md={12} className={classes.textCenterAlign}>
<Tooltip key={`update-${vin}`} title={`Update "${vin}"`}>
<Link to={`/vehicle-update?vin=${vin}`} style={{ margin: 5 }}>
<EditIcon aria-label={`Update "${vin}"`} />
</Link>
</Tooltip>
<Tooltip key={`delete-${vin}`} title={`Delete "${vin}"`}>
<Link to="#" onClick={onDelete}>
<DeleteIcon aria-label={`Delete "${vin}"`} />
</Link>
</Tooltip>
</Grid>
</Grid>
</div >
);
};
const CarDetails = (props) => (
<VehicleProvider>
<MainForm {...props} />
</VehicleProvider>
);
export default CarDetails;

View File

@@ -0,0 +1,36 @@
jest.mock("../../../Contexts/VehicleContext");
jest.mock("../../../Contexts/StatusContext");
jest.mock("../../../Contexts/UserContext");
import { render, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import { VehicleProvider } from "../../../Contexts/VehicleContext";
import { StatusProvider } from "../../../Contexts/StatusContext";
import { UserProvider, setToken } from "../../../Contexts/UserContext";
import { TEST_AUTH_OBJECT } from "../../../../utils/testing";
import MainForm from "./index"
const renderVehicleDetailsTab = async () => {
const { container } = render(
<VehicleProvider>
<StatusProvider>
<UserProvider>
<BrowserRouter>
<MainForm />
</BrowserRouter>
</UserProvider>
</StatusProvider>
</VehicleProvider>
);
await waitFor(() => { /* render */ });
return container;
};
describe("VehicleDetailsTab", () => {
it("Render", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderVehicleDetailsTab();
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,21 @@
import React from "react";
import { useParams } from "react-router";
import clsx from "clsx";
import { Typography } from "@material-ui/core";
import CarDetails from "./Details";
import useStyles from "../../useStyles";
const CarDetailsTab = () => {
const { vin } = useParams();
const classes = useStyles();
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Typography variant="h6">Vehicle Details</Typography>
<CarDetails vin={vin} classes={classes} />
</div >
);
};
export default CarDetailsTab;

View File

@@ -0,0 +1,41 @@
jest.mock("../../Contexts/VehicleContext");
jest.mock("../../Contexts/StatusContext");
jest.mock("../../Contexts/UserContext");
jest.mock('@material-ui/core/utils/unstable_useId', () =>
jest.fn().mockReturnValue('mui-test-id'),
);
import { render, waitFor } from "@testing-library/react";
import { MemoryRouter, Route } from "react-router-dom";
import { VehicleProvider } from "../../Contexts/VehicleContext";
import { StatusProvider } from "../../Contexts/StatusContext";
import { UserProvider, setToken } from "../../Contexts/UserContext";
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
import MainForm from "./DetailsTab"
const renderDetailsTab = async () => {
const { container } = render(
<VehicleProvider>
<StatusProvider>
<UserProvider>
<MemoryRouter initialEntries={['/testroute/TESTVIN1234567890']}>
<Route path="/testroute/:vin">
<MainForm vin="TESTVIN1234567890" />
</Route>
</MemoryRouter>
</UserProvider>
</StatusProvider>
</VehicleProvider >
);
await waitFor(() => { /* render */ });
return container;
};
describe("DetailsTab", () => {
it("Render", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderDetailsTab();
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,450 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CANFiltersTab Render 1`] = `
<div>
<div
class="makeStyles-paper-3 makeStyles-tableSize-53"
>
<h6
class="MuiTypography-root MuiTypography-h6"
>
CAN Filters
</h6>
<div
data-testid="mocked-canfiltersprovider"
>
<div
class="makeStyles-paper-3 makeStyles-tableSize-53"
>
<div
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
>
<div
class="MuiGrid-root makeStyles-textJustifyAlign-47 MuiGrid-item MuiGrid-grid-md-4"
>
<a
class="makeStyles-labelInline-9"
href="/filter-add?vin=undefined"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"
/>
</svg>
</a>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-8"
>
<div
class="MuiFormControl-root makeStyles-margin-28 makeStyles-fullWidth-50"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated"
data-shrink="false"
for="search"
>
Search
</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="search"
type="text"
value=""
/>
<div
class="MuiInputAdornment-root MuiInputAdornment-positionEnd"
>
<button
aria-label="search"
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="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
</div>
</div>
</div>
</div>
<table
class="MuiTable-root"
>
<thead
class="MuiTableHead-root"
>
<tr
class="MuiTableRow-root MuiTableRow-head"
>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
CAN ID
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Interval (ms)
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
Actions
</th>
</tr>
</thead>
<tbody
class="MuiTableBody-root"
>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
123
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
1000
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
class=""
href="/filter-update?vin=undefined&can_id=123&interval=1000"
style="margin: 5px;"
title="Update \\"123\\""
>
<svg
aria-hidden="true"
aria-label="Update 123"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"123\\""
>
<svg
aria-hidden="true"
aria-label="Delete 123"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
456-789
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
2000
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
class=""
href="/filter-update?vin=undefined&can_id=456-789&interval=2000"
style="margin: 5px;"
title="Update \\"456-789\\""
>
<svg
aria-hidden="true"
aria-label="Update 456-789"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"456-789\\""
>
<svg
aria-hidden="true"
aria-label="Delete 456-789"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
1
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
0
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
class=""
href="/filter-update?vin=undefined&can_id=1&interval=0"
style="margin: 5px;"
title="Update \\"1\\""
>
<svg
aria-hidden="true"
aria-label="Update 1"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"1\\""
>
<svg
aria-hidden="true"
aria-label="Delete 1"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
</tbody>
<tfoot
class="MuiTableFooter-root"
>
<tr
class="MuiTableRow-root MuiTableRow-footer"
>
<td
class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root"
colspan="8"
>
<div
class="MuiToolbar-root MuiToolbar-regular MuiTablePagination-toolbar MuiToolbar-gutters"
>
<div
class="MuiTablePagination-spacer"
/>
<p
class="MuiTypography-root MuiTablePagination-caption MuiTypography-body2 MuiTypography-colorInherit"
>
Rows per page:
</p>
<div
class="MuiInputBase-root MuiTablePagination-input MuiTablePagination-selectRoot"
>
<select
aria-label="rows per page"
class="MuiSelect-root MuiSelect-select MuiTablePagination-select MuiInputBase-input"
>
<option
class="MuiTablePagination-menuItem"
value="5"
>
5
</option>
<option
class="MuiTablePagination-menuItem"
value="10"
>
10
</option>
<option
class="MuiTablePagination-menuItem"
value="25"
>
25
</option>
<option
class="MuiTablePagination-menuItem"
value="100"
>
100
</option>
</select>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSelect-icon MuiTablePagination-selectIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
</svg>
</div>
<p
class="MuiTypography-root MuiTablePagination-caption MuiTypography-body2 MuiTypography-colorInherit"
>
1-3 of 3
</p>
<div
class="MuiTablePagination-actions"
>
<button
aria-label="Previous page"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
title="Previous page"
type="button"
>
<span
class="MuiIconButton-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z"
/>
</svg>
</span>
</button>
<button
aria-label="Next page"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
title="Next page"
type="button"
>
<span
class="MuiIconButton-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z"
/>
</svg>
</span>
</button>
</div>
</div>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,606 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CarUpdatesTab Render 1`] = `
<div>
<div
data-testid="mocked-canfiltersprovider"
>
<div
data-testid="mocked-statusprovider"
>
<div
data-testid="mocked-userprovider"
>
<div
class="makeStyles-paper-3 makeStyles-tableSize-53"
>
<h6
class="MuiTypography-root MuiTypography-h6"
>
Car Updates
</h6>
<table
class="MuiTable-root"
>
<thead
class="MuiTableHead-root"
>
<tr
class="MuiTableRow-root MuiTableRow-head"
>
<th
aria-sort="descending"
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root MuiTableSortLabel-active"
role="button"
tabindex="0"
>
ID
<span
class="makeStyles-hiddenSortSpan-27"
>
sorted descending
</span>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionDesc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Name
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Status
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Created
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Updated
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
</tr>
</thead>
<tbody
class="MuiTableBody-root"
/>
<tfoot
class="MuiTableFooter-root"
>
<tr
class="MuiTableRow-root MuiTableRow-footer"
>
<td
class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root"
colspan="5"
>
<div
class="MuiToolbar-root MuiToolbar-regular MuiTablePagination-toolbar MuiToolbar-gutters"
>
<div
class="MuiTablePagination-spacer"
/>
<p
class="MuiTypography-root MuiTablePagination-caption MuiTypography-body2 MuiTypography-colorInherit"
>
Rows per page:
</p>
<div
class="MuiInputBase-root MuiTablePagination-input MuiTablePagination-selectRoot"
>
<select
aria-label="rows per page"
class="MuiSelect-root MuiSelect-select MuiTablePagination-select MuiInputBase-input"
>
<option
class="MuiTablePagination-menuItem"
value="5"
>
5
</option>
<option
class="MuiTablePagination-menuItem"
value="10"
>
10
</option>
<option
class="MuiTablePagination-menuItem"
value="25"
>
25
</option>
<option
class="MuiTablePagination-menuItem"
value="100"
>
100
</option>
</select>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSelect-icon MuiTablePagination-selectIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
</svg>
</div>
<p
class="MuiTypography-root MuiTablePagination-caption MuiTypography-body2 MuiTypography-colorInherit"
>
0-0 of 0
</p>
<div
class="MuiTablePagination-actions"
>
<button
aria-label="Previous page"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
title="Previous page"
type="button"
>
<span
class="MuiIconButton-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z"
/>
</svg>
</span>
</button>
<button
aria-label="Next page"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
title="Next page"
type="button"
>
<span
class="MuiIconButton-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z"
/>
</svg>
</span>
</button>
</div>
</div>
</td>
</tr>
</tfoot>
</table>
<div
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
>
<div
class="MuiGrid-root makeStyles-textJustifyAlign-47 MuiGrid-item MuiGrid-grid-md-4"
/>
<div
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-4"
>
<h6
class="MuiTypography-root makeStyles-labelInline-9 MuiTypography-h6"
>
Car ECUs
</h6>
</div>
<div
class="MuiGrid-root makeStyles-textRightAlign-49 MuiGrid-item MuiGrid-grid-md-4"
>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-formControl-7 makeStyles-textField-29 MuiButton-containedPrimary"
tabindex="0"
type="submit"
>
<span
class="MuiButton-label"
>
Refresh
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
</div>
<div
class="makeStyles-paper-3 makeStyles-tableSize-53"
>
<table
class="MuiTable-root"
>
<thead
class="MuiTableHead-root"
>
<tr
class="MuiTableRow-root MuiTableRow-head"
>
<th
aria-sort="descending"
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root MuiTableSortLabel-active"
role="button"
tabindex="0"
>
ECU
<span
class="makeStyles-hiddenSortSpan-27"
>
sorted descending
</span>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionDesc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
SW Version
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
HW Version
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Config
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Created
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Updated
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
</tr>
</thead>
<tbody
class="MuiTableBody-root"
/>
<tfoot
class="MuiTableFooter-root"
>
<tr
class="MuiTableRow-root MuiTableRow-footer"
>
<td
class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root"
colspan="10"
>
<div
class="MuiToolbar-root MuiToolbar-regular MuiTablePagination-toolbar MuiToolbar-gutters"
>
<div
class="MuiTablePagination-spacer"
/>
<p
class="MuiTypography-root MuiTablePagination-caption MuiTypography-body2 MuiTypography-colorInherit"
>
Rows per page:
</p>
<div
class="MuiInputBase-root MuiTablePagination-input MuiTablePagination-selectRoot"
>
<select
aria-label="rows per page"
class="MuiSelect-root MuiSelect-select MuiTablePagination-select MuiInputBase-input"
>
<option
class="MuiTablePagination-menuItem"
value="5"
>
5
</option>
<option
class="MuiTablePagination-menuItem"
value="10"
>
10
</option>
<option
class="MuiTablePagination-menuItem"
value="25"
>
25
</option>
<option
class="MuiTablePagination-menuItem"
value="100"
>
100
</option>
</select>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSelect-icon MuiTablePagination-selectIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
</svg>
</div>
<p
class="MuiTypography-root MuiTablePagination-caption MuiTypography-body2 MuiTypography-colorInherit"
>
0-0 of 0
</p>
<div
class="MuiTablePagination-actions"
>
<button
aria-label="Previous page"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
title="Previous page"
type="button"
>
<span
class="MuiIconButton-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z"
/>
</svg>
</span>
</button>
<button
aria-label="Next page"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
title="Next page"
type="button"
>
<span
class="MuiIconButton-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z"
/>
</svg>
</span>
</button>
</div>
</div>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,138 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DetailsTab Render 1`] = `
<div>
<div
data-testid="mocked-vehicleprovider"
>
<div
data-testid="mocked-statusprovider"
>
<div
data-testid="mocked-userprovider"
>
<div
class="makeStyles-paper-3 makeStyles-tableSize-53"
>
<h6
class="MuiTypography-root MuiTypography-h6"
>
Vehicle Details
</h6>
<div
data-testid="mocked-vehicleprovider"
>
<div
class="makeStyles-paper-3 makeStyles-tableSize-53"
>
<div
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
>
<div
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-12"
>
<p>
<b>
VIN
</b>
:
TESTVIN1234567890
</p>
<p>
<b>
Log Level
</b>
:
info
</p>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-12"
>
<b>
CANBus
</b>
<p>
<b>
Enabled
</b>
:
true
</p>
<p>
<b>
Max Memory Buffer Size
</b>
:
1
</p>
<p>
<b>
Enabled
</b>
:
true
</p>
<p>
<b>
Max Disk Buffer Size
</b>
:
2
</p>
<p>
<b>
Filters
</b>
:
3
</p>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-12"
>
<a
class=""
href="/vehicle-update?vin=TESTVIN1234567890"
style="margin: 5px;"
title="Update \\"TESTVIN1234567890\\""
>
<svg
aria-hidden="true"
aria-label="Update \\"TESTVIN1234567890\\""
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
/>
</svg>
</a>
<a
class=""
href="/testroute/TESTVIN1234567890"
title="Delete \\"TESTVIN1234567890\\""
>
<svg
aria-hidden="true"
aria-label="Delete \\"TESTVIN1234567890\\""
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,187 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CarStatus Render 1`] = `
<div>
<div
data-testid="mocked-canfiltersprovider"
>
<div
data-testid="mocked-statusprovider"
>
<div
data-testid="mocked-userprovider"
>
<div
class="makeStyles-paper-3 makeStyles-tableSize-53"
>
<div
class="MuiBox-root MuiBox-root-62 makeStyles-tableToolbar-30"
>
<div
class="MuiTabs-root"
>
<div
class="MuiTabs-scroller MuiTabs-fixed"
style="overflow: hidden;"
>
<div
aria-label="car tabs"
class="MuiTabs-flexContainer"
role="tablist"
>
<button
aria-controls="tabpanel-0"
aria-selected="true"
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit Mui-selected"
id="tab-0"
role="tab"
tabindex="0"
type="button"
>
<span
class="MuiTab-wrapper"
>
Details
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
<button
aria-controls="tabpanel-1"
aria-selected="false"
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit"
id="tab-1"
role="tab"
tabindex="-1"
type="button"
>
<span
class="MuiTab-wrapper"
>
Car Updates
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
<button
aria-controls="tabpanel-2"
aria-selected="false"
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit"
id="tab-2"
role="tab"
tabindex="-1"
type="button"
>
<span
class="MuiTab-wrapper"
>
CAN Filters
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
<span
class="PrivateTabIndicator-root-63 PrivateTabIndicator-colorSecondary-65 MuiTabs-indicator"
style="left: 0px; width: 0px;"
/>
</div>
</div>
</div>
<div
aria-labelledby="tab-0"
id="tabpanel-0"
role="tabpanel"
>
<div
class="MuiBox-root MuiBox-root-67"
>
<div
class="makeStyles-paper-3 makeStyles-tableSize-53"
>
<h6
class="MuiTypography-root MuiTypography-h6"
>
Vehicle Details
</h6>
<div
class="makeStyles-paper-3 makeStyles-tableSize-53"
>
<div
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
>
<div
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-12"
>
<p>
<b>
VIN
</b>
:
</p>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-12"
>
<a
class=""
href="/vehicle-update?vin=undefined"
style="margin: 5px;"
title="Update \\"undefined\\""
>
<svg
aria-hidden="true"
aria-label="Update \\"undefined\\""
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"undefined\\""
>
<svg
aria-hidden="true"
aria-label="Delete \\"undefined\\""
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div
aria-labelledby="tab-1"
hidden=""
id="tabpanel-1"
role="tabpanel"
/>
<div
aria-labelledby="tab-2"
hidden=""
id="tabpanel-2"
role="tabpanel"
/>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -1,38 +1,30 @@
import React, { useEffect } from "react";
import { useParams } from "react-router";
import { useLocation } from "react-router-dom";
import clsx from "clsx";
import { Button, Grid, Typography } from "@material-ui/core";
import { Box, Tab, Tabs } from "@material-ui/core";
import CarECUsTable from "../../Controls/CarECUsTable";
import CarUpdatesTable from "../../Controls/CarUpdatesTable";
import { logger } from "../../../services/monitoring";
import {
VehicleProvider,
useVehicleContext,
} from "../../Contexts/VehicleContext";
import { useUserContext } from "../../Contexts/UserContext";
import CarDetailsTab from "./DetailsTab";
import CarUpdatesTab from "./CarUpdatesTab";
import CANFiltersTab from "./CANFiltersTab";
import TabPanel from "../../Controls/TabPanel";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
const MainForm = () => {
const tabHashes = ["details", "updates", "filters"];
const CarStatus = () => {
const { vin } = useParams();
const classes = useStyles();
const { setTitle, setSitePath, setMessage } = useStatusContext();
const { busy, sendCommand } = useVehicleContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
const updateHandler = async (e) => {
try {
await sendCommand([vin], "ecu", "", token);
setMessage(`Sent command to ${vin}`);
} catch (error) {
setMessage(error.message);
logger.error(error.stack);
}
};
const { setTitle, setSitePath } = useStatusContext();
const { hash } = useLocation();
const [tabIndex, setTabIndex] = React.useState(0);
useEffect(() => {
const key = hash.replace("#", "");
const index = tabHashes.findIndex((element) => element === key);
if (index >= 0) setTabIndex(index);
}, [hash]);
useEffect(() => {
const title = `Vehicle ${vin} Details`;
@@ -49,39 +41,48 @@ const MainForm = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [vin]);
const handleTabChange = (_event, newIndex) => {
setTabIndex(newIndex);
};
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Typography variant="h6">Car Updates</Typography>
<CarUpdatesTable vin={vin} token={token} classes={classes} />
<Grid container className={classes.root} spacing={2}>
<Grid item md={4} className={classes.textJustifyAlign}></Grid>
<Grid item md={4} className={classes.textCenterAlign}>
<Typography variant="h6" className={classes.labelInline}>
Car ECUs
</Typography>
</Grid>
<Grid item md={4} className={classes.textRightAlign}>
<Button
type="submit"
disabled={busy}
variant="contained"
color="primary"
className={clsx(classes.formControl, classes.textField)}
onClick={updateHandler}
<Box
className={classes.tableToolbar}
sx={{ borderBottom: 1, borderColor: "divider" }}
>
{busy ? "Sending..." : "Refresh"}
</Button>
</Grid>
</Grid>
<CarECUsTable vin={vin} token={token} classes={classes} />
<Tabs
value={tabIndex}
onChange={handleTabChange}
aria-label="car tabs"
indicatorColor="secondary"
>
<Tab label="Details" {...tabProps(0)} />
<Tab label="Car Updates" {...tabProps(1)} />
<Tab label="CAN Filters" {...tabProps(2)} />
</Tabs>
</Box>
<TabPanel value={tabIndex} index={0}>
<CarDetailsTab />
</TabPanel>
<TabPanel value={tabIndex} index={1}>
<CarUpdatesTab />
</TabPanel>
<TabPanel value={tabIndex} index={2}>
<CANFiltersTab />
</TabPanel>
</div>
);
};
const CarStatus = () => (
<VehicleProvider>
<MainForm />
</VehicleProvider>
);
function tabProps(index) {
return {
id: `tab-${index}`,
"aria-controls": `tabpanel-${index}`,
};
}
export default CarStatus;

View File

@@ -0,0 +1,39 @@
jest.mock("../../Contexts/CANFiltersContext");
jest.mock("../../Contexts/StatusContext");
jest.mock("../../Contexts/UserContext");
jest.mock('@material-ui/core/utils/unstable_useId', () =>
jest.fn().mockReturnValue('mui-test-id'),
);
import { render, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import { CANFiltersProvider } from "../../Contexts/CANFiltersContext";
import { StatusProvider } from "../../Contexts/StatusContext";
import { UserProvider, setToken } from "../../Contexts/UserContext";
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
import CarStatus from "./index"
const renderCarStatus = async () => {
const { container } = render(
<CANFiltersProvider>
<StatusProvider>
<UserProvider>
<BrowserRouter>
<CarStatus vin="TESTVIN1234567890" />
</BrowserRouter>
</UserProvider>
</StatusProvider>
</CANFiltersProvider>
);
await waitFor(() => { /* render */ });
return container;
};
describe("CarStatus", () => {
it("Render", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderCarStatus();
expect(container).toMatchSnapshot();
});
});

View File

@@ -1,91 +0,0 @@
import React, { useEffect, useState } from "react";
import {
Backdrop,
Modal,
Fade,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
} from "@material-ui/core";
import useStyles from "../../useStyles";
import { useUpdatesContext } from "../../Contexts/UpdatesContext";
import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import { LocalDateTimeString } from "../../../utils/dates";
export default function CarStatusModal(props) {
const classes = useStyles();
const [updates, setUpdates] = useState([]);
const { setMessage } = useStatusContext();
const { getVINUpdates } = useUpdatesContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
useEffect(() => {
(async () => {
try {
if (!props.vin) return;
const result = await getVINUpdates(props.vin, token);
if (result.error) {
throw new Error(`Get VIN updates error. ${result.message}`);
} else {
setUpdates(result.data);
}
} catch (e) {
setMessage(e.message);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.vin]);
return (
<div>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
className={classes.modal}
open={props.vin !== null && props.vin !== undefined}
onClose={props.handleClose}
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
>
<Fade in={props.vin}>
<div className={classes.modaldialog}>
<h2 id="transition-modal-title">{props.vin} Updates</h2>
<Table>
<TableHead>
<TableRow>
<TableCell align="center">Date</TableCell>
<TableCell align="center">Update</TableCell>
<TableCell align="center">Status</TableCell>
<TableCell align="center">Updated</TableCell>
</TableRow>
</TableHead>
<TableBody>
{updates.map((update) => (
<TableRow key={update.id}>
<TableCell align="center">
{LocalDateTimeString(update.created)}
</TableCell>
<TableCell align="center">{`${update.updatepackage.package_name} ${update.updatepackage.version}`}</TableCell>
<TableCell align="center">{update.status}</TableCell>
<TableCell align="center">
{LocalDateTimeString(update.updated)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</Fade>
</Modal>
</div>
);
}

View File

@@ -0,0 +1,731 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VehicleUpdate Render 1`] = `
<div>
<div
data-testid="mocked-vehicleprovider"
>
<div
data-testid="mocked-statusprovider"
>
<div
data-testid="mocked-userprovider"
>
<div
data-testid="mocked-vehicleprovider"
>
<div
class="makeStyles-paper-3"
>
<form
action="{onSubmit}"
class="makeStyles-form-5"
novalidate=""
>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-disabled Mui-disabled Mui-required Mui-required"
data-shrink="false"
for="vin"
id="vin-label"
>
VIN
<span
aria-hidden="true"
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
>
*
</span>
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root Mui-disabled Mui-disabled MuiInputBase-fullWidth MuiInputBase-formControl"
>
<input
aria-invalid="false"
class="MuiInputBase-input MuiOutlinedInput-input Mui-disabled Mui-disabled"
disabled=""
id="vin"
maxlength="17"
name="vin"
readonly=""
required=""
type="text"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64"
>
<span>
VIN
 *
</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-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required"
data-shrink="true"
for="model"
id="model-label"
>
Model
<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="model"
maxlength="255"
name="model"
required=""
type="text"
value="Ocean"
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
>
<span>
Model
 *
</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-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required"
data-shrink="true"
for="year"
id="year-label"
>
Year
<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="year"
maxlength="4"
minlength="4"
name="year"
required=""
type="number"
value="2022"
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
>
<span>
Year
 *
</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-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required"
data-shrink="true"
for="trim"
id="trim-label"
>
Trim
<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="trim"
maxlength="4"
minlength="4"
name="trim"
required=""
type="text"
value="Base"
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
>
<span>
Trim
 *
</span>
</legend>
</fieldset>
</div>
</div>
<label
class="MuiFormLabel-root"
id="demo-row-radio-buttons-group-label"
>
Log Level
</label>
<div
aria-labelledby="demo-row-radio-buttons-group-label"
class="MuiFormGroup-root MuiFormGroup-row"
margin="normal"
role="radiogroup"
>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-69"
name="log-level-group"
type="radio"
value="trace"
/>
<div
class="PrivateRadioButtonIcon-root-70"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden="true"
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</div>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Trace
</span>
</label>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-69"
name="log-level-group"
type="radio"
value="debug"
/>
<div
class="PrivateRadioButtonIcon-root-70"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden="true"
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</div>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Debug
</span>
</label>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary PrivateSwitchBase-checked-67 Mui-checked MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
checked=""
class="PrivateSwitchBase-input-69"
name="log-level-group"
type="radio"
value="info"
/>
<div
class="PrivateRadioButtonIcon-root-70 PrivateRadioButtonIcon-checked-72"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden="true"
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</div>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Info
</span>
</label>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-69"
name="log-level-group"
type="radio"
value="warn"
/>
<div
class="PrivateRadioButtonIcon-root-70"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden="true"
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</div>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Warn
</span>
</label>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-69"
name="log-level-group"
type="radio"
value="error"
/>
<div
class="PrivateRadioButtonIcon-root-70"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden="true"
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</div>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Error
</span>
</label>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-69"
name="log-level-group"
type="radio"
value="critical"
/>
<div
class="PrivateRadioButtonIcon-root-70"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden="true"
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</div>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Critical
</span>
</label>
</div>
<label
class="MuiFormLabel-root"
id="demo-row-radio-buttons-group-label"
>
CAN Bus
</label>
<div
class="MuiFormGroup-root"
>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiCheckbox-root MuiCheckbox-colorSecondary PrivateSwitchBase-checked-67 Mui-checked MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
checked=""
class="PrivateSwitchBase-input-69"
data-indeterminate="false"
type="checkbox"
value=""
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
CAN Bus Enabled
</span>
</label>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required"
data-shrink="true"
for="max_mem_buffer_size"
id="max_mem_buffer_size-label"
>
Max Memory Buffer Size (0 uses default size)
<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="max_mem_buffer_size"
maxlength="12"
name="max_mem_buffer_size"
required=""
type="number"
value="1"
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
>
<span>
Max Memory Buffer Size (0 uses default size)
 *
</span>
</legend>
</fieldset>
</div>
</div>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiCheckbox-root MuiCheckbox-colorSecondary PrivateSwitchBase-checked-67 Mui-checked MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-69"
data-indeterminate="false"
type="checkbox"
value=""
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Data Logger Enabled
</span>
</label>
</div>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required"
data-shrink="true"
for="max_disk_buffer_size"
id="max_disk_buffer_size-label"
>
Max Disk Buffer Size (0 uses default size)
<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="max_disk_buffer_size"
maxlength="12"
name="max_disk_buffer_size"
required=""
type="number"
value="2"
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
>
<span>
Max Disk Buffer Size (0 uses default size)
 *
</span>
</legend>
</fieldset>
</div>
</div>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary MuiButton-fullWidth"
tabindex="0"
type="submit"
>
<span
class="MuiButton-label"
>
Submit
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,285 @@
import React, { useEffect, useRef, useState } from "react";
import { Redirect } from "react-router";
import { useLocation } from "react-router-dom";
import {
Button,
Checkbox,
FormControlLabel,
FormGroup,
FormLabel,
Radio,
RadioGroup,
TextField
} from "@material-ui/core";
import useStyles from "../../useStyles";
import {
useVehicleContext,
VehicleProvider
} from "../../Contexts/VehicleContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext";
import { logger } from "../../../services/monitoring";
const MainForm = () => {
const queries = new URLSearchParams(useLocation().search);
const vin = queries.get("vin") ?? "";
const { vehicle, getVehicle, updateVehicle, busy } = useVehicleContext();
const { token: { idToken: { jwtToken: token } } } = useUserContext();
const { setMessage, setTitle, setSitePath } = useStatusContext();
const [redirect, setRedirect] = useState(null);
const classes = useStyles();
const modelEl = useRef(null);
const yearEl = useRef(null);
const trimEl = useRef(null);
const [selectedLogLevel, setSelectedLogLevel] = useState("info");
const [canbusEnabled, setCANBusEnabled] = useState(true);
const [dataLoggerEnabled, setDataLoggerEnabled] = useState(false);
const [maxMemBufferSize, setMaxMemBufferSize] = useState(0);
const [maxDiskBufferSize, setMaxDiskBufferSize] = useState(0);
useEffect(() => {
setTitle("Update Vehicle");
setSitePath([
{
label: `Vehicle ${vin}`,
link: "/vehicles",
},
{
label: "Update Vehicle",
},
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
(async () => {
try {
if (!vin || !token) return;
await getVehicle(vin, token);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
useEffect(() => {
setSelectedLogLevel(vehicle.log_level ?? selectedLogLevel);
if (vehicle.canbus) {
setCANBusEnabled(vehicle.canbus.enabled ?? canbusEnabled);
setDataLoggerEnabled(vehicle.canbus.data_logger_enabled ?? dataLoggerEnabled);
setMaxMemBufferSize(vehicle.canbus.max_mem_buffer_size ?? maxMemBufferSize);
setMaxDiskBufferSize(vehicle.canbus.max_disk_buffer_size ?? maxDiskBufferSize);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [vehicle]);
const onLogLevelChange = (event) => {
setSelectedLogLevel(event.target.value);
}
const onCANBusChange = (event) => {
setCANBusEnabled(event.target.checked);
}
const onDataLoggerChange = (event) => {
setDataLoggerEnabled(event.target.checked);
}
const onMaxMemBufferSizeChange = (event) => {
setMaxMemBufferSize(event.target.value);
}
const onMaxDiskBufferSizeChange = (event) => {
setMaxDiskBufferSize(event.target.value);
}
const onSubmit = async (event) => {
try {
event.preventDefault();
const formData = {
vin: vin,
model: modelEl.current.value,
year: parseInt(yearEl.current.value),
trim: trimEl.current.value,
log_level: selectedLogLevel,
canbus: {
enabled: canbusEnabled,
data_logger_enabled: canbusEnabled ? dataLoggerEnabled : false,
max_mem_buffer_size: canbusEnabled ? parseInt(maxMemBufferSize) : 0,
max_disk_buffer_size: canbusEnabled && dataLoggerEnabled ? parseInt(maxDiskBufferSize) : 0
}
};
const result = await updateVehicle(vin, formData, token);
if (!result || result.error) return;
setMessage(`Updated ${result.vin}`);
setRedirect(`/vehicle-status/${result.vin}`);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
};
if (redirect && redirect.length > 0) {
return <Redirect to={redirect} />;
}
return (
<div className={classes.paper}>
<form className={classes.form} noValidate action="{onSubmit}">
<TextField
id="vin"
name="vin"
label="VIN"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "17",
readOnly: true,
}}
disabled
value={vin}
required
fullWidth
/>
<TextField
id="model"
name="model"
label="Model"
defaultValue="Ocean"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "255",
}}
required
fullWidth
inputRef={modelEl}
/>
<TextField
id="year"
name="year"
label="Year"
type="number"
defaultValue="2022"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "4",
minLength: "4",
}}
required
fullWidth
inputRef={yearEl}
/>
<TextField
id="trim"
name="trim"
label="Trim"
defaultValue="Base"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "4",
minLength: "4",
}}
required
fullWidth
inputRef={trimEl}
/>
<FormLabel id="demo-row-radio-buttons-group-label">Log Level</FormLabel>
<RadioGroup
row
aria-labelledby="demo-row-radio-buttons-group-label"
name="log-level-group"
value={selectedLogLevel}
onChange={onLogLevelChange}
margin="normal"
>
<FormControlLabel value="trace" control={<Radio />} label="Trace" />
<FormControlLabel value="debug" control={<Radio />} label="Debug" />
<FormControlLabel value="info" control={<Radio />} label="Info" />
<FormControlLabel value="warn" control={<Radio />} label="Warn" />
<FormControlLabel value="error" control={<Radio />} label="Error" />
<FormControlLabel value="critical" control={<Radio />} label="Critical" />
</RadioGroup>
<FormLabel id="demo-row-radio-buttons-group-label">CAN Bus</FormLabel>
<FormGroup>
<FormControlLabel control={
<Checkbox
checked={canbusEnabled}
onChange={onCANBusChange}
/>
} label="CAN Bus Enabled" />
<TextField
id="max_mem_buffer_size"
name="max_mem_buffer_size"
label='Max Memory Buffer Size (0 uses default size)'
value={maxMemBufferSize}
onChange={onMaxMemBufferSizeChange}
variant="outlined"
margin="normal"
inputProps={{
maxLength: "12",
}}
type="number"
disabled={!canbusEnabled}
required
fullWidth
/>
<FormControlLabel control={
<Checkbox
checked={dataLoggerEnabled}
onChange={onDataLoggerChange}
disabled={!canbusEnabled}
/>
} label="Data Logger Enabled" />
</FormGroup>
<TextField
id="max_disk_buffer_size"
name="max_disk_buffer_size"
label='Max Disk Buffer Size (0 uses default size)'
value={maxDiskBufferSize}
onChange={onMaxDiskBufferSizeChange}
variant="outlined"
margin="normal"
inputProps={{
maxLength: "12",
}}
type="number"
disabled={!dataLoggerEnabled}
required
fullWidth
/>
<Button
type="submit"
disabled={busy}
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={onSubmit}
>
Submit
</Button>
</form>
</div>
);
};
const VehicleUpdateForm = (props) => (
<VehicleProvider>
<MainForm {...props} />
</VehicleProvider>
);
export default VehicleUpdateForm;

View File

@@ -0,0 +1,36 @@
jest.mock("../../Contexts/VehicleContext");
jest.mock("../../Contexts/StatusContext");
jest.mock("../../Contexts/UserContext");
import { render, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import { VehicleProvider } from "../../Contexts/VehicleContext";
import { StatusProvider } from "../../Contexts/StatusContext";
import { UserProvider, setToken } from "../../Contexts/UserContext";
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
import MainForm from "./index"
const renderVehicleUpdate = async () => {
const { container } = render(
<VehicleProvider>
<StatusProvider>
<UserProvider>
<BrowserRouter>
<MainForm />
</BrowserRouter>
</UserProvider>
</StatusProvider>
</VehicleProvider>
);
await waitFor(() => { /* render */ });
return container;
};
describe("VehicleUpdate", () => {
it("Render", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderVehicleUpdate();
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,90 @@
import React, { useRef, useState } from "react";
import {
Button,
FormControlLabel,
FormLabel,
Radio,
RadioGroup,
TextField,
} from "@material-ui/core";
import useStyles from "../../useStyles";
import { CertTypes } from "../../Contexts/CertificateContext";
const CreateForm = ({ onCreate, busy }) => {
const classes = useStyles();
const vinEl = useRef(null);
const [certType, setCertType] = useState(CertTypes.TBOX);
const onSubmit = async (event) => {
event.preventDefault();
if (onCreate)
onCreate({
vin: vinEl.current.value,
type: certType,
});
};
const onCertTypeChange = (event) => {
setCertType(event.target.value);
};
return (
<div className={classes.paper}>
<form className={classes.form} noValidate action="{onSubmit}">
<TextField
id="vin"
name="vin"
label="VIN"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "17",
}}
required
fullWidth
inputRef={vinEl}
/>
<FormLabel id="cert-type-group-label">Type</FormLabel>
<RadioGroup
row
aria-labelledby="cert-type-group-label"
name="cert-type"
value={certType}
onChange={onCertTypeChange}
margin="normal"
>
<FormControlLabel
value={CertTypes.TBOX}
control={<Radio />}
label="TBOX"
/>
<FormControlLabel
value={CertTypes.ICC}
control={<Radio />}
label="ICC"
/>
<FormControlLabel
value={CertTypes.Charging}
control={<Radio />}
label="Charging"
/>
</RadioGroup>
<Button
type="submit"
disabled={busy}
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={onSubmit}
>
{busy ? "Submitting..." : "Submit"}
</Button>
</form>
</div>
);
};
export default CreateForm;

View File

@@ -0,0 +1,50 @@
import { Button } from "@material-ui/core";
import React from "react";
import DownloadFileLink from "../../Controls/DownloadFileLink";
import useStyles from "../../useStyles";
const CertMimeType = "application/x-pem-file";
const DownloadCerts = ({ vin, publicCert, privateCert, onChangeView }) => {
const classes = useStyles();
const onNewCert = (event) => {
event.preventDefault();
if (!onChangeView) return;
onChangeView();
};
return (
<div>
<h2>Download Certificates</h2>
<ul>
<li>
<DownloadFileLink
data={publicCert}
filename={`${vin}_cert.pem`}
mimetype={CertMimeType}
/>
</li>
<li>
<DownloadFileLink
data={privateCert}
filename={`${vin}_key.pem`}
mimetype={CertMimeType}
/>
</li>
</ul>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={onNewCert}
>
Done
</Button>
</div>
);
};
export default DownloadCerts;

View File

@@ -0,0 +1,25 @@
import React from "react";
import { render, waitFor } from "@testing-library/react";
import DownloadCerts from "./DownloadCerts";
describe("DownloadCerts", () => {
beforeAll(() => {
global.URL.createObjectURL = jest.fn();
global.URL.revokeObjectURL = jest.fn();
});
it("Render", async () => {
const { container } = render(
<DownloadCerts
vin={"TESTVIN"}
publicCert={"PUBLIC"}
privateCert={"PRIVATE"}
/>
);
await waitFor(() => {
/* render */
});
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DownloadCerts Render 1`] = `
<div>
<div>
<h2>
Download Certificates
</h2>
<ul>
<li>
<a
download="TESTVIN_cert.pem"
>
TESTVIN_cert.pem
</a>
</li>
<li>
<a
download="TESTVIN_key.pem"
>
TESTVIN_key.pem
</a>
</li>
</ul>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary MuiButton-fullWidth"
tabindex="0"
type="submit"
>
<span
class="MuiButton-label"
>
Done
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
</div>
`;

View File

@@ -0,0 +1,85 @@
import React, { useEffect, useState } from "react";
import {
useCertificateContext,
CertificateProvider,
} from "../../Contexts/CertificateContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext";
import { logger } from "../../../services/monitoring";
import CreateForm from "./CreateForm";
import DownloadCerts from "./DownloadCerts";
const VIEW_FORM = 0;
const VIEW_DOWNLOAD = 1;
const MainForm = () => {
const { busy, createCert } = useCertificateContext();
const { setMessage, setTitle, setSitePath } = useStatusContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
const [view, setView] = useState(VIEW_FORM);
const [pubCert, setPubCert] = useState(null);
const [privCert, setPrivCert] = useState(null);
const [vin, setVIN] = useState(null);
useEffect(() => {
setTitle("Create Certificate");
setSitePath([
{
label: "Tools",
link: "/tools/certificates/add",
},
{
label: "Create Certificate",
},
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onCreate = async (data) => {
try {
const result = await createCert(data, token);
setPubCert(result.public_key);
setPrivCert(result.private_key);
setVIN(data.vin);
setMessage(`Created ${data.vin} certificate`);
setView(VIEW_DOWNLOAD);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
};
const onChangeView = () => {
setPubCert(null);
setPrivCert(null);
setVIN(null);
setView(VIEW_FORM);
};
if (view === VIEW_DOWNLOAD)
return (
<DownloadCerts
vin={vin}
publicCert={pubCert}
privateCert={privCert}
onChangeView={onChangeView}
/>
);
return <CreateForm onCreate={onCreate} busy={busy} />;
};
const CertificateCreate = () => (
<CertificateProvider>
<MainForm />
</CertificateProvider>
);
export default CertificateCreate;

View File

@@ -0,0 +1,127 @@
import React, { useContext, useState } from "react";
import api from "../../services/CANFiltersAPI";
const CANFiltersContext = React.createContext();
export const CANFiltersProvider = ({ children }) => {
const [busy, setBusy] = useState(false);
const [filters, setFilters] = useState([]);
const [totalFilters, setTotalFilters] = useState(0);
const addFilter = async (vin, filter, token) => {
try {
setBusy(true);
validateVIN(vin);
validateFilter(filter);
const result = await api.addFilter(vin, filter, token);
if (result.error) throw new Error(`Add filter error. ${result.message}`);
return result;
} finally {
setBusy(false);
}
};
const getFilters = async (vin, search, token) => {
try {
setBusy(true);
validateVIN(vin);
const result = await api.getFilters(vin, search, token);
if (result.error) {
setFilters([])
throw new Error(`Get filters error. ${result.message}`);
}
setFilters(result.data)
if (result.total) {
setTotalFilters(result.total);
}
return result;
} finally {
setBusy(false);
}
};
const updateFilter = async (vin, canID, filter, token) => {
try {
setBusy(true);
validateVIN(vin);
validateID(canID);
validateFilter(filter);
const result = await api.updateFilter(vin, canID, filter, token);
if (result.error) {
throw new Error(`Update filters error. ${result.message}`);
}
return result;
} finally {
setBusy(false);
}
}
const deleteFilter = async (vin, canID, token) => {
try {
setBusy(true);
validateVIN(vin);
validateID(canID);
const result = await api.deleteFilter(vin, canID, token);
if (result.error) {
throw new Error(`Delete filter error. ${result.message}`);
}
const index = filters.findIndex(element => element.can_id === canID);
if (index >= 0) filters.splice(index, 1);
return result;
} finally {
setBusy(false);
}
}
return (
<CANFiltersContext.Provider
value={{
busy,
filters,
totalFilters,
addFilter,
getFilters,
updateFilter,
deleteFilter
}}
>
{children}
</CANFiltersContext.Provider>
);
};
const validateVIN = (vin) => {
if (vin == null || vin.length !== 17) {
throw new Error("Invalid VIN");
}
}
const validateID = (can_id) => {
if (can_id == null || can_id === "") {
throw new Error("CAN ID required");
}
}
const validateFilter = (filter) => {
if (filter == null) {
throw new Error("No filter data");
}
validateID(filter.can_id)
if (filter.interval == null) {
throw new Error("Interval required");
}
};
export const useCANFiltersContext = () => useContext(CANFiltersContext);

View File

@@ -0,0 +1,289 @@
jest.mock("../../services/CANFiltersAPI")
import {
render,
cleanup,
screen,
fireEvent,
waitFor,
} from "@testing-library/react";
import { CANFiltersProvider, useCANFiltersContext } from "./CANFiltersContext";
import { StatusProvider, useStatusContext } from "./StatusContext";
const checkFiltersResults = (error, busy, filters) => {
checkBaseResults(error, busy);
expect(screen.getByTestId("filters").innerHTML).toEqual(filters);
};
const checkBaseResults = (error, busy) => {
expect(screen.getByTestId("error").innerHTML).toEqual(error);
expect(screen.getByTestId("busy").innerHTML).toEqual(busy);
};
describe("CANFiltersContext", () => {
describe("getFilters", () => {
beforeEach(() => {
const TestComp = () => {
const { busy, error, filters, getFilters } = useCANFiltersContext();
return (
<>
<div data-testid="error">{error}</div>
<div data-testid="busy">{busy.toString()}</div>
<div data-testid="filters">{JSON.stringify(filters)}</div>
<button
data-testid="getFilters"
onClick={() => getFilters("TESTVIN1234567890")}
/>
</>
);
};
render(
<CANFiltersProvider>
<TestComp />
</CANFiltersProvider>
);
});
afterEach(() => {
cleanup();
});
it("initial state", () => {
checkFiltersResults("", "false", "[]");
});
it("getFilters", async () => {
fireEvent.click(screen.getByTestId("getFilters"));
await waitFor(() =>
expect(screen.getByTestId("filters").innerHTML).not.toBe("[]")
);
checkFiltersResults("", "false", JSON.stringify(expectedFiltersData));
});
});
describe("addFilter", () => {
beforeEach(async () => {
const TestComp = () => {
const { busy, addFilter } = useCANFiltersContext();
const { message, setMessage } = useStatusContext();
const add = async (data) => {
try {
await addFilter("TESTVIN1234567890", data);
} catch (e) {
setMessage(e.message);
}
};
return (
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="addFilterNull" onClick={() => add(null)} />
<button data-testid="addFilterNoCANID" onClick={() => add({})} />
<button
data-testid="addFilter"
onClick={() =>
add({ can_id: "123", interval: 1000 })
}
/>
</>
);
};
render(
<StatusProvider>
<CANFiltersProvider>
<TestComp />
</CANFiltersProvider>
</StatusProvider>
);
});
afterEach(() => {
cleanup();
});
it("initial state", () => {
checkBaseResults("", "false");
});
it("addFilterNull", async () => {
fireEvent.click(screen.getByTestId("addFilterNull"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("No filter data", "false");
});
it("addFilterNoCANID", async () => {
fireEvent.click(screen.getByTestId("addFilterNoCANID"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("CAN ID required", "false");
});
it("addFilter", async () => {
fireEvent.click(screen.getByTestId("addFilter"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("", "false");
});
});
describe("updateFilter", () => {
beforeEach(async () => {
const TestComp = () => {
const { busy, updateFilter } = useCANFiltersContext();
const { message, setMessage } = useStatusContext();
const update = async (data) => {
try {
await updateFilter("TESTVIN1234567890", data.can_id, data);
} catch (e) {
setMessage(e.message);
}
};
return (
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="updateFilterNull" onClick={() => update(null)} />
<button data-testid="updateFilterNoCANID" onClick={() => update({})} />
<button
data-testid="updateFilter"
onClick={() =>
update({ can_id: "123", interval: 1000 })
}
/>
</>
);
};
render(
<StatusProvider>
<CANFiltersProvider>
<TestComp />
</CANFiltersProvider>
</StatusProvider>
);
});
afterEach(() => {
cleanup();
});
it("initial state", () => {
checkBaseResults("", "false");
});
it("updateFilterNull", async () => {
fireEvent.click(screen.getByTestId("updateFilterNull"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("Cannot read property 'can_id' of null", "false");
});
it("updateFilterNoCANID", async () => {
fireEvent.click(screen.getByTestId("updateFilterNoCANID"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("CAN ID required", "false");
});
it("updateFilter", async () => {
fireEvent.click(screen.getByTestId("updateFilter"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("", "false");
});
});
describe("deleteFilter", () => {
beforeEach(async () => {
const TestComp = () => {
const { busy, deleteFilter } = useCANFiltersContext();
const { message, setMessage } = useStatusContext();
const deleteF = async (can_id) => {
try {
await deleteFilter("TESTVIN1234567890", can_id);
} catch (e) {
setMessage(e.message);
}
};
return (
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="deleteFilterNull" onClick={() => deleteF(null)} />
<button data-testid="deleteFilterNonexistent" onClick={() => deleteF(-1)} />
<button
data-testid="deleteFilter"
onClick={() =>
deleteF(123)
}
/>
</>
);
};
render(
<StatusProvider>
<CANFiltersProvider>
<TestComp />
</CANFiltersProvider>
</StatusProvider>
);
});
afterEach(() => {
cleanup();
});
it("initial state", () => {
checkBaseResults("", "false");
});
it("deleteFilterNull", async () => {
fireEvent.click(screen.getByTestId("deleteFilterNull"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("CAN ID required", "false");
});
it("deleteFilterNonexistent", async () => {
fireEvent.click(screen.getByTestId("deleteFilterNonexistent"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("", "false");
});
it("deleteFilter", async () => {
fireEvent.click(screen.getByTestId("deleteFilter"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("", "false");
});
});
});
const expectedFiltersData = [
{
can_id: "123",
interval: 1000
},
{
can_id: "456",
interval: 0
},
{
can_id: "789-1000",
interval: 5
},
];

View File

@@ -0,0 +1,50 @@
import React, { useContext, useState } from "react";
import api from "../../services/certificatesAPI";
const CertificateContext = React.createContext();
export const CertTypes = {
TBOX: "TBOX",
ICC: "ICC",
Charging: "Charging",
};
const validateCreate = (data) => {
if (!data.type) throw new Error("type is required");
if (!data.vin) throw new Error("vin is required");
};
export const CertificateProvider = ({ children }) => {
const [busy, setBusy] = useState(false);
const createCert = async (data, token) => {
try {
setBusy(true);
validateCreate(data);
const result = await api.create(data, token);
if (result.error) {
throw new Error(`Create certificate error. ${result.message}`);
}
return result;
} finally {
setBusy(false);
}
};
return (
<CertificateContext.Provider
value={{
busy,
createCert,
}}
>
{children}
</CertificateContext.Provider>
);
};
export const useCertificateContext = () => useContext(CertificateContext);

View File

@@ -0,0 +1,307 @@
import React, { useContext, useState } from "react";
import api from "../../services/fleetsAPI";
const FleetContext = React.createContext();
export const FleetProvider = ({ children }) => {
const [busy, setBusy] = useState(false);
const [fleet, setFleet] = useState({});
const [fleets, setFleets] = useState([]);
const [totalFleets, setTotalFleets] = useState(0);
const [fleetVehicles, setFleetVehicles] = useState([]);
const [totalFleetVehicles, setTotalFleetVehicles] = useState(0);
const [fleetCANFilters, setFleetCANFilters] = useState([]);
const [totalFleetCANFilters, setTotalFleetCANFilters] = useState(0);
const addFleet = async (f, token) => {
try {
setBusy(true);
validateFleet(f);
const result = await api.addFleet(f, token);
if (result.error) throw new Error(`Add fleet error. ${result.message}`);
return result;
} finally {
setBusy(false);
}
};
const getFleet = async (name, token) => {
try {
setBusy(true);
validateFleetName(name);
const result = await api.getFleet(name, token);
if (result.error) {
setFleet({});
throw new Error(`Get fleet error. ${result.message}`);
}
setFleet(result);
return result;
} finally {
setBusy(false);
}
};
const getFleets = async (search, token) => {
try {
setBusy(true);
const result = await api.getFleets(search, token);
if (result.error) {
setFleets([])
throw new Error(`Get fleets error. ${result.message}`);
}
setFleets(result.data)
if (result.total) {
setTotalFleets(result.total);
}
return result;
} finally {
setBusy(false);
}
};
const updateFleet = async (name, f, token) => {
try {
setBusy(true);
validateFleetName(name);
validateFleet(f);
const result = await api.updateFleet(name, f, token);
if (result.error) {
throw new Error(`Update fleet error. ${result.message}`);
}
return result;
} finally {
setBusy(false);
}
};
const deleteFleet = async (name, token) => {
try {
setBusy(true);
validateFleetName(name);
const result = await api.deleteFleet(name, token);
if (result.error) {
throw new Error(`Delete filter error. ${result.message}`);
}
return result;
} finally {
setBusy(false);
}
};
const getFleetVehicles = async (name, search, token) => {
try {
setBusy(true);
const result = await api.getFleetVehicles(name, search, token);
if (result.error) {
setFleetVehicles([])
throw new Error(`Get fleet vehicles error. ${result.message}`);
}
setFleetVehicles(result.data)
if (result.total) {
setTotalFleetVehicles(result.total);
}
return result;
} finally {
setBusy(false);
}
};
const addFleetVehicle = async (name, vehicle, token) => {
try {
setBusy(true);
validateFleetName(name);
validateVIN(vehicle.vin);
const result = await api.addFleetVehicle(name, vehicle, token);
if (result.error) {
throw new Error(`Add fleet vehicle error. ${result.message}`);
}
return result;
} finally {
setBusy(false);
}
};
const deleteFleetVehicle = async (name, vehicle, token) => {
try {
setBusy(true);
validateFleetName(name);
validateVIN(vehicle.vin);
const result = await api.deleteFleetVehicle(name, vehicle, token);
if (result.error) {
throw new Error(`Delete fleet vehicle error. ${result.message}`);
}
const index = fleetVehicles.findIndex(element => element === vehicle.vin);
if (index >= 0) fleetVehicles.splice(index, 1);
return result;
} finally {
setBusy(false);
}
};
const getFleetCANFilters = async (name, search, token) => {
try {
setBusy(true);
const result = await api.getFleetCANFilters(name, search, token);
if (result.error) {
setFleetCANFilters([])
throw new Error(`Get fleet filters error. ${result.message}`);
}
setFleetCANFilters(result.data)
if (result.total) {
setTotalFleetCANFilters(result.total);
}
return result;
} finally {
setBusy(false);
}
};
const addFleetCANFilter = async (name, filter, token) => {
try {
setBusy(true);
validateFleetName(name);
validateFilter(filter);
const result = await api.addFleetCANFilter(name, filter, token);
if (result.error) {
throw new Error(`Add fleet CAN filter error. ${result.message}`);
}
return result;
} finally {
setBusy(false);
}
}
const updateFleetCANFilter = async (name, can_id, filter, token) => {
try {
setBusy(true);
validateFleetName(name);
validateCANID(can_id);
validateFilter(filter);
const result = await api.updateFleetCANFilter(name, can_id, filter, token);
if (result.error) {
throw new Error(`Update fleet CAN filter error. ${result.message}`);
}
return result;
} finally {
setBusy(false);
}
}
const deleteFleetCANFilter = async (name, can_id, token) => {
try {
setBusy(true);
validateFleetName(name);
validateCANID(can_id);
const result = await api.deleteFleetCANFilter(name, can_id, token);
if (result.error) {
throw new Error(`Delete fleet vehicle error. ${result.message}`);
}
const index = fleetCANFilters.findIndex(element => element.can_id === can_id);
if (index >= 0) fleetCANFilters.splice(index, 1);
return result;
} finally {
setBusy(false);
}
};
return (
<FleetContext.Provider
value={{
busy,
fleet,
fleets,
totalFleets,
addFleet,
getFleet,
getFleets,
updateFleet,
deleteFleet,
fleetVehicles,
totalFleetVehicles,
getFleetVehicles,
addFleetVehicle,
deleteFleetVehicle,
fleetCANFilters,
totalFleetCANFilters,
getFleetCANFilters,
addFleetCANFilter,
updateFleetCANFilter,
deleteFleetCANFilter
}}
>
{children}
</FleetContext.Provider>
);
};
const validateFleet = (f) => {
if (f == null) {
throw new Error("No fleet data");
}
validateFleetName(f.name);
}
const validateFleetName = (name) => {
if (name == null || !/^[\w-]+$/.test(name)) {
throw new Error("Invalid name");
}
}
const validateVIN = (vin) => {
if (vin == null || vin.length !== 17) {
throw new Error("Invalid VIN");
}
}
const validateFilter = (filter) => {
if (filter == null) {
throw new Error("No CAN filter data");
}
validateCANID(filter.can_id)
if (filter.interval == null || !/^\d+$/.test(filter.interval)) {
throw new Error("Invalid interval");
}
}
const validateCANID = (can_id) => {
if (can_id == null || !/^\d+(-\d+)?$/.test(can_id)) {
throw new Error("Invalid CAN ID");
}
}
export const useFleetContext = () => useContext(FleetContext);

View File

@@ -0,0 +1,752 @@
jest.mock("../../services/fleetsAPI")
import {
render,
cleanup,
screen,
fireEvent,
waitFor,
} from "@testing-library/react";
import { FleetProvider, useFleetContext } from "./FleetContext";
import { StatusProvider, useStatusContext } from "./StatusContext";
const checkFleetResults = (error, busy, fleet) => {
checkBaseResults(error, busy);
expect(screen.getByTestId("fleet").innerHTML).toEqual(fleet);
};
const checkFleetsResults = (error, busy, fleets) => {
checkBaseResults(error, busy);
expect(screen.getByTestId("fleets").innerHTML).toEqual(fleets);
};
const checkFleetVehicleResults = (error, busy, vehicles) => {
checkBaseResults(error, busy);
expect(screen.getByTestId("fleet-vehicles").innerHTML).toEqual(vehicles);
}
const checkFleetCANFilterResults = (error, busy, filters) => {
checkBaseResults(error, busy);
expect(screen.getByTestId("fleet-filters").innerHTML).toEqual(filters);
}
const checkBaseResults = (error, busy) => {
expect(screen.getByTestId("error").innerHTML).toEqual(error);
expect(screen.getByTestId("busy").innerHTML).toEqual(busy);
};
describe("FleetContext", () => {
describe("getFleets", () => {
beforeEach(() => {
const TestComp = () => {
const { busy, error, fleets, getFleets } = useFleetContext();
return (
<>
<div data-testid="error">{error}</div>
<div data-testid="busy">{busy.toString()}</div>
<div data-testid="fleets">{JSON.stringify(fleets)}</div>
<button
data-testid="getFleets"
onClick={() => getFleets()}
/>
</>
);
};
render(
<FleetProvider>
<TestComp />
</FleetProvider>
);
});
afterEach(() => {
cleanup();
});
it("initial state", () => {
checkFleetsResults("", "false", "[]");
});
it("getFleets", async () => {
fireEvent.click(screen.getByTestId("getFleets"));
await waitFor(() =>
expect(screen.getByTestId("fleets").innerHTML).not.toBe("[]")
);
checkFleetsResults("", "false", JSON.stringify(expectedFleetsData));
});
});
describe("getFleet", () => {
beforeEach(() => {
const TestComp = () => {
const { busy, error, fleet, getFleet } = useFleetContext();
return (
<>
<div data-testid="error">{error}</div>
<div data-testid="busy">{busy.toString()}</div>
<div data-testid="fleet">{JSON.stringify(fleet)}</div>
<button
data-testid="getFleet"
onClick={() => getFleet("US-WEST")}
/>
</>
);
};
render(
<FleetProvider>
<TestComp />
</FleetProvider>
);
});
afterEach(() => {
cleanup();
});
it("initial state", () => {
checkFleetResults("", "false", "{}");
});
it("getFleet", async () => {
fireEvent.click(screen.getByTestId("getFleet"));
await waitFor(() =>
expect(screen.getByTestId("fleet").innerHTML).not.toBe("{}")
);
checkFleetResults("", "false", JSON.stringify(expectedFleetData));
});
});
describe("addFleet", () => {
beforeEach(async () => {
const TestComp = () => {
const { busy, addFleet } = useFleetContext();
const { message, setMessage } = useStatusContext();
const add = async (fleet) => {
try {
await addFleet(fleet);
} catch (e) {
setMessage(e.message);
}
};
return (
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="addFleetNull" onClick={() => add(null)} />
<button data-testid="addFleetNoName" onClick={() => add({})} />
<button
data-testid="addFleet"
onClick={() =>
add({ name: "EU-WEST", log_level: "warn", canbus: { enabled: false } })
}
/>
</>
);
};
render(
<StatusProvider>
<FleetProvider>
<TestComp />
</FleetProvider>
</StatusProvider>
);
});
afterEach(() => {
cleanup();
});
it("initial state", () => {
checkBaseResults("", "false");
});
it("addFleetNull", async () => {
fireEvent.click(screen.getByTestId("addFleetNull"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("No fleet data", "false");
});
it("addFleetNoName", async () => {
fireEvent.click(screen.getByTestId("addFleetNoName"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("Invalid name", "false");
});
it("addFleet", async () => {
fireEvent.click(screen.getByTestId("addFleet"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("", "false");
});
});
describe("updateFleet", () => {
beforeEach(async () => {
const TestComp = () => {
const { busy, updateFleet } = useFleetContext();
const { message, setMessage } = useStatusContext();
const update = async (data) => {
try {
await updateFleet("EU-WEST", data);
} catch (e) {
setMessage(e.message);
}
};
return (
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="updateFleetNull" onClick={() => update(null)} />
<button data-testid="updateFleetNoName" onClick={() => update({})} />
<button
data-testid="updateFleet"
onClick={() =>
update({ name: "EU-WEST", log_level: "warn", canbus: { enabled: false } })
}
/>
</>
);
};
render(
<StatusProvider>
<FleetProvider>
<TestComp />
</FleetProvider>
</StatusProvider>
);
});
afterEach(() => {
cleanup();
});
it("initial state", () => {
checkBaseResults("", "false");
});
it("updateFleetNull", async () => {
fireEvent.click(screen.getByTestId("updateFleetNull"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("No fleet data", "false");
});
it("updateFleetNoName", async () => {
fireEvent.click(screen.getByTestId("updateFleetNoName"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("Invalid name", "false");
});
it("updateFleet", async () => {
fireEvent.click(screen.getByTestId("updateFleet"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("", "false");
});
});
describe("deleteFleet", () => {
beforeEach(async () => {
const TestComp = () => {
const { busy, deleteFleet } = useFleetContext();
const { message, setMessage } = useStatusContext();
const deleteF = async (name) => {
try {
await deleteFleet(name);
} catch (e) {
setMessage(e.message);
}
};
return (
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="deleteFleetNull" onClick={() => deleteF(null)} />
<button data-testid="deleteFleetNonexistent" onClick={() => deleteF("INVALID")} />
<button
data-testid="deleteFleet"
onClick={() =>
deleteF("US-WEST")
}
/>
</>
);
};
render(
<StatusProvider>
<FleetProvider>
<TestComp />
</FleetProvider>
</StatusProvider>
);
});
afterEach(() => {
cleanup();
});
it("initial state", () => {
checkBaseResults("", "false");
});
it("deleteFleetNull", async () => {
fireEvent.click(screen.getByTestId("deleteFleetNull"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("Invalid name", "false");
});
it("deleteFleetNonexistent", async () => {
fireEvent.click(screen.getByTestId("deleteFleetNonexistent"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("", "false");
});
it("deleteFleet", async () => {
fireEvent.click(screen.getByTestId("deleteFleet"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("", "false");
});
});
describe("getFleetVehicles", () => {
beforeEach(() => {
const TestComp = () => {
const { busy, error, fleetVehicles, getFleetVehicles } = useFleetContext();
return (
<>
<div data-testid="error">{error}</div>
<div data-testid="busy">{busy.toString()}</div>
<div data-testid="fleet-vehicles">{JSON.stringify(fleetVehicles)}</div>
<button
data-testid="getFleetVehicles"
onClick={() => getFleetVehicles("US-WEST")}
/>
</>
);
};
render(
<FleetProvider>
<TestComp />
</FleetProvider>
);
});
afterEach(() => {
cleanup();
});
it("initial state", () => {
checkFleetVehicleResults("", "false", "[]");
});
it("getFleetVehicles", async () => {
fireEvent.click(screen.getByTestId("getFleetVehicles"));
await waitFor(() =>
expect(screen.getByTestId("fleet-vehicles").innerHTML).not.toBe("[]")
);
checkFleetVehicleResults("", "false", JSON.stringify(expectedFleetVehiclesData));
});
});
describe("addFleetVehicle", () => {
beforeEach(async () => {
const TestComp = () => {
const { busy, addFleetVehicle } = useFleetContext();
const { message, setMessage } = useStatusContext();
const add = async (name, vehicle) => {
try {
await addFleetVehicle(name, vehicle);
} catch (e) {
setMessage(e.message);
}
};
return (
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="addFleetVehicleNull" onClick={() => add(null)} />
<button data-testid="addFleetVehicleNoName" onClick={() => add({})} />
<button
data-testid="addFleetVehicle"
onClick={() =>
add("US-TEST", { vin: "TESTVIN1234567890" })
}
/>
</>
);
};
render(
<StatusProvider>
<FleetProvider>
<TestComp />
</FleetProvider>
</StatusProvider>
);
});
afterEach(() => {
cleanup();
});
it("initial state", () => {
checkBaseResults("", "false");
});
it("addFleetVehicleNull", async () => {
fireEvent.click(screen.getByTestId("addFleetVehicleNull"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("Invalid name", "false");
});
it("addFleetVehicleNoName", async () => {
fireEvent.click(screen.getByTestId("addFleetVehicleNoName"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("Invalid name", "false");
});
it("addFleetVehicle", async () => {
fireEvent.click(screen.getByTestId("addFleetVehicle"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("", "false");
});
});
describe("deleteFleetVehicle", () => {
beforeEach(async () => {
const TestComp = () => {
const { busy, deleteFleetVehicle } = useFleetContext();
const { message, setMessage } = useStatusContext();
const deleteFV = async (name, vehicle) => {
try {
await deleteFleetVehicle(name, vehicle);
} catch (e) {
setMessage(e.message);
}
};
return (
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="deleteFleetVehicleNull" onClick={() => deleteFV("US-WEST", null)} />
<button data-testid="deleteFleetVehicleInvalid" onClick={() => deleteFV("US-WEST", "INVALID")} />
<button
data-testid="deleteFleetVehicle"
onClick={() =>
deleteFV("US-WEST", { vin: "USWESTVIN12345678" })
}
/>
</>
);
};
render(
<StatusProvider>
<FleetProvider>
<TestComp />
</FleetProvider>
</StatusProvider>
);
});
afterEach(() => {
cleanup();
});
it("initial state", () => {
checkBaseResults("", "false");
});
it("deleteFleetVehicleNull", async () => {
fireEvent.click(screen.getByTestId("deleteFleetVehicleNull"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("Cannot read property 'vin' of null", "false");
});
it("deleteFleetVehicleNonexistent", async () => {
fireEvent.click(screen.getByTestId("deleteFleetVehicleInvalid"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("Invalid VIN", "false");
});
it("deleteFleetVehicle", async () => {
fireEvent.click(screen.getByTestId("deleteFleetVehicle"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("", "false");
});
});
describe("getFleetCANFilters", () => {
beforeEach(() => {
const TestComp = () => {
const { busy, error, fleetCANFilters, getFleetCANFilters } = useFleetContext();
return (
<>
<div data-testid="error">{error}</div>
<div data-testid="busy">{busy.toString()}</div>
<div data-testid="fleet-filters">{JSON.stringify(fleetCANFilters)}</div>
<button
data-testid="getFleetCANFilters"
onClick={() => getFleetCANFilters("US-TEST")}
/>
</>
);
};
render(
<FleetProvider>
<TestComp />
</FleetProvider>
);
});
afterEach(() => {
cleanup();
});
it("initial state", () => {
checkFleetCANFilterResults("", "false", "[]");
});
it("getFleetCANFilters", async () => {
fireEvent.click(screen.getByTestId("getFleetCANFilters"));
await waitFor(() =>
expect(screen.getByTestId("fleet-filters").innerHTML).not.toBe("[]")
);
checkFleetCANFilterResults("", "false", JSON.stringify(expectedFleetCANFiltersData));
});
});
describe("addFleetCANFilter", () => {
beforeEach(async () => {
const TestComp = () => {
const { busy, addFleetCANFilter } = useFleetContext();
const { message, setMessage } = useStatusContext();
const add = async (name, filter) => {
try {
await addFleetCANFilter(name, filter);
} catch (e) {
setMessage(e.message);
}
};
return (
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="addFleetCANFilterNull" onClick={() => add(null)} />
<button data-testid="addFleetCANFilterNoName" onClick={() => add({})} />
<button
data-testid="addFleetCANFilter"
onClick={() =>
add("US-TEST", { can_id: "111", interval: 222 })
}
/>
</>
);
};
render(
<StatusProvider>
<FleetProvider>
<TestComp />
</FleetProvider>
</StatusProvider>
);
});
afterEach(() => {
cleanup();
});
it("initial state", () => {
checkBaseResults("", "false");
});
it("addFleetCANFilterNull", async () => {
fireEvent.click(screen.getByTestId("addFleetCANFilterNull"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("Invalid name", "false");
});
it("addFleetCANFilterNoName", async () => {
fireEvent.click(screen.getByTestId("addFleetCANFilterNoName"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("Invalid name", "false");
});
it("addFleetCANFilter", async () => {
fireEvent.click(screen.getByTestId("addFleetCANFilter"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("", "false");
});
});
describe("deleteFleetCANFilter", () => {
beforeEach(async () => {
const TestComp = () => {
const { busy, deleteFleetCANFilter } = useFleetContext();
const { message, setMessage } = useStatusContext();
const deleteFF = async (name, can_id) => {
try {
await deleteFleetCANFilter(name, can_id);
} catch (e) {
setMessage(e.message);
}
};
return (
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="deleteFleetCANFilterNull" onClick={() => deleteFF("US-WEST", null)} />
<button data-testid="deleteFleetCANFilterInvalid" onClick={() => deleteFF("US-WEST", "INVALID")} />
<button
data-testid="deleteFleetCANFilter"
onClick={() =>
deleteFF("US-WEST", "123-456")
}
/>
</>
);
};
render(
<StatusProvider>
<FleetProvider>
<TestComp />
</FleetProvider>
</StatusProvider>
);
});
afterEach(() => {
cleanup();
});
it("initial state", () => {
checkBaseResults("", "false");
});
it("deleteFleetCANFilterNull", async () => {
fireEvent.click(screen.getByTestId("deleteFleetCANFilterNull"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("Invalid CAN ID", "false");
});
it("deleteFleetCANFilterNonexistent", async () => {
fireEvent.click(screen.getByTestId("deleteFleetCANFilterInvalid"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("Invalid CAN ID", "false");
});
it("deleteFleetCANFilter", async () => {
fireEvent.click(screen.getByTestId("deleteFleetCANFilter"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("", "false");
});
});
});
const expectedFilters = [
{
can_id: "123-456",
interval: 789
},
{
can_id: "1",
interval: 1000
},
{
can_id: "1000",
interval: 1
}
]
const expectedFleetData = {
name: "US-WEST",
log_level: "info",
canbus: { enabled: true, data_logger_enabled: true, max_mem_buffer_size: 1, max_disk_buffer_size: 2, filters: expectedFilters },
vehicles: ["USWESTVIN12345678", "USWESTVIN12345679", "USWESTVIN12345670"]
}
const expectedFleetsData = [
{
name: "US-WEST",
log_level: "info",
canbus: { enabled: true, data_logger_enabled: true, max_mem_buffer_size: 1, max_disk_buffer_size: 2, filters: expectedFilters },
vehicles: ["USWESTVIN12345678", "USWESTVIN12345679", "USWESTVIN12345670"]
},
{
name: "US-CENTRAL",
log_level: "warn",
canbus: { enabled: false, data_logger_enabled: false, max_mem_buffer_size: 0, max_disk_buffer_size: 0 },
vehicles: ["USCENTVIN12345678", "USCENTVIN12345679", "USCENTVIN12345670"]
},
{
name: "US-EAST",
log_level: "error",
canbus: { enabled: true },
vehicles: ["USEASTVIN12345678", "USEASTVIN12345679", "USEASTVIN12345670"]
},
];
const expectedFleetVehiclesData = ["USWESTVIN12345678", "USWESTVIN12345679", "USWESTVIN12345670"];
const expectedFleetCANFiltersData = [
{
can_id: "123-456",
interval: 789
},
{
can_id: "1",
interval: 1000
},
{
can_id: "1000",
interval: 1
}
];

View File

@@ -22,13 +22,10 @@ const checkExistingManifest = async (data, token) => {
const ECUTemplate = {
name: "AGS",
part_number: "",
version: "",
serial_number: "",
hw_version: "",
vendor: "",
configuration_mask: "",
configuration: "",
fingerprint: "",
files: [],
manifest_id: 0,
};

View File

@@ -46,16 +46,16 @@ export const ManifestsProvider = ({ children }) => {
const deleteManifest = async (package_id, token) => {
let result;
const index = manifests.findIndex((element) => {
return element.id === package_id;
});
manifests.splice(index, 1);
try {
setBusy(true);
result = await api.deleteManifest(package_id, token);
if (result.error)
throw new Error(`Delete manifest error. ${result.message}`);
const index = manifests.findIndex((element) => {
return element.id === package_id;
});
manifests.splice(index, 1);
} finally {
setBusy(false);
}

View File

@@ -5,7 +5,7 @@ import api from "../../services/vehiclesAPI";
const VehicleContext = React.createContext();
const validateAdd = (vehicle) => {
if (vehicle === null) {
if (vehicle == null) {
throw new Error("No vehicle data");
}
@@ -28,6 +28,7 @@ const validateAdd = (vehicle) => {
export const VehicleProvider = ({ children }) => {
const [busy, setBusy] = useState(false);
const [vehicle, setVehicle] = useState({});
const [vehicles, setVehicles] = useState([]);
const [totalVehicles, setTotalVehicles] = useState(0);
const [models, setModels] = useState([]);
@@ -49,11 +50,11 @@ export const VehicleProvider = ({ children }) => {
}
};
const addVehicle = async (vehicle, token) => {
const addVehicle = async (v, token) => {
try {
setBusy(true);
validateAdd(vehicle);
const result = await api.addVehicle(vehicle, token);
validateAdd(v);
const result = await api.addVehicle(v, token);
if (result.error) throw new Error(`Add vehicle error. ${result.message}`);
return result;
} finally {
@@ -118,6 +119,21 @@ export const VehicleProvider = ({ children }) => {
}
};
const getVehicle = async (vin, token) => {
try {
setBusy(true);
validateVIN(vin);
const result = await api.getVehicle(vin, token);
if (result.error) throw new Error(`Get vehicle error. ${result.message}`);
setVehicle(result);
return result;
} finally {
setBusy(false);
}
}
const getVehicles = async (search, token) => {
try {
setBusy(true);
@@ -159,23 +175,56 @@ export const VehicleProvider = ({ children }) => {
}
};
const updateVehicle = async (vin, v, token) => {
try {
setBusy(true);
validateVIN(vin);
validateVehicle(v);
const result = await api.updateVehicle(vin, v, token);
if (result.error)
throw new Error(`Update vehicle error. ${result.message}`);
return result;
} finally {
setBusy(false);
}
}
const deleteVehicle = async (vin, token) => {
try {
setBusy(true);
validateVIN(vin);
const result = await api.deleteVehicle(vin, token);
if (result.error)
throw new Error(`Delete vehicle error. ${result.message}`);
return result;
} finally {
setBusy(false);
}
}
return (
<VehicleContext.Provider
value={{
busy,
models,
totalVehicles,
vehicle,
vehicles,
years,
addVehicle,
deleteVehicle,
getConnections,
getECUs,
getLocations,
getModels,
getState,
getYears,
getVehicle,
getVehicles,
sendCommand,
updateVehicle
}}
>
{children}
@@ -183,4 +232,19 @@ export const VehicleProvider = ({ children }) => {
);
};
const validateVehicle = (v) => {
if (v == null) {
throw new Error("No vehicle data");
}
validateVIN(v.vin);
};
const validateVIN = (vin) => {
if (vin == null || vin.length !== 17) {
throw new Error("Invalid VIN");
}
};
export const useVehicleContext = () => useContext(VehicleContext);

View File

@@ -10,7 +10,12 @@ import {
import { VehicleProvider, useVehicleContext } from "./VehicleContext";
import { StatusProvider, useStatusContext } from "./StatusContext";
const checkVehicleResults = (error, busy, vehicles) => {
const checkVehicleResult = (error, busy, vehicle) => {
checkBaseResults(error, busy);
expect(screen.getByTestId("vehicle").innerHTML).toEqual(vehicle);
}
const checkVehiclesResult = (error, busy, vehicles) => {
checkBaseResults(error, busy);
expect(screen.getByTestId("vehicles").innerHTML).toEqual(vehicles);
};
@@ -50,7 +55,7 @@ describe("VehicleContext", () => {
});
it("Initial state", () => {
checkVehicleResults("", "false", "[]");
checkVehiclesResult("", "false", "[]");
});
it("getVehicles", async () => {
@@ -58,7 +63,48 @@ describe("VehicleContext", () => {
await waitFor(() =>
expect(screen.getByTestId("vehicles").innerHTML).not.toBe("[]")
);
checkVehicleResults("", "false", JSON.stringify(expectedVehicleData));
checkVehiclesResult("", "false", JSON.stringify(expectedVehiclesData));
});
});
describe("getVehicle", () => {
beforeEach(() => {
const TestComp = () => {
const { busy, error, vehicle, getVehicle } = useVehicleContext();
return (
<>
<div data-testid="error">{error}</div>
<div data-testid="busy">{busy.toString()}</div>
<div data-testid="vehicle">{JSON.stringify(vehicle)}</div>
<button
data-testid="getVehicle"
onClick={() => getVehicle("3C4PDCBG0ET127145")}
/>
</>
);
};
render(
<VehicleProvider>
<TestComp />
</VehicleProvider>
);
});
afterEach(() => {
cleanup();
});
it("Initial state", () => {
checkVehicleResult("", "false", "{}");
});
it("getVehicle", async () => {
fireEvent.click(screen.getByTestId("getVehicle"));
await waitFor(() =>
expect(screen.getByTestId("vehicle").innerHTML).not.toBe("{}")
);
checkVehicleResult("", "false", JSON.stringify(expectedVehicleData));
});
});
@@ -131,15 +177,183 @@ describe("VehicleContext", () => {
checkBaseResults("", "false");
});
});
describe("updateVehicle", () => {
beforeEach(async () => {
const TestComp = () => {
const { busy, updateVehicle } = useVehicleContext();
const { message, setMessage } = useStatusContext();
const update = async (data) => {
try {
await updateVehicle("3C4PDCBG0ET127145", data);
} catch (e) {
setMessage(e.message);
}
};
return (
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="updateVehicleNull" onClick={() => update(null)} />
<button data-testid="updateVehicleNoVIN" onClick={() => update({})} />
<button
data-testid="updateVehicle"
onClick={() =>
update({ vin: "3C4PDCBG0ET127145", log_level: "warn", canbus: { enabled: false } })
}
/>
</>
);
};
render(
<StatusProvider>
<VehicleProvider>
<TestComp />
</VehicleProvider>
</StatusProvider>
);
});
afterEach(() => {
cleanup();
});
it("initial state", () => {
checkBaseResults("", "false");
});
it("updateVehicleNull", async () => {
fireEvent.click(screen.getByTestId("updateVehicleNull"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("No vehicle data", "false");
});
it("updateVehicleNoVIN", async () => {
fireEvent.click(screen.getByTestId("updateVehicleNoVIN"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("Invalid VIN", "false");
});
it("updateVehicle", async () => {
fireEvent.click(screen.getByTestId("updateVehicle"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("", "false");
});
});
describe("deleteVehicle", () => {
beforeEach(async () => {
const TestComp = () => {
const { busy, deleteVehicle } = useVehicleContext();
const { message, setMessage } = useStatusContext();
const deleteV = async (name) => {
try {
await deleteVehicle(name);
} catch (e) {
setMessage(e.message);
}
};
return (
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="deleteVehicleNull" onClick={() => deleteV(null)} />
<button data-testid="deleteVehicleNonexistent" onClick={() => deleteV("11111111111111111")} />
<button
data-testid="deleteVehicle"
onClick={() =>
deleteV("3C4PDCBG0ET127145")
}
/>
</>
);
};
render(
<StatusProvider>
<VehicleProvider>
<TestComp />
</VehicleProvider>
</StatusProvider>
);
});
afterEach(() => {
cleanup();
});
it("initial state", () => {
checkBaseResults("", "false");
});
it("deleteVehicleNull", async () => {
fireEvent.click(screen.getByTestId("deleteVehicleNull"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("Invalid VIN", "false");
});
it("deleteVehicleNonexistent", async () => {
fireEvent.click(screen.getByTestId("deleteVehicleNonexistent"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("", "false");
});
it("deleteVehicle", async () => {
fireEvent.click(screen.getByTestId("deleteVehicle"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("", "false");
});
});
});
const expectedVehicleData = [
const expectedFilters = [
{
can_id: "123-456",
interval: 789
},
{
can_id: "1",
interval: 1000
},
{
can_id: "1000",
interval: 1
}
]
const expectedVehicleData = {
vin: "3C4PDCBG0ET127145",
year: 2021,
model: "Ocean",
trim: "Basic",
ecu_list: "ECUA 2.0.0, ECUB 2.1.1",
log_level: "info",
canbus: { enabled: true, data_logger_enabled: true, max_mem_buffer_size: 1, max_disk_buffer_size: 2, filters: expectedFilters },
connected: true,
}
const expectedVehiclesData = [
{
vin: "3C4PDCBG0ET127145",
year: 2021,
model: "Ocean",
trim: "Basic",
ecu_list: "ECUA 2.0.0, ECUB 2.1.1",
log_level: "info",
canbus: { enabled: true, data_logger_enabled: true, max_mem_buffer_size: 1, max_disk_buffer_size: 2, filters: expectedFilters },
connected: true,
},
{ vin: "1G1FP87S3GN100062", connected: true },

View File

@@ -0,0 +1,30 @@
let busy = false;
let filters = [
{
can_id: "123",
interval: 1000
},
{
can_id: "456-789",
interval: 2000
},
{
can_id: "1",
interval: 0
},
];
let totalFilters = 3;
export const CANFiltersProvider = ({ children }) => {
return <div data-testid="mocked-canfiltersprovider">{children}</div>;
};
export const useCANFiltersContext = () => ({
busy,
filters,
totalFilters,
addFilter: jest.fn(),
getFilters: jest.fn(),
updateFilter: jest.fn(),
deleteFilter: jest.fn(),
});

View File

@@ -44,8 +44,17 @@ let carUpdateLog = {
created: "2021-08-23T17:06:38.030052Z",
updated: "2021-08-23T17:06:38.030052Z",
},
{
id: 88,
carupdate_id: 284,
status: "install_approval_await",
error_code: 0,
info: "TEST",
created: "2021-08-23T17:06:38.030052Z",
updated: "2021-08-23T17:06:38.030052Z",
},
],
total: 2,
total: 3,
};
export const CarUpdatesProvider = ({ children }) => {
@@ -64,4 +73,5 @@ export const useCarUpdatesContext = () => ({
getVINUpdates: jest.fn(() => carUpdates),
startMonitor: jest.fn(),
stopMonitor: jest.fn(),
approveUpdate: jest.fn(),
});

View File

@@ -0,0 +1,75 @@
let busy = false;
let fleetCANFilters = [
{
can_id: "123-456",
interval: 789
},
{
can_id: "1",
interval: 1000
},
{
can_id: "1000",
interval: 1
}
]
let fleet = {
name: "US-WEST",
log_level: "info",
canbus: { enabled: true, data_logger_enabled: true, max_mem_buffer_size: 1, max_disk_buffer_size: 2, filters: fleetCANFilters },
vehicles: ["USWESTVIN12345678", "USWESTVIN12345679", "USWESTVIN12345670"],
}
let fleets = [
{
name: "US-WEST",
log_level: "info",
canbus: { enabled: true, data_logger_enabled: true, max_mem_buffer_size: 1, max_disk_buffer_size: 2, filters: fleetCANFilters },
vehicles: ["USWESTVIN12345678", "USWESTVIN12345679", "USWESTVIN12345670"]
},
{
name: "US-CENTRAL",
log_level: "warn",
canbus: { enabled: false, data_logger_enabled: false, max_mem_buffer_size: 0, max_disk_buffer_size: 0 },
vehicles: ["USCENTVIN12345678", "USCENTVIN12345679", "USCENTVIN12345670"]
},
{
name: "US-EAST",
log_level: "error",
canbus: { enabled: true },
vehicles: ["USEASTVIN12345678", "USEASTVIN12345679", "USEASTVIN12345670"]
},
];
let totalFleets = 3;
let fleetVehicles = ["USWESTVIN12345678", "USWESTVIN12345679", "USWESTVIN12345670"];
let totalFleetVehicles = 3;
let totalFleetCANFilters = 3;
export const FleetProvider = ({ children }) => {
return <div data-testid="mocked-fleetprovider">{children}</div>;
};
export const useFleetContext = () => ({
busy,
fleet,
fleets,
totalFleets,
addFleet: jest.fn(),
getFleet: jest.fn(),
getFleets: jest.fn(),
updateFleet: jest.fn(),
deleteFleet: jest.fn(),
fleetVehicles,
totalFleetVehicles,
getFleetVehicles: jest.fn(),
addFleetVehicle: jest.fn(),
deleteFleetVehicle: jest.fn(),
fleetCANFilters,
totalFleetCANFilters,
getFleetCANFilters: jest.fn(),
addFleetCANFilter: jest.fn(),
updateFleetCANFilter: jest.fn(),
deleteFleetCANFilter: jest.fn(),
});

View File

@@ -7,13 +7,8 @@ let ecus = [
{
data_id: 0,
name: "AGS",
part_number: "",
version: "",
serial_number: "",
hw_version: "",
vendor: "",
configuration: "",
fingerprint: "",
manifest_id: 0,
files: [
{
@@ -21,6 +16,8 @@ let ecus = [
order: 0,
offset: "0",
checksum: "",
self_download: false,
mode: "D",
type: 1,
},
],

View File

@@ -20,8 +20,29 @@ let manifest = {
file_id: "b0cda514c94080b4",
filename: "LARGE.jpg",
url: "https://upload-dev.fiskerdps.com/92bbc448-99c8-4851-91ad-f8042e4deb49/LARGE.jpg",
write_region: {
offset: 100,
length: 14488498,
},
erase_region: {
offset: 0,
length: 120559274,
},
file_size: 14559274,
size: 14488498,
type: "ODX Data",
created: "2021-12-09T22:38:29.102813Z",
updated: "2021-12-09T22:38:29.102813Z",
},
{
file_id: "4B897b1DcbeCds8e9",
filename: "SMALL.jpg",
url: "https://upload-dev.fiskerdps.com/92bbc448-99c8-4851-91ad-f8042e4deb49/SMALL.jpg",
write_region: {
offset: 120559274,
length: 559274,
},
checksum: "0a06d87c",
file_size: 488498,
type: "ODX Data",
created: "2021-12-09T22:38:29.102813Z",
updated: "2021-12-09T22:38:29.102813Z",

View File

@@ -0,0 +1,16 @@
let message = ""
let title = ""
let sitePath = {}
export const StatusProvider = ({ children }) => {
return <div data-testid="mocked-statusprovider">{children}</div>;
};
export const useStatusContext = () => ({
message,
title,
sitePath,
setMessage: jest.fn(m => message = m),
setTitle: jest.fn(t => title = t),
setSitePath: jest.fn(s => sitePath = s),
});

View File

@@ -1,6 +1,31 @@
import React from "react";
let busy = false;
const filters = [
{
can_id: "123-456",
interval: 789
},
{
can_id: "1",
interval: 1000
},
{
can_id: "1000",
interval: 1
}
]
let vehicle = {
vin: "3C4PDCBG0ET127145",
year: 2021,
model: "Ocean",
trim: "Basic",
ecu_list: "ECUA 2.0.0, ECUB 2.1.1",
log_level: "info",
canbus: { enabled: true, data_logger_enabled: true, max_mem_buffer_size: 1, max_disk_buffer_size: 2, filters: filters },
};
let vehicles = [];
let models = ["Ocean", "PEAR"];
let years = [2023, 2024];
@@ -15,10 +40,11 @@ export const useVehicleContext = () => ({
busy,
models,
totalVehicles,
vehicle,
vehicles,
years,
addVehicle: jest.fn(),
getConnections: jest.fn((vins, token) => {
getConnections: jest.fn((vins, _token) => {
const result = {};
vins.forEach((vin) => {
@@ -31,28 +57,19 @@ export const useVehicleContext = () => ({
return {
data: [
{
boot_loader_version: "BLVERSION",
config: "CONFIG",
created: "2021-07-14T20:09:40.98187Z",
ecu: "ECUA",
fingerprint: "FINGERPRINT",
hw_version: "HWVERSION",
serial_number: "SERIAL",
sw_version: "SWVERSION",
updated: "2021-07-14T20:09:40.98187Z",
vendor: "VENDOR",
},
{
boot_loader_version: "BLVERSION",
config: "CONFIG",
created: "2021-07-14T20:09:40.98187Z",
ecu: "ECUB",
fingerprint: "FINGERPRINT",
hw_version: "HWVERSION",
serial_number: "SERIAL",
sw_version: "SWVERSION",
updated: "2021-07-14T20:09:40.98187Z",
vendor: "VENDOR",
},
],
total: 2,
@@ -70,8 +87,9 @@ export const useVehicleContext = () => ({
getYears: jest.fn(() => {
years = [2023, 2024];
}),
getVehicle: jest.fn(),
getVehicles: jest.fn(() => vehicles),
sendCommand: jest.fn((vins, command, parameters, token) => ({
sendCommand: jest.fn((vins, command, parameters, _token) => ({
vins,
command,
parameters,

View File

@@ -24,30 +24,14 @@ const tableColumns = [
id: "sw_version",
label: "SW Version",
},
{
id: "boot_loader_version",
label: "BL Version",
},
{
id: "hw_version",
label: "HW Version",
},
{
id: "vendor",
label: "Vendor",
},
{
id: "config",
label: "Config",
},
{
id: "fingerprint",
label: "Fingerprint",
},
{
id: "serial_number",
label: "Serial",
},
{
id: "created_at",
label: "Created",
@@ -128,16 +112,12 @@ const CarECUsTable = ({ vin, token, classes }) => {
onSortRequest={handleSort}
/>
<TableBody>
{ecus.map((row) => (
<TableRow key={row.ecu}>
{ecus.map((row, i) => (
<TableRow key={row.ecu + i}>
<TableCell align="center">{row.ecu}</TableCell>
<TableCell align="center">{row.sw_version}</TableCell>
<TableCell align="center">{row.boot_loader_version}</TableCell>
<TableCell align="center">{row.hw_version}</TableCell>
<TableCell align="center">{row.vendor}</TableCell>
<TableCell align="center">{row.config}</TableCell>
<TableCell align="center">{row.fingerprint}</TableCell>
<TableCell align="center">{row.serial_number}</TableCell>
<TableCell align="center">
{LocalDateTimeString(row.created)}
</TableCell>

View File

@@ -48,7 +48,7 @@ const tableColumns = [
];
const CarSelectionTable = (props) => {
const { token, classes, search, selected, onSelect, onSelectAll } = props;
const { token, classes, search, multiSelect, selected, onSelect, onSelectAll } = props;
const [pageSize, setPageSize] = useState(10);
const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("vin");
@@ -56,7 +56,7 @@ const CarSelectionTable = (props) => {
const { getVehicles, vehicles, totalVehicles } = useVehicleContext();
const { setMessage } = useStatusContext();
const { search: searchTerm } = search;
const sortHandler = (event, property) => {
const sortHandler = (_event, property) => {
if (property === orderBy) {
if (order === "asc") {
setOrder("desc");
@@ -69,7 +69,7 @@ const CarSelectionTable = (props) => {
}
};
const handleChangePageIndex = (event, newIndex) => {
const handleChangePageIndex = (_event, newIndex) => {
setPageIndex(newIndex);
};
@@ -123,22 +123,22 @@ const CarSelectionTable = (props) => {
order={order}
columnData={tableColumns}
onSortRequest={sortHandler}
multiSelect={true}
multiSelect={multiSelect}
onSelectAll={handleSelectAll}
selectCount={selected.length}
selectCount={selected ? selected.length : 0}
rowCount={vehicles.length}
/>
<TableBody>
{vehicles.map((row) => {
const isSelected = selected.indexOf(row.vin) !== -1;
const isSelected = selected ? selected.indexOf(row.vin) !== -1 : false;
return (
<TableRow key={row.vin}>
<TableCell padding="checkbox">
{multiSelect && (<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
onChange={(event) => handleSelect(event, row.vin)}
/>
</TableCell>
</TableCell>)}
<TableCell align="center">
<ConnectedIcon
connected={row.connected}
@@ -195,9 +195,10 @@ CarSelectionTable.propTypes = {
token: PropTypes.string.isRequired,
classes: PropTypes.object.isRequired,
search: PropTypes.object.isRequired,
selected: PropTypes.array.isRequired,
onSelect: PropTypes.func.isRequired,
onSelectAll: PropTypes.func.isRequired,
multiSelect: PropTypes.bool.isRequired,
selected: PropTypes.array,
onSelect: PropTypes.func,
onSelectAll: PropTypes.func,
connectionStatus: PropTypes.bool,
};

View File

@@ -3,6 +3,7 @@ import { render, waitFor } from "@testing-library/react";
import CarUpdateStatusProgress from "../CarUpdateStatusProgress";
import useStyles from "../../useStyles";
import s from "./Statuses";
const TestWrapper = ({ status }) => {
const classes = useStyles();
@@ -21,7 +22,7 @@ describe("CarUpdateStatusProgress", () => {
name: "manifest_received",
status: {
car_update_id: 297,
msg: "manifest_received",
msg: s.ManifestReceived,
err: -6,
extra_info: "",
},
@@ -30,7 +31,7 @@ describe("CarUpdateStatusProgress", () => {
name: "manifest_accepted",
status: {
car_update_id: 297,
msg: "manifest_accepted",
msg: s.ManifestAccepted,
err: -7,
extra_info: "",
},
@@ -39,7 +40,7 @@ describe("CarUpdateStatusProgress", () => {
name: "download_started",
status: {
car_update_id: 297,
msg: "download_started",
msg: s.DownloadStarted,
err: -14,
extra_info: "",
},
@@ -53,7 +54,7 @@ describe("CarUpdateStatusProgress", () => {
file_total: 1264672,
package_current: 0,
package_total: 2529856,
msg: "download_start",
msg: s.DownloadStarted,
err: 0,
},
},
@@ -66,7 +67,7 @@ describe("CarUpdateStatusProgress", () => {
file_total: 1264672,
package_current: 1048576,
package_total: 2529856,
msg: "downloading",
msg: s.Downloading,
err: 0,
},
},
@@ -79,7 +80,7 @@ describe("CarUpdateStatusProgress", () => {
file_total: 1264672,
package_current: 1264672,
package_total: 2529856,
msg: "download_complete",
msg: s.DownloadCompleted,
err: 0,
},
},
@@ -87,7 +88,7 @@ describe("CarUpdateStatusProgress", () => {
name: "package_download_complete",
status: {
car_update_id: 297,
msg: "package_download_complete",
msg: s.PackageDownloadCompleted,
err: -15,
extra_info: "",
},
@@ -101,7 +102,7 @@ describe("CarUpdateStatusProgress", () => {
file_total: 100,
package_current: 0,
package_total: 1000,
msg: "download_error",
msg: s.DownloadFailed,
err: 0,
},
},
@@ -112,7 +113,7 @@ describe("CarUpdateStatusProgress", () => {
ecu: "TEST",
installed: 5,
total_files: 10,
msg: "installing",
msg: s.Installing,
err: 0,
},
},
@@ -123,7 +124,7 @@ describe("CarUpdateStatusProgress", () => {
ecu: "TEST",
installed: 10,
total_files: 10,
msg: "install_complete",
msg: s.InstallSucceeded,
err: 0,
},
},
@@ -134,7 +135,7 @@ describe("CarUpdateStatusProgress", () => {
ecu: "TEST",
installed: 5,
total_files: 10,
msg: "install_error",
msg: s.InstallFailed,
err: 0,
},
},

View File

@@ -0,0 +1,38 @@
const Statuses = {
Pending: "pending",
ManifestReceived: "manifest_received",
ManifestAccepted: "manifest_accepted",
ManifestRejected: "manifest_rejected",
PreconditionAwait: "requirements_await",
PreconditionSuceeded: "requirements_succeeded",
ManifestCancelReceived: "manifest_cancel_received",
ManifestCancelAccepted: "manifest_cancel_accepted",
ManifestCancelRejected: "manifest_cancel_rejected",
ManifestValidationSucceeded: "manifest_validation_succeeded",
ManifestValidationFailed: "manifest_validation_failed",
DownloadStarted: "download_started",
Downloading: "downloading",
DownloadCompleted: "download_completed",
DownloadFailed: "download_failed",
InstallApprovalAwait: "install_approval_await",
InstallApprovalReceived: "install_approval_received",
InstallStarted: "install_started",
Installing: "installing",
InstallSucceeded: "install_succeeded",
InstallFailed: "install_failed",
RollbackStarted: "rollback_started",
RollbackSucceeded: "rollback_succeeded",
RollbackFailed: "rollback_failed",
CleanupSucceeded: "cleanup_succeeded",
CleanupFailed: "cleanup_failed",
ManifestError: "manifest_error",
ManifestRollback: "manifest_rollback",
ManifestSucceeded: "manifest_succeeded",
ManifesCanceled: "manifest_canceled",
PackageDownloadStarted: "package_download_start",
PackageDownloadCompleted: "package_download_complete",
PackageInstallStarted: "package_install_start",
PackageInstallCompleted: "package_install_complete",
};
export default Statuses;

View File

@@ -4,78 +4,65 @@ import Typography from "@material-ui/core/Typography";
import clsx from "clsx";
import CircularProgress from "../CircularProgress";
import s from "./Statuses";
const AwaitStatus = -1;
const ErrorStatus = -100;
const PHASES = [
{
label: "Pending",
events: ["pending"],
events: [s.Pending],
progress: () => 100,
},
{
label: "Recieved",
events: ["manifest_accepted", "manifest_received"],
label: "Received",
events: [s.ManifestAccepted, s.ManifestReceived, s.ManifestRejected],
progress: () => 100,
},
{
label: "Precondition",
events: ["requirements_succeeded"],
events: [s.PreconditionAwait, s.PreconditionSuceeded],
progress: () => 100,
},
{
label: "Download",
events: [
"downloading",
"download_start",
"download_complete",
"download_error",
"package_download_start",
s.Downloading,
s.DownloadStarted,
s.DownloadCompleted,
s.DownloadFailed,
s.PackageDownloadStarted,
],
progress: (msg, progress) =>
[
"package_download_start",
"downloading",
"download_start",
"download_complete",
].indexOf(msg) > -1
? progress
: -100,
progress: (msg, progress) => {
if (msg === s.DownloadFailed) return ErrorStatus;
return progress;
},
},
{
label: "Approved",
events: ["package_download_complete", "install_approval_await"],
progress: () => -1,
events: [s.PackageDownloadCompleted, s.InstallApprovalAwait],
progress: () => AwaitStatus,
},
{
label: "Install",
events: [
"install_approval_received",
"install_start",
"installing",
"install_complete",
"install_error",
s.InstallApprovalReceived,
s.InstallStarted,
s.Installing,
s.InstallSucceeded,
s.InstallFailed,
],
progress: (msg, progress) =>
[
"install_approval_received",
"install_start",
"installing",
"install_complete",
].indexOf(msg) > -1
? progress
: -100,
},
{
label: "Clean up",
events: ["package_install_complete", "cleanup_failed"],
progress: (msg, progress) => {
if (msg === "package_install_complete") return -1;
return -100;
if (msg === s.InstallFailed) return ErrorStatus;
if (msg === s.PackageInstallCompleted) return 100;
return progress;
},
},
{
label: "Updated",
events: ["cleanup_success", "manifest_succeeded"],
progress: (msg, progress) => 100,
events: [s.ManifestSucceeded],
progress: (_msg, _progress) => 100,
},
];

View File

@@ -153,6 +153,28 @@ exports[`CarUpdateStatusTable Render 1`] = `
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
/>
</tr>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
8/23/2021 5:06:38 PM
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
install_approval_await
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
TEST
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
/>
</tr>
</tbody>
<tfoot
class="MuiTableFooter-root"
@@ -223,7 +245,7 @@ exports[`CarUpdateStatusTable Render 1`] = `
<p
class="MuiTypography-root MuiTablePagination-caption MuiTypography-body2 MuiTypography-colorInherit"
>
1-2 of 2
1-3 of 3
</p>
<div
class="MuiTablePagination-actions"

View File

@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DownloadFileLink Render 1`] = `
<div>
<a
download="test.txt"
>
test.txt
</a>
</div>
`;

View File

@@ -0,0 +1,32 @@
import React, { useEffect, useState } from "react";
const DownloadFileLink = ({ data, filename, mimetype }) => {
const [link, setLink] = useState("");
const releaseLink = () => {
if (link === "") return;
URL.revokeObjectURL(link);
};
const makeFile = () => {
const file = new Blob([data], { type: mimetype ?? "text/plain" });
releaseLink();
setLink(URL.createObjectURL(file));
};
useEffect(() => {
if (!data) return;
makeFile();
return releaseLink;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, filename, mimetype]);
return (
<a download={filename ?? "file.txt"} href={link}>
{filename}
</a>
);
};
export default DownloadFileLink;

View File

@@ -0,0 +1,21 @@
import React from "react";
import { render, waitFor } from "@testing-library/react";
import DownloadFileLink from ".";
describe("DownloadFileLink", () => {
beforeAll(() => {
global.URL.createObjectURL = jest.fn();
global.URL.revokeObjectURL = jest.fn();
});
it("Render", async () => {
const { container } = render(
<DownloadFileLink data={"ABCDEFGHIJK"} filename="test.txt" />
);
await waitFor(() => {
/* render */
});
expect(container).toMatchSnapshot();
});
});

View File

@@ -31,22 +31,10 @@ const options = [
field: "version",
required: true,
},
{
label: "Part Number",
field: "part_number",
},
{
label: "Serial",
field: "serial_number",
},
{
label: "Hardware",
field: "hw_version",
},
{
label: "Vendor",
field: "vendor",
},
{
label: "",
delete: true,
@@ -74,7 +62,7 @@ const ManifestECUList = () => {
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={8} align="center">
<TableCell colSpan={5} align="center">
<Button onClick={addECU}>Add ECU</Button>
</TableCell>
</TableRow>

View File

@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TabPanel Render 1`] = `
<div>
<div
aria-labelledby="tab-0"
id="tabpanel-0"
role="tabpanel"
>
<div
class="MuiBox-root MuiBox-root-1"
>
<div>
Test
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,24 @@
import React from "react";
import { Box } from "@material-ui/core";
function TabPanel(props) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`tabpanel-${index}`}
aria-labelledby={`tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
{children}
</Box>
)}
</div >
);
}
export default TabPanel;

View File

@@ -0,0 +1,21 @@
import { render, waitFor } from "@testing-library/react";
import TabPanel from "./index"
const renderTabPanel = async () => {
const { container } = render(
<TabPanel value={0} index={0}>
<div>Test</div>
</TabPanel>
);
await waitFor(() => { });
return container;
};
describe("TabPanel", () => {
it("Render", async () => {
const container = await renderTabPanel();
expect(container).toMatchSnapshot();
});
});

View File

@@ -1,160 +0,0 @@
import React, { useEffect, useState } from "react";
import {
FormControl,
Grid,
InputLabel,
MenuItem,
Paper,
Select,
TextField,
} from "@material-ui/core";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import ResponsiveIFrame from "../../Controls/ResponsiveIFrame";
import { grafanaCharts } from "../../../services/grafanaCharts";
const Battery = () => {
const classes = useStyles();
const { setTitle, setSitePath } = useStatusContext();
useEffect(() => {
setTitle("Battery");
setSitePath([
{
label: "Datascope",
link: "/datascope",
},
{
label: "Battery",
},
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [vin, setVIN] = useState("1F15K3R45N1234567");
const [cellNum, setCellNum] = useState(1);
const handleVINForm = (e) => {
if (e.target.value.length === 17) {
setVIN(e.target.value);
}
};
return (
<div className={classes.paper}>
<Grid container spacing={2}>
<Grid container item md={4} space={2}>
<Grid item md={12}>
<Paper className={classes.grafanaContainer}>
<form className={classes.formControl}>
<TextField
id="vin"
label="VIN"
defaultValue="1F15K3R45N1234567"
variant="outlined"
onChange={handleVINForm}
style={{ width: "100%" }}
/>
</form>
<FormControl variant="outlined" className={classes.formControl}>
<InputLabel id="demo-simple-select-outlined-label">
Cell
</InputLabel>
<Select
labelId="demo-simple-select-outlined-label"
id="demo-simple-select-outlined"
value={cellNum}
onChange={(e) => setCellNum(e.target.value)}
label="Cell"
>
{[...Array(112)].map((_, i) => (
<MenuItem key={i + 1} value={i + 1}>
{i + 1}
</MenuItem>
))}
</Select>
</FormControl>
</Paper>
</Grid>
<Grid item md={12}>
<Paper className={classes.grafanaContainer}>
Cell Voltage {cellNum}
<ResponsiveIFrame
classes={classes}
src={grafanaCharts.CELLVOLTAGE_CHART({ vin, cellNum })}
title="Cell Voltage"
/>
</Paper>
</Grid>
<Grid item md={12}>
<Paper className={classes.grafanaContainer}>
Cell Temperature {cellNum}
<ResponsiveIFrame
classes={classes}
src={grafanaCharts.CELLTEMP_CHART({ vin, cellNum })}
title="Cell Temperature"
/>
</Paper>
</Grid>
<Grid item md={12}>
<Paper className={classes.grafanaContainer}>
<ResponsiveIFrame
classes={classes}
src={grafanaCharts.BATTERYTEMP_CHART({ vin })}
title="Battery Temperature Time Series"
/>
</Paper>
</Grid>
</Grid>
<Grid container item md={8} space={2}>
<Grid item md={12}>
<Paper className={classes.grafanaContainer}>
<ResponsiveIFrame
classes={classes}
src={grafanaCharts.BATTERYCAP_CHART({ vin })}
title="Battery Capacity Time Series"
/>
</Paper>
</Grid>
<Grid item md={12}>
<Paper className={classes.grafanaContainer}>
<ResponsiveIFrame
classes={classes}
src={grafanaCharts.BATTERYPERCENT_CHART({ vin })}
title="Battery Percent Time Series"
/>
</Paper>
</Grid>
<Grid item md={6}>
<Paper className={classes.grafanaContainer}>
<ResponsiveIFrame
classes={classes}
src={grafanaCharts.BATTERY12VPERCENT_CHART({ vin })}
title="12V Battery Percentage Time Series"
/>
</Paper>
</Grid>
<Grid item md={6}>
<Paper className={classes.grafanaContainer}>
<ResponsiveIFrame
classes={classes}
src={grafanaCharts.BATTERY12VVOLTAGE_CHART({ vin })}
title="12V Battery Voltage Time Series"
/>
</Paper>
</Grid>
</Grid>
</Grid>
</div>
);
};
export default Battery;

View File

@@ -1,98 +0,0 @@
import React, { useEffect, useState } from "react";
import { Button, Grid, Link, Paper } from "@material-ui/core";
import CreateIcon from "@material-ui/icons/Create";
import api from "../../../services/grafanaAPI";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import ResponsiveIFrame from "../../Controls/ResponsiveIFrame";
import { logger } from "../../../services/monitoring";
import { grafanaCharts } from "../../../services/grafanaCharts";
const Datascope = () => {
const classes = useStyles();
const { setTitle, setSitePath } = useStatusContext();
const REQUEST_INTERVAL = 10000;
useEffect(() => {
setTitle("Datascope");
setSitePath([]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [carsCount, setCarsCount] = useState(0);
useEffect(() => {
api
.getCarsCount()
.then((result) => setCarsCount(result))
.catch((error) => logger.warn(error.stack));
}, []);
const [signalsCount, setSignalsCount] = useState("0");
useEffect(() => {
storeSignals();
const id = setInterval(function () {
storeSignals();
}, REQUEST_INTERVAL);
return () => {
clearInterval(id);
};
}, []);
const storeSignals = () => {
api
.getSignalsCount()
.then((result) => {
let num = result.toLocaleString();
setSignalsCount(num);
})
.catch((error) => logger.warn(error.stack));
};
return (
<div className={classes.paper}>
<Grid container className={classes.root} spacing={2}>
<Grid item md={6}>
<Paper className={classes.grafanaContainer} style={{ height: 150 }}>
<h1 className={classes.datascopeContainerValue}>{carsCount}</h1>
<h2 className={classes.datascopeContainerText}>Cars</h2>
</Paper>
</Grid>
<Grid item md={6}>
<Paper className={classes.grafanaContainer} style={{ height: 150 }}>
<h1 className={classes.datascopeContainerValue}>{signalsCount}</h1>
<h2 className={classes.datascopeContainerText}>
Signals Collected
</h2>
</Paper>
</Grid>
<Grid item md={12}>
<Paper className={classes.grafanaContainer}>
<ResponsiveIFrame
classes={classes}
src={grafanaCharts.HOME_CHART}
title="Signals Time Series"
/>
</Paper>
</Grid>
</Grid>
<Button
style={{ marginTop: 10 }}
aria-label="create"
color="primary"
component={Link}
href={grafanaCharts.BASE}
rel="noopener"
target="_blank"
>
<CreateIcon fontSize="large" />
Go to Grafana
</Button>
</div>
);
};
export default Datascope;

View File

@@ -0,0 +1,590 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FleetAddForm Render 1`] = `
<div>
<div
data-testid="mocked-fleetprovider"
>
<div
data-testid="mocked-statusprovider"
>
<div
data-testid="mocked-userprovider"
>
<div
data-testid="mocked-fleetprovider"
>
<div
class="makeStyles-paper-3"
>
<form
action="{onSubmit}"
class="makeStyles-form-5"
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="name"
id="name-label"
>
Name
<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="name"
maxlength="17"
name="name"
required=""
type="text"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64"
>
<span>
Name
 *
</span>
</legend>
</fieldset>
</div>
</div>
<label
class="MuiFormLabel-root"
id="demo-row-radio-buttons-group-label"
>
Log Level
</label>
<div
aria-labelledby="demo-row-radio-buttons-group-label"
class="MuiFormGroup-root MuiFormGroup-row"
margin="normal"
role="radiogroup"
>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-69"
name="log-level-group"
type="radio"
value="trace"
/>
<div
class="PrivateRadioButtonIcon-root-70"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden="true"
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</div>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Trace
</span>
</label>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-69"
name="log-level-group"
type="radio"
value="debug"
/>
<div
class="PrivateRadioButtonIcon-root-70"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden="true"
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</div>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Debug
</span>
</label>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary PrivateSwitchBase-checked-67 Mui-checked MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
checked=""
class="PrivateSwitchBase-input-69"
name="log-level-group"
type="radio"
value="info"
/>
<div
class="PrivateRadioButtonIcon-root-70 PrivateRadioButtonIcon-checked-72"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden="true"
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</div>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Info
</span>
</label>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-69"
name="log-level-group"
type="radio"
value="warn"
/>
<div
class="PrivateRadioButtonIcon-root-70"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden="true"
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</div>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Warn
</span>
</label>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-69"
name="log-level-group"
type="radio"
value="error"
/>
<div
class="PrivateRadioButtonIcon-root-70"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden="true"
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</div>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Error
</span>
</label>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-69"
name="log-level-group"
type="radio"
value="critical"
/>
<div
class="PrivateRadioButtonIcon-root-70"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden="true"
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</div>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Critical
</span>
</label>
</div>
<label
class="MuiFormLabel-root"
id="demo-row-radio-buttons-group-label"
>
CAN Bus
</label>
<div
class="MuiFormGroup-root"
>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiCheckbox-root MuiCheckbox-colorSecondary PrivateSwitchBase-checked-67 Mui-checked MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
checked=""
class="PrivateSwitchBase-input-69"
data-indeterminate="false"
type="checkbox"
value=""
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
CAN Bus Enabled
</span>
</label>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required"
data-shrink="true"
for="max_mem_buffer_size"
id="max_mem_buffer_size-label"
>
Max Memory Buffer Size (0 uses default size)
<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="max_mem_buffer_size"
maxlength="12"
name="max_mem_buffer_size"
required=""
type="number"
value="0"
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
>
<span>
Max Memory Buffer Size (0 uses default size)
 *
</span>
</legend>
</fieldset>
</div>
</div>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiCheckbox-root MuiCheckbox-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-69"
data-indeterminate="false"
type="checkbox"
value=""
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Data Logger Enabled
</span>
</label>
</div>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined Mui-disabled Mui-disabled MuiFormLabel-filled Mui-required Mui-required"
data-shrink="true"
for="max_disk_buffer_size"
id="max_disk_buffer_size-label"
>
Max Disk Buffer Size (0 uses default size)
<span
aria-hidden="true"
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
>
*
</span>
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root Mui-disabled Mui-disabled MuiInputBase-fullWidth MuiInputBase-formControl"
>
<input
aria-invalid="false"
class="MuiInputBase-input MuiOutlinedInput-input Mui-disabled Mui-disabled"
disabled=""
id="max_disk_buffer_size"
maxlength="12"
name="max_disk_buffer_size"
required=""
type="number"
value="0"
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
>
<span>
Max Disk Buffer Size (0 uses default size)
 *
</span>
</legend>
</fieldset>
</div>
</div>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary MuiButton-fullWidth"
tabindex="0"
type="submit"
>
<span
class="MuiButton-label"
>
Submit
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,206 @@
import React, { useEffect, useRef, useState } from "react";
import { Redirect } from "react-router";
import {
Button,
Checkbox,
FormControlLabel,
FormGroup,
FormLabel,
Radio,
RadioGroup,
TextField
} from "@material-ui/core";
import useStyles from "../../useStyles";
import {
useFleetContext,
FleetProvider
} from "../../Contexts/FleetContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext";
import { logger } from "../../../services/monitoring";
const MainForm = () => {
const { setMessage, setTitle, setSitePath } = useStatusContext();
const { addFleet, busy } = useFleetContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
const classes = useStyles();
const [redirect, setRedirect] = useState(null);
const nameEl = useRef(null);
const [selectedLogLevel, setSelectedLogLevel] = useState("info");
const [canbusEnabled, setCANBusEnabled] = useState(true);
const [dataLoggerEnabled, setDataLoggerEnabled] = useState(false);
const [maxMemBufferSize, setMaxMemBufferSize] = useState(0);
const [maxDiskBufferSize, setMaxDiskBufferSize] = useState(0);
useEffect(() => {
setTitle("Add Fleet");
setSitePath([
{
label: "Fleets",
link: "/fleets",
},
{
label: "Add Fleet",
},
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onLogLevelChange = (event) => {
setSelectedLogLevel(event.target.value);
}
const onCANBusChange = (event) => {
setCANBusEnabled(event.target.checked);
}
const onDataLoggerChange = (event) => {
setDataLoggerEnabled(event.target.checked);
}
const onMaxMemBufferSizeChange = (event) => {
setMaxMemBufferSize(event.target.value);
}
const onMaxDiskBufferSizeChange = (event) => {
setMaxDiskBufferSize(event.target.value);
}
const onSubmit = async (event) => {
try {
event.preventDefault();
const formData = {
name: nameEl.current.value,
log_level: selectedLogLevel,
canbus: {
enabled: canbusEnabled,
data_logger_enabled: canbusEnabled ? dataLoggerEnabled : false,
max_mem_buffer_size: canbusEnabled ? parseInt(maxMemBufferSize) : 0,
max_disk_buffer_size: canbusEnabled && dataLoggerEnabled ? parseInt(maxDiskBufferSize) : 0
}
};
const result = await addFleet(formData, token);
if (!result || result.error) return;
setMessage(`Added ${result.name}`);
setRedirect(`/fleets`);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
};
if (redirect && redirect.length > 0) {
return <Redirect to={redirect} />;
}
return (
<div className={classes.paper}>
<form className={classes.form} noValidate action="{onSubmit}">
<TextField
id="name"
name="name"
label="Name"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "17",
}}
required
fullWidth
inputRef={nameEl}
/>
<FormLabel id="demo-row-radio-buttons-group-label">Log Level</FormLabel>
<RadioGroup
row
aria-labelledby="demo-row-radio-buttons-group-label"
name="log-level-group"
value={selectedLogLevel}
onChange={onLogLevelChange}
margin="normal"
>
<FormControlLabel value="trace" control={<Radio />} label="Trace" />
<FormControlLabel value="debug" control={<Radio />} label="Debug" />
<FormControlLabel value="info" control={<Radio />} label="Info" />
<FormControlLabel value="warn" control={<Radio />} label="Warn" />
<FormControlLabel value="error" control={<Radio />} label="Error" />
<FormControlLabel value="critical" control={<Radio />} label="Critical" />
</RadioGroup>
<FormLabel id="demo-row-radio-buttons-group-label">CAN Bus</FormLabel>
<FormGroup>
<FormControlLabel control={
<Checkbox
checked={canbusEnabled}
onChange={onCANBusChange}
/>
} label="CAN Bus Enabled" />
<TextField
id="max_mem_buffer_size"
name="max_mem_buffer_size"
label='Max Memory Buffer Size (0 uses default size)'
value={maxMemBufferSize}
onChange={onMaxMemBufferSizeChange}
variant="outlined"
margin="normal"
inputProps={{
maxLength: "12",
}}
type="number"
disabled={!canbusEnabled}
required
fullWidth
/>
<FormControlLabel control={
<Checkbox
checked={dataLoggerEnabled}
onChange={onDataLoggerChange}
disabled={!canbusEnabled}
/>
} label="Data Logger Enabled" />
</FormGroup>
<TextField
id="max_disk_buffer_size"
name="max_disk_buffer_size"
label='Max Disk Buffer Size (0 uses default size)'
value={maxDiskBufferSize}
onChange={onMaxDiskBufferSizeChange}
variant="outlined"
margin="normal"
inputProps={{
maxLength: "12",
}}
type="number"
disabled={!dataLoggerEnabled}
required
fullWidth
/>
<Button
type="submit"
disabled={busy}
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={onSubmit}
>
{busy ? "Submitting..." : "Submit"}
</Button>
</form>
</div>
);
};
const FleetAddForm = () => (
<FleetProvider>
<MainForm />
</FleetProvider>
);
export default FleetAddForm;

View File

@@ -0,0 +1,36 @@
jest.mock("../../Contexts/FleetContext");
jest.mock("../../Contexts/StatusContext");
jest.mock("../../Contexts/UserContext");
import { render, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import { FleetProvider } from "../../Contexts/FleetContext";
import { StatusProvider } from "../../Contexts/StatusContext";
import { UserProvider, setToken } from "../../Contexts/UserContext";
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
import MainForm from "./index"
const renderFleetAdd = async () => {
const { container } = render(
<FleetProvider>
<StatusProvider>
<UserProvider>
<BrowserRouter>
<MainForm />
</BrowserRouter>
</UserProvider>
</StatusProvider>
</FleetProvider>
);
await waitFor(() => { /* render */ });
return container;
};
describe("FleetAddForm", () => {
it("Render", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderFleetAdd();
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,176 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FleetCANFilterAdd Render 1`] = `
<div>
<div
data-testid="mocked-fleetprovider"
>
<div
data-testid="mocked-statusprovider"
>
<div
data-testid="mocked-userprovider"
>
<div
data-testid="mocked-fleetprovider"
>
<div
class="makeStyles-paper-3"
>
<form
action="{onSubmit}"
class="makeStyles-form-5"
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="name"
id="name-label"
>
Fleet Name
<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="name"
maxlength="255"
name="name"
readonly=""
required=""
type="text"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64"
>
<span>
Fleet Name
 *
</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="canId"
id="canId-label"
>
CAN ID
<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="canId"
maxlength="255"
name="canId"
required=""
type="text"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64"
>
<span>
CAN ID
 *
</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"
data-shrink="false"
for="interval"
id="interval-label"
>
Interval
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
>
<input
aria-invalid="false"
class="MuiInputBase-input MuiOutlinedInput-input"
id="interval"
maxlength="255"
name="interval"
type="text"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-64"
>
<span>
Interval
</span>
</legend>
</fieldset>
</div>
</div>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary MuiButton-fullWidth"
tabindex="0"
type="submit"
>
<span
class="MuiButton-label"
>
Submit
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,127 @@
import React, { useEffect, useRef, useState } from "react";
import { Redirect, useParams } from "react-router";
import { Button, TextField } from "@material-ui/core";
import { useUserContext } from "../../../../Contexts/UserContext";
import { useStatusContext } from "../../../../Contexts/StatusContext";
import { useFleetContext, FleetProvider } from "../../../../Contexts/FleetContext";
import useStyles from "../../../../useStyles";
import { logger } from "../../../../../services/monitoring";
const MainForm = () => {
const { name } = useParams();
const { setMessage, setTitle, setSitePath } = useStatusContext();
const { addFleetCANFilter, busy } = useFleetContext();
const { token: { idToken: { jwtToken: token } } } = useUserContext();
const classes = useStyles();
const canIdEl = useRef(null);
const intervalEl = useRef(null);
const [redirect, setRedirect] = useState(null);
useEffect(() => {
const title = "Add CAN Filter"
setTitle(title);
setSitePath([
{
label: `Fleets`,
link: "/fleets",
},
{
label: `${name}`,
link: `/fleet/${name}`
},
{
label: title
},
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onSubmit = async (event) => {
try {
event.preventDefault();
const formData = {
can_id: canIdEl.current.value,
interval: parseInt(intervalEl.current.value)
};
const result = await addFleetCANFilter(name, formData, token);
if (!result || result.error) return;
setMessage(`Added CAN filter ${result.can_id}`);
setRedirect(`/fleet/${name}#filters`);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
};
if (redirect && redirect.length > 0) {
return <Redirect to={redirect} />;
}
return (
<div className={classes.paper}>
<form className={classes.form} noValidate action="{onSubmit}">
<TextField
id="name"
name="name"
label="Fleet Name"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "255",
readOnly: true,
}}
value={name}
required
fullWidth
/>
<TextField
id="canId"
name="canId"
label="CAN ID"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "255",
}}
required
fullWidth
inputRef={canIdEl}
/>
<TextField
id="interval"
name="interval"
label="Interval"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "255",
}}
fullWidth
inputRef={intervalEl}
/>
<Button
type="submit"
disabled={busy}
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={onSubmit}
>
{busy ? "Submitting..." : "Submit"}
</Button>
</form>
</div>
);
};
const FleetAddCANFilterForm = (props) => (
<FleetProvider>
<MainForm {...props} />
</FleetProvider>
);
export default FleetAddCANFilterForm;

View File

@@ -0,0 +1,36 @@
jest.mock("../../../../Contexts/FleetContext");
jest.mock("../../../../Contexts/StatusContext");
jest.mock("../../../../Contexts/UserContext");
import { render, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import { FleetProvider } from "../../../../Contexts/FleetContext";
import { StatusProvider } from "../../../../Contexts/StatusContext";
import { UserProvider, setToken } from "../../../../Contexts/UserContext";
import { TEST_AUTH_OBJECT } from "../../../../../utils/testing";
import MainForm from "./index"
const renderFleetCANFilterAdd = async () => {
const { container } = render(
<FleetProvider>
<StatusProvider>
<UserProvider>
<BrowserRouter>
<MainForm />
</BrowserRouter>
</UserProvider>
</StatusProvider>
</FleetProvider>
);
await waitFor(() => { });
return container;
};
describe("FleetCANFilterAdd", () => {
it("Render", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderFleetCANFilterAdd();
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,454 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FleetCANFiltersTable Render 1`] = `
<div>
<div
data-testid="mocked-fleetprovider"
>
<div
data-testid="mocked-statusprovider"
>
<div
data-testid="mocked-userprovider"
>
<div
data-testid="mocked-fleetprovider"
>
<div
class="makeStyles-paper-3 makeStyles-tableSize-53"
>
<div
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
>
<div
class="MuiGrid-root makeStyles-textJustifyAlign-47 MuiGrid-item MuiGrid-grid-md-4"
>
<a
class="makeStyles-labelInline-9"
href="/fleet/undefined/filter-add"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"
/>
</svg>
</a>
</div>
<div
align="right"
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-8"
>
<div
class="MuiFormControl-root makeStyles-margin-28 makeStyles-fullWidth-50"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated"
data-shrink="false"
for="search"
>
Search
</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="search"
type="text"
value=""
/>
<div
class="MuiInputAdornment-root MuiInputAdornment-positionEnd"
>
<button
aria-label="search"
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="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
</div>
</div>
</div>
</div>
<table
class="MuiTable-root"
>
<thead
class="MuiTableHead-root"
>
<tr
class="MuiTableRow-root MuiTableRow-head"
>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
CAN ID
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Interval (ms)
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
Actions
</th>
</tr>
</thead>
<tbody
class="MuiTableBody-root"
>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
123-456
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
789
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
class=""
href="/fleet/undefined/filter-update?name=undefined&can_id=123-456&interval=789"
style="margin: 5px;"
title="Update \\"123-456\\""
>
<svg
aria-hidden="true"
aria-label="Update 123-456"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"123-456\\""
>
<svg
aria-hidden="true"
aria-label="Delete 123-456"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
1
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
1000
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
class=""
href="/fleet/undefined/filter-update?name=undefined&can_id=1&interval=1000"
style="margin: 5px;"
title="Update \\"1\\""
>
<svg
aria-hidden="true"
aria-label="Update 1"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"1\\""
>
<svg
aria-hidden="true"
aria-label="Delete 1"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
1000
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
1
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
class=""
href="/fleet/undefined/filter-update?name=undefined&can_id=1000&interval=1"
style="margin: 5px;"
title="Update \\"1000\\""
>
<svg
aria-hidden="true"
aria-label="Update 1000"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"1000\\""
>
<svg
aria-hidden="true"
aria-label="Delete 1000"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
</tbody>
<tfoot
class="MuiTableFooter-root"
>
<tr
class="MuiTableRow-root MuiTableRow-footer"
>
<td
class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root"
colspan="8"
>
<div
class="MuiToolbar-root MuiToolbar-regular MuiTablePagination-toolbar MuiToolbar-gutters"
>
<div
class="MuiTablePagination-spacer"
/>
<p
class="MuiTypography-root MuiTablePagination-caption MuiTypography-body2 MuiTypography-colorInherit"
>
Rows per page:
</p>
<div
class="MuiInputBase-root MuiTablePagination-input MuiTablePagination-selectRoot"
>
<select
aria-label="rows per page"
class="MuiSelect-root MuiSelect-select MuiTablePagination-select MuiInputBase-input"
>
<option
class="MuiTablePagination-menuItem"
value="5"
>
5
</option>
<option
class="MuiTablePagination-menuItem"
value="10"
>
10
</option>
<option
class="MuiTablePagination-menuItem"
value="25"
>
25
</option>
<option
class="MuiTablePagination-menuItem"
value="100"
>
100
</option>
</select>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSelect-icon MuiTablePagination-selectIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
</svg>
</div>
<p
class="MuiTypography-root MuiTablePagination-caption MuiTypography-body2 MuiTypography-colorInherit"
>
1-3 of 3
</p>
<div
class="MuiTablePagination-actions"
>
<button
aria-label="Previous page"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
title="Previous page"
type="button"
>
<span
class="MuiIconButton-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z"
/>
</svg>
</span>
</button>
<button
aria-label="Next page"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
title="Next page"
type="button"
>
<span
class="MuiIconButton-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z"
/>
</svg>
</span>
</button>
</div>
</div>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,205 @@
import React, { useEffect, useState } from "react";
import { Link } from 'react-router-dom';
import {
Grid,
Table,
TableBody,
TableCell,
TableFooter,
TablePagination,
TableRow,
Tooltip,
} from "@material-ui/core";
import AddCircleIcon from "@material-ui/icons/AddCircle";
import DeleteIcon from "@material-ui/icons/Delete";
import EditIcon from '@material-ui/icons/Edit';
import clsx from "clsx";
import TableHeaderSortable from "../../../../Table/HeaderSortable";
import { useUserContext } from "../../../../Contexts/UserContext"
import { useStatusContext } from "../../../../Contexts/StatusContext";
import { FleetProvider, useFleetContext } from "../../../../Contexts/FleetContext"
import useStyles from "../../../../useStyles";
import SearchField from "../../../../Controls/SearchField";
import { logger } from "../../../../../services/monitoring";
import { Roles, hasRole } from "../../../../../utils/roles";
const tableColumns = [
{
id: "can_id",
label: "CAN ID"
},
{
id: "interval",
label: "Interval (ms)"
},
{
id: "",
label: "Actions"
}
];
const MainForm = ({ name }) => {
const [pageSize, setPageSize] = useState(10);
const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("id");
const [order, setOrder] = useState("desc");
const classes = useStyles();
const { setMessage } = useStatusContext();
const { fleetCANFilters, totalFleetCANFilters, getFleetCANFilters, deleteFleetCANFilter } = useFleetContext();
const { token: { idToken: { jwtToken: token } }, groups } = useUserContext();
useEffect(() => {
(async () => {
try {
if (!name || !token) return;
await getFleetCANFilters(
name,
{
limit: pageSize,
offset: pageSize * pageIndex,
order: `${orderBy} ${order}`,
},
token
);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token, pageIndex, pageSize, orderBy, order]);
const handleChangePageIndex = (event, newIndex) => {
setPageIndex(newIndex);
};
const handleChangePageSize = (event) => {
setPageSize(parseInt(event.target.value, 10));
setPageIndex(0);
};
const handleSort = (event, property) => {
try {
if (property === orderBy) {
if (order === "asc") {
setOrder("desc");
} else {
setOrder("asc");
}
} else {
setOrderBy(property);
setOrder("asc");
}
} catch (e) {
logger.warn(e.stack);
}
};
const onDelete = async (can_id) => {
try {
await deleteFleetCANFilter(name, can_id, token);
setMessage(`Deleted ${can_id}`)
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
};
const Actions = (row) => {
let actions = [];
if (hasRole([Roles.CREATE], groups)) {
actions.push({
tip: `Update "${row.can_id}"`,
link: `/fleet/${name}/filter-update?name=${name}&can_id=${row.can_id}&interval=${row.interval}`,
icon: <EditIcon aria-label={`Update ${row.can_id}`} />
});
}
if (hasRole([Roles.DELETE], groups)) {
actions.push({
tip: `Delete "${row.can_id}"`,
id: row.can_id,
icon: <DeleteIcon aria-label={`Delete ${row.can_id}`} />
})
}
if (actions.length === 0) return ["No actions"];
return actions.map((action) => {
if (action.link != null) {
return (
<Tooltip key={action.link} title={action.tip}>
<Link to={action.link} style={{ margin: 5 }}>
{action.icon}
</Link>
</Tooltip>
);
} else {
return (
<Tooltip key={`delete-${action.id}`} title={action.tip}>
<Link to="#" onClick={() => onDelete(action.id)}>
{action.icon}
</Link>
</Tooltip>
);
}
});
};
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Grid container className={classes.root} spacing={2}>
<Grid item md={4} className={classes.textJustifyAlign}>
<Link to={`/fleet/${name}/filter-add`} className={classes.labelInline}>
<AddCircleIcon fontSize="large" />
</Link>
</Grid>
<Grid item md={8} align="right" className={classes.textCenterAlign}>
<SearchField classes={classes} />
</Grid>
</Grid>
<Table>
<TableHeaderSortable
classes={classes}
orderBy={orderBy}
order={order}
columnData={tableColumns}
onSortRequest={handleSort}
/>
<TableBody>
{fleetCANFilters.map(row => (
<TableRow key={row.can_id}>
<TableCell align="center">{row.can_id}</TableCell>
<TableCell align="center">{row.interval}</TableCell>
<TableCell align="center">{Actions(row)}</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 100]}
colSpan={8}
count={totalFleetCANFilters}
rowsPerPage={pageSize}
page={pageIndex}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true,
}}
onPageChange={handleChangePageIndex}
onRowsPerPageChange={handleChangePageSize}
/>
</TableRow>
</TableFooter>
</Table>
</div >
);
};
const FleetCANFiltersTable = (props) => (
<FleetProvider>
<MainForm {...props} />
</FleetProvider>
);
export default FleetCANFiltersTable;

View File

@@ -0,0 +1,39 @@
jest.mock("../../../../Contexts/FleetContext");
jest.mock("../../../../Contexts/StatusContext");
jest.mock("../../../../Contexts/UserContext");
jest.mock('@material-ui/core/utils/unstable_useId', () =>
jest.fn().mockReturnValue('mui-test-id'),
);
import { render, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import { FleetProvider } from "../../../../Contexts/FleetContext";
import { StatusProvider } from "../../../../Contexts/StatusContext";
import { UserProvider, setToken } from "../../../../Contexts/UserContext";
import { TEST_AUTH_OBJECT } from "../../../../../utils/testing";
import MainForm from "./index"
const renderFleetCANFiltersTable = async () => {
const { container } = render(
<FleetProvider>
<StatusProvider>
<UserProvider>
<BrowserRouter>
<MainForm />
</BrowserRouter>
</UserProvider>
</StatusProvider>
</FleetProvider>
);
await waitFor(() => { });
return container;
};
describe("FleetCANFiltersTable", () => {
it("Render", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderFleetCANFiltersTable();
expect(container).toMatchSnapshot();
});
});

Some files were not shown because too many files have changed in this diff Show More