committed by
GitHub
29 changed files with 5898 additions and 5237 deletions
@ -4,9 +4,9 @@ name: meshtastic-web build |
|||
on: |
|||
# Triggers the workflow on push or pull request events but only for the master branch |
|||
push: |
|||
branches: [ master ] |
|||
branches: [master] |
|||
pull_request: |
|||
branches: [ master ] |
|||
branches: [master] |
|||
|
|||
# Allows you to run this workflow manually from the Actions tab |
|||
workflow_dispatch: |
|||
@ -19,31 +19,31 @@ jobs: |
|||
# Checks-out repository |
|||
- name: Checkout |
|||
uses: actions/checkout@v2 |
|||
|
|||
# Build project |
|||
- uses: pnpm/[email protected] |
|||
with: |
|||
version: 6.14.3 |
|||
- uses: actions/setup-node@v2 |
|||
with: |
|||
node-version: '14' |
|||
cache: 'yarn' |
|||
- run: yarn install --ignore-optional |
|||
- run: yarn lint |
|||
- run: yarn build |
|||
- run: yarn package |
|||
node-version: '16' |
|||
cache: 'pnpm' |
|||
- run: pnpm lint |
|||
- run: pnpm build |
|||
- run: pnpm package |
|||
- run: tree build/output |
|||
|
|||
|
|||
# Create a zip file from the output folder |
|||
- name: Create output zip file |
|||
uses: papeloto/action-zip@v1 |
|||
with: |
|||
files: build/output/ |
|||
dest: output.zip |
|||
|
|||
|
|||
# Upload Artifact |
|||
- name: Upload a Build Artifact |
|||
uses: "marvinpinto/action-automatic-releases@latest" |
|||
uses: 'marvinpinto/action-automatic-releases@latest' |
|||
with: |
|||
repo_token: "${{ secrets.GITHUB_TOKEN }}" |
|||
automatic_release_tag: "latest" |
|||
repo_token: '${{ secrets.GITHUB_TOKEN }}' |
|||
automatic_release_tag: 'latest' |
|||
prerelease: false |
|||
files: | |
|||
output.zip |
|||
|
|||
File diff suppressed because it is too large
@ -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> |
|||
); |
|||
}; |
|||
@ -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> |
|||
); |
|||
}; |
|||
@ -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,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> |
|||
); |
|||
}; |
|||
@ -1,3 +1,9 @@ |
|||
import { IHTTPConnection } from '@meshtastic/meshtasticjs'; |
|||
import { |
|||
IBLEConnection, |
|||
IHTTPConnection, |
|||
ISerialConnection, |
|||
} from '@meshtastic/meshtasticjs'; |
|||
|
|||
export const connection = new IHTTPConnection(); |
|||
export const bleConnection = new IBLEConnection(); |
|||
export const serialConnection = new ISerialConnection(); |
|||
|
|||
@ -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> |
|||
); |
|||
}; |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue