-
-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/mqtt json schema #732
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
jona159
wants to merge
28
commits into
dev
Choose a base branch
from
feat/mqtt-json-schema
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
dc5e597
feat: add integrations schema
jona159 30b1d4e
feat: add internal endpoint to post measurements via mqtt
jona159 53163a2
feat: mqtt server functions
jona159 cd09724
feat: endpoint for active mqtt configurations
jona159 2a2869b
fix: make createdAt mandatory to preserver history for batch measurem…
jona159 4e65d7d
feat: add mqtt client
jona159 7ac3a31
feat: mqtt routes and db methods
jona159 24fd9c7
feat: add mqtt service url and key to env
jona159 92eddcb
feat: install json to form package and define base widgets for styling
jona159 884a16c
fix: check if measurement is posted by mqtt service
jona159 0fdec95
feat: form to edit mqtt config from json schema provided by mqtt service
jona159 a25d0a5
feat: add route to fetch schema from mqtt service
jona159 9323e3a
feat: replace mqtt form with form generated from json schema
jona159 6f2bc26
fix: adjust validation for new mqtt form
jona159 d377ec3
feat: add more widgets
jona159 460c134
feat: save mqtt config in mqtt service db when creating device#
jona159 4a7565d
feat: rm mqtt specific apis
jona159 25e47b3
feat: install openapi-types, seed integrations
jona159 ffc4d71
feat: adjust integration schema
jona159 ee9a306
feat: integration db functions
jona159 5e8c7c5
feat: create integrations via generic api routes
jona159 f787108
feat: show integrations dynamically in sidebar
jona159 6d6962a
feat:icon map
jona159 3c59032
feat: document integrations
jona159 d05f3d2
fix: minor adjustments
jona159 7764e12
feat: edit ttn config
jona159 873d328
Merge branch 'dev' into feat/mqtt-json-schema
jona159 73c7700
fix: migration and deps conflicts
jona159 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,246 +1,130 @@ | ||
| 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' | ||
| import Form from "@rjsf/core"; | ||
| import validator from "@rjsf/validator-ajv8"; | ||
| import { useEffect, useState } from "react"; | ||
| import { useFormContext } from "react-hook-form"; | ||
| import { CheckboxWidget } from "~/components/rjsf/checkboxWidget"; | ||
| import { FieldTemplate } from "~/components/rjsf/fieldTemplate"; | ||
| import { BaseInputTemplate } from "~/components/rjsf/inputTemplate"; | ||
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; | ||
| import { Label } from "~/components/ui/label"; | ||
| import { Switch } from "~/components/ui/switch"; | ||
| import { type IntegrationMetadata } from "~/routes/api.integrations"; | ||
|
|
||
| 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="mt-6 w-full"> | ||
| <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> | ||
| </> | ||
| ) | ||
| } | ||
| const { watch, setValue, resetField } = useFormContext(); | ||
| const [integrations, setIntegrations] = useState<IntegrationMetadata[]>([]); | ||
| const [schemas, setSchemas] = useState<Record<string, { schema: any; uiSchema: any }>>({}); | ||
| const [loading, setLoading] = useState<Record<string, boolean>>({}); | ||
|
|
||
| // Load available integrations on mount | ||
| useEffect(() => { | ||
| const loadIntegrations = async () => { | ||
| try { | ||
| const res = await fetch("/api/integrations"); | ||
| if (!res.ok) throw new Error("Failed to fetch integrations"); | ||
| const data = await res.json(); | ||
| setIntegrations(data); | ||
| } catch (err) { | ||
| console.error("Failed to load integrations", err); | ||
| } | ||
| }; | ||
|
|
||
| void loadIntegrations(); | ||
| }, []); | ||
|
|
||
| // Load schema when integration is enabled | ||
| const loadSchema = async (slug: string, schemaUrl: string) => { | ||
| if (schemas[slug]) return; // Already loaded | ||
|
|
||
| setLoading((prev) => ({ ...prev, [slug]: true })); | ||
|
|
||
| try { | ||
| console.log("schema url", schemaUrl) | ||
| const res = await fetch(schemaUrl); | ||
| if (!res.ok) throw new Error(`Failed to fetch ${slug} schema`); | ||
|
|
||
| const data = await res.json(); | ||
| setSchemas((prev) => ({ ...prev, [slug]: data })); | ||
| } catch (err) { | ||
| console.error(`Failed to load ${slug} schema`, err); | ||
| } finally { | ||
| setLoading((prev) => ({ ...prev, [slug]: false })); | ||
| } | ||
| }; | ||
|
|
||
| // Toggle handler | ||
| const handleToggle = (slug: string, checked: boolean, schemaUrl: string) => { | ||
| setValue(`${slug}Enabled`, checked); | ||
|
|
||
| if (checked) { | ||
| void loadSchema(slug, schemaUrl); | ||
| } else { | ||
| resetField(`${slug}Config`); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <> | ||
| {integrations.map((intg) => { | ||
| const enabled = watch(`${intg.slug}Enabled`) ?? false; | ||
| const config = watch(`${intg.slug}Config`) ?? {}; | ||
| const isLoading = loading[intg.slug] ?? false; | ||
| const schema = schemas[intg.slug]; | ||
|
|
||
| return ( | ||
| <Card key={intg.id} className="w-full mb-6"> | ||
| <CardHeader> | ||
| <CardTitle>{intg.name} Configuration</CardTitle> | ||
| {intg.description && ( | ||
| <CardDescription>{intg.description}</CardDescription> | ||
| )} | ||
| </CardHeader> | ||
|
|
||
| <CardContent className="space-y-4"> | ||
| <div className="flex items-center justify-between"> | ||
| <Label htmlFor={`${intg.slug}Enabled`} className="text-base font-semibold"> | ||
| Enable {intg.name} | ||
| </Label> | ||
| <Switch | ||
| id={`${intg.slug}Enabled`} | ||
| checked={enabled} | ||
| onCheckedChange={(checked) => handleToggle(intg.slug, checked, intg.schemaUrl)} | ||
| /> | ||
| </div> | ||
|
|
||
| {enabled && ( | ||
| <> | ||
| {isLoading && ( | ||
| <p className="text-sm text-muted-foreground"> | ||
| Loading {intg.name} configuration… | ||
| </p> | ||
| )} | ||
|
|
||
| {schema && ( | ||
| <Form | ||
| widgets={{ CheckboxWidget }} | ||
| templates={{ FieldTemplate, BaseInputTemplate }} | ||
| schema={schema.schema} | ||
| uiSchema={schema.uiSchema} | ||
| validator={validator} | ||
| formData={config} | ||
| onChange={(e) => { | ||
| setValue(`${intg.slug}Config`, e.formData, { | ||
| shouldDirty: true, | ||
| shouldValidate: true, | ||
| }); | ||
| }} | ||
| onSubmit={() => {}} | ||
| > | ||
| <></> | ||
| </Form> | ||
| )} | ||
| </> | ||
| )} | ||
| </CardContent> | ||
| </Card> | ||
| ); | ||
| })} | ||
| </> | ||
| ); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be cleaner if components don't do data fetching by themselves.
Instead it should be done in the route loader and passed as props to the component, what do you think?
That will also allow to use the loading (and eventually Suspense) mechanisms.