Check for dependencies conflict before deleting a device

This commit is contained in:
Pierre HUBERT 2024-08-31 20:46:02 +02:00
parent bbe128e055
commit 78663854cc
4 changed files with 102 additions and 15 deletions

View File

@ -23,6 +23,8 @@ pub enum DevicesListError {
DeviceNotFound, DeviceNotFound,
#[error("Requested device is not validated")] #[error("Requested device is not validated")]
DeviceNotValidated, DeviceNotValidated,
#[error("Failed to delete device: {0}")]
DeleteDeviceFailed(&'static str),
#[error("Failed to update relay configuration: {0}")] #[error("Failed to update relay configuration: {0}")]
UpdateRelayFailed(&'static str), UpdateRelayFailed(&'static str),
#[error("Failed to delete relay: {0}")] #[error("Failed to delete relay: {0}")]
@ -178,6 +180,19 @@ impl DevicesList {
/// Delete a device /// Delete a device
pub fn delete(&mut self, id: &DeviceId) -> anyhow::Result<()> { pub fn delete(&mut self, id: &DeviceId) -> anyhow::Result<()> {
// Check for conflicts
let device = self
.get_single(id)
.ok_or(DevicesListError::DeleteDeviceFailed("Device not found!"))?;
for r in &device.relays {
if !self.relay_get_direct_dependencies(r.id).is_empty() {
return Err(DevicesListError::DeleteDeviceFailed(
"A relay of this device is required by another relay!",
)
.into());
}
}
let crt_path = AppConfig::get().device_cert_path(id); let crt_path = AppConfig::get().device_cert_path(id);
if crt_path.is_file() { if crt_path.is_file() {
let cert = self.get_cert(id)?; let cert = self.get_cert(id)?;

View File

@ -6,13 +6,14 @@ import {
} from "react-router-dom"; } from "react-router-dom";
import { AuthApi } from "./api/AuthApi"; import { AuthApi } from "./api/AuthApi";
import { ServerApi } from "./api/ServerApi"; import { ServerApi } from "./api/ServerApi";
import { DeviceRoute } from "./routes/DeviceRoute/DeviceRoute";
import { DevicesRoute } from "./routes/DevicesRoute";
import { HomeRoute } from "./routes/HomeRoute";
import { LoginRoute } from "./routes/LoginRoute"; import { LoginRoute } from "./routes/LoginRoute";
import { NotFoundRoute } from "./routes/NotFoundRoute"; import { NotFoundRoute } from "./routes/NotFoundRoute";
import { HomeRoute } from "./routes/HomeRoute";
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
import { PendingDevicesRoute } from "./routes/PendingDevicesRoute"; import { PendingDevicesRoute } from "./routes/PendingDevicesRoute";
import { DevicesRoute } from "./routes/DevicesRoute"; import { RelaysListRoute } from "./routes/RelaysListRoute";
import { DeviceRoute } from "./routes/DeviceRoute/DeviceRoute"; import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
export function App() { export function App() {
if (!AuthApi.SignedIn && !ServerApi.Config.auth_disabled) if (!AuthApi.SignedIn && !ServerApi.Config.auth_disabled)
@ -25,6 +26,7 @@ export function App() {
<Route path="pending_devices" element={<PendingDevicesRoute />} /> <Route path="pending_devices" element={<PendingDevicesRoute />} />
<Route path="devices" element={<DevicesRoute />} /> <Route path="devices" element={<DevicesRoute />} />
<Route path="dev/:id" element={<DeviceRoute />} /> <Route path="dev/:id" element={<DeviceRoute />} />
<Route path="relays" element={<RelaysListRoute />} />
<Route path="*" element={<NotFoundRoute />} /> <Route path="*" element={<NotFoundRoute />} />
</Route> </Route>
) )

View File

@ -0,0 +1,67 @@
import CheckIcon from "@mui/icons-material/Check";
import DeleteIcon from "@mui/icons-material/Delete";
import RefreshIcon from "@mui/icons-material/Refresh";
import {
IconButton,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Tooltip,
} from "@mui/material";
import React from "react";
import { Device, DeviceApi, DeviceRelay } 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 { TimeWidget } from "../widgets/TimeWidget";
import { RelayApi } from "../api/RelayApi";
export function RelaysListRoute(): React.ReactElement {
const loadKey = React.useRef(1);
const [list, setList] = React.useState<DeviceRelay[] | undefined>();
const load = async () => {
setList(await RelayApi.GetList());
};
const reload = () => {
loadKey.current += 1;
setList(undefined);
};
return (
<SolarEnergyRouteContainer
label="Relays list"
actions={
<Tooltip title="Refresh list">
<IconButton onClick={reload}>
<RefreshIcon />
</IconButton>
</Tooltip>
}
>
<AsyncWidget
loadKey={loadKey.current}
ready={!!list}
errMsg="Failed to load the list of relays!"
load={load}
build={() => <RelaysList onReload={reload} list={list!} />}
/>
</SolarEnergyRouteContainer>
);
}
function RelaysList(p: {
list: DeviceRelay[];
onReload: () => void;
}): React.ReactElement {
return <>todo</>;
}

View File

@ -1,10 +1,9 @@
import { mdiChip, mdiHome, mdiNewBox } from "@mdi/js"; import { mdiChip, mdiElectricSwitch, mdiHome, mdiNewBox } from "@mdi/js";
import Icon from "@mdi/react"; import Icon from "@mdi/react";
import { import {
List, List,
ListItemButton, ListItemButton,
ListItemIcon, ListItemIcon,
ListItemSecondaryAction,
ListItemText, ListItemText,
} from "@mui/material"; } from "@mui/material";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
@ -31,25 +30,29 @@ export function SolarEnergyNavList(): React.ReactElement {
uri="/pending_devices" uri="/pending_devices"
icon={<Icon path={mdiNewBox} size={1} />} icon={<Icon path={mdiNewBox} size={1} />}
/> />
<NavLink
label="Relays"
uri="/relays"
icon={<Icon path={mdiElectricSwitch} size={1} />}
/>
</List> </List>
); );
} }
function NavLink(p: { function NavLink(
icon: React.ReactElement; p: Readonly<{
uri: string; icon: React.ReactElement;
label: string; uri: string;
secondaryAction?: React.ReactElement; label: string;
}): React.ReactElement { secondaryAction?: React.ReactElement;
}>
): React.ReactElement {
const location = useLocation(); const location = useLocation();
return ( return (
<RouterLink to={p.uri}> <RouterLink to={p.uri}>
<ListItemButton selected={p.uri === location.pathname}> <ListItemButton selected={p.uri === location.pathname}>
<ListItemIcon>{p.icon}</ListItemIcon> <ListItemIcon>{p.icon}</ListItemIcon>
<ListItemText primary={p.label} /> <ListItemText primary={p.label} />
{p.secondaryAction && (
<ListItemSecondaryAction>{p.secondaryAction}</ListItemSecondaryAction>
)}
</ListItemButton> </ListItemButton>
</RouterLink> </RouterLink>
); );