General cleanup and refactoring.

This commit is contained in:
James Pace 2026-08-18 21:06:14 -04:00
parent 1e16d0e482
commit b46806ada1
7 changed files with 210 additions and 213 deletions

View File

@ -12,9 +12,8 @@ import { useState } from "react";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { Provider as JotaiProvider } from "jotai";
import { Home } from "./Home.tsx";
import { Autonomy } from "./Autonomy.tsx";
import { Video } from "./Video.tsx";
import { Version } from "./Version.tsx";
import { Diagnostics } from "./Diagnostics.tsx";
import "./App.css";
function App() {
@ -41,12 +40,8 @@ const router = createBrowserRouter([
element: <Version />,
},
{
path: "/autonomy",
element: <Autonomy />,
},
{
path: "/diagnostics",
element: <Diagnostics />,
path: "/video",
element: <Video />,
},
]);

View File

@ -1,90 +0,0 @@
//
// Copyright 2026 James Pace
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// This Source Code Form is "Incompatible With Secondary Licenses", as
// defined by the Mozilla Public License, v. 2.0.
//
import { AppNav, Footer } from "./components/AppNav.tsx";
import { Container, Row, Col, Image, Card } from "react-bootstrap";
import { atom, useAtomValue } from "jotai";
import { atomWithQuery } from "jotai-tanstack-query";
import { WebRTCVideo } from "mediamtx-webrtc-react";
const statusStringQuery = async () => {
const resp = await fetch("api/status");
if (!resp.ok) {
throw new Error("Network response was not ok");
}
return resp.json();
};
const statusStringQueryAtom = atomWithQuery(() => ({
queryKey: ["status_string"],
queryFn: statusStringQuery,
refetchInterval: 500, // 0.5s
}));
const statusStringAtom = atom((get) => {
const response = get(statusStringQueryAtom);
if (response.isPending || response.isError || !response.data.status) {
return "N/A";
}
return response.data.message;
});
const latLongQuery = async () => {
const resp = await fetch("api/position");
if (!resp.ok) {
throw new Error("Network response was not ok");
}
return resp.json();
};
const latLongQueryAtom = atomWithQuery(() => ({
queryKey: ["latlong"],
queryFn: latLongQuery,
refetchInterval: 500, // 0.5s
}));
const latLongAtom = atom((get) => {
const response = get(latLongQueryAtom);
if (response.isPending || response.isError || !response.data.status) {
return "N/A";
}
return `(${response.data.latitude}, ${response.data.longitude})`;
});
export function Autonomy() {
const statusString = useAtomValue(statusStringAtom);
const latLong = useAtomValue(latLongAtom);
const imageStreamURI =
"http://" + window.location.hostname + ":8889/image/whep";
return (
<div>
<AppNav />
<Container className="vert-padded">
<Card className="padded">
<Row>
<Col lg={8}>
<WebRTCVideo
url={imageStreamURI}
controls
autoPlay
style={{ width: "100%", maxWidth: "800px" }}
/>
</Col>
<Col lg={4}>
<p>Status:</p>
<pre>{statusString}</pre>
<p>Latitude, Longitude:</p>
<pre>{latLong}</pre>
</Col>
</Row>
</Card>
</Container>
<Footer />
</div>
);
}

View File

@ -1,66 +0,0 @@
//
// Copyright 2026 James Pace
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// This Source Code Form is "Incompatible With Secondary Licenses", as
// defined by the Mozilla Public License, v. 2.0.
//
import { AppNav, Footer } from "./components/AppNav.tsx";
import { TreeView } from "./components/DiagnosticView.tsx";
import { Container, Row, Col, Card } from "react-bootstrap";
import { atom, useAtomValue } from "jotai";
import { atomWithQuery } from "jotai-tanstack-query";
const diagQueryFn = async () => {
const resp = await fetch("api/diagnostics");
if (!resp.ok) {
throw new Error("Network response was not ok");
}
return resp.json();
};
const diagQueryAtom = atomWithQuery(() => ({
queryKey: ["diagnostics"],
queryFn: diagQueryFn,
}));
const diagAtom = atom((get) => {
const diag = get(diagQueryAtom);
if (diag.isPending) {
return "Loading...";
}
if (diag.isError) {
return "Error loading!";
}
if (!diag.data.status) {
return "Failed to get diagnostic information.";
}
return diag.data.message;
});
export function Diagnostics() {
const diagnostics = useAtomValue(diagAtom);
const diagContent = () => {
if (typeof diagnostics === "string" || diagnostics instanceof String) {
return <pre> {diagnostics} </pre>;
}
return <TreeView data={diagnostics} />;
};
return (
<div>
<AppNav />
<Container className="vert-padded">
<Card className="padded">
<Card.Title>Diagnostics</Card.Title>
<Card.Body>{diagContent()}</Card.Body>
</Card>
</Container>
<Footer />
</div>
);
}

View File

@ -1,20 +1,64 @@
import React, { useState } from "react";
import { Button, ListGroup, Collapse, Card } from "react-bootstrap";
import { Button, ListGroup, Collapse } from "react-bootstrap";
import {
Dash,
ChevronDown,
ChevronRight,
CircleFill,
} from "react-bootstrap-icons";
import Table from "react-bootstrap/Table";
import Container from "react-bootstrap/Container";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import { Container, Row, Col, Table, Card } from "react-bootstrap";
import { atom, useAtomValue } from "jotai";
import { atomWithQuery } from "jotai-tanstack-query";
export function TreeView(props: Any) {
const diagQueryFn = async () => {
const resp = await fetch("api/diagnostics");
if (!resp.ok) {
throw new Error("Network response was not ok");
}
return resp.json();
};
const diagQueryAtom = atomWithQuery(() => ({
queryKey: ["diagnostics"],
queryFn: diagQueryFn,
}));
const diagAtom = atom((get) => {
const diag = get(diagQueryAtom);
if (diag.isPending) {
return "Loading...";
}
if (diag.isError) {
return "Error loading!";
}
if (!diag.data.status) {
return "Failed to get diagnostic information.";
}
return diag.data.message;
});
export function DiagnosticsCard() {
const diagnostics = useAtomValue(diagAtom);
const diagContent = () => {
if (typeof diagnostics === "string" || diagnostics instanceof String) {
return <pre> {diagnostics} </pre>;
}
return <TreeView data={diagnostics} />;
};
return (
<Card className="padded">
<Card.Title>Diagnostics</Card.Title>
<Card.Body>{diagContent()}</Card.Body>
</Card>
);
}
function TreeView(props: Any) {
const data = props.data;
return (
<ListGroup className="border-0">
<ListGroup>
{data.children.map((node) => (
<SingleNode key={node.id} node={node} />
))}
@ -70,17 +114,15 @@ function StatusContent(props: Any) {
const status = props.status;
console.log(status);
return (
<Card.Body>
<Card.Title>
<LevelIndicator level={status.level} /> {status.name}
</Card.Title>
<Card.Text>
<div>
<p>{status.name}</p>
<p>
<MessageText message={status.message} />
</Card.Text>
<Card.Text>
</p>
<p>
<ValuesTable values={status.values} />
</Card.Text>
</Card.Body>
</p>
</div>
);
}
@ -106,7 +148,7 @@ function ChildStatuses(props: Any) {
return (
<Collapse in={isOpen}>
<div>
<ListGroup className="ms-2 border-start">
<ListGroup>
{node.children.map((child) => (
<SingleNode key={child.id} node={child} />
))}
@ -128,8 +170,7 @@ function SingleNode(props: Any) {
};
return (
<ListGroup.Item className="border-0 ps-3 py-1">
<Card>
<ListGroup.Item>
<Container>
<Row>
<Col xs={1}>
@ -139,6 +180,9 @@ function SingleNode(props: Any) {
onClick={openFunction}
/>
</Col>
<Col xs={1}>
<LevelIndicator level={node.status.level} />
</Col>
<Col xs={"auto"}>
<StatusContent status={node.status} />
</Col>
@ -147,7 +191,6 @@ function SingleNode(props: Any) {
<ChildStatuses isOpen={isOpen} node={node} />
</Row>
</Container>
</Card>
</ListGroup.Item>
);
}

View File

@ -12,17 +12,94 @@ import { AppNav, Footer } from "./components/AppNav.tsx";
import { Container, Row, Col, Image, Card } from "react-bootstrap";
import { atom, useAtomValue } from "jotai";
import { atomWithQuery } from "jotai-tanstack-query";
import { DiagnosticsCard } from "./DiagnosticsCard.tsx";
const statusStringQuery = async () => {
const resp = await fetch("api/status");
if (!resp.ok) {
throw new Error("Network response was not ok");
}
return resp.json();
};
const statusStringQueryAtom = atomWithQuery(() => ({
queryKey: ["status_string"],
queryFn: statusStringQuery,
refetchInterval: 500, // 0.5s
}));
const statusStringAtom = atom((get) => {
const response = get(statusStringQueryAtom);
if (response.isPending || response.isError || !response.data.status) {
return "N/A";
}
return response.data.message;
});
const uptimeQuery = async () => {
const resp = await fetch("api/uptime");
if (!resp.ok) {
throw new Error("Network response was not ok");
}
return resp.json();
};
const uptimeQueryAtom = atomWithQuery(() => ({
queryKey: ["uptime"],
queryFn: uptimeQuery,
refetchInterval: 1000, // 1s
}));
const uptimeAtom = atom((get) => {
const response = get(uptimeQueryAtom);
if (response.isPending || response.isError) {
return "N/A";
}
return response.data.uptime.toFixed(1);
});
function StatusCard() {
const statusString = useAtomValue(statusStringAtom);
return (
<Card className="padded">
<Row>
<p>Status:</p>
</Row>
<Row>
<p>{statusString}</p>
</Row>
</Card>
);
}
function UptimeCard() {
const uptime = useAtomValue(uptimeAtom);
return (
<Card className="padded">
<Row>
<p>Uptime:</p>
</Row>
<Row>
<p>{uptime}s</p>
</Row>
</Card>
);
}
export function Home() {
return (
<div>
<AppNav />
<Container className="vert-padded">
<Card className="padded">
<Row>
<p>Hello World!</p>
<Col>
<UptimeCard />
</Col>
<Col>
<StatusCard />
</Col>
</Row>
<Row className="vert-padded">
<Col>
<DiagnosticsCard />
</Col>
</Row>
</Card>
</Container>
<Footer />
</div>

41
src/Video.tsx Normal file
View File

@ -0,0 +1,41 @@
//
// Copyright 2026 James Pace
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// This Source Code Form is "Incompatible With Secondary Licenses", as
// defined by the Mozilla Public License, v. 2.0.
//
import { AppNav, Footer } from "./components/AppNav.tsx";
import { Container, Row, Col, Image, Card } from "react-bootstrap";
import { WebRTCVideo } from "mediamtx-webrtc-react";
export function Video() {
const streamName = "image";
const imageStreamURI =
"http://" + window.location.hostname + ":8889/" + streamName + "/whep";
return (
<div>
<AppNav />
<Container className="vert-padded">
<Card className="padded">
<Row>
<Col lg={8}>
<WebRTCVideo
url={imageStreamURI}
controls
autoPlay
style={{ width: "100%", maxWidth: "800px" }}
/>
</Col>
<Col lg={4}>Showing stream: "{streamName}".</Col>
</Row>
</Card>
</Container>
<Footer />
</div>
);
}

View File

@ -24,15 +24,12 @@ export function AppNav() {
<Nav.Link as={Link} to="/">
Home
</Nav.Link>
<Nav.Link as={Link} to="/video">
Video
</Nav.Link>
<Nav.Link as={Link} to="/version">
Version
</Nav.Link>
<Nav.Link as={Link} to="/autonomy">
Autonomy
</Nav.Link>
<Nav.Link as={Link} to="/diagnostics">
Diagnostics
</Nav.Link>
</Nav>
</Navbar.Collapse>
</Container>