Skip to content

Commit

Permalink
feat: device registration (#456)
Browse files Browse the repository at this point in the history
* upgrade drizzle and drizzle-kit

* add location to measurements for mobile devices

* drop last migration

* add locations for mobile devices

* workaround to handle postgis geometry in query

* remove migrations

* add locations

* disable esLint for now

* get rid of `button can not be a descendant of button` error

* make tags optional

* mobile box overview

* add dynamic popup to mobile trajectories

* enhance mobile boxes

* add build target

* fix `top level await` error

* add trips switch

* remove colors when trips are deactivated

* allow to select two sensors on mobile device

* enable switching overlaying mobile sensor

* add sensor unit to dependencies in MobileBoxLayer effect

* refactor(routes): update route configuration and dependencies

* refactor(donut-chart-cluster): simplify destructuring and formatting in component

* refactor(device): comment out time column in getDevice function

* refactor(graph): rename LineWithZoom to GraphWithZoom and update zoom state management

* refactor(mobile): update MobileBoxView structure and clean up MobileOverviewLayer formatting

* refactor(device): add time column to extras in getDevice function

* refactor(package): remove chartjs-scale-timestack dependency from package.json

* update package-lock.json

* refactor(color): update color palette and introduce dynamic color calculation

* refactor(device-detail): improve image handling and update log entry styles

* delete relation to location on device deletion

* init new device without sensorsWiki

* refactor: enhance layout

* refactor(sensor-selection): group sensors by type and update selection logic

* feat(location-info): integrate map component for location selection

* feat(auth): add user authentication check and enhance login page layout

* feat(join): add profile existence check during user registration

* feat(device): update device model enum and enhance form validation

* feat(location-info): sync marker state with form values on mount

* feat(device): enhance sensor selection with dynamic device model handling and add images for sensor groups

* feat(device): add summary info component and update device stepper with new summary step

* feat(device): add custom device configuration component and enforce sensor selection validation

* feat(device): update location info handling and enhance stepper with tooltips for better user guidance

* feat(device): implement breadcrumb navigation

* fix(device): update device selection handling to reset values to null

* feat(device): get rid of render errors

* feat(device): define device models and update model column type

* refactor(device): only allow to go back in breadcrumbs (for now)

* feat(device): implement advanced form handling and validation for MQTT and TTN settings

* feat(device): add device to database (still need to post to old db)

* feat(device): update device creation to include model and tags

* fix(device): reset selected sensors on device change and clean up unused code

* feat(device): add temporary expiration date for devices and implement automatic deletion procedure

* fix(device): remove debug logs and ensure expiration date is correctly formatted during device creation

* redirect after device registration from completed

* feat(device): modify temporary expiration date handling in device creation

* feat(device): disable edit and data upload options in device dropdown menu

---------

Co-authored-by: Matthias Pfeil <[email protected]>
  • Loading branch information
freds-dev and mpfeil authored Dec 18, 2024
1 parent e04bf5f commit dc87ef8
Show file tree
Hide file tree
Showing 30 changed files with 5,365 additions and 682 deletions.
10 changes: 10 additions & 0 deletions app/components/device-detail/device-detail-box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,16 @@ export default function DeviceDetailBox() {
title="Created At"
text={format(new Date(data.device.createdAt), "PPP")}
/>
{data.device.expiresAt && (
<>
<Separator className="my-2" />
<InfoItem
icon={CalendarPlus}
title="Expires At"
text={format(new Date(data.device.expiresAt), "PPP")}
/>
</>
)}
</div>
</div>
{data.device.tags?.length > 0 && (
Expand Down
246 changes: 246 additions & 0 deletions app/components/device/new/advanced-info.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import { useFormContext } from "react-hook-form";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import { Switch } from "~/components/ui/switch";
import { Textarea } from "~/components/ui/textarea";

export function AdvancedStep() {
const { register, setValue, watch, resetField } = useFormContext();

// Watch field states
const isMqttEnabled = watch("mqttEnabled") || false;
const isTtnEnabled = watch("ttnEnabled") || false;

// Clear corresponding fields when disabling
const handleMqttToggle = (checked: boolean) => {
setValue("mqttEnabled", checked);
if (!checked) {
resetField("url");
resetField("topic");
resetField("messageFormat");
resetField("decodeOptions");
resetField("connectionOptions");
}
};

const handleTtnToggle = (checked: boolean) => {
setValue("ttnEnabled", checked);
if (!checked) {
resetField("dev_id");
resetField("app_id");
resetField("profile");
resetField("decodeOptions");
resetField("port");
}
};

const handleInputChange = (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const { name, value } = event.target;
setValue(name, value);
};

const handleSelectChange = (field: string, value: string) => {
setValue(field, value);
};

return (
<>
{/* MQTT Configuration */}
<Card className="w-full">
<CardHeader>
<CardTitle>MQTT Configuration</CardTitle>
<CardDescription>
Configure your MQTT settings for data streaming
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between space-x-2">
<Label htmlFor="mqttEnabled" className="text-base font-semibold">
Enable MQTT
</Label>
<Switch
disabled
id="mqttEnabled"
checked={isMqttEnabled}
onCheckedChange={handleMqttToggle}
/>
</div>

{isMqttEnabled && (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="mqtt-url">MQTT URL</Label>
<Input
id="mqtt-url"
placeholder="mqtt://example.com:1883"
{...register("url")}
onChange={handleInputChange}
/>
</div>

<div className="space-y-2">
<Label htmlFor="mqtt-topic">MQTT Topic</Label>
<Input
id="mqtt-topic"
placeholder="my/mqtt/topic"
{...register("topic")}
onChange={handleInputChange}
/>
</div>

<div className="space-y-2">
<Label htmlFor="mqtt-message-format">Message Format</Label>
<Select
onValueChange={(value) =>
handleSelectChange("messageFormat", value)
}
defaultValue={watch("messageFormat")}
>
<SelectTrigger id="mqtt-message-format">
<SelectValue placeholder="Select a message format" />
</SelectTrigger>
<SelectContent>
<SelectItem value="json">JSON</SelectItem>
<SelectItem value="csv">CSV</SelectItem>
</SelectContent>
</Select>
</div>

<div className="space-y-2">
<Label htmlFor="mqtt-decode-options">Decode Options</Label>
<Textarea
id="mqtt-decode-options"
placeholder="Enter decode options as JSON"
className="resize-none"
{...register("decodeOptions")}
onChange={handleInputChange}
/>
</div>

<div className="space-y-2">
<Label htmlFor="mqtt-connection-options">
Connection Options
</Label>
<Textarea
id="mqtt-connection-options"
placeholder="Enter connection options as JSON"
className="resize-none"
{...register("connectionOptions")}
onChange={handleInputChange}
/>
</div>
</div>
)}
</CardContent>
</Card>

{/* TTN Configuration */}
<Card className="w-full mt-6">
<CardHeader>
<CardTitle>TTN Configuration</CardTitle>
<CardDescription>
Configure your TTN (The Things Network) settings
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between space-x-2">
<Label htmlFor="ttnEnabled" className="text-base font-semibold">
Enable TTN
</Label>
<Switch
disabled
id="ttnEnabled"
checked={isTtnEnabled}
onCheckedChange={handleTtnToggle}
/>
</div>

{isTtnEnabled && (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="ttn-dev-id">Device ID</Label>
<Input
id="ttn-dev-id"
placeholder="Enter TTN Device ID"
{...register("dev_id")}
onChange={handleInputChange}
/>
</div>

<div className="space-y-2">
<Label htmlFor="ttn-app-id">Application ID</Label>
<Input
id="ttn-app-id"
placeholder="Enter TTN Application ID"
{...register("app_id")}
onChange={handleInputChange}
/>
</div>

<div className="space-y-2">
<Label htmlFor="ttn-profile">Profile</Label>
<Select
onValueChange={(value) =>
handleSelectChange("profile", value)
}
defaultValue={watch("profile")}
>
<SelectTrigger id="ttn-profile">
<SelectValue placeholder="Select a profile" />
</SelectTrigger>
<SelectContent>
<SelectItem value="lora-serialization">
Lora Serialization
</SelectItem>
<SelectItem value="sensebox/home">Sensebox/Home</SelectItem>
<SelectItem value="json">JSON</SelectItem>
<SelectItem value="debug">Debug</SelectItem>
<SelectItem value="cayenne-lpp">Cayenne LPP</SelectItem>
</SelectContent>
</Select>
</div>

<div className="space-y-2">
<Label htmlFor="ttn-decode-options">Decode Options</Label>
<Textarea
id="ttn-decode-options"
placeholder="Enter decode options as JSON"
className="resize-none"
{...register("decodeOptions")}
onChange={handleInputChange}
/>
</div>

<div className="space-y-2">
<Label htmlFor="ttn-port">Port</Label>
<Input
id="ttn-port"
placeholder="Enter TTN Port"
type="number"
{...register("port", { valueAsNumber: true })}
onChange={handleInputChange}
/>
</div>
</div>
)}
</CardContent>
</Card>
</>
);
}
116 changes: 116 additions & 0 deletions app/components/device/new/custom-device-config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { useState, useEffect } from "react";
import { useFormContext } from "react-hook-form";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent } from "@/components/ui/card";
import type { Sensor } from "./sensors-info";
import { X } from "lucide-react";
import { Separator } from "~/components/ui/separator";

export function CustomDeviceConfig() {
const { setValue, watch } = useFormContext();

// Initialize state from form context
const [sensors, setSensors] = useState<Sensor[]>(
() => watch("selectedSensors") || [],
);
const [newSensor, setNewSensor] = useState<Sensor>({
title: "",
unit: "",
sensorType: "",
});

// Sync state with form context on mount
useEffect(() => {
const savedSensors = watch("selectedSensors") || [];
if (savedSensors.length > 0) {
setSensors(savedSensors);
}
}, [watch]);

const updateNewSensor = (field: keyof Sensor, value: string) => {
setNewSensor((prev) => ({ ...prev, [field]: value }));
};

const addSensor = () => {
if (newSensor.title && newSensor.unit && newSensor.sensorType) {
const updatedSensors = [...sensors, newSensor];
setSensors(updatedSensors);
setValue("selectedSensors", updatedSensors); // Sync with form
setNewSensor({ title: "", unit: "", sensorType: "" });
}
};

const removeSensor = (index: number) => {
const updatedSensors = sensors.filter((_, i) => i !== index);
setSensors(updatedSensors);
setValue("selectedSensors", updatedSensors); // Sync with form
};

return (
<div className="space-y-4 p-2">
<div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div>
<Label htmlFor="phenomenon">Phenomenon</Label>
<Input
id="phenomenon"
value={newSensor.title}
onChange={(e) => updateNewSensor("title", e.target.value)}
placeholder="e.g., Temperature"
/>
</div>
<div>
<Label htmlFor="unit">Unit</Label>
<Input
id="unit"
value={newSensor.unit}
onChange={(e) => updateNewSensor("unit", e.target.value)}
placeholder="e.g., °C"
/>
</div>
<div>
<Label htmlFor="type">Type</Label>
<Input
id="type"
value={newSensor.sensorType}
onChange={(e) => updateNewSensor("sensorType", e.target.value)}
placeholder="e.g., HDC1080"
/>
</div>
</div>
<Button
onClick={addSensor}
disabled={
!newSensor.title || !newSensor.unit || !newSensor.sensorType
}
>
Add Sensor
</Button>
</div>

{sensors.length > 0 && <Separator />}
{sensors.map((sensor, index) => (
<Card key={index} className="mb-2">
<CardContent className="p-4 flex justify-between items-center">
<div>
<span className="font-medium">{sensor.title}</span> ({sensor.unit}
) - {sensor.sensorType}
</div>
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.preventDefault();
removeSensor(index);
}}
>
<X className="h-4 w-4" />
</Button>
</CardContent>
</Card>
))}
</div>
);
}
Loading

0 comments on commit dc87ef8

Please sign in to comment.