* WIP

* WIP
This commit is contained in:
Sacha Weatherstone
2021-08-25 13:34:24 +10:00
committed by GitHub
parent a797cf7be1
commit eacdf11a6d
29 changed files with 5898 additions and 5237 deletions
+15 -15
View File
@@ -4,9 +4,9 @@ name: meshtastic-web build
on: on:
# Triggers the workflow on push or pull request events but only for the master branch # Triggers the workflow on push or pull request events but only for the master branch
push: push:
branches: [ master ] branches: [master]
pull_request: pull_request:
branches: [ master ] branches: [master]
# Allows you to run this workflow manually from the Actions tab # Allows you to run this workflow manually from the Actions tab
workflow_dispatch: workflow_dispatch:
@@ -19,31 +19,31 @@ jobs:
# Checks-out repository # Checks-out repository
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: pnpm/action-setup@v2.0.1
# Build project with:
version: 6.14.3
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: '14' node-version: '16'
cache: 'yarn' cache: 'pnpm'
- run: yarn install --ignore-optional - run: pnpm lint
- run: yarn lint - run: pnpm build
- run: yarn build - run: pnpm package
- run: yarn package
- run: tree build/output - run: tree build/output
# Create a zip file from the output folder # Create a zip file from the output folder
- name: Create output zip file - name: Create output zip file
uses: papeloto/action-zip@v1 uses: papeloto/action-zip@v1
with: with:
files: build/output/ files: build/output/
dest: output.zip dest: output.zip
# Upload Artifact # Upload Artifact
- name: Upload a Build Artifact - name: Upload a Build Artifact
uses: "marvinpinto/action-automatic-releases@latest" uses: 'marvinpinto/action-automatic-releases@latest'
with: with:
repo_token: "${{ secrets.GITHUB_TOKEN }}" repo_token: '${{ secrets.GITHUB_TOKEN }}'
automatic_release_tag: "latest" automatic_release_tag: 'latest'
prerelease: false prerelease: false
files: | files: |
output.zip output.zip
+2 -2
View File
@@ -11,11 +11,11 @@ Official [Meshtastic](https://meshtastic.org) web interface, that can be run ind
Build the project: Build the project:
```bash ```bash
yarn build pnpm build
``` ```
GZip the output: GZip the output:
```bash ```bash
yarn package pnpm package
``` ```
+8 -6
View File
@@ -6,27 +6,29 @@
"scripts": { "scripts": {
"start": "NODE_ENV=development snowpack dev", "start": "NODE_ENV=development snowpack dev",
"build": "snowpack build", "build": "snowpack build",
"package": "yarn gzipper c -i html,js,css build build/output", "package": "pnpm gzipper c -i html,js,css build build/output",
"format": "prettier --write 'src/**/*.{ts,tsx}'", "format": "prettier --write 'src/**/*.{ts,tsx}'",
"lint": "eslint 'src/**/*.{ts,tsx}'" "lint": "eslint 'src/**/*.{ts,tsx}'"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^1.4.0", "@headlessui/react": "^1.4.0",
"@heroicons/react": "^1.0.1", "@heroicons/react": "^1.0.1",
"@meshtastic/meshtasticjs": "^0.6.16", "@meshtastic/meshtasticjs": "^0.6.17",
"@reduxjs/toolkit": "^1.6.0", "@reduxjs/toolkit": "^1.6.0",
"apexcharts": "^3.27.3",
"boring-avatars": "^1.5.8", "boring-avatars": "^1.5.8",
"i18next": "^20.3.5", "i18next": "^20.3.5",
"i18next-browser-languagedetector": "^6.1.2", "i18next-browser-languagedetector": "^6.1.2",
"moment": "^2.29.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-apexcharts": "^1.3.9",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-flags-select": "^2.1.2", "react-flags-select": "^2.1.2",
"react-hook-form": "^7.9.0", "react-hook-form": "^7.13.0-next.5",
"react-i18next": "^11.11.4", "react-i18next": "^11.11.4",
"react-redux": "^7.2.4", "react-redux": "^7.2.4",
"type-route": "^0.6.0", "type-route": "^0.6.0",
"use-breakpoint": "^2.0.1", "use-breakpoint": "^2.0.1"
"yarn": "^1.22.11"
}, },
"devDependencies": { "devDependencies": {
"@snowpack/plugin-dotenv": "^2.0.5", "@snowpack/plugin-dotenv": "^2.0.5",
@@ -46,7 +48,7 @@
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-babel-module": "^5.3.1", "eslint-import-resolver-babel-module": "^5.3.1",
"eslint-import-resolver-typescript": "^2.4.0", "eslint-import-resolver-typescript": "^2.4.0",
"eslint-plugin-import": "^2.24.0", "eslint-plugin-import": "^2.24.1",
"eslint-plugin-react": "^7.24.0", "eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-hooks": "^4.2.0",
"gzipper": "^5.0.0", "gzipper": "^5.0.0",
+5254
View File
File diff suppressed because it is too large Load Diff
-10
View File
@@ -34,15 +34,5 @@
<div id="root"></div> <div id="root"></div>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<script type="module" src="/static/index.js"></script> <script type="module" src="/static/index.js"></script>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body> </body>
</html> </html>
+3 -5
View File
@@ -137,26 +137,24 @@ const App = (): JSX.Element => {
> >
<div className="flex flex-col h-full bg-gray-200 dark:bg-primaryDark"> <div className="flex flex-col h-full bg-gray-200 dark:bg-primaryDark">
<div className="flex flex-shrink-0 overflow-hidden bg-primary dark:bg-primary"> <div className="flex flex-shrink-0 overflow-hidden bg-primary dark:bg-primary">
<div className="w-full overflow-hidden bg-white border-b md:mt-12 md:mx-8 md:pt-4 md:pb-3 md:rounded-t-xl dark:border-gray-600 md:shadow-md dark:bg-primaryDark"> <div className="w-full overflow-hidden bg-white border-b md:mt-8 md:mx-8 md:pt-4 md:pb-3 md:rounded-t-3xl dark:border-gray-600 md:shadow-md dark:bg-primaryDark">
<div className="flex items-center justify-between h-16 px-4 md:px-6"> <div className="flex items-center justify-between h-16 px-4 md:px-6">
<div className="hidden md:flex"> <div className="hidden md:flex">
<Logo /> <Logo />
</div> </div>
<Navigation className="hidden md:flex" />
<MobileNavToggle /> <MobileNavToggle />
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<DeviceStatusDropdown /> <DeviceStatusDropdown />
{/* <LanguageDropdown /> */}
<ThemeToggle /> <ThemeToggle />
</div> </div>
</div> </div>
<Navigation className="hidden md:flex" />
</div> </div>
</div> </div>
<MobileNav /> <MobileNav />
<div className="flex flex-grow w-full min-h-0 md:px-8 md:mb-8"> <div className="flex flex-grow w-full min-h-0 md:px-8 md:mb-8">
<div className="flex w-full bg-gray-100 md:shadow-xl md:overflow-hidden dark:bg-secondaryDark md:rounded-b-xl"> <div className="flex w-full bg-gray-100 md:shadow-xl md:overflow-hidden dark:bg-secondaryDark md:rounded-b-3xl">
{route.name === 'messages' && <Messages />} {route.name === 'messages' && <Messages />}
{route.name === 'nodes' && <Nodes />} {route.name === 'nodes' && <Nodes />}
{route.name === 'settings' && <Settings />} {route.name === 'settings' && <Settings />}
+1 -3
View File
@@ -2,12 +2,10 @@ import React from 'react';
type DefaultDivProps = JSX.IntrinsicElements['div']; type DefaultDivProps = JSX.IntrinsicElements['div'];
interface LocalBlurProps { interface BlurProps extends DefaultDivProps {
disableOnMd?: boolean; disableOnMd?: boolean;
} }
export type BlurProps = LocalBlurProps & DefaultDivProps;
export const Blur = ({ export const Blur = ({
disableOnMd, disableOnMd,
className, className,
+1 -3
View File
@@ -2,15 +2,13 @@ import React from 'react';
type DefaultButtonProps = JSX.IntrinsicElements['button']; type DefaultButtonProps = JSX.IntrinsicElements['button'];
interface LocalButtonProps { interface ButtonProps extends DefaultButtonProps {
icon?: JSX.Element; icon?: JSX.Element;
circle?: boolean; circle?: boolean;
active?: boolean; active?: boolean;
border?: boolean; border?: boolean;
} }
export type ButtonProps = LocalButtonProps & DefaultButtonProps;
export const Button = ({ export const Button = ({
icon, icon,
circle, circle,
+158
View File
@@ -0,0 +1,158 @@
import React from 'react';
import ApexChart from 'react-apexcharts';
import { Button } from './Button';
type DefaultDivProps = JSX.IntrinsicElements['div'];
interface ISeries {
name: string;
data: {
x: string | Date;
y: number;
}[];
}
interface ChartProps extends DefaultDivProps {
title: string;
description: string;
hasMultipleSeries: boolean;
series: ISeries[];
}
export const Chart = ({
title,
description,
hasMultipleSeries,
series,
...props
}: ChartProps): JSX.Element => {
const [activeSeries, setActiveSeries] = React.useState<ISeries>(series[0]);
return (
<div
className="flex flex-col flex-auto text-white shadow-md dark bg-primaryDark rounded-3xl"
{...props}
>
<div className="flex items-center justify-between mx-10 mt-10">
<div className="flex flex-col">
<div className="mr-4 text-2xl font-semibold leading-7 tracking-tight md:text-3xl">
{title}
</div>
<div className="font-medium text-gray-400">{description}</div>
</div>
{hasMultipleSeries && (
<div className="flex space-x-2">
{series.map((data, index) => (
<Button
active={data.name === activeSeries.name}
key={index}
className="font-medium"
onClick={(): void => {
setActiveSeries(series[index]);
}}
>
{data.name}
</Button>
))}
</div>
)}
</div>
<div className="h-80">
<ApexChart
height="96%"
type="area"
options={{
chart: {
animations: {
speed: 400,
animateGradually: {
enabled: false,
},
},
toolbar: {
show: false,
},
zoom: {
enabled: false,
},
},
colors: ['#818CF8'],
dataLabels: {
enabled: false,
},
fill: {
colors: ['#312E81'],
},
grid: {
padding: {
top: 10,
left: 0,
right: 0,
},
xaxis: {
lines: {
show: false,
},
},
yaxis: {
lines: {
show: false,
},
},
},
stroke: {
width: 2,
},
tooltip: {
followCursor: true,
theme: 'dark',
x: {
format: 'MMM dd, yyyy',
},
y: {
formatter: (value: number): string => `${value}`,
},
},
xaxis: {
axisBorder: {
show: false,
},
axisTicks: {
show: false,
},
crosshairs: {
stroke: {
color: '#475569',
dashArray: 0,
width: 2,
},
},
labels: {
style: {
colors: '#CBD5E1',
},
},
tooltip: {
enabled: false,
},
type: 'datetime',
},
yaxis: {
axisTicks: {
show: false,
},
axisBorder: {
show: false,
},
show: false,
},
}}
series={[activeSeries]}
/>
</div>
</div>
);
};
+1 -2
View File
@@ -4,12 +4,11 @@ import { Blur } from '@components/generic/Blur';
type DefaultAsideProps = JSX.IntrinsicElements['aside']; type DefaultAsideProps = JSX.IntrinsicElements['aside'];
interface LocalDrawerProps { interface DrawerProps extends DefaultAsideProps {
open: boolean; open: boolean;
permenant?: boolean; permenant?: boolean;
onClose: () => void; onClose: () => void;
} }
export type DrawerProps = LocalDrawerProps & DefaultAsideProps;
export const Drawer = ({ export const Drawer = ({
open, open,
+16 -5
View File
@@ -2,18 +2,24 @@ import React from 'react';
type DefaultInputProps = JSX.IntrinsicElements['input']; type DefaultInputProps = JSX.IntrinsicElements['input'];
interface LocalInputProps { interface InputProps extends DefaultInputProps {
icon?: JSX.Element; icon?: JSX.Element;
label?: string; label?: string;
valid?: boolean; valid?: boolean;
validationMessage?: string; validationMessage?: string;
} }
export type InputProps = LocalInputProps & DefaultInputProps;
export const Input = React.forwardRef<HTMLInputElement, InputProps>( export const Input = React.forwardRef<HTMLInputElement, InputProps>(
function Input( function Input(
{ icon, label, valid, validationMessage, id, ...props }: InputProps, {
icon,
label,
valid,
validationMessage,
id,
disabled,
...props
}: InputProps,
ref, ref,
) { ) {
return ( return (
@@ -35,9 +41,14 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input <input
id={id} id={id}
ref={ref} ref={ref}
disabled={disabled}
{...props} {...props}
className={`block w-full h-11 rounded-md border shadow-sm focus:outline-none focus:border-primary dark:focus:border-primary bg-white dark:bg-secondaryDark dark:border-gray-600 dark:text-white ${ className={`block w-full h-11 rounded-md border shadow-sm focus:outline-none focus:border-primary dark:focus:border-primary dark:border-gray-600 dark:text-white ${
icon ? 'pl-9' : 'pl-2' icon ? 'pl-9' : 'pl-2'
} ${
disabled
? 'bg-gray-200 dark:bg-primaryDark cursor-not-allowed'
: 'bg-white dark:bg-secondaryDark'
}`} }`}
/> />
</div> </div>
+10 -5
View File
@@ -11,7 +11,11 @@ export interface SelectProps {
icon: JSX.Element; icon: JSX.Element;
}[]; }[];
id: string; id: string;
value: string; active: {
name: string;
value: string;
icon: JSX.Element;
};
onChange: (value: string) => void; onChange: (value: string) => void;
} }
@@ -19,7 +23,7 @@ export const Select = ({
label, label,
options, options,
id, id,
value, active,
onChange, onChange,
}: SelectProps): JSX.Element => { }: SelectProps): JSX.Element => {
return ( return (
@@ -28,10 +32,11 @@ export const Select = ({
{label} {label}
</label> </label>
<Listbox value={value} onChange={onChange}> <Listbox value={active.value} onChange={onChange}>
<div className="relative mt-1"> <div className="relative mt-1">
<Listbox.Button className="relative w-full text-left bg-white border rounded-md shadow-sm h-11 focus:outline-none focus:border-primary dark:focus:border-primary dark:bg-secondaryDark dark:border-gray-600 dark:text-white"> <Listbox.Button className="flex w-full text-left bg-white border rounded-md shadow-sm h-11 focus:outline-none focus:border-primary dark:focus:border-primary dark:bg-secondaryDark dark:border-gray-600 dark:text-white">
<span className="block truncate">{value}</span> <div className="">{active.icon}</div>
<span className="block truncate">{active.name}</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"> <span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon <SelectorIcon
className="w-5 h-5 text-gray-400" className="w-5 h-5 text-gray-400"
+3 -3
View File
@@ -2,26 +2,26 @@ import React from 'react';
type DefaultDivProps = JSX.IntrinsicElements['div']; type DefaultDivProps = JSX.IntrinsicElements['div'];
interface LocalSidebarItemProps { interface SidebarItemProps extends DefaultDivProps {
title: string; title: string;
description: string; description: string;
selected: boolean; selected: boolean;
icon: JSX.Element; icon: JSX.Element;
} }
export type SidebarItemProps = LocalSidebarItemProps & DefaultDivProps;
export const SidebarItem = ({ export const SidebarItem = ({
title, title,
description, description,
selected, selected,
icon, icon,
...props
}: SidebarItemProps): JSX.Element => { }: SidebarItemProps): JSX.Element => {
return ( return (
<div <div
className={`flex p-5 cursor-pointer select-none dark:hover:bg-primaryDark ${ className={`flex p-5 cursor-pointer select-none dark:hover:bg-primaryDark ${
selected ? 'bg-gray-200 dark:bg-primaryDark' : 'dark:bg-secondaryDark' selected ? 'bg-gray-200 dark:bg-primaryDark' : 'dark:bg-secondaryDark'
}`} }`}
{...props}
> >
<div className="text-gray-500 dark:text-gray-400">{icon}</div> <div className="text-gray-500 dark:text-gray-400">{icon}</div>
<div className="ml-3 text-left"> <div className="ml-3 text-left">
+47
View File
@@ -0,0 +1,47 @@
import React from 'react';
import { Tab } from '@headlessui/react';
type DefaultDivProps = JSX.IntrinsicElements['div'];
interface TabProps extends DefaultDivProps {
tabs: {
name: string;
body: JSX.Element;
}[];
}
export const Tabs = ({ tabs, className, ...props }: TabProps): JSX.Element => {
return (
<Tab.Group as="div" className={className}>
<Tab.List className="flex border-l border-r border-t shadow-md rounded-t-3xl dark:border-gray-600">
{tabs.map((tab) => (
<Tab
key={tab.name}
className={({ selected }): string =>
`w-full text-lg font-medium p-2 border-b-2 ${
selected
? 'dark:border-gray-200 border-gray-600'
: 'border-transparent dark:border-transparent'
}`
}
>
{tab.name}
</Tab>
))}
</Tab.List>
<Tab.Panels className="h-full">
{tabs.map((tab, index) => (
<Tab.Panel
key={index}
className={
'border dark:border-gray-600 rounded-b-3xl p-4 h-full shadow-md'
}
>
{tab.body}
</Tab.Panel>
))}
</Tab.Panels>
</Tab.Group>
);
};
+51
View File
@@ -0,0 +1,51 @@
import React from 'react';
import { Switch } from '@headlessui/react';
type DefaultButtonProps = JSX.IntrinsicElements['button'];
interface ToggleProps extends DefaultButtonProps {
label?: string;
valid?: boolean;
validationMessage?: string;
}
export const Toggle = ({
label,
valid,
validationMessage,
id,
...props
}: ToggleProps): JSX.Element => {
const [enabled, setEnabled] = React.useState(false);
return (
<div className="w-full">
<label htmlFor={id} className="block text-sm font-medium dark:text-white">
{label}
</label>
<div className="float-right">
<Switch
id={id}
{...props}
checked={enabled}
onChange={setEnabled}
className={`${
enabled ? 'bg-primary' : 'bg-gray-200 dark:bg-primaryDark'
}
relative inline-flex flex-shrink-0 h-[38px] w-[74px] border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`}
>
<span className="sr-only">Use setting</span>
<span
aria-hidden="true"
className={`${enabled ? 'translate-x-9' : 'translate-x-0'}
pointer-events-none inline-block h-[34px] w-[34px] rounded-full bg-white shadow-lg transform ring-0 transition ease-in-out duration-200`}
/>
</Switch>
</div>
{!valid && (
<div className="text-sm text-gray-600">{validationMessage}</div>
)}
</div>
);
};
+1 -1
View File
@@ -19,7 +19,7 @@ export const MobileNav = (): JSX.Element => {
dispatch(closeMobileNav()); dispatch(closeMobileNav());
}} }}
> >
<div className="flex flex-col w-64"> <div className="flex flex-col">
<div className="m-auto my-6"> <div className="m-auto my-6">
<Logo /> <Logo />
</div> </div>
+1 -1
View File
@@ -21,7 +21,7 @@ export const Navigation = ({
const route = useRoute(); const route = useRoute();
return ( return (
<div <div
className={`h-16 px-4 md:space-x-2 space-y-2 md:space-y-0 ${className}`} className={`px-4 md:space-x-2 space-y-2 md:space-y-0 ${className}`}
{...props} {...props}
> >
<div onClick={onClick}> <div onClick={onClick}>
-31
View File
@@ -1,31 +0,0 @@
import React from 'react';
import Avatar from 'boring-avatars';
import type { Protobuf } from '@meshtastic/meshtasticjs';
type DefaultDivProps = JSX.IntrinsicElements['div'];
export interface NodeProps {
node: Protobuf.NodeInfo;
}
export const Node = ({
node,
...props
}: NodeProps & DefaultDivProps): JSX.Element => {
return (
<div
{...props}
className="flex space-x-4 items-center w-full rounded-md dark:bg-primaryDark shadow-md border dark:border-gray-600 p-2 mt-6 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-900"
>
<Avatar
size={30}
name={node.user?.longName ?? 'UNK'}
variant="beam"
colors={['#213435', '#46685B', '#648A64', '#A6B985', '#E1E3AC']}
/>
<div>{node.user?.longName}</div>
</div>
);
};
+3 -3
View File
@@ -17,7 +17,7 @@ export const PrimaryTemplate = ({
}: PrimaryTemplateProps): JSX.Element => { }: PrimaryTemplateProps): JSX.Element => {
return ( return (
<div className="flex flex-col flex-auto min-w-0"> <div className="flex flex-col flex-auto min-w-0">
<div className="flex p-6 bg-white border-b md:flex-row flex-0 md:items-center md:justify-between md:py-8 md:px-10 dark:border-gray-600 dark:bg-secondaryDark"> <div className="flex p-4 bg-white border-b md:flex-row flex-0 md:items-center md:justify-between md:px-10 dark:border-gray-600 dark:bg-secondaryDark">
{button && <div className="pr-2 m-auto md:hidden">{button}</div>} {button && <div className="pr-2 m-auto md:hidden">{button}</div>}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center font-medium"> <div className="flex flex-wrap items-center font-medium">
@@ -32,11 +32,11 @@ export const PrimaryTemplate = ({
</div> </div>
</div> </div>
</div> </div>
<div className="flex-auto flex-grow p-6 bg-white md:p-10 dark:bg-secondaryDark"> <div className="flex-auto flex-grow p-6 bg-white md:p-10 dark:bg-secondaryDark overflow-y-auto">
{children} {children}
</div> </div>
{footer && ( {footer && (
<div className="flex p-6 bg-white border-t md:flex-row flex-0 md:items-center md:justify-between md:py-8 md:px-10 dark:border-gray-600 dark:bg-secondaryDark"> <div className="flex p-4 bg-white border-t md:flex-row flex-0 md:items-center md:justify-between md:px-10 dark:border-gray-600 dark:bg-secondaryDark">
{button && <div className="pr-2 m-auto md:hidden">{button}</div>} {button && <div className="pr-2 m-auto md:hidden">{button}</div>}
<div className="flex-1 min-w-0">{footer}</div> <div className="flex-1 min-w-0">{footer}</div>
</div> </div>
+7 -1
View File
@@ -1,3 +1,9 @@
import { IHTTPConnection } from '@meshtastic/meshtasticjs'; import {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
} from '@meshtastic/meshtasticjs';
export const connection = new IHTTPConnection(); export const connection = new IHTTPConnection();
export const bleConnection = new IBLEConnection();
export const serialConnection = new ISerialConnection();
+1 -1
View File
@@ -3,7 +3,7 @@
import useBreakpointHook from 'use-breakpoint'; import useBreakpointHook from 'use-breakpoint';
const BREAKPOINTS = { const BREAKPOINTS = {
sm: 640, sm: 0,
// => @media (min-width: 640px) { ... } // => @media (min-width: 640px) { ... }
md: 768, md: 768,
+6
View File
@@ -46,6 +46,12 @@ export const Nodes = (): JSX.Element => {
</div> </div>
</div> </div>
{!nodes.length && (
<span className="p-4 text-sm text-gray-400 dark:text-gray-600">
No nodes discovered yet...
</span>
)}
{nodes.map((node) => ( {nodes.map((node) => (
<Tab <Tab
onClick={(): void => { onClick={(): void => {
+118 -1
View File
@@ -1,5 +1,8 @@
import React from 'react'; import React from 'react';
import moment from 'moment';
import { Chart } from '@app/components/generic/Chart';
import { Button } from '@components/generic/Button'; import { Button } from '@components/generic/Button';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate'; import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { MenuIcon } from '@heroicons/react/outline'; import { MenuIcon } from '@heroicons/react/outline';
@@ -26,7 +29,121 @@ export const Node = ({ navOpen, setNavOpen, node }: NodeProps): JSX.Element => {
/> />
} }
> >
<div className="w-full max-w-3xl space-y-2 md:max-w-xl">Content</div> <div className="w-full space-y-2">
<Chart
title="Visitors Overview"
description="Number of unique visitors"
hasMultipleSeries={true}
series={[
{
name: 'Series 1',
data: [
{
x: moment().subtract(12, 'months').day(1).toDate(),
y: 4884,
},
{
x: moment().subtract(12, 'months').day(4).toDate(),
y: 5351,
},
{
x: moment().subtract(12, 'months').day(7).toDate(),
y: 5293,
},
{
x: moment().subtract(12, 'months').day(10).toDate(),
y: 4908,
},
{
x: moment().subtract(12, 'months').day(13).toDate(),
y: 5027,
},
{
x: moment().subtract(12, 'months').day(16).toDate(),
y: 4837,
},
{
x: moment().subtract(12, 'months').day(19).toDate(),
y: 4484,
},
{
x: moment().subtract(12, 'months').day(22).toDate(),
y: 4071,
},
{
x: moment().subtract(12, 'months').day(25).toDate(),
y: 4124,
},
{
x: moment().subtract(12, 'months').day(28).toDate(),
y: 4563,
},
{
x: moment().subtract(11, 'months').day(1).toDate(),
y: 3820,
},
{
x: moment().subtract(11, 'months').day(4).toDate(),
y: 3968,
},
],
},
{
name: 'Series 2',
data: [
{
x: moment().subtract(12, 'months').day(1).toDate(),
y: 4332,
},
{
x: moment().subtract(12, 'months').day(4).toDate(),
y: 6642,
},
{
x: moment().subtract(12, 'months').day(7).toDate(),
y: 5531,
},
{
x: moment().subtract(12, 'months').day(10).toDate(),
y: 2231,
},
{
x: moment().subtract(12, 'months').day(13).toDate(),
y: 5532,
},
{
x: moment().subtract(12, 'months').day(16).toDate(),
y: 3352,
},
{
x: moment().subtract(12, 'months').day(19).toDate(),
y: 6633,
},
{
x: moment().subtract(12, 'months').day(22).toDate(),
y: 1442,
},
{
x: moment().subtract(12, 'months').day(25).toDate(),
y: 4332,
},
{
x: moment().subtract(12, 'months').day(28).toDate(),
y: 6332,
},
{
x: moment().subtract(11, 'months').day(1).toDate(),
y: 5334,
},
{
x: moment().subtract(11, 'months').day(4).toDate(),
y: 5253,
},
],
},
]}
/>
</div>
</PrimaryTemplate> </PrimaryTemplate>
); );
}; };
+137
View File
@@ -0,0 +1,137 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { Input } from '@app/components/generic/Input';
import { Tabs } from '@app/components/generic/Tabs';
import { Toggle } from '@app/components/generic/Toggle';
import {
bleConnection,
connection,
serialConnection,
} from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { Button } from '@components/generic/Button';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { LinkIcon, MenuIcon, SaveIcon } from '@heroicons/react/outline';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export interface ConnectionProps {
navOpen: boolean;
setNavOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
export const Connection = ({
navOpen,
setNavOpen,
}: ConnectionProps): JSX.Element => {
const { t } = useTranslation();
const user = useAppSelector((state) => state.meshtastic.user);
const { register, handleSubmit, formState } = useForm<Protobuf.User>({
defaultValues: user,
});
const onSubmit = handleSubmit((data) => {
void connection.setOwner(data);
});
return (
<PrimaryTemplate
title="Connection"
tagline="Settings"
button={
<Button
icon={<MenuIcon className="w-5 h-5" />}
onClick={(): void => {
setNavOpen(!navOpen);
}}
circle
/>
}
footer={
<Button
className="px-10 ml-auto"
icon={<SaveIcon className="w-5 h-5" />}
disabled={!formState.isDirty}
active
border
>
{t('strings.save_changes')}
</Button>
}
>
<div className="w-full max-w-3xl md:max-w-xl">
<div className="mb-2 flex w-full border dark:border-gray-600 rounded-3xl p-2">
Current connection method:
<div className="ml-2 rounded-full bg-gray-400 dark:bg-primaryDark text-sm px-1 my-auto">
BLE
</div>
</div>
<form className="space-y-2" onSubmit={onSubmit}>
<Tabs
className="h-60"
tabs={[
{
name: 'HTTP',
body: (
<div className="space-y-2">
<Input label={'Device URL'} />
<Toggle label="Use TLS?" />
</div>
),
},
{
name: 'Bluetooth',
body: (
<div className="space-y-2">
Devices:
<Button
onClick={async (): Promise<void> => {
console.log(await bleConnection.getDevices());
}}
>
Get Devices
</Button>
<div className="flex justify-between rounded-3xl border dark:border-600 p-2">
Device Name
<LinkIcon className="my-auto mr-2 w-5 h-5 text-gray-300" />
</div>
<div className="flex justify-between rounded-3xl border dark:border-600 p-2">
Device Name
<LinkIcon className="my-auto mr-2 w-5 h-5 text-gray-600" />
</div>
</div>
),
},
{
name: 'Serial',
body: (
<div className="space-y-2">
Devices:
<Button
onClick={async (): Promise<void> => {
console.log(await serialConnection.getPorts());
}}
>
Get Devices
</Button>
<div className="flex justify-between rounded-3xl border dark:border-600 p-2">
Device Name
<LinkIcon className="my-auto mr-2 w-5 h-5 text-gray-300" />
</div>
<div className="flex justify-between rounded-3xl border dark:border-600 p-2">
Device Name
<LinkIcon className="my-auto mr-2 w-5 h-5 text-gray-600" />
</div>
</div>
),
},
]}
/>
</form>
</div>
</PrimaryTemplate>
);
};
+20 -5
View File
@@ -3,13 +3,14 @@ import React from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Toggle } from '@app/components/generic/Toggle';
import { connection } from '@app/core/connection'; import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux'; import { useAppSelector } from '@app/hooks/redux';
import { Button } from '@components/generic/Button'; import { Button } from '@components/generic/Button';
import { Input } from '@components/generic/Input'; import { Input } from '@components/generic/Input';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate'; import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { MenuIcon, SaveIcon } from '@heroicons/react/outline'; import { MenuIcon, SaveIcon } from '@heroicons/react/outline';
import type { Protobuf } from '@meshtastic/meshtasticjs'; import { Protobuf } from '@meshtastic/meshtasticjs';
export interface DeviceProps { export interface DeviceProps {
navOpen: boolean; navOpen: boolean;
@@ -19,13 +20,21 @@ export interface DeviceProps {
export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => { export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const user = useAppSelector((state) => state.meshtastic.user); const user = useAppSelector((state) => state.meshtastic.user);
const { register, handleSubmit, formState } = useForm<{
const { register, handleSubmit, formState } = useForm<Protobuf.User>({ isLicensed: boolean;
defaultValues: user, shortName: string;
longName: string;
}>({
defaultValues: {
isLicensed: user.isLicensed,
shortName: user.shortName,
longName: user.longName,
},
}); });
const onSubmit = handleSubmit((data) => { const onSubmit = handleSubmit((data) => {
void connection.setOwner(data); Protobuf.User.mergePartial(user, data);
void connection.setOwner(user);
}); });
return ( return (
@@ -56,6 +65,12 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
<div className="w-full max-w-3xl md:max-w-xl"> <div className="w-full max-w-3xl md:max-w-xl">
<form className="space-y-2" onSubmit={onSubmit}> <form className="space-y-2" onSubmit={onSubmit}>
<Input label={'Device Name'} {...register('longName')} /> <Input label={'Device Name'} {...register('longName')} />
<Input
label={'Short Name'}
maxLength={3}
{...register('shortName')}
/>
<Toggle label="Licenced Operator?" {...register('isLicensed')} />
</form> </form>
</div> </div>
</PrimaryTemplate> </PrimaryTemplate>
+19
View File
@@ -8,10 +8,12 @@ import { Tab } from '@headlessui/react';
import { import {
CollectionIcon, CollectionIcon,
DeviceMobileIcon, DeviceMobileIcon,
LinkIcon,
WifiIcon, WifiIcon,
XCircleIcon, XCircleIcon,
} from '@heroicons/react/outline'; } from '@heroicons/react/outline';
import { Connection } from './Connection';
import { Device } from './Device'; import { Device } from './Device';
import { Interface } from './Interface'; import { Interface } from './Interface';
import { Radio } from './Radio'; import { Radio } from './Radio';
@@ -46,6 +48,20 @@ export const Settings = (): JSX.Element => {
/> />
</div> </div>
</div> </div>
<Tab
onClick={(): void => {
setNavOpen(false);
}}
>
{({ selected }): JSX.Element => (
<SidebarItem
title="Connection"
description="Method and peramaters for connecting to the device"
selected={selected}
icon={<LinkIcon className="flex-shrink-0 w-6 h-6" />}
/>
)}
</Tab>
<Tab <Tab
onClick={(): void => { onClick={(): void => {
setNavOpen(false); setNavOpen(false);
@@ -84,6 +100,9 @@ export const Settings = (): JSX.Element => {
</Drawer> </Drawer>
<div className="flex w-full"> <div className="flex w-full">
<Tab.Panels className="flex w-full"> <Tab.Panels className="flex w-full">
<Tab.Panel className="flex w-full">
<Connection navOpen={navOpen} setNavOpen={setNavOpen} />
</Tab.Panel>
<Tab.Panel className="flex w-full"> <Tab.Panel className="flex w-full">
<Device navOpen={navOpen} setNavOpen={setNavOpen} /> <Device navOpen={navOpen} setNavOpen={setNavOpen} />
</Tab.Panel> </Tab.Panel>
+5 -1
View File
@@ -47,7 +47,11 @@ export const Interface = ({
<div className="w-full max-w-3xl space-y-2 md:max-w-xl"> <div className="w-full max-w-3xl space-y-2 md:max-w-xl">
<Select <Select
label="Language" label="Language"
value={i18n.language} active={{
name: '',
value: '',
icon: <Us />,
}}
onChange={(value): void => { onChange={(value): void => {
void i18n.changeLanguage(value); void i18n.changeLanguage(value);
}} }}
+10
View File
@@ -61,6 +61,16 @@ export const Radio = ({ navOpen, setNavOpen }: RadioProps): JSX.Element => {
label={t('strings.wifi_psk')} label={t('strings.wifi_psk')}
{...register('wifiPassword')} {...register('wifiPassword')}
/> />
<Input
label={'Charge current'}
disabled
{...register('chargeCurrent')}
/>
<Input
label={'Last GPS Attempt'}
disabled
{...register('gpsAttemptTime')}
/>
</form> </form>
</div> </div>
</PrimaryTemplate> </PrimaryTemplate>
-5133
View File
File diff suppressed because it is too large Load Diff