Skip to content

Commit

Permalink
Add Delete device button into DeviceInfoCard
Browse files Browse the repository at this point in the history
Add possibility to delete device through Astarte Dashboard

closes #397

Signed-off-by: Armin Ahmetovic <armin.ahmetovic@secomind.com>
  • Loading branch information
arahmarchak committed Jan 11, 2024
1 parent c26dacd commit 6d588a4
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 20 deletions.
5 changes: 5 additions & 0 deletions src/DeviceStatusPage/DeviceInfoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,15 @@ interface DeviceInfoCardProps {
onInhibitCredentialsClick: () => void;
onEnableCredentialsClick: () => void;
onWipeCredentialsClick: () => void;
onDeleteDeviceClick: () => void;
}

const DeviceInfoCard = ({
device,
onInhibitCredentialsClick,
onEnableCredentialsClick,
onWipeCredentialsClick,
onDeleteDeviceClick,
}: DeviceInfoCardProps): React.ReactElement => (
<FullHeightCard xs={12} md={6} className="mb-4">
<Card.Header as="h5">Device Info</Card.Header>
Expand Down Expand Up @@ -96,6 +98,9 @@ const DeviceInfoCard = ({
<Button variant="danger" onClick={onWipeCredentialsClick}>
Wipe credential secret
</Button>
<Button variant="danger" className="ml-1" onClick={onDeleteDeviceClick}>
Delete device
</Button>
</div>
</Card.Body>
</FullHeightCard>
Expand Down
71 changes: 68 additions & 3 deletions src/DeviceStatusPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
*/

import React, { useCallback, useMemo, useState } from 'react';
import { Col, Container, Row, Spinner } from 'react-bootstrap';
import { Link, useParams } from 'react-router-dom';
import { Col, Container, Form, Row, Spinner } from 'react-bootstrap';
import { Link, useNavigate, useParams } from 'react-router-dom';

import type { AstarteDevice } from 'astarte-client';
import BackButton from '../ui/BackButton';
Expand Down Expand Up @@ -95,6 +95,11 @@ type ReregisterDeviceModalT = {
kind: 'reregister_device_modal';
};

type DeleteDeviceModalT = {
kind: 'delete_device_modal';
isDeletingDevice: boolean;
};

function isWipeCredentialsModal(modal: PageModal): modal is WipeCredentialsModalT {
return modal.kind === 'wipe_credentials_modal';
}
Expand Down Expand Up @@ -131,6 +136,10 @@ function isDeviceReregistrationModal(modal: PageModal): modal is ReregisterDevic
return modal.kind === 'reregister_device_modal';
}

function isDeleteDeviceModal(modal: PageModal): modal is DeleteDeviceModalT {
return modal.kind === 'delete_device_modal';
}

type PageModal =
| WipeCredentialsModalT
| AddToGroupModalT
Expand All @@ -140,15 +149,19 @@ type PageModal =
| NewAttributeModalT
| EditAttributeModalT
| DeleteAttributeModalT
| ReregisterDeviceModalT;
| ReregisterDeviceModalT
| DeleteDeviceModalT;

export default (): React.ReactElement => {
const { deviceId = '' } = useParams();
const astarte = useAstarte();
const navigate = useNavigate();
const deviceFetcher = useFetch(() => astarte.client.getDeviceInfo(deviceId));
const groupsFetcher = useFetch(() => astarte.client.getGroupList());
const [devicePageAlers, devicePageAlersController] = useAlerts();
const [activeModal, setActiveModal] = useState<PageModal | null>(null);
const [confirmString, setConfirmString] = useState('');
const canDelete = confirmString === deviceId;

const unjoinedGroups = useMemo(() => {
if (deviceFetcher.status === 'ok' && groupsFetcher.status === 'ok') {
Expand Down Expand Up @@ -190,6 +203,20 @@ export default (): React.ReactElement => {
});
}, [astarte.client, deviceId, dismissModal, devicePageAlersController]);

const handleDeleteDevice = useCallback(() => {
astarte.client
.deleteDevice(deviceId)
.then(() => {
setActiveModal({ kind: 'delete_device_modal', isDeletingDevice: true });
deviceFetcher.refresh();
navigate('/devices');
})
.catch(() => {
devicePageAlersController.showError(`Couldn't delete a device`);
dismissModal();
});
}, [astarte.client, deviceId, dismissModal, devicePageAlersController, deviceFetcher, navigate]);

const addDeviceToGroup = useCallback(
(groupName) => {
astarte.client
Expand Down Expand Up @@ -313,6 +340,12 @@ export default (): React.ReactElement => {
isWipingCredentials: false,
})
}
onDeleteDeviceClick={() =>
setActiveModal({
kind: 'delete_device_modal',
isDeletingDevice: false,
})
}
/>
<AliasesCard
device={device}
Expand Down Expand Up @@ -401,6 +434,38 @@ export default (): React.ReactElement => {
</p>
</ConfirmModal>
)}
{activeModal && isDeleteDeviceModal(activeModal) && (
<ConfirmModal
title="Delete device"
confirmLabel="Delete device"
confirmVariant="danger"
onCancel={dismissModal}
onConfirm={() => {
setActiveModal({ ...activeModal, isDeletingDevice: true });
handleDeleteDevice();
}}
isConfirming={activeModal.isDeletingDevice}
disabled={!canDelete}
>
<p>
You are going to delete&nbsp;
<b>{deviceId}</b>. This might cause data loss, deleted device cannot be restored.
</p>
<p>
Please type <b>{deviceId}</b> to proceed.
</p>
<Form.Group controlId="deleteDevice">
<Form.Control
type="text"
placeholder="Device Id"
value={confirmString}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setConfirmString(e.target.value)
}
/>
</Form.Group>
</ConfirmModal>
)}
{activeModal && isAddToGroupModal(activeModal) && (
<AddToGroupModal
onCancel={dismissModal}
Expand Down
43 changes: 26 additions & 17 deletions src/astarte-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,31 +200,36 @@ class AstarteClient {
auth: astarteAPIurl`${config.realmManagementApiUrl}v1/${'realm'}/config/auth`,
interfaces: astarteAPIurl`${config.realmManagementApiUrl}v1/${'realm'}/interfaces`,
interfaceMajors: astarteAPIurl`${config.realmManagementApiUrl}v1/${'realm'}/interfaces/${'interfaceName'}`,
interface: astarteAPIurl`${config.realmManagementApiUrl}v1/${'realm'}/interfaces/${'interfaceName'}/${'interfaceMajor'}`,
interfaceData: astarteAPIurl`${config.realmManagementApiUrl}v1/${'realm'}/interfaces/${'interfaceName'}/${'interfaceMajor'}`,
interface:
astarteAPIurl`${config.realmManagementApiUrl}v1/${'realm'}/interfaces/${'interfaceName'}/${'interfaceMajor'}`,
interfaceData:
astarteAPIurl`${config.realmManagementApiUrl}v1/${'realm'}/interfaces/${'interfaceName'}/${'interfaceMajor'}`,
trigger: astarteAPIurl`${config.realmManagementApiUrl}v1/${'realm'}/triggers/${'triggerName'}`,
triggers: astarteAPIurl`${config.realmManagementApiUrl}v1/${'realm'}/triggers`,
policies: astarteAPIurl`${config.realmManagementApiUrl}v1/${'realm'}/policies`,
policy: astarteAPIurl`${config.realmManagementApiUrl}v1/${'realm'}/policies/${'policyName'}`,
device: astarteAPIurl`${config.realmManagementApiUrl}v1/${'realm'}/devices/${'deviceId'}`,
appengineHealth: astarteAPIurl`${config.appEngineApiUrl}health`,
devicesStats: astarteAPIurl`${config.appEngineApiUrl}v1/${'realm'}/stats/devices`,
devices: astarteAPIurl`${config.appEngineApiUrl}v1/${'realm'}/devices`,
deviceInfo: astarteAPIurl`${config.appEngineApiUrl}v1/${'realm'}/devices/${'deviceId'}`,
deviceData: astarteAPIurl`${config.appEngineApiUrl}v1/${'realm'}/devices/${'deviceId'}/interfaces/${'interfaceName'}${'path'}?keep_milliseconds=true&since=${'since'}&since_after=${'sinceAfter'}&to=${'to'}&limit=${'limit'}`,
groups: astarteAPIurl`${config.appEngineApiUrl}v1/${'realm'}/groups`,
groupDevices: astarteAPIurl`${config.appEngineApiUrl}v1/${'realm'}/groups/${'groupName'}/devices`,
deviceInGroup: astarteAPIurl`${config.appEngineApiUrl}v1/${'realm'}/groups/${'groupName'}/devices/${'deviceId'}`,
phoenixSocket: astarteAPIurl`${config.appEngineApiUrl}v1/socket`,
pairingHealth: astarteAPIurl`${config.pairingApiUrl}health`,
registerDevice: astarteAPIurl`${config.pairingApiUrl}v1/${'realm'}/agent/devices`,
deviceAgent: astarteAPIurl`${config.pairingApiUrl}v1/${'realm'}/agent/devices/${'deviceId'}`,
flowHealth: astarteAPIurl`${config.flowApiUrl}health`,
flows: astarteAPIurl`${config.flowApiUrl}v1/${'realm'}/flows`,
flowInstance: astarteAPIurl`${config.flowApiUrl}v1/${'realm'}/flows/${'instanceName'}`,
pipelines: astarteAPIurl`${config.flowApiUrl}v1/${'realm'}/pipelines`,
pipelineSource: astarteAPIurl`${config.flowApiUrl}v1/${'realm'}/pipelines/${'pipelineId'}`,
blocks: astarteAPIurl`${config.flowApiUrl}v1/${'realm'}/blocks`,
blockSource: astarteAPIurl`${config.flowApiUrl}v1/${'realm'}/blocks/${'blockId'}`,
deviceData:
astarteAPIurl`${config.appEngineApiUrl}v1/${'realm'}/devices/${'deviceId'}/interfaces/${'interfaceName'}${'path'}?
keep_milliseconds=true&since=${'since'}&since_after=${'sinceAfter'}&to=${'to'}&limit=${'limit'}`,
groups: astarteAPIurl`${config.appEngineApiUrl}v1/${'realm'}/groups`,
groupDevices: astarteAPIurl`${config.appEngineApiUrl}v1/${'realm'}/groups/${'groupName'}/devices`,
deviceInGroup: astarteAPIurl`${config.appEngineApiUrl}v1/${'realm'}/groups/${'groupName'}/devices/${'deviceId'}`,
phoenixSocket: astarteAPIurl`${config.appEngineApiUrl}v1/socket`,
pairingHealth: astarteAPIurl`${config.pairingApiUrl}health`,
registerDevice: astarteAPIurl`${config.pairingApiUrl}v1/${'realm'}/agent/devices`,
deviceAgent: astarteAPIurl`${config.pairingApiUrl}v1/${'realm'}/agent/devices/${'deviceId'}`,
flowHealth: astarteAPIurl`${config.flowApiUrl}health`,
flows: astarteAPIurl`${config.flowApiUrl}v1/${'realm'}/flows`,
flowInstance: astarteAPIurl`${config.flowApiUrl}v1/${'realm'}/flows/${'instanceName'}`,
pipelines: astarteAPIurl`${config.flowApiUrl}v1/${'realm'}/pipelines`,
pipelineSource: astarteAPIurl`${config.flowApiUrl}v1/${'realm'}/pipelines/${'pipelineId'}`,
blocks: astarteAPIurl`${config.flowApiUrl}v1/${'realm'}/blocks`,
blockSource: astarteAPIurl`${config.flowApiUrl}v1/${'realm'}/blocks/${'blockId'}`,
};
}

Expand Down Expand Up @@ -613,6 +618,10 @@ class AstarteClient {
await this.$delete(this.apiConfig.deviceAgent({ deviceId, ...this.config }));
}

async deleteDevice(deviceId: AstarteDevice['id']): Promise<void> {
await this.$delete(this.apiConfig.device({ ...this.config, deviceId }));
}

async getFlowInstances(): Promise<Array<AstarteFlow['name']>> {
const response = await this.$get(this.apiConfig.flows(this.config));
return response.data;
Expand Down

0 comments on commit 6d588a4

Please sign in to comment.