Skip to content

Commit

Permalink
add web client
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickdemers6 committed Jan 27, 2024
1 parent e0eca25 commit 1b1cf96
Show file tree
Hide file tree
Showing 62 changed files with 11,715 additions and 3 deletions.
41 changes: 41 additions & 0 deletions .github/workflows/client.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Client Checks

on: [push, pull_request]

jobs:
setup:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm install
working-directory: ./client

build:
needs: setup
runs-on: ubuntu-latest
steps:
- name: Build
run: npm run build
working-directory: ./client

lint:
needs: setup
runs-on: ubuntu-latest
steps:
- name: Lint
run: npm run lint
working-directory: ./client

test:
needs: setup
runs-on: ubuntu-latest
steps:
- name: Test
run: npm run test
working-directory: ./client
Binary file added client/.assets/configuration_page.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/.assets/vehicle_dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions client/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"extends": ["next/core-web-vitals", "airbnb", "airbnb-typescript"],
"rules": {
"import/extensions": "off",
"react/react-in-jsx-scope": "off",
"react/require-default-props": "off",
"react/jsx-props-no-spreading": "off",
"react-hooks/exhaustive-deps": "off"
},
"parserOptions": {
"project": ["./tsconfig.json"]
},
"overrides": [
{
"files": ["./spec/**/*"],
"rules": {
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/no-static-element-interactions": "off"
}
}
]
}
34 changes: 34 additions & 0 deletions client/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local
.env

# typescript
*.tsbuildinfo
next-env.d.ts
15 changes: 15 additions & 0 deletions client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Phantom Fleet Client

This web client allows sending data to a fleet-telemetry server. It is in its early stages and only has minimal functionality at this time.

More documentation to come.

![](./.assets/configuration_page.png)

![](./.assets/vehicle_dashboard.png)

## Configuration

**Environment Variables**:

- `NEXT_PUBLIC_PHANTOM_FLEET_API_URL`: the URL the phantom-fleet API is exposed on
12 changes: 12 additions & 0 deletions client/api/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Methods from "./methods";

export const setConfig = async (host: string, port: number) => {
try {
const res = await Methods.post("/config", {
body: JSON.stringify({ host, port }),
});
return res.json();
} catch (e) {
return { reason: `Unexpected error: ${(e as Error).message}` };
}
};
36 changes: 36 additions & 0 deletions client/api/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { KeyData, Vehicle } from "@/context/types";
import Methods from "./methods";
import { Data, VehicleData } from "./types.d";

let msgCount = 0;

const createData = (data: KeyData) => {
return Object.entries(data).reduce((arr, [key, value]) => {
arr.push({ key, value });
return arr;
}, [] as Data[]);
};

const sendData = async (vin: string, data: Vehicle) => {
const id = `msg-${msgCount++}`;
const payload: VehicleData = {
cert: data.cert,
key: data.key,
data: createData(data.data),
messageId: id,
createdAt: Date.now(),
txid: id,
topic: "V",
vin,
device_type: "vehicle_device",
};

const res = await Methods.post("/data", {
body: JSON.stringify(payload),
});
if (!res.ok) {
throw new Error("Failed to send data");
}
};

export default sendData;
16 changes: 16 additions & 0 deletions client/api/methods.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class Methods {
static async post(path: string, init?: RequestInit) {
return fetch(
`${process.env.NEXT_PUBLIC_PHANTOM_FLEET_API_URL as string}${path}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
...(init?.headers ?? {}),
},
...(init ?? {}),
}
);
}
}
export default Methods;
16 changes: 16 additions & 0 deletions client/api/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export type VehicleData = {
txid: string;
key: string;
cert: string;
topic: string;
vin: string;
device_type: string;
createdAt: number;
messageId: string;
data: Data[];
};

export type Data = {
key: string;
value: unknown;
};
150 changes: 150 additions & 0 deletions client/app/configure/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
'use client';

import { setConfig } from '@/api/config';
import AppBar from '@/components/AppBar/AppBar';
import { useApp } from '@/context/ApplicationProvider';
import { useSnackbar } from '@/components/SnackbarContext';
import { FLEET } from '@/constants/paths';
import {
Box,
Button,
Grid,
Skeleton,
TextField,
Typography,
} from '@mui/material';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { FormEventHandler, useRef, useState } from 'react';
import Link from 'next/link';

const validateFormInput = (host?: string, port?: string) => {
const helpText = { host: '', port: '' };
let isValid = true;
if (host?.trim() === '') {
helpText.host = 'Host cannot be empty';
isValid = false;
}
if (port?.trim() === '') {
helpText.port = 'Port cannot be empty';
isValid = false;
} else if (port?.match(/^\d+$/) === null) {
helpText.port = 'Port must be a number';
isValid = false;
}

return { isValid, helpText };
};

export default function ConfigurePage() {
const { server, isLoading, configureServer } = useApp();
const [helpText, setHelpText] = useState({
host: '',
port: '',
});
const hostRef = useRef<HTMLInputElement>(null);
const portRef = useRef<HTMLInputElement>(null);
const snackbar = useSnackbar();
const router = useRouter();

const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
const host = hostRef.current?.value as string;
const port = portRef.current?.value as string;
const validation = validateFormInput(host, port);
if (!validation.isValid) {
setHelpText(validation.helpText);
return;
}

const { valid, reason } = await setConfig(host, Number.parseInt(port, 10));
if (!valid) {
snackbar.openSnackbar(
`Unable to connect to fleet-telemetry server: ${reason}`,
'error',
);
} else {
snackbar.openSnackbar('Connection successful', 'success');
configureServer(host, port);
router.push(FLEET);
}
};

return (
<AppBar>
<Grid container spacing={4} sx={{ mt: 4 }}>
<Grid item xs={12} sm={6} textAlign="right">
<Image
src="/fleet.jpeg"
alt="fleet"
width="400"
height="400"
style={{ borderRadius: 15 }}
/>
</Grid>
<Grid
item
xs={12}
sm={6}
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
}}
>
<Typography variant="h4">Welcome to Phantom Fleet</Typography>
<Typography variant="body1">
To get started, enter your fleet-telemetry server information.
</Typography>
<Box
sx={{ mt: 1, maxWidth: 300 }}
onSubmit={handleSubmit}
component="form"
>
{isLoading ? (
<>
<Skeleton>
<TextField variant="outlined" margin="normal" fullWidth />
</Skeleton>
<Skeleton>
<TextField variant="outlined" margin="normal" fullWidth />
</Skeleton>
</>
) : (
<>
<TextField
error={helpText.host !== ''}
helperText={helpText.host}
label="Host"
variant="outlined"
margin="normal"
fullWidth
inputRef={hostRef}
defaultValue={server?.host}
/>
<TextField
error={helpText.port !== ''}
helperText={helpText.port}
label="Port"
variant="outlined"
margin="normal"
fullWidth
inputRef={portRef}
defaultValue={server?.port}
/>
</>
)}
<Button variant="contained" type="submit" sx={{ mt: 3, mb: 2 }}>
Validate Connection
</Button>
<Typography>
<Link style={{ color: 'grey' }} href={FLEET}>
Skip connection setup
</Link>
</Typography>
</Box>
</Grid>
</Grid>
</AppBar>
);
}
Binary file added client/app/favicon.ico
Binary file not shown.
Loading

0 comments on commit 1b1cf96

Please sign in to comment.