Display the list of pending devices in the UI
This commit is contained in:
@ -10,6 +10,7 @@ import { LoginRoute } from "./routes/LoginRoute";
|
||||
import { NotFoundRoute } from "./routes/NotFoundRoute";
|
||||
import { HomeRoute } from "./routes/HomeRoute";
|
||||
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
|
||||
import { PendingDevicesRoute } from "./routes/PendingDevicesRoute";
|
||||
|
||||
export function App() {
|
||||
if (!AuthApi.SignedIn && !ServerApi.Config.auth_disabled)
|
||||
@ -19,7 +20,7 @@ export function App() {
|
||||
createRoutesFromElements(
|
||||
<Route path="*" element={<BaseAuthenticatedPage />}>
|
||||
<Route path="" element={<HomeRoute />} />
|
||||
|
||||
<Route path="pending_devices" element={<PendingDevicesRoute />} />
|
||||
<Route path="*" element={<NotFoundRoute />} />
|
||||
</Route>
|
||||
)
|
||||
|
52
central_frontend/src/api/DeviceApi.ts
Normal file
52
central_frontend/src/api/DeviceApi.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { APIClient } from "./ApiClient";
|
||||
|
||||
export interface DeviceInfo {
|
||||
reference: string;
|
||||
version: string;
|
||||
max_relays: number;
|
||||
}
|
||||
|
||||
export interface DailyMinRuntime {
|
||||
min_runtime: number;
|
||||
reset_time: number;
|
||||
catch_up_hours: number[];
|
||||
}
|
||||
|
||||
export interface DeviceRelay {
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
priority: number;
|
||||
consumption: number;
|
||||
minimal_uptime: number;
|
||||
minimal_downtime: number;
|
||||
daily_runtime?: DailyMinRuntime;
|
||||
depends_on: DeviceRelay[];
|
||||
conflicts_with: DeviceRelay[];
|
||||
}
|
||||
|
||||
export interface Device {
|
||||
id: string;
|
||||
info: DeviceInfo;
|
||||
time_create: number;
|
||||
time_update: number;
|
||||
name: string;
|
||||
description: string;
|
||||
validated: boolean;
|
||||
enabled: boolean;
|
||||
relays: DeviceRelay[];
|
||||
}
|
||||
|
||||
export class DeviceApi {
|
||||
/**
|
||||
* Get the list of pending devices
|
||||
*/
|
||||
static async PendingList(): Promise<Device[]> {
|
||||
return (
|
||||
await APIClient.exec({
|
||||
uri: "/devices/list_pending",
|
||||
method: "GET",
|
||||
})
|
||||
).data;
|
||||
}
|
||||
}
|
79
central_frontend/src/routes/PendingDevicesRoute.tsx
Normal file
79
central_frontend/src/routes/PendingDevicesRoute.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import React from "react";
|
||||
import { AsyncWidget } from "../widgets/AsyncWidget";
|
||||
import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer";
|
||||
import { Device, DeviceApi } from "../api/DeviceApi";
|
||||
import {
|
||||
TableContainer,
|
||||
Paper,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
} from "@mui/material";
|
||||
import { TimeWidget } from "../widgets/TimeWidget";
|
||||
|
||||
export function PendingDevicesRoute(): React.ReactElement {
|
||||
const loadKey = React.useRef(1);
|
||||
|
||||
const [pending, setPending] = React.useState<Device[] | undefined>();
|
||||
|
||||
const load = async () => {
|
||||
setPending(await DeviceApi.PendingList());
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
loadKey.current += 1;
|
||||
setPending(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<SolarEnergyRouteContainer label="Pending devices">
|
||||
<AsyncWidget
|
||||
loadKey={loadKey.current}
|
||||
ready={!!pending}
|
||||
errMsg="Failed to load the list of pending devices!"
|
||||
load={load}
|
||||
build={() => (
|
||||
<PendingDevicesList onReload={reload} pending={pending!} />
|
||||
)}
|
||||
/>
|
||||
</SolarEnergyRouteContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function PendingDevicesList(p: {
|
||||
pending: Device[];
|
||||
onReload: () => void;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>Model</TableCell>
|
||||
<TableCell>Version</TableCell>
|
||||
<TableCell>Maximum number of relays</TableCell>
|
||||
<TableCell>Created</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{p.pending.map((dev) => (
|
||||
<TableRow key={dev.id}>
|
||||
<TableCell component="th" scope="row">
|
||||
{dev.id}
|
||||
</TableCell>
|
||||
<TableCell>{dev.info.reference}</TableCell>
|
||||
<TableCell>{dev.info.version}</TableCell>
|
||||
<TableCell>{dev.info.max_relays}</TableCell>
|
||||
<TableCell>
|
||||
<TimeWidget time={dev.time_create} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
6
central_frontend/src/utils/DateUtils.ts
Normal file
6
central_frontend/src/utils/DateUtils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Get current UNIX time, in seconds
|
||||
*/
|
||||
export function time(): number {
|
||||
return Math.floor(new Date().getTime() / 1000);
|
||||
}
|
@ -1,14 +1,4 @@
|
||||
import {
|
||||
mdiAccountMultiple,
|
||||
mdiAccountMusic,
|
||||
mdiAlbum,
|
||||
mdiApi,
|
||||
mdiChartLine,
|
||||
mdiCog,
|
||||
mdiHome,
|
||||
mdiInbox,
|
||||
mdiMusic,
|
||||
} from "@mdi/js";
|
||||
import { mdiHome, mdiNewBox } from "@mdi/js";
|
||||
import Icon from "@mdi/react";
|
||||
import {
|
||||
List,
|
||||
@ -16,14 +6,11 @@ import {
|
||||
ListItemIcon,
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
ListSubheader,
|
||||
} from "@mui/material";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useAuthInfo } from "./BaseAuthenticatedPage";
|
||||
import { RouterLink } from "./RouterLink";
|
||||
|
||||
export function SolarEnergyNavList(): React.ReactElement {
|
||||
const user = useAuthInfo().info;
|
||||
return (
|
||||
<List
|
||||
dense
|
||||
@ -34,6 +21,11 @@ export function SolarEnergyNavList(): React.ReactElement {
|
||||
}}
|
||||
>
|
||||
<NavLink label="Home" uri="/" icon={<Icon path={mdiHome} size={1} />} />
|
||||
<NavLink
|
||||
label="Pending devices"
|
||||
uri="/pending_devices"
|
||||
icon={<Icon path={mdiNewBox} size={1} />}
|
||||
/>
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
27
central_frontend/src/widgets/SolarEnergyRouteContainer.tsx
Normal file
27
central_frontend/src/widgets/SolarEnergyRouteContainer.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { Typography } from "@mui/material";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
|
||||
export function SolarEnergyRouteContainer(
|
||||
p: {
|
||||
label: string;
|
||||
actions?: React.ReactElement;
|
||||
} & PropsWithChildren
|
||||
): React.ReactElement {
|
||||
return (
|
||||
<div style={{ margin: "50px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: "20px",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4">{p.label}</Typography>
|
||||
{p.actions ?? <></>}
|
||||
</div>
|
||||
|
||||
{p.children}
|
||||
</div>
|
||||
);
|
||||
}
|
65
central_frontend/src/widgets/TimeWidget.tsx
Normal file
65
central_frontend/src/widgets/TimeWidget.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { Tooltip } from "@mui/material";
|
||||
import date from "date-and-time";
|
||||
import { time } from "../utils/DateUtils";
|
||||
|
||||
export function formatDate(time: number): string {
|
||||
const t = new Date();
|
||||
t.setTime(1000 * time);
|
||||
return date.format(t, "DD/MM/YYYY HH:mm:ss");
|
||||
}
|
||||
|
||||
export function timeDiff(a: number, b: number): string {
|
||||
let diff = b - a;
|
||||
|
||||
if (diff === 0) return "now";
|
||||
if (diff === 1) return "1 second";
|
||||
|
||||
if (diff < 60) {
|
||||
return `${diff} seconds`;
|
||||
}
|
||||
|
||||
diff = Math.floor(diff / 60);
|
||||
|
||||
if (diff === 1) return "1 minute";
|
||||
if (diff < 24) {
|
||||
return `${diff} minutes`;
|
||||
}
|
||||
|
||||
diff = Math.floor(diff / 60);
|
||||
|
||||
if (diff === 1) return "1 hour";
|
||||
if (diff < 24) {
|
||||
return `${diff} hours`;
|
||||
}
|
||||
|
||||
const diffDays = Math.floor(diff / 24);
|
||||
|
||||
if (diffDays === 1) return "1 day";
|
||||
if (diffDays < 31) {
|
||||
return `${diffDays} days`;
|
||||
}
|
||||
|
||||
diff = Math.floor(diffDays / 31);
|
||||
|
||||
if (diff < 12) {
|
||||
return `${diff} month`;
|
||||
}
|
||||
|
||||
const diffYears = Math.floor(diffDays / 365);
|
||||
|
||||
if (diffYears === 1) return "1 year";
|
||||
return `${diffYears} years`;
|
||||
}
|
||||
|
||||
export function timeDiffFromNow(t: number): string {
|
||||
return timeDiff(t, time());
|
||||
}
|
||||
|
||||
export function TimeWidget(p: { time?: number }): React.ReactElement {
|
||||
if (!p.time) return <></>;
|
||||
return (
|
||||
<Tooltip title={formatDate(p.time)} arrow>
|
||||
<span>{timeDiffFromNow(p.time)}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user