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 { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||||
import { Provider as JotaiProvider } from "jotai";
|
import { Provider as JotaiProvider } from "jotai";
|
||||||
import { Home } from "./Home.tsx";
|
import { Home } from "./Home.tsx";
|
||||||
import { Autonomy } from "./Autonomy.tsx";
|
import { Video } from "./Video.tsx";
|
||||||
import { Version } from "./Version.tsx";
|
import { Version } from "./Version.tsx";
|
||||||
import { Diagnostics } from "./Diagnostics.tsx";
|
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
@ -41,12 +40,8 @@ const router = createBrowserRouter([
|
||||||
element: <Version />,
|
element: <Version />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/autonomy",
|
path: "/video",
|
||||||
element: <Autonomy />,
|
element: <Video />,
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/diagnostics",
|
|
||||||
element: <Diagnostics />,
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 React, { useState } from "react";
|
||||||
import { Button, ListGroup, Collapse, Card } from "react-bootstrap";
|
import { Button, ListGroup, Collapse } from "react-bootstrap";
|
||||||
import {
|
import {
|
||||||
Dash,
|
Dash,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
CircleFill,
|
CircleFill,
|
||||||
} from "react-bootstrap-icons";
|
} from "react-bootstrap-icons";
|
||||||
import Table from "react-bootstrap/Table";
|
import { Container, Row, Col, Table, Card } from "react-bootstrap";
|
||||||
import Container from "react-bootstrap/Container";
|
import { atom, useAtomValue } from "jotai";
|
||||||
import Row from "react-bootstrap/Row";
|
import { atomWithQuery } from "jotai-tanstack-query";
|
||||||
import Col from "react-bootstrap/Col";
|
|
||||||
|
|
||||||
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;
|
const data = props.data;
|
||||||
return (
|
return (
|
||||||
<ListGroup className="border-0">
|
<ListGroup>
|
||||||
{data.children.map((node) => (
|
{data.children.map((node) => (
|
||||||
<SingleNode key={node.id} node={node} />
|
<SingleNode key={node.id} node={node} />
|
||||||
))}
|
))}
|
||||||
|
|
@ -70,17 +114,15 @@ function StatusContent(props: Any) {
|
||||||
const status = props.status;
|
const status = props.status;
|
||||||
console.log(status);
|
console.log(status);
|
||||||
return (
|
return (
|
||||||
<Card.Body>
|
<div>
|
||||||
<Card.Title>
|
<p>{status.name}</p>
|
||||||
<LevelIndicator level={status.level} /> {status.name}
|
<p>
|
||||||
</Card.Title>
|
|
||||||
<Card.Text>
|
|
||||||
<MessageText message={status.message} />
|
<MessageText message={status.message} />
|
||||||
</Card.Text>
|
</p>
|
||||||
<Card.Text>
|
<p>
|
||||||
<ValuesTable values={status.values} />
|
<ValuesTable values={status.values} />
|
||||||
</Card.Text>
|
</p>
|
||||||
</Card.Body>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,7 +148,7 @@ function ChildStatuses(props: Any) {
|
||||||
return (
|
return (
|
||||||
<Collapse in={isOpen}>
|
<Collapse in={isOpen}>
|
||||||
<div>
|
<div>
|
||||||
<ListGroup className="ms-2 border-start">
|
<ListGroup>
|
||||||
{node.children.map((child) => (
|
{node.children.map((child) => (
|
||||||
<SingleNode key={child.id} node={child} />
|
<SingleNode key={child.id} node={child} />
|
||||||
))}
|
))}
|
||||||
|
|
@ -128,8 +170,7 @@ function SingleNode(props: Any) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListGroup.Item className="border-0 ps-3 py-1">
|
<ListGroup.Item>
|
||||||
<Card>
|
|
||||||
<Container>
|
<Container>
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs={1}>
|
<Col xs={1}>
|
||||||
|
|
@ -139,6 +180,9 @@ function SingleNode(props: Any) {
|
||||||
onClick={openFunction}
|
onClick={openFunction}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col xs={1}>
|
||||||
|
<LevelIndicator level={node.status.level} />
|
||||||
|
</Col>
|
||||||
<Col xs={"auto"}>
|
<Col xs={"auto"}>
|
||||||
<StatusContent status={node.status} />
|
<StatusContent status={node.status} />
|
||||||
</Col>
|
</Col>
|
||||||
|
|
@ -147,7 +191,6 @@ function SingleNode(props: Any) {
|
||||||
<ChildStatuses isOpen={isOpen} node={node} />
|
<ChildStatuses isOpen={isOpen} node={node} />
|
||||||
</Row>
|
</Row>
|
||||||
</Container>
|
</Container>
|
||||||
</Card>
|
|
||||||
</ListGroup.Item>
|
</ListGroup.Item>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
83
src/Home.tsx
83
src/Home.tsx
|
|
@ -12,17 +12,94 @@ import { AppNav, Footer } from "./components/AppNav.tsx";
|
||||||
import { Container, Row, Col, Image, Card } from "react-bootstrap";
|
import { Container, Row, Col, Image, Card } from "react-bootstrap";
|
||||||
import { atom, useAtomValue } from "jotai";
|
import { atom, useAtomValue } from "jotai";
|
||||||
import { atomWithQuery } from "jotai-tanstack-query";
|
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() {
|
export function Home() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<AppNav />
|
<AppNav />
|
||||||
<Container className="vert-padded">
|
<Container className="vert-padded">
|
||||||
<Card className="padded">
|
|
||||||
<Row>
|
<Row>
|
||||||
<p>Hello World!</p>
|
<Col>
|
||||||
|
<UptimeCard />
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<StatusCard />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row className="vert-padded">
|
||||||
|
<Col>
|
||||||
|
<DiagnosticsCard />
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Card>
|
|
||||||
</Container>
|
</Container>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</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="/">
|
<Nav.Link as={Link} to="/">
|
||||||
Home
|
Home
|
||||||
</Nav.Link>
|
</Nav.Link>
|
||||||
|
<Nav.Link as={Link} to="/video">
|
||||||
|
Video
|
||||||
|
</Nav.Link>
|
||||||
<Nav.Link as={Link} to="/version">
|
<Nav.Link as={Link} to="/version">
|
||||||
Version
|
Version
|
||||||
</Nav.Link>
|
</Nav.Link>
|
||||||
<Nav.Link as={Link} to="/autonomy">
|
|
||||||
Autonomy
|
|
||||||
</Nav.Link>
|
|
||||||
<Nav.Link as={Link} to="/diagnostics">
|
|
||||||
Diagnostics
|
|
||||||
</Nav.Link>
|
|
||||||
</Nav>
|
</Nav>
|
||||||
</Navbar.Collapse>
|
</Navbar.Collapse>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue