Can update device general information
This commit is contained in:
		@@ -12,7 +12,7 @@ import { HomeRoute } from "./routes/HomeRoute";
 | 
			
		||||
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
 | 
			
		||||
import { PendingDevicesRoute } from "./routes/PendingDevicesRoute";
 | 
			
		||||
import { DevicesRoute } from "./routes/DevicesRoute";
 | 
			
		||||
import { DeviceRoute } from "./routes/DeviceRoute";
 | 
			
		||||
import { DeviceRoute } from "./routes/DeviceRoute/DeviceRoute";
 | 
			
		||||
 | 
			
		||||
export function App() {
 | 
			
		||||
  if (!AuthApi.SignedIn && !ServerApi.Config.auth_disabled)
 | 
			
		||||
 
 | 
			
		||||
@@ -37,6 +37,12 @@ export interface Device {
 | 
			
		||||
  relays: DeviceRelay[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface UpdatedInfo {
 | 
			
		||||
  name: string;
 | 
			
		||||
  description: string;
 | 
			
		||||
  enabled: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function DeviceURL(d: Device): string {
 | 
			
		||||
  return `/dev/${encodeURIComponent(d.id)}`;
 | 
			
		||||
}
 | 
			
		||||
@@ -88,6 +94,17 @@ export class DeviceApi {
 | 
			
		||||
    ).data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Update a device general information
 | 
			
		||||
   */
 | 
			
		||||
  static async Update(d: Device, info: UpdatedInfo): Promise<void> {
 | 
			
		||||
    await APIClient.exec({
 | 
			
		||||
      uri: `/device/${encodeURIComponent(d.id)}`,
 | 
			
		||||
      method: "PATCH",
 | 
			
		||||
      jsonData: info,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Delete a device
 | 
			
		||||
   */
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,17 @@ import { APIClient } from "./ApiClient";
 | 
			
		||||
 | 
			
		||||
export interface ServerConfig {
 | 
			
		||||
  auth_disabled: boolean;
 | 
			
		||||
  constraints: ServerConstraint;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ServerConstraint {
 | 
			
		||||
  dev_name_len: LenConstraint;
 | 
			
		||||
  dev_description_len: LenConstraint;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface LenConstraint {
 | 
			
		||||
  min: number;
 | 
			
		||||
  max: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let config: ServerConfig | null = null;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										87
									
								
								central_frontend/src/dialogs/EditDeviceMetadataDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								central_frontend/src/dialogs/EditDeviceMetadataDialog.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,87 @@
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogActions,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
} from "@mui/material";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { Device, DeviceApi } from "../api/DeviceApi";
 | 
			
		||||
import { ServerApi } from "../api/ServerApi";
 | 
			
		||||
import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
 | 
			
		||||
import { useLoadingMessage } from "../hooks/context_providers/LoadingMessageProvider";
 | 
			
		||||
import { useSnackbar } from "../hooks/context_providers/SnackbarProvider";
 | 
			
		||||
import { lenValid } from "../utils/StringsUtils";
 | 
			
		||||
import { CheckboxInput } from "../widgets/forms/CheckboxInput";
 | 
			
		||||
import { TextInput } from "../widgets/forms/TextInput";
 | 
			
		||||
 | 
			
		||||
export function EditDeviceMetadataDialog(p: {
 | 
			
		||||
  onClose: () => void;
 | 
			
		||||
  device: Device;
 | 
			
		||||
  onUpdated: () => void;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  const loadingMessage = useLoadingMessage();
 | 
			
		||||
  const alert = useAlert();
 | 
			
		||||
  const snackbar = useSnackbar();
 | 
			
		||||
 | 
			
		||||
  const [name, setName] = React.useState(p.device.name);
 | 
			
		||||
  const [description, setDescription] = React.useState(p.device.description);
 | 
			
		||||
  const [enabled, setEnabled] = React.useState(p.device.enabled);
 | 
			
		||||
 | 
			
		||||
  const onSubmit = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      loadingMessage.show("Updating device information");
 | 
			
		||||
      await DeviceApi.Update(p.device, {
 | 
			
		||||
        name,
 | 
			
		||||
        description,
 | 
			
		||||
        enabled,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      snackbar("The device information have been successfully updated!");
 | 
			
		||||
      p.onUpdated();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.error("Failed to update device general information!" + e);
 | 
			
		||||
      alert(`Failed to update device general information! ${e}`);
 | 
			
		||||
    } finally {
 | 
			
		||||
      loadingMessage.hide();
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const canSubmit =
 | 
			
		||||
    lenValid(name, ServerApi.Config.constraints.dev_name_len) &&
 | 
			
		||||
    lenValid(description, ServerApi.Config.constraints.dev_description_len);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog open>
 | 
			
		||||
      <DialogTitle>Edit device general information</DialogTitle>
 | 
			
		||||
      <DialogContent>
 | 
			
		||||
        <TextInput
 | 
			
		||||
          editable
 | 
			
		||||
          label="Device name"
 | 
			
		||||
          value={name}
 | 
			
		||||
          onValueChange={(s) => setName(s ?? "")}
 | 
			
		||||
          size={ServerApi.Config.constraints.dev_name_len}
 | 
			
		||||
        />
 | 
			
		||||
        <TextInput
 | 
			
		||||
          editable
 | 
			
		||||
          label="Device description"
 | 
			
		||||
          value={description}
 | 
			
		||||
          onValueChange={(s) => setDescription(s ?? "")}
 | 
			
		||||
          size={ServerApi.Config.constraints.dev_description_len}
 | 
			
		||||
        />
 | 
			
		||||
        <CheckboxInput
 | 
			
		||||
          editable
 | 
			
		||||
          label="Enable device"
 | 
			
		||||
          checked={enabled}
 | 
			
		||||
          onValueChange={setEnabled}
 | 
			
		||||
        />
 | 
			
		||||
      </DialogContent>
 | 
			
		||||
      <DialogActions>
 | 
			
		||||
        <Button onClick={p.onClose}>Cancel</Button>
 | 
			
		||||
        <Button onClick={onSubmit} autoFocus disabled={!canSubmit}>
 | 
			
		||||
          Submit
 | 
			
		||||
        </Button>
 | 
			
		||||
      </DialogActions>
 | 
			
		||||
    </Dialog>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -1,143 +0,0 @@
 | 
			
		||||
import {
 | 
			
		||||
  IconButton,
 | 
			
		||||
  Paper,
 | 
			
		||||
  Table,
 | 
			
		||||
  TableBody,
 | 
			
		||||
  TableCell,
 | 
			
		||||
  TableContainer,
 | 
			
		||||
  TableRow,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  Typography,
 | 
			
		||||
} from "@mui/material";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import DeleteIcon from "@mui/icons-material/Delete";
 | 
			
		||||
import { useParams } from "react-router-dom";
 | 
			
		||||
import { Device, DeviceApi } from "../api/DeviceApi";
 | 
			
		||||
import { AsyncWidget } from "../widgets/AsyncWidget";
 | 
			
		||||
import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer";
 | 
			
		||||
import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
 | 
			
		||||
import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider";
 | 
			
		||||
import { useLoadingMessage } from "../hooks/context_providers/LoadingMessageProvider";
 | 
			
		||||
import { useSnackbar } from "../hooks/context_providers/SnackbarProvider";
 | 
			
		||||
 | 
			
		||||
export function DeviceRoute(): React.ReactElement {
 | 
			
		||||
  const { id } = useParams();
 | 
			
		||||
  const [device, setDevice] = React.useState<Device | undefined>();
 | 
			
		||||
 | 
			
		||||
  const loadKey = React.useRef(1);
 | 
			
		||||
 | 
			
		||||
  const load = async () => {
 | 
			
		||||
    setDevice(await DeviceApi.GetSingle(id!));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const reload = () => {
 | 
			
		||||
    loadKey.current += 1;
 | 
			
		||||
    setDevice(undefined);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <AsyncWidget
 | 
			
		||||
      loadKey={loadKey.current}
 | 
			
		||||
      errMsg="Failed to load device information"
 | 
			
		||||
      load={load}
 | 
			
		||||
      ready={!!device}
 | 
			
		||||
      build={() => <DeviceRouteInner device={device!} onReload={reload} />}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function DeviceRouteInner(p: {
 | 
			
		||||
  device: Device;
 | 
			
		||||
  onReload: () => void;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  const alert = useAlert();
 | 
			
		||||
  const confirm = useConfirm();
 | 
			
		||||
  const snackbar = useSnackbar();
 | 
			
		||||
  const loadingMessage = useLoadingMessage();
 | 
			
		||||
 | 
			
		||||
  const deleteDevice = async (d: Device) => {
 | 
			
		||||
    try {
 | 
			
		||||
      if (
 | 
			
		||||
        !(await confirm(
 | 
			
		||||
          `Do you really want to delete the device ${d.id}? The operation cannot be reverted!`
 | 
			
		||||
        ))
 | 
			
		||||
      )
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
      loadingMessage.show("Deleting device...");
 | 
			
		||||
      await DeviceApi.Delete(d);
 | 
			
		||||
 | 
			
		||||
      snackbar("The device has been successfully deleted!");
 | 
			
		||||
      p.onReload();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.error(`Failed to delete device! ${e})`);
 | 
			
		||||
      alert("Failed to delete device!");
 | 
			
		||||
    } finally {
 | 
			
		||||
      loadingMessage.hide();
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <SolarEnergyRouteContainer
 | 
			
		||||
      label={`Device ${p.device.name}`}
 | 
			
		||||
      actions={
 | 
			
		||||
        <Tooltip title="Delete device">
 | 
			
		||||
          <IconButton onClick={() => deleteDevice(p.device)}>
 | 
			
		||||
            <DeleteIcon />
 | 
			
		||||
          </IconButton>
 | 
			
		||||
        </Tooltip>
 | 
			
		||||
      }
 | 
			
		||||
    >
 | 
			
		||||
      <GeneralDeviceInfo {...p} />
 | 
			
		||||
    </SolarEnergyRouteContainer>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function GeneralDeviceInfo(p: { device: Device }): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <TableContainer component={Paper}>
 | 
			
		||||
      <Typography variant="h6" style={{ padding: "6px" }}>
 | 
			
		||||
        General device information
 | 
			
		||||
      </Typography>
 | 
			
		||||
      <Table size="small">
 | 
			
		||||
        <TableBody>
 | 
			
		||||
          <DeviceInfoProperty label="ID" value={p.device.id} />
 | 
			
		||||
          <DeviceInfoProperty
 | 
			
		||||
            label="Reference"
 | 
			
		||||
            value={p.device.info.reference}
 | 
			
		||||
          />
 | 
			
		||||
          <DeviceInfoProperty label="Version" value={p.device.info.version} />
 | 
			
		||||
          <DeviceInfoProperty label="Name" value={p.device.name} />
 | 
			
		||||
          <DeviceInfoProperty
 | 
			
		||||
            label="Description"
 | 
			
		||||
            value={p.device.description}
 | 
			
		||||
          />
 | 
			
		||||
          <DeviceInfoProperty
 | 
			
		||||
            label="Enabled"
 | 
			
		||||
            value={p.device.enabled ? "YES" : "NO"}
 | 
			
		||||
          />
 | 
			
		||||
          <DeviceInfoProperty
 | 
			
		||||
            label="Maximum number of relays"
 | 
			
		||||
            value={p.device.info.max_relays.toString()}
 | 
			
		||||
          />
 | 
			
		||||
          <DeviceInfoProperty
 | 
			
		||||
            label="Number of configured relays"
 | 
			
		||||
            value={p.device.relays.length.toString()}
 | 
			
		||||
          />
 | 
			
		||||
        </TableBody>
 | 
			
		||||
      </Table>
 | 
			
		||||
    </TableContainer>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function DeviceInfoProperty(p: {
 | 
			
		||||
  icon?: React.ReactElement;
 | 
			
		||||
  label: string;
 | 
			
		||||
  value: string;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <TableRow hover sx={{ "&:last-child td, &:last-child th": { border: 0 } }}>
 | 
			
		||||
      <TableCell>{p.label}</TableCell>
 | 
			
		||||
      <TableCell>{p.value}</TableCell>
 | 
			
		||||
    </TableRow>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										85
									
								
								central_frontend/src/routes/DeviceRoute/DeviceRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								central_frontend/src/routes/DeviceRoute/DeviceRoute.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
			
		||||
import DeleteIcon from "@mui/icons-material/Delete";
 | 
			
		||||
import { IconButton, Tooltip } from "@mui/material";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { useNavigate, useParams } from "react-router-dom";
 | 
			
		||||
import { Device, DeviceApi } from "../../api/DeviceApi";
 | 
			
		||||
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
 | 
			
		||||
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider";
 | 
			
		||||
import { useLoadingMessage } from "../../hooks/context_providers/LoadingMessageProvider";
 | 
			
		||||
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider";
 | 
			
		||||
import { AsyncWidget } from "../../widgets/AsyncWidget";
 | 
			
		||||
import { SolarEnergyRouteContainer } from "../../widgets/SolarEnergyRouteContainer";
 | 
			
		||||
import { GeneralDeviceInfo } from "./GeneralDeviceInfo";
 | 
			
		||||
 | 
			
		||||
export function DeviceRoute(): React.ReactElement {
 | 
			
		||||
  const { id } = useParams();
 | 
			
		||||
  const [device, setDevice] = React.useState<Device | undefined>();
 | 
			
		||||
 | 
			
		||||
  const loadKey = React.useRef(1);
 | 
			
		||||
 | 
			
		||||
  const load = async () => {
 | 
			
		||||
    setDevice(await DeviceApi.GetSingle(id!));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const reload = () => {
 | 
			
		||||
    loadKey.current += 1;
 | 
			
		||||
    setDevice(undefined);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <AsyncWidget
 | 
			
		||||
      loadKey={loadKey.current}
 | 
			
		||||
      errMsg="Failed to load device information"
 | 
			
		||||
      load={load}
 | 
			
		||||
      ready={!!device}
 | 
			
		||||
      build={() => <DeviceRouteInner device={device!} onReload={reload} />}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function DeviceRouteInner(p: {
 | 
			
		||||
  device: Device;
 | 
			
		||||
  onReload: () => void;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  const alert = useAlert();
 | 
			
		||||
  const confirm = useConfirm();
 | 
			
		||||
  const snackbar = useSnackbar();
 | 
			
		||||
  const loadingMessage = useLoadingMessage();
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
  const deleteDevice = async (d: Device) => {
 | 
			
		||||
    try {
 | 
			
		||||
      if (
 | 
			
		||||
        !(await confirm(
 | 
			
		||||
          `Do you really want to delete the device ${d.id}? The operation cannot be reverted!`
 | 
			
		||||
        ))
 | 
			
		||||
      )
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
      loadingMessage.show("Deleting device...");
 | 
			
		||||
      await DeviceApi.Delete(d);
 | 
			
		||||
 | 
			
		||||
      snackbar("The device has been successfully deleted!");
 | 
			
		||||
      navigate("/devices");
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.error(`Failed to delete device! ${e})`);
 | 
			
		||||
      alert("Failed to delete device!");
 | 
			
		||||
    } finally {
 | 
			
		||||
      loadingMessage.hide();
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <SolarEnergyRouteContainer
 | 
			
		||||
      label={`Device ${p.device.name}`}
 | 
			
		||||
      actions={
 | 
			
		||||
        <Tooltip title="Delete device">
 | 
			
		||||
          <IconButton onClick={() => deleteDevice(p.device)}>
 | 
			
		||||
            <DeleteIcon />
 | 
			
		||||
          </IconButton>
 | 
			
		||||
        </Tooltip>
 | 
			
		||||
      }
 | 
			
		||||
    >
 | 
			
		||||
      <GeneralDeviceInfo {...p} />
 | 
			
		||||
    </SolarEnergyRouteContainer>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								central_frontend/src/routes/DeviceRoute/DeviceRouteCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								central_frontend/src/routes/DeviceRoute/DeviceRouteCard.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
import { Card, Paper, Typography } from "@mui/material";
 | 
			
		||||
 | 
			
		||||
export function DeviceRouteCard(
 | 
			
		||||
  p: React.PropsWithChildren<{ title: string; actions?: React.ReactElement }>
 | 
			
		||||
): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <Card component={Paper}>
 | 
			
		||||
      <div
 | 
			
		||||
        style={{
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          flexDirection: "row",
 | 
			
		||||
          justifyContent: "space-between",
 | 
			
		||||
          alignItems: "center",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Typography variant="h6" style={{ padding: "6px" }}>
 | 
			
		||||
          {p.title}
 | 
			
		||||
        </Typography>
 | 
			
		||||
        {p.actions}
 | 
			
		||||
      </div>
 | 
			
		||||
      {p.children}
 | 
			
		||||
    </Card>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,92 @@
 | 
			
		||||
import EditIcon from "@mui/icons-material/Edit";
 | 
			
		||||
import {
 | 
			
		||||
  IconButton,
 | 
			
		||||
  Table,
 | 
			
		||||
  TableBody,
 | 
			
		||||
  TableCell,
 | 
			
		||||
  TableRow,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
} from "@mui/material";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { Device } from "../../api/DeviceApi";
 | 
			
		||||
import { EditDeviceMetadataDialog } from "../../dialogs/EditDeviceMetadataDialog";
 | 
			
		||||
import { formatDate } from "../../widgets/TimeWidget";
 | 
			
		||||
import { DeviceRouteCard } from "./DeviceRouteCard";
 | 
			
		||||
 | 
			
		||||
export function GeneralDeviceInfo(p: {
 | 
			
		||||
  device: Device;
 | 
			
		||||
  onReload: () => void;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  const [dialogOpen, setDialogOpen] = React.useState(false);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {dialogOpen && (
 | 
			
		||||
        <EditDeviceMetadataDialog
 | 
			
		||||
          device={p.device}
 | 
			
		||||
          onClose={() => setDialogOpen(false)}
 | 
			
		||||
          onUpdated={p.onReload}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      <DeviceRouteCard
 | 
			
		||||
        title="General device information"
 | 
			
		||||
        actions={
 | 
			
		||||
          <Tooltip title="Edit device information">
 | 
			
		||||
            <IconButton onClick={() => setDialogOpen(true)}>
 | 
			
		||||
              <EditIcon />
 | 
			
		||||
            </IconButton>
 | 
			
		||||
          </Tooltip>
 | 
			
		||||
        }
 | 
			
		||||
      >
 | 
			
		||||
        <Table size="small">
 | 
			
		||||
          <TableBody>
 | 
			
		||||
            <DeviceInfoProperty label="ID" value={p.device.id} />
 | 
			
		||||
            <DeviceInfoProperty
 | 
			
		||||
              label="Reference"
 | 
			
		||||
              value={p.device.info.reference}
 | 
			
		||||
            />
 | 
			
		||||
            <DeviceInfoProperty label="Version" value={p.device.info.version} />
 | 
			
		||||
            <DeviceInfoProperty label="Name" value={p.device.name} />
 | 
			
		||||
            <DeviceInfoProperty
 | 
			
		||||
              label="Description"
 | 
			
		||||
              value={p.device.description}
 | 
			
		||||
            />
 | 
			
		||||
            <DeviceInfoProperty
 | 
			
		||||
              label="Created"
 | 
			
		||||
              value={formatDate(p.device.time_create)}
 | 
			
		||||
            />
 | 
			
		||||
            <DeviceInfoProperty
 | 
			
		||||
              label="Updated"
 | 
			
		||||
              value={formatDate(p.device.time_update)}
 | 
			
		||||
            />
 | 
			
		||||
            <DeviceInfoProperty
 | 
			
		||||
              label="Enabled"
 | 
			
		||||
              value={p.device.enabled ? "YES" : "NO"}
 | 
			
		||||
            />
 | 
			
		||||
            <DeviceInfoProperty
 | 
			
		||||
              label="Maximum number of relays"
 | 
			
		||||
              value={p.device.info.max_relays.toString()}
 | 
			
		||||
            />
 | 
			
		||||
            <DeviceInfoProperty
 | 
			
		||||
              label="Number of configured relays"
 | 
			
		||||
              value={p.device.relays.length.toString()}
 | 
			
		||||
            />
 | 
			
		||||
          </TableBody>
 | 
			
		||||
        </Table>
 | 
			
		||||
      </DeviceRouteCard>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function DeviceInfoProperty(p: {
 | 
			
		||||
  icon?: React.ReactElement;
 | 
			
		||||
  label: string;
 | 
			
		||||
  value: string;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <TableRow hover sx={{ "&:last-child td, &:last-child th": { border: 0 } }}>
 | 
			
		||||
      <TableCell>{p.label}</TableCell>
 | 
			
		||||
      <TableCell>{p.value}</TableCell>
 | 
			
		||||
    </TableRow>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								central_frontend/src/utils/StringsUtils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								central_frontend/src/utils/StringsUtils.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
import { LenConstraint } from "../api/ServerApi";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Check whether a string length is valid or not
 | 
			
		||||
 */
 | 
			
		||||
export function lenValid(s: string, c: LenConstraint): boolean {
 | 
			
		||||
  return s.length >= c.min && s.length <= c.max;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								central_frontend/src/widgets/forms/CheckboxInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								central_frontend/src/widgets/forms/CheckboxInput.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
import { Checkbox, FormControlLabel } from "@mui/material";
 | 
			
		||||
 | 
			
		||||
export function CheckboxInput(p: {
 | 
			
		||||
  editable: boolean;
 | 
			
		||||
  label: string;
 | 
			
		||||
  checked: boolean | undefined;
 | 
			
		||||
  onValueChange: (v: boolean) => void;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <FormControlLabel
 | 
			
		||||
      control={
 | 
			
		||||
        <Checkbox
 | 
			
		||||
          disabled={!p.editable}
 | 
			
		||||
          checked={p.checked}
 | 
			
		||||
          onChange={(e) => p.onValueChange(e.target.checked)}
 | 
			
		||||
        />
 | 
			
		||||
      }
 | 
			
		||||
      label={p.label}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										61
									
								
								central_frontend/src/widgets/forms/TextInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								central_frontend/src/widgets/forms/TextInput.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
import { TextField } from "@mui/material";
 | 
			
		||||
import { LenConstraint } from "../../api/ServerApi";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Text property edition
 | 
			
		||||
 */
 | 
			
		||||
export function TextInput(p: {
 | 
			
		||||
  label?: string;
 | 
			
		||||
  editable: boolean;
 | 
			
		||||
  value?: string;
 | 
			
		||||
  onValueChange?: (newVal: string | undefined) => void;
 | 
			
		||||
  size?: LenConstraint;
 | 
			
		||||
  checkValue?: (s: string) => boolean;
 | 
			
		||||
  multiline?: boolean;
 | 
			
		||||
  minRows?: number;
 | 
			
		||||
  maxRows?: number;
 | 
			
		||||
  type?: React.HTMLInputTypeAttribute;
 | 
			
		||||
  style?: React.CSSProperties;
 | 
			
		||||
  helperText?: string;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  if (!p.editable && (p.value ?? "") === "") return <></>;
 | 
			
		||||
 | 
			
		||||
  let valueError = undefined;
 | 
			
		||||
  if (p.value && p.value.length > 0) {
 | 
			
		||||
    if (p.size?.min && p.type !== "number" && p.value.length < p.size.min)
 | 
			
		||||
      valueError = "Value is too short!";
 | 
			
		||||
    if (p.checkValue && !p.checkValue(p.value)) valueError = "Invalid value!";
 | 
			
		||||
    if (
 | 
			
		||||
      p.type === "number" &&
 | 
			
		||||
      p.size &&
 | 
			
		||||
      (Number(p.value) > p.size.max || Number(p.value) < p.size.min)
 | 
			
		||||
    )
 | 
			
		||||
      valueError = "Invalide size range!";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <TextField
 | 
			
		||||
      label={p.label}
 | 
			
		||||
      value={p.value ?? ""}
 | 
			
		||||
      onChange={(e) =>
 | 
			
		||||
        p.onValueChange?.(
 | 
			
		||||
          e.target.value.length === 0 ? undefined : e.target.value
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      inputProps={{
 | 
			
		||||
        maxLength: p.size?.max,
 | 
			
		||||
      }}
 | 
			
		||||
      InputProps={{
 | 
			
		||||
        readOnly: !p.editable,
 | 
			
		||||
        type: p.type,
 | 
			
		||||
      }}
 | 
			
		||||
      variant={"standard"}
 | 
			
		||||
      style={p.style ?? { width: "100%", marginBottom: "15px" }}
 | 
			
		||||
      multiline={p.multiline}
 | 
			
		||||
      minRows={p.minRows}
 | 
			
		||||
      maxRows={p.maxRows}
 | 
			
		||||
      error={valueError !== undefined}
 | 
			
		||||
      helperText={valueError ?? p.helperText}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user