Display the list of pending devices in the UI
This commit is contained in:
parent
01ffe085d7
commit
716af6219a
@ -81,7 +81,7 @@ impl DevicesList {
|
|||||||
let dev = self
|
let dev = self
|
||||||
.0
|
.0
|
||||||
.get(id)
|
.get(id)
|
||||||
.ok_or_else(|| DevicesListError::PersistFailedDeviceNotFound)?;
|
.ok_or(DevicesListError::PersistFailedDeviceNotFound)?;
|
||||||
|
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
AppConfig::get().device_config_path(id),
|
AppConfig::get().device_config_path(id),
|
||||||
@ -90,4 +90,9 @@ impl DevicesList {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a copy of the full list of devices
|
||||||
|
pub fn full_list(&self) -> Vec<Device> {
|
||||||
|
self.0.clone().into_values().collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::constants;
|
use crate::constants;
|
||||||
use crate::devices::device::{DeviceId, DeviceInfo};
|
use crate::devices::device::{Device, DeviceId, DeviceInfo};
|
||||||
use crate::devices::devices_list::DevicesList;
|
use crate::devices::devices_list::DevicesList;
|
||||||
use crate::energy::consumption;
|
use crate::energy::consumption;
|
||||||
use crate::energy::consumption::EnergyConsumption;
|
use crate::energy::consumption::EnergyConsumption;
|
||||||
@ -93,3 +93,16 @@ impl Handler<EnrollDevice> for EnergyActor {
|
|||||||
self.devices.enroll(&msg.0, &msg.1, &msg.2)
|
self.devices.enroll(&msg.0, &msg.1, &msg.2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the list of devices
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "Vec<Device>")]
|
||||||
|
pub struct GetDeviceLists;
|
||||||
|
|
||||||
|
impl Handler<GetDeviceLists> for EnergyActor {
|
||||||
|
type Result = Vec<Device>;
|
||||||
|
|
||||||
|
fn handle(&mut self, _msg: GetDeviceLists, _ctx: &mut Context<Self>) -> Self::Result {
|
||||||
|
self.devices.full_list()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -131,6 +131,14 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()>
|
|||||||
"/web_api/energy/cached_consumption",
|
"/web_api/energy/cached_consumption",
|
||||||
web::get().to(energy_controller::cached_consumption),
|
web::get().to(energy_controller::cached_consumption),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/web_api/devices/list_pending",
|
||||||
|
web::get().to(devices_controller::list_pending),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/web_api/devices/list_validated",
|
||||||
|
web::get().to(devices_controller::list_validated),
|
||||||
|
)
|
||||||
// Devices API
|
// Devices API
|
||||||
.route(
|
.route(
|
||||||
"/devices_api/utils/time",
|
"/devices_api/utils/time",
|
||||||
|
28
central_backend/src/server/web_api/devices_controller.rs
Normal file
28
central_backend/src/server/web_api/devices_controller.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
use crate::energy::energy_actor;
|
||||||
|
use crate::server::custom_error::HttpResult;
|
||||||
|
use crate::server::WebEnergyActor;
|
||||||
|
use actix_web::HttpResponse;
|
||||||
|
|
||||||
|
/// Get the list of pending (not accepted yet) devices
|
||||||
|
pub async fn list_pending(actor: WebEnergyActor) -> HttpResult {
|
||||||
|
let list = actor
|
||||||
|
.send(energy_actor::GetDeviceLists)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|d| !d.validated)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(list))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the list of validated (not accepted yet) devices
|
||||||
|
pub async fn list_validated(actor: WebEnergyActor) -> HttpResult {
|
||||||
|
let list = actor
|
||||||
|
.send(energy_actor::GetDeviceLists)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|d| d.validated)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(list))
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
pub mod auth_controller;
|
pub mod auth_controller;
|
||||||
|
pub mod devices_controller;
|
||||||
pub mod energy_controller;
|
pub mod energy_controller;
|
||||||
pub mod server_controller;
|
pub mod server_controller;
|
||||||
|
6
central_frontend/package-lock.json
generated
6
central_frontend/package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
"@mdi/react": "^1.6.1",
|
"@mdi/react": "^1.6.1",
|
||||||
"@mui/icons-material": "^5.15.21",
|
"@mui/icons-material": "^5.15.21",
|
||||||
"@mui/material": "^5.15.21",
|
"@mui/material": "^5.15.21",
|
||||||
|
"date-and-time": "^3.3.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.24.0"
|
"react-router-dom": "^6.24.0"
|
||||||
@ -2205,6 +2206,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/date-and-time": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-UguWfh9LkUecVrGSE0B7SpAnGRMPATmpwSoSij24/lDnwET3A641abfDBD/TdL0T+E04f8NWlbMkD9BscVvIZg=="
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.5",
|
"version": "4.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"@mdi/react": "^1.6.1",
|
"@mdi/react": "^1.6.1",
|
||||||
"@mui/icons-material": "^5.15.21",
|
"@mui/icons-material": "^5.15.21",
|
||||||
"@mui/material": "^5.15.21",
|
"@mui/material": "^5.15.21",
|
||||||
|
"date-and-time": "^3.3.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.24.0"
|
"react-router-dom": "^6.24.0"
|
||||||
|
@ -10,6 +10,7 @@ import { LoginRoute } from "./routes/LoginRoute";
|
|||||||
import { NotFoundRoute } from "./routes/NotFoundRoute";
|
import { NotFoundRoute } from "./routes/NotFoundRoute";
|
||||||
import { HomeRoute } from "./routes/HomeRoute";
|
import { HomeRoute } from "./routes/HomeRoute";
|
||||||
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
|
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
|
||||||
|
import { PendingDevicesRoute } from "./routes/PendingDevicesRoute";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
if (!AuthApi.SignedIn && !ServerApi.Config.auth_disabled)
|
if (!AuthApi.SignedIn && !ServerApi.Config.auth_disabled)
|
||||||
@ -19,7 +20,7 @@ export function App() {
|
|||||||
createRoutesFromElements(
|
createRoutesFromElements(
|
||||||
<Route path="*" element={<BaseAuthenticatedPage />}>
|
<Route path="*" element={<BaseAuthenticatedPage />}>
|
||||||
<Route path="" element={<HomeRoute />} />
|
<Route path="" element={<HomeRoute />} />
|
||||||
|
<Route path="pending_devices" element={<PendingDevicesRoute />} />
|
||||||
<Route path="*" element={<NotFoundRoute />} />
|
<Route path="*" element={<NotFoundRoute />} />
|
||||||
</Route>
|
</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 {
|
import { mdiHome, mdiNewBox } from "@mdi/js";
|
||||||
mdiAccountMultiple,
|
|
||||||
mdiAccountMusic,
|
|
||||||
mdiAlbum,
|
|
||||||
mdiApi,
|
|
||||||
mdiChartLine,
|
|
||||||
mdiCog,
|
|
||||||
mdiHome,
|
|
||||||
mdiInbox,
|
|
||||||
mdiMusic,
|
|
||||||
} from "@mdi/js";
|
|
||||||
import Icon from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import {
|
import {
|
||||||
List,
|
List,
|
||||||
@ -16,14 +6,11 @@ import {
|
|||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
ListItemSecondaryAction,
|
ListItemSecondaryAction,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
ListSubheader,
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { useAuthInfo } from "./BaseAuthenticatedPage";
|
|
||||||
import { RouterLink } from "./RouterLink";
|
import { RouterLink } from "./RouterLink";
|
||||||
|
|
||||||
export function SolarEnergyNavList(): React.ReactElement {
|
export function SolarEnergyNavList(): React.ReactElement {
|
||||||
const user = useAuthInfo().info;
|
|
||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
dense
|
dense
|
||||||
@ -34,6 +21,11 @@ export function SolarEnergyNavList(): React.ReactElement {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<NavLink label="Home" uri="/" icon={<Icon path={mdiHome} size={1} />} />
|
<NavLink label="Home" uri="/" icon={<Icon path={mdiHome} size={1} />} />
|
||||||
|
<NavLink
|
||||||
|
label="Pending devices"
|
||||||
|
uri="/pending_devices"
|
||||||
|
icon={<Icon path={mdiNewBox} size={1} />}
|
||||||
|
/>
|
||||||
</List>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user