Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add web client #9

Merged
merged 1 commit into from
Jan 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/client.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Client Checks

on: [push, pull_request]

jobs:
build:
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
- name: Build
run: npm run build
working-directory: client

lint:
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
- name: Lint
run: npm run lint
working-directory: client

test:
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
- 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
Loading