Skip to content

Commit d50c8ff

Browse files
authored
Feat/mqtt json schema (#732)
* feat: add integrations schema * feat: add internal endpoint to post measurements via mqtt * feat: mqtt server functions * feat: endpoint for active mqtt configurations * fix: make createdAt mandatory to preserver history for batch measurements * feat: add mqtt client * feat: mqtt routes and db methods * feat: add mqtt service url and key to env * feat: install json to form package and define base widgets for styling * fix: check if measurement is posted by mqtt service * feat: form to edit mqtt config from json schema provided by mqtt service * feat: add route to fetch schema from mqtt service * feat: replace mqtt form with form generated from json schema * fix: adjust validation for new mqtt form * feat: add more widgets * feat: save mqtt config in mqtt service db when creating device# * feat: rm mqtt specific apis * feat: install openapi-types, seed integrations * feat: adjust integration schema * feat: integration db functions * feat: create integrations via generic api routes * feat: show integrations dynamically in sidebar * feat:icon map * feat: document integrations * fix: minor adjustments * feat: edit ttn config * fix: migration and deps conflicts * fix: rm unused files * fix: rm stuff * fix: seed integrations * fix: rename service key * feat: integration service * fix: order not nullable * feat: load integrations in root loader and pass down to step * fix: general integratoin edit component * fix: rm generated files and add to gitignore * feat: support deep linking * feat: improve docs * fix: rm from gitignore * feat: add step to generate docs * fix: command * fix: dont pin buildx * fix: replace import with fetch * feat: bypass device authentication for requests from trusted services * feat: readd script for seeding integrations * fix: device auth check only if not trusted service * fix: rm help text
1 parent 0f3145c commit d50c8ff

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+4918
-1416
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ OSEM_API_URL="https://api.opensensemap.org/"
1818
DIRECTUS_URL="https://coelho.opensensemap.org"
1919
SENSORWIKI_API_URL="https://api.sensors.wiki/"
2020

21+
MQTT_SERVICE_URL="http://localhost:3001"
22+
MQTT_SERVICE_KEY="dev-service-key-change-in-production"
23+
2124
MYBADGES_API_URL = "https://api.v2.mybadges.org/"
2225
MYBADGES_URL = "https://mybadges.org/"
2326
MYBADGES_SERVERADMIN_USERNAME = ""

.github/workflows/deploy.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ jobs:
5353
- name: 📥 Install deps
5454
run: npm install
5555

56+
- name: 🧾 Generate OpenAPI spec
57+
run: npm run build:docs
58+
5659
- name: 🔎 Type check
5760
run: npm run typecheck --if-present
5861

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@ measurements.csv
2424

2525
/coverage
2626

27-
/minio-data
27+
/minio-data
28+
29+
/public/openapi.json
Lines changed: 121 additions & 244 deletions
Original file line numberDiff line numberDiff line change
@@ -1,246 +1,123 @@
1-
import { useFormContext } from 'react-hook-form'
2-
import {
3-
Card,
4-
CardContent,
5-
CardDescription,
6-
CardHeader,
7-
CardTitle,
8-
} from '~/components/ui/card'
9-
import { Input } from '~/components/ui/input'
10-
import { Label } from '~/components/ui/label'
11-
import {
12-
Select,
13-
SelectContent,
14-
SelectItem,
15-
SelectTrigger,
16-
SelectValue,
17-
} from '~/components/ui/select'
18-
import { Switch } from '~/components/ui/switch'
19-
import { Textarea } from '~/components/ui/textarea'
20-
21-
export function AdvancedStep() {
22-
const { register, setValue, watch, resetField } = useFormContext()
23-
24-
// Watch field states
25-
const isMqttEnabled = watch('mqttEnabled') || false
26-
const isTtnEnabled = watch('ttnEnabled') || false
27-
28-
// Clear corresponding fields when disabling
29-
const handleMqttToggle = (checked: boolean) => {
30-
setValue('mqttEnabled', checked)
31-
if (!checked) {
32-
resetField('url')
33-
resetField('topic')
34-
resetField('messageFormat')
35-
resetField('decodeOptions')
36-
resetField('connectionOptions')
37-
}
38-
}
39-
40-
const handleTtnToggle = (checked: boolean) => {
41-
setValue('ttnEnabled', checked)
42-
if (!checked) {
43-
resetField('dev_id')
44-
resetField('app_id')
45-
resetField('profile')
46-
resetField('decodeOptions')
47-
resetField('port')
48-
}
49-
}
50-
51-
const handleInputChange = (
52-
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
53-
) => {
54-
const { name, value } = event.target
55-
setValue(name, value)
56-
}
57-
58-
const handleSelectChange = (field: string, value: string) => {
59-
setValue(field, value)
60-
}
61-
62-
return (
63-
<>
64-
{/* MQTT Configuration */}
65-
<Card className="w-full">
66-
<CardHeader>
67-
<CardTitle>MQTT Configuration</CardTitle>
68-
<CardDescription>
69-
Configure your MQTT settings for data streaming
70-
</CardDescription>
71-
</CardHeader>
72-
<CardContent>
73-
<div className="flex items-center justify-between space-x-2">
74-
<Label htmlFor="mqttEnabled" className="text-base font-semibold">
75-
Enable MQTT
76-
</Label>
77-
<Switch
78-
disabled
79-
id="mqttEnabled"
80-
checked={isMqttEnabled}
81-
onCheckedChange={handleMqttToggle}
82-
/>
83-
</div>
84-
85-
{isMqttEnabled && (
86-
<div className="space-y-4">
87-
<div className="space-y-2">
88-
<Label htmlFor="mqtt-url">MQTT URL</Label>
89-
<Input
90-
id="mqtt-url"
91-
placeholder="mqtt://example.com:1883"
92-
{...register('url')}
93-
onChange={handleInputChange}
94-
/>
95-
</div>
96-
97-
<div className="space-y-2">
98-
<Label htmlFor="mqtt-topic">MQTT Topic</Label>
99-
<Input
100-
id="mqtt-topic"
101-
placeholder="my/mqtt/topic"
102-
{...register('topic')}
103-
onChange={handleInputChange}
104-
/>
105-
</div>
106-
107-
<div className="space-y-2">
108-
<Label htmlFor="mqtt-message-format">Message Format</Label>
109-
<Select
110-
onValueChange={(value) =>
111-
handleSelectChange('messageFormat', value)
112-
}
113-
defaultValue={watch('messageFormat')}
114-
>
115-
<SelectTrigger id="mqtt-message-format">
116-
<SelectValue placeholder="Select a message format" />
117-
</SelectTrigger>
118-
<SelectContent>
119-
<SelectItem value="json">JSON</SelectItem>
120-
<SelectItem value="csv">CSV</SelectItem>
121-
</SelectContent>
122-
</Select>
123-
</div>
124-
125-
<div className="space-y-2">
126-
<Label htmlFor="mqtt-decode-options">Decode Options</Label>
127-
<Textarea
128-
id="mqtt-decode-options"
129-
placeholder="Enter decode options as JSON"
130-
className="resize-none"
131-
{...register('decodeOptions')}
132-
onChange={handleInputChange}
133-
/>
134-
</div>
135-
136-
<div className="space-y-2">
137-
<Label htmlFor="mqtt-connection-options">
138-
Connection Options
139-
</Label>
140-
<Textarea
141-
id="mqtt-connection-options"
142-
placeholder="Enter connection options as JSON"
143-
className="resize-none"
144-
{...register('connectionOptions')}
145-
onChange={handleInputChange}
146-
/>
147-
</div>
148-
</div>
149-
)}
150-
</CardContent>
151-
</Card>
152-
153-
{/* TTN Configuration */}
154-
<Card className="mt-6 w-full">
155-
<CardHeader>
156-
<CardTitle>TTN Configuration</CardTitle>
157-
<CardDescription>
158-
Configure your TTN (The Things Network) settings
159-
</CardDescription>
160-
</CardHeader>
161-
<CardContent>
162-
<div className="flex items-center justify-between space-x-2">
163-
<Label htmlFor="ttnEnabled" className="text-base font-semibold">
164-
Enable TTN
165-
</Label>
166-
<Switch
167-
disabled
168-
id="ttnEnabled"
169-
checked={isTtnEnabled}
170-
onCheckedChange={handleTtnToggle}
171-
/>
172-
</div>
173-
174-
{isTtnEnabled && (
175-
<div className="space-y-4">
176-
<div className="space-y-2">
177-
<Label htmlFor="ttn-dev-id">Device ID</Label>
178-
<Input
179-
id="ttn-dev-id"
180-
placeholder="Enter TTN Device ID"
181-
{...register('dev_id')}
182-
onChange={handleInputChange}
183-
/>
184-
</div>
185-
186-
<div className="space-y-2">
187-
<Label htmlFor="ttn-app-id">Application ID</Label>
188-
<Input
189-
id="ttn-app-id"
190-
placeholder="Enter TTN Application ID"
191-
{...register('app_id')}
192-
onChange={handleInputChange}
193-
/>
194-
</div>
195-
196-
<div className="space-y-2">
197-
<Label htmlFor="ttn-profile">Profile</Label>
198-
<Select
199-
onValueChange={(value) =>
200-
handleSelectChange('profile', value)
201-
}
202-
defaultValue={watch('profile')}
203-
>
204-
<SelectTrigger id="ttn-profile">
205-
<SelectValue placeholder="Select a profile" />
206-
</SelectTrigger>
207-
<SelectContent>
208-
<SelectItem value="lora-serialization">
209-
Lora Serialization
210-
</SelectItem>
211-
<SelectItem value="sensebox/home">Sensebox/Home</SelectItem>
212-
<SelectItem value="json">JSON</SelectItem>
213-
<SelectItem value="debug">Debug</SelectItem>
214-
<SelectItem value="cayenne-lpp">Cayenne LPP</SelectItem>
215-
</SelectContent>
216-
</Select>
217-
</div>
218-
219-
<div className="space-y-2">
220-
<Label htmlFor="ttn-decode-options">Decode Options</Label>
221-
<Textarea
222-
id="ttn-decode-options"
223-
placeholder="Enter decode options as JSON"
224-
className="resize-none"
225-
{...register('decodeOptions')}
226-
onChange={handleInputChange}
227-
/>
228-
</div>
1+
import Form from "@rjsf/core";
2+
import validator from "@rjsf/validator-ajv8";
3+
import { useEffect, useState } from "react";
4+
import { useFormContext } from "react-hook-form";
5+
import { CheckboxWidget } from "~/components/rjsf/checkboxWidget";
6+
import { FieldTemplate } from "~/components/rjsf/fieldTemplate";
7+
import { BaseInputTemplate } from "~/components/rjsf/inputTemplate";
8+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
9+
import { Label } from "~/components/ui/label";
10+
import { Switch } from "~/components/ui/switch";
11+
12+
interface Integration {
13+
id: string;
14+
name: string;
15+
slug: string;
16+
icon?: string | null;
17+
description?: string | null;
18+
order: number;
19+
}
22920

230-
<div className="space-y-2">
231-
<Label htmlFor="ttn-port">Port</Label>
232-
<Input
233-
id="ttn-port"
234-
placeholder="Enter TTN Port"
235-
type="number"
236-
{...register('port', { valueAsNumber: true })}
237-
onChange={handleInputChange}
238-
/>
239-
</div>
240-
</div>
241-
)}
242-
</CardContent>
243-
</Card>
244-
</>
245-
)
21+
interface AdvancedStepProps {
22+
integrations: Integration[];
24623
}
24+
25+
export function AdvancedStep({ integrations }: AdvancedStepProps) {
26+
const { watch, setValue, resetField } = useFormContext();
27+
const [schemas, setSchemas] = useState<Record<string, { schema: any; uiSchema: any }>>({});
28+
const [loading, setLoading] = useState<Record<string, boolean>>({});
29+
30+
31+
const loadSchema = async (slug: string) => {
32+
if (schemas[slug]) return;
33+
34+
setLoading((prev) => ({ ...prev, [slug]: true }));
35+
36+
try {
37+
const res = await fetch(`/api/integrations/schema/${slug}`);
38+
if (!res.ok) throw new Error(`Failed to fetch ${slug} schema`);
39+
40+
const data = await res.json();
41+
setSchemas((prev) => ({ ...prev, [slug]: data }));
42+
} catch (err) {
43+
console.error(`Failed to load ${slug} schema`, err);
44+
} finally {
45+
setLoading((prev) => ({ ...prev, [slug]: false }));
46+
}
47+
}
48+
49+
const handleToggle = (slug: string, checked: boolean) => {
50+
setValue(`${slug}Enabled`, checked);
51+
52+
if (checked) {
53+
void loadSchema(slug);
54+
} else {
55+
resetField(`${slug}Config`);
56+
}
57+
};
58+
59+
return (
60+
<>
61+
{integrations.map((intg) => {
62+
const enabled = watch(`${intg.slug}Enabled`) ?? false;
63+
const config = watch(`${intg.slug}Config`) ?? {};
64+
const isLoading = loading[intg.slug] ?? false;
65+
const schema = schemas[intg.slug];
66+
67+
return (
68+
<Card key={intg.id} className="w-full mb-6">
69+
<CardHeader>
70+
<CardTitle>{intg.name} Configuration</CardTitle>
71+
{intg.description && (
72+
<CardDescription>{intg.description}</CardDescription>
73+
)}
74+
</CardHeader>
75+
76+
<CardContent className="space-y-4">
77+
<div className="flex items-center justify-between">
78+
<Label htmlFor={`${intg.slug}Enabled`} className="text-base font-semibold">
79+
Enable {intg.name}
80+
</Label>
81+
<Switch
82+
id={`${intg.slug}Enabled`}
83+
checked={enabled}
84+
onCheckedChange={(checked) => handleToggle(intg.slug, checked)}
85+
/>
86+
</div>
87+
88+
{enabled && (
89+
<>
90+
{isLoading && (
91+
<p className="text-sm text-muted-foreground">
92+
Loading {intg.name} configuration…
93+
</p>
94+
)}
95+
96+
{schema && (
97+
<Form
98+
widgets={{ CheckboxWidget }}
99+
templates={{ FieldTemplate, BaseInputTemplate }}
100+
schema={schema.schema}
101+
uiSchema={schema.uiSchema}
102+
validator={validator}
103+
formData={config}
104+
onChange={(e) => {
105+
setValue(`${intg.slug}Config`, e.formData, {
106+
shouldDirty: true,
107+
shouldValidate: true,
108+
});
109+
}}
110+
onSubmit={() => {}}
111+
>
112+
<></>
113+
</Form>
114+
)}
115+
</>
116+
)}
117+
</CardContent>
118+
</Card>
119+
);
120+
})}
121+
</>
122+
);
123+
}

0 commit comments

Comments
 (0)