Skip to content

Commit aea2d27

Browse files
committed
feat(web): allow device deletion
1 parent 9586712 commit aea2d27

File tree

2 files changed

+124
-7
lines changed

2 files changed

+124
-7
lines changed

web/app/(app)/dashboard/(components)/device-list.tsx

Lines changed: 123 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,21 @@
33
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
44
import { Badge } from '@/components/ui/badge'
55
import { Button } from '@/components/ui/button'
6-
import { Smartphone, Battery, Signal, Copy, Plus, ExternalLink } from 'lucide-react'
6+
import {
7+
Smartphone,
8+
Battery,
9+
Signal,
10+
Copy,
11+
Plus,
12+
ExternalLink,
13+
Loader2,
14+
MoreVertical,
15+
} from 'lucide-react'
716
import { useToast } from '@/hooks/use-toast'
817
import httpBrowserClient from '@/lib/httpBrowserClient'
918
import { ApiEndpoints } from '@/config/api'
1019
import { Routes } from '@/config/routes'
11-
import { useQuery } from '@tanstack/react-query'
20+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
1221
import { useRef, useState } from 'react'
1322
import {
1423
Dialog,
@@ -19,6 +28,12 @@ import {
1928
DialogTitle,
2029
} from '@/components/ui/dialog'
2130
import { Skeleton } from '@/components/ui/skeleton'
31+
import {
32+
DropdownMenu,
33+
DropdownMenuContent,
34+
DropdownMenuItem,
35+
DropdownMenuTrigger,
36+
} from '@/components/ui/dropdown-menu'
2237
import { formatDeviceName } from '@/lib/utils'
2338
import GenerateApiKey, {
2439
type GenerateApiKeyHandle,
@@ -30,11 +45,20 @@ import {
3045
latestAppVersionCode,
3146
} from './update-app-helpers'
3247

48+
type DeviceRow = DeviceVersionCandidate & {
49+
createdAt: string
50+
status?: string
51+
enabled?: boolean
52+
}
53+
3354
export default function DeviceList() {
3455
const addDeviceKeyRef = useRef<GenerateApiKeyHandle>(null)
3556
const [addDeviceInstructionOpen, setAddDeviceInstructionOpen] =
3657
useState(false)
58+
const [devicePendingDelete, setDevicePendingDelete] =
59+
useState<DeviceRow | null>(null)
3760
const { toast } = useToast()
61+
const queryClient = useQueryClient()
3862
const {
3963
isPending,
4064
error,
@@ -48,6 +72,35 @@ export default function DeviceList() {
4872
// select: (res) => res.data,
4973
})
5074

75+
const {
76+
mutate: deleteDevice,
77+
isPending: isDeletingDevice,
78+
} = useMutation({
79+
mutationFn: (id: string) =>
80+
httpBrowserClient.delete(ApiEndpoints.gateway.deleteDevice(id)),
81+
onSuccess: () => {
82+
setDevicePendingDelete(null)
83+
toast({
84+
title: 'Device removed',
85+
})
86+
void queryClient.invalidateQueries({ queryKey: ['devices'] })
87+
},
88+
onError: (err: unknown) => {
89+
const message =
90+
err &&
91+
typeof err === 'object' &&
92+
'message' in err &&
93+
typeof (err as { message: unknown }).message === 'string'
94+
? (err as { message: string }).message
95+
: 'Something went wrong'
96+
toast({
97+
variant: 'destructive',
98+
title: 'Error removing device',
99+
description: message,
100+
})
101+
},
102+
})
103+
51104
const handleCopyId = (id: string) => {
52105
navigator.clipboard.writeText(id)
53106
toast({
@@ -77,8 +130,8 @@ export default function DeviceList() {
77130
{[1, 2, 3].map((i) => (
78131
<Card key={i} className='border-0 shadow-none'>
79132
<CardContent className='flex items-center p-3'>
80-
<Skeleton className='h-6 w-6 rounded-full mr-3' />
81-
<div className='flex-1'>
133+
<Skeleton className='h-6 w-6 rounded-full mr-3 shrink-0' />
134+
<div className='min-w-0 flex-1'>
82135
<div className='flex items-center justify-between'>
83136
<Skeleton className='h-4 w-[120px]' />
84137
<Skeleton className='h-4 w-[60px]' />
@@ -90,6 +143,7 @@ export default function DeviceList() {
90143
<Skeleton className='h-3 w-[200px]' />
91144
</div>
92145
</div>
146+
<Skeleton className='h-6 w-6 shrink-0' />
93147
</CardContent>
94148
</Card>
95149
))}
@@ -110,9 +164,9 @@ export default function DeviceList() {
110164

111165
{devices?.data?.map((device) => (
112166
<Card key={device._id} className='border-0 shadow-none'>
113-
<CardContent className='flex items-center p-3'>
114-
<Smartphone className='h-6 w-6 mr-3' />
115-
<div className='flex-1'>
167+
<CardContent className='flex items-center gap-1 p-3'>
168+
<Smartphone className='h-6 w-6 mr-2 shrink-0' />
169+
<div className='min-w-0 flex-1'>
116170
<div className='flex items-center justify-between'>
117171
<h3 className='font-semibold text-sm'>
118172
{formatDeviceName(device)}
@@ -196,6 +250,28 @@ export default function DeviceList() {
196250
</div>
197251
)}
198252
</div>
253+
<DropdownMenu>
254+
<DropdownMenuTrigger asChild>
255+
<Button
256+
variant='ghost'
257+
size='icon'
258+
className='h-8 w-8 shrink-0'
259+
aria-label='Device actions'
260+
>
261+
<MoreVertical className='h-4 w-4' />
262+
</Button>
263+
</DropdownMenuTrigger>
264+
<DropdownMenuContent align='end'>
265+
<DropdownMenuItem
266+
className='text-destructive focus:text-destructive'
267+
onClick={() =>
268+
setDevicePendingDelete(device as DeviceRow)
269+
}
270+
>
271+
Delete
272+
</DropdownMenuItem>
273+
</DropdownMenuContent>
274+
</DropdownMenu>
199275
</CardContent>
200276
</Card>
201277
))}
@@ -266,6 +342,46 @@ export default function DeviceList() {
266342
</DialogFooter>
267343
</DialogContent>
268344
</Dialog>
345+
346+
<Dialog
347+
open={!!devicePendingDelete}
348+
onOpenChange={(open) => {
349+
if (!open) setDevicePendingDelete(null)
350+
}}
351+
>
352+
<DialogContent>
353+
<DialogHeader>
354+
<DialogTitle>Remove this device?</DialogTitle>
355+
<DialogDescription>
356+
{devicePendingDelete
357+
? `This removes ${formatDeviceName(devicePendingDelete)} from your account. You will not be able to send or receive SMS through it until you register the app again.`
358+
: 'This removes the device from your account. You will not be able to send or receive SMS through it until you register the app again.'}
359+
</DialogDescription>
360+
</DialogHeader>
361+
<DialogFooter>
362+
<Button
363+
variant='outline'
364+
onClick={() => setDevicePendingDelete(null)}
365+
disabled={isDeletingDevice}
366+
>
367+
Cancel
368+
</Button>
369+
<Button
370+
variant='destructive'
371+
onClick={() =>
372+
devicePendingDelete &&
373+
deleteDevice(devicePendingDelete._id)
374+
}
375+
disabled={isDeletingDevice}
376+
>
377+
{isDeletingDevice ? (
378+
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
379+
) : null}
380+
Remove
381+
</Button>
382+
</DialogFooter>
383+
</DialogContent>
384+
</Dialog>
269385
</>
270386
)
271387
}

web/config/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const ApiEndpoints = {
2626
},
2727
gateway: {
2828
listDevices: () => '/gateway/devices',
29+
deleteDevice: (id: string) => `/gateway/devices/${id}`,
2930
sendSMS: (id: string) => `/gateway/devices/${id}/send-sms`,
3031
sendBulkSMS: (id: string) => `/gateway/devices/${id}/send-bulk-sms`,
3132
getReceivedSMS: (id: string) => `/gateway/devices/${id}/get-received-sms`,

0 commit comments

Comments
 (0)