Display the list of pending devices in the UI
This commit is contained in:
		@@ -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>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user