General cleanup and refactoring.
This commit is contained in:
parent
1e16d0e482
commit
b46806ada1
11
src/App.tsx
11
src/App.tsx
|
|
@ -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 />,
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
<ValuesTable values={status.values} />
|
||||
</Card.Text>
|
||||
</Card.Body>
|
||||
</p>
|
||||
<p>
|
||||
<ValuesTable values={status.values} />
|
||||
</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,26 +170,27 @@ function SingleNode(props: Any) {
|
|||
};
|
||||
|
||||
return (
|
||||
<ListGroup.Item className="border-0 ps-3 py-1">
|
||||
<Card>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col xs={1}>
|
||||
<DirectionIcon
|
||||
isOpen={isOpen}
|
||||
hasChildren={hasChildren}
|
||||
onClick={openFunction}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={"auto"}>
|
||||
<StatusContent status={node.status} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<ChildStatuses isOpen={isOpen} node={node} />
|
||||
</Row>
|
||||
</Container>
|
||||
</Card>
|
||||
<ListGroup.Item>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col xs={1}>
|
||||
<DirectionIcon
|
||||
isOpen={isOpen}
|
||||
hasChildren={hasChildren}
|
||||
onClick={openFunction}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={1}>
|
||||
<LevelIndicator level={node.status.level} />
|
||||
</Col>
|
||||
<Col xs={"auto"}>
|
||||
<StatusContent status={node.status} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<ChildStatuses isOpen={isOpen} node={node} />
|
||||
</Row>
|
||||
</Container>
|
||||
</ListGroup.Item>
|
||||
);
|
||||
}
|
||||
87
src/Home.tsx
87
src/Home.tsx
|
|
@ -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>
|
||||
</Row>
|
||||
</Card>
|
||||
<Row>
|
||||
<Col>
|
||||
<UptimeCard />
|
||||
</Col>
|
||||
<Col>
|
||||
<StatusCard />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="vert-padded">
|
||||
<Col>
|
||||
<DiagnosticsCard />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<Footer />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue