Skip to content
12 changes: 12 additions & 0 deletions app/db/models/measurement.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
measurements1hourView,
measurements1monthView,
measurements1yearView,
sensor,
device,
} from '~/db/schema'
import { drizzleClient } from '~/db.server'
Expand Down Expand Up @@ -335,6 +336,17 @@ export async function deleteMeasurementsForTime(date: Date) {
.where(eq(measurement.time, date))
}

export async function deleteMeasurementsForDevice(deviceId: string) {
const sensorIds = drizzleClient
.select({ id: sensor.id })
.from(sensor)
.where(eq(sensor.deviceId, deviceId))

return await drizzleClient
.delete(measurement)
.where(inArray(measurement.sensorId, sensorIds))
}

export async function getMeasurementsCount() {
return await drizzleClient.$count(measurement)
}
105 changes: 98 additions & 7 deletions app/routes/device.$deviceId.edit.sensors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
useNavigation,
useOutletContext,
useSubmit,
data,
} from 'react-router'
import invariant from 'tiny-invariant'
import { type Route } from './+types/device.$deviceId.edit.sensors'
Expand All @@ -42,10 +43,10 @@ import {
} from '~/db/models/device.server'
import { getSharedDeviceSchemaVersion } from '~/db/models/device-schema.server'
import { assignIcon, getIcon, iconsList } from '~/lib/sensoricons'
import { getUserId } from '~/services/session-service.server'
import { getUserEmail, getUserId } from '~/services/session-service.server'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { useToast } from '@/components/ui/use-toast'
import { useTranslation } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
import { Button } from '~/components/ui/button'
import { Callout } from '~/components/ui/alert'
import { Input } from '~/components/ui/input'
Expand All @@ -61,8 +62,16 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from '~/components/ui/alert-dialog'
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from '~/components/ui/card'
import { deleteMeasurementsForDevice } from '~/db/models/measurement.server'
import { verifyLogin } from '~/db/models/user.server'

//*****************************************************
export async function loader({ request, params }: Route.LoaderArgs) {
//* if user is not logged in, redirect to home
const userId = await getUserId(request)
Expand All @@ -86,6 +95,7 @@ export async function loader({ request, params }: Route.LoaderArgs) {
])

return {
device: device,
sensors: rawSensorsData,
deviceSchema: deviceSchema
? {
Expand All @@ -97,13 +107,13 @@ export async function loader({ request, params }: Route.LoaderArgs) {
} as any
}

//*****************************************************
export async function action({ request, params }: Route.ActionArgs) {
const userId = await getUserId(request)
if (!userId) return redirect('/')

const formData = await request.formData()
const { intent, updatedSensorsData } = Object.fromEntries(formData)
const { intent, updatedSensorsData, passwordConfirm } =
Object.fromEntries(formData)

const deviceId = params.deviceId
invariant(deviceId, 'deviceID not found!')
Expand All @@ -116,6 +126,28 @@ export async function action({ request, params }: Route.ActionArgs) {
return { isUpdated: true, isDetached: true }
}

if (intent === 'delete-measurements') {
invariant(typeof passwordConfirm === 'string', 'password must be a string')
const userEmail = await getUserEmail(request)
invariant(typeof userEmail === 'string', 'email not found')
const user = await verifyLogin(userEmail, passwordConfirm)
if (!user) {
return data(
{
isUpdated: false,
noMeasurements: false,
message: 'Invalid password',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
{ status: 400 },
)
}
const results = await deleteMeasurementsForDevice(deviceId)
return {
isUpdated: true,
noMeasurements: results.count === 0,
}
}

if (typeof updatedSensorsData !== 'string') {
return { isUpdated: false, message: 'No sensor data submitted.' }
}
Expand Down Expand Up @@ -254,12 +286,12 @@ export async function action({ request, params }: Route.ActionArgs) {
return { isUpdated: true }
}

//**********************************
export default function EditBoxSensors() {
const data = useLoaderData<typeof loader>()
const actionData = useActionData<typeof action>()
const navigation = useNavigation()
const submit = useSubmit()
const [password, setPassword] = React.useState('')
const isSubmitting = navigation.state !== 'idle'

const { copyToClipboard } = useCopyToClipboard()
Expand Down Expand Up @@ -299,6 +331,7 @@ export default function EditBoxSensors() {
React.useEffect(() => {
//* if sensors data were updated successfully
if (actionData && actionData?.isUpdated) {
setPassword('')
//* show notification when data is successfully updated
setToastOpen(true)
// window.location.reload();
Expand All @@ -321,7 +354,7 @@ export default function EditBoxSensors() {
variant: 'destructive',
})
}
}, [actionData, setToastOpen, toast, t]) // eslint-disable-line react-hooks/exhaustive-deps
}, [actionData, setToastOpen, toast, t])

React.useEffect(() => {
setSensorsData(originalSensorsData)
Expand Down Expand Up @@ -857,6 +890,64 @@ export default function EditBoxSensors() {
value={JSON.stringify(sensorsData)}
/>
</Form>
<Form method="post" className="mt-7" noValidate>
<Card className="dark:bg-dark-boxes dark:border-white">
<CardHeader>
<CardTitle className="text-red-500">
{t('delete_measurements')}
</CardTitle>
<CardDescription>
<Callout variant="caution">
<Trans
t={t}
i18nKey="confirm_permanent_deletion"
values={{ device: data.device.name }}
components={{ b: <b /> }}
/>
</Callout>
</CardDescription>
</CardHeader>

<CardContent className="space-y-4">
{actionData?.isUpdated &&
actionData.noMeasurements !== undefined &&
!actionData.noMeasurements && (
<Callout variant="tip">{t('delete_success')}</Callout>
)}

{actionData?.isUpdated && actionData.noMeasurements && (
<Callout variant="note">{t('no_measurements')}</Callout>
)}

<div className="space-y-2">
<Label htmlFor="passwordConfirm">{t('password')}</Label>
<Input
id="passwordConfirm"
name="passwordConfirm"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
{actionData?.message?.includes('password') && (
<div className="text-sm text-red-500">
{t('wrong_password')}
</div>
)}
</div>

<Button
type="submit"
variant="destructive"
name="intent"
value="delete-measurements"
disabled={isSubmitting || !password}
>
{t('delete_measurements')}
</Button>
</CardContent>
</Card>
</Form>
</div>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions app/routes/device.$deviceId.edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Cpu,
ArrowLeft,
NotepadText,
Trash2,
} from 'lucide-react'
import { useState } from 'react'
import { redirect, Link, Outlet, useParams, useLoaderData } from 'react-router'
Expand Down
8 changes: 7 additions & 1 deletion public/locales/de/edit-device-sensors.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,11 @@
"schema_notice_title": "Dieses Gerät verwendet das Schema {{name}} v{{version}}.",
"schema_notice_text": "Phänomen, Einheit und Typ sind gesperrt, damit Messungen vergleichbar bleiben. Du kannst Sensoren weiterhin sortieren und ihre Icons ändern.",
"schema_fields_locked": "Dieser Sensor ist durch das Geräteschema definiert. Phänomen, Einheit und Typ sind gesperrt; hier kann nur das Icon geändert werden.",
"schema_out_of_sync": "Die aktuellen Sensoren passen nicht mehr zu diesem Schema. Löse das Gerät vom Schema, bevor du Sensoren bearbeitest."
"schema_out_of_sync": "Die aktuellen Sensoren passen nicht mehr zu diesem Schema. Löse das Gerät vom Schema, bevor du Sensoren bearbeitest.",
"delete_measurements": "Alle Messungen löschen",
"confirm_permanent_deletion": "Dadurch werden alle Messungen für alle Sensoren von <b>{{device}}</b> dauerhaft gelöscht. Das Gerät und seine Sensoren bleiben erhalten. Bitte bestätige mit deinem Passwort.",
"password": "Passwort",
"wrong_password": "Falsches Passwort",
"delete_success": "Alle Messungen wurden erfolgreich gelöscht.",
"no_measurements": "Dieses Gerät hat keine Messungen zum Löschen."
}
8 changes: 7 additions & 1 deletion public/locales/en/edit-device-sensors.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,11 @@
"schema_notice_title": "This device uses schema {{name}} v{{version}}.",
"schema_notice_text": "Phenomenon, unit, and type are locked to keep measurements comparable. You can still reorder sensors and change their icons.",
"schema_fields_locked": "This sensor is defined by the device schema. Phenomenon, unit, and type are locked; only the icon can be changed here.",
"schema_out_of_sync": "The current sensors no longer match this schema. Detach the device from the schema before editing sensors."
"schema_out_of_sync": "The current sensors no longer match this schema. Detach the device from the schema before editing sensors.",
"delete_measurements": "Delete all measurements",
"confirm_permanent_deletion": "This will permanently delete all measurements for all sensors of <b>{{device}}</b>. The device and its sensors will remain. Please confirm with your password.",
"password": "Password",
"wrong_password": "Invalid password",
"delete_success": "All measurements have been successfully deleted.",
"no_measurements": "This device has no measurements to delete."
}
Loading