Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 130 additions & 105 deletions app/components/device/new/advanced-info.tsx
Original file line number Diff line number Diff line change
@@ -1,124 +1,149 @@
import Form from "@rjsf/core";
import validator from "@rjsf/validator-ajv8";
import { useEffect, useState } from "react";
import { useFormContext } from "react-hook-form";
import { ArrayFieldTemplate } from "~/components/rjsf/arrayFieldTemplate";
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 Form from '@rjsf/core'
import validator from '@rjsf/validator-ajv8'
import { T } from 'node_modules/vitest/dist/chunks/traces.d.402V_yFI'

Check warning on line 3 in app/components/device/new/advanced-info.tsx

View workflow job for this annotation

GitHub Actions / ⬣ Lint

'T' is defined but never used. Allowed unused vars must match /^ignored/u
import { useState } from 'react'
import { useFormContext } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { ArrayFieldTemplate } from '~/components/rjsf/arrayFieldTemplate'
import { CheckboxWidget } from '~/components/rjsf/checkboxWidget'
import { FieldTemplate } from '~/components/rjsf/fieldTemplate'
import { BaseInputTemplate } from '~/components/rjsf/inputTemplate'
import { Callout } from '~/components/ui/alert'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '~/components/ui/card'
import { Label } from '~/components/ui/label'
import { Switch } from '~/components/ui/switch'

interface Integration {
id: string;
name: string;
slug: string;
icon?: string | null;
description?: string | null;
order: number;
id: string
name: string
slug: string
icon?: string | null
description?: string | null
order: number
}

interface AdvancedStepProps {
integrations: Integration[];
integrations: Integration[]
}

export function AdvancedStep({ integrations }: AdvancedStepProps) {
const { watch, setValue, resetField } = useFormContext();
const [schemas, setSchemas] = useState<Record<string, { schema: any; uiSchema: any }>>({});
const [loading, setLoading] = useState<Record<string, boolean>>({});
const { watch, setValue, resetField } = useFormContext()
const [schemas, setSchemas] = useState<
Record<string, { schema: any; uiSchema: any }>
>({})
const [loading, setLoading] = useState<Record<string, boolean>>({})
const { t } = useTranslation('newdevice')

const loadSchema = async (slug: string) => {
if (schemas[slug]) return

const loadSchema = async (slug: string) => {
if (schemas[slug]) return;
setLoading((prev) => ({ ...prev, [slug]: true }))

setLoading((prev) => ({ ...prev, [slug]: true }));
try {
const res = await fetch(`/api/integrations/schema/${slug}`)
if (!res.ok) throw new Error(`Failed to fetch ${slug} schema`)

try {
const res = await fetch(`/api/integrations/schema/${slug}`);
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 }))
}
}

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 }));
}
}
const handleToggle = (slug: string, checked: boolean) => {
setValue(`${slug}Enabled`, checked)

const handleToggle = (slug: string, checked: boolean) => {
setValue(`${slug}Enabled`, checked);
if (checked) {
void loadSchema(slug)
} else {
resetField(`${slug}Config`)
}
}

if (checked) {
void loadSchema(slug);
} 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 (
<>
{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="mb-6 w-full">
<CardHeader>
<CardTitle>
{intg.name} {t('configuration')}
</CardTitle>
{intg.description && (
<CardDescription>{intg.description}</CardDescription>
)}
</CardHeader>

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"
>
{t('enable')} {intg.name}
</Label>
<Switch
id={`${intg.slug}Enabled`}
checked={enabled}
onCheckedChange={(checked) =>
handleToggle(intg.slug, checked)
}
/>
</div>

<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)}
/>
</div>
{enabled && (
<>
{isLoading && (
<p className="text-sm text-muted-foreground">
{t('loading')} {intg.name} {t('configuration')}...
</p>
)}

{enabled && (
<>
{isLoading && (
<p className="text-sm text-muted-foreground">
Loading {intg.name} configuration…
</p>
)}

{schema && (
<Form
widgets={{ CheckboxWidget }}
templates={{ FieldTemplate, ArrayFieldTemplate, 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>
);
})}
</>
);
}
{schema && (
<Form
widgets={{ CheckboxWidget }}
templates={{
FieldTemplate,
ArrayFieldTemplate,
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>
)
})}
{integrations.length === 0 && (
<Callout variant="note">{t('no_integrations_available')}</Callout>
)}
</>
)
}
4 changes: 3 additions & 1 deletion app/components/device/new/device-info.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { X } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useFormContext } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { Button } from '~/components/ui/button'
import { Card, CardContent } from '~/components/ui/card'
import { Label } from '~/components/ui/label'
Expand Down Expand Up @@ -35,6 +36,7 @@ const connectionTypes = ['Wifi', 'Lora', 'Ethernet']

export function DeviceSelectionStep() {
const { setValue, watch } = useFormContext()
const { t } = useTranslation('newdevice')

// Watch the existing values from the form state
const model = watch('model')
Expand Down Expand Up @@ -154,7 +156,7 @@ export function DeviceSelectionStep() {
<Separator className="my-2" />
<div className="w-full max-w-xs">
<h4 className="mb-2 text-sm font-medium">
Connection Type:
{t('connection_type')}
</h4>
<RadioGroup
value={selectedConnectionType}
Expand Down
8 changes: 4 additions & 4 deletions app/components/device/new/general-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,17 @@ export function GeneralInfoStep() {
icon: React.ReactNode
label: string
}[] = [
{ value: 'outdoor', icon: <Cloud className="h-6 w-6" />, label: 'Outdoor' },
{ value: 'indoor', icon: <Home className="h-6 w-6" />, label: 'Indoor' },
{ value: 'outdoor', icon: <Cloud className="h-6 w-6" />, label: t('outdoor') },
{ value: 'indoor', icon: <Home className="h-6 w-6" />, label: t('indoor') },
{
value: 'mobile',
icon: <Bike className="h-6 w-6" />,
label: 'Mobile',
label: t('mobile'),
},
{
value: 'unknown',
icon: <HelpCircle className="h-6 w-6" />,
label: 'Unknown',
label: t('unknown'),
},
]

Expand Down
10 changes: 6 additions & 4 deletions app/components/device/new/new-device-stepper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Info, Slash } from 'lucide-react'
import { useEffect, useState } from 'react'
import { type FieldErrors, FormProvider, useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { Form, useLoaderData, useSubmit } from 'react-router'
import { Form, useLoaderData, useSubmit, useNavigation } from 'react-router'
import { z } from 'zod'
import { AdvancedStep } from './advanced-info'
import { DeviceSelectionStep } from './device-info'
Expand Down Expand Up @@ -159,6 +159,8 @@ export default function NewDeviceStepper() {
const { toast } = useToast()
const { t } = useTranslation('newdevice')
const [isFirst, setIsFirst] = useState(false)
const navigation = useNavigation()
const isSubmitting = navigation.state === 'submitting'

useEffect(() => {
setIsFirst(stepper.isFirst)
Expand Down Expand Up @@ -286,12 +288,12 @@ export default function NewDeviceStepper() {
type="button"
variant="secondary"
onClick={stepper.prev}
disabled={isFirst}
disabled={isFirst || isSubmitting}
>
{t('back')}
</Button>
<Button type="submit">
{stepper.isLast ? t('complete') : t('next')}
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? t('submitting') : stepper.isLast ? t('complete') : t('next')}
</Button>
</div>
</Form>
Expand Down
3 changes: 1 addition & 2 deletions app/components/device/new/sensors-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,7 @@ export function SensorSelectionStep() {
<div className="flex h-full flex-col">
<div className="mb-4 flex items-center justify-between">
<p className="text-sm text-muted-foreground">
{selectedSensors.length} {t('sensor')}
{selectedSensors.length !== 1 ? 's' : ''} {t('selected')}
{t('selectedSensors', { count: selectedSensors.length })}
</p>
{selectedSensors.length > 0 && (
<button
Expand Down
23 changes: 14 additions & 9 deletions app/components/device/new/summary-info.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { MapPin, Tag, Smartphone, Cpu, Cog } from 'lucide-react'
import { useFormContext } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { Badge } from '@/components/ui/badge'

Check warning on line 4 in app/components/device/new/summary-info.tsx

View workflow job for this annotation

GitHub Actions / ⬣ Lint

'Badge' is defined but never used. Allowed unused vars must match /^ignored/u
import { Card, CardContent } from '@/components/ui/card'


Expand Down Expand Up @@ -63,21 +63,26 @@
<div className="space-y-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{sections.map((section, index) => (
<Card key={index} className="overflow-hidden">
<Card key={index} className="overflow-hidden border border-border bg-card shadow-sm">
<CardContent className="p-0">
<div className="to-purple-500 flex items-center space-x-2 bg-gradient-to-r from-blue-500 p-4">
{section.icon}
<h4 className="text-lg font-semibold text-white">
<div className="flex items-center gap-3 border-b bg-muted/40 px-4 py-3">
<div className="flex h-9 w-9 items-center justify-center rounded-md bg-background text-muted-foreground shadow-sm">
{section.icon}
</div>
<h4 className="text-sm font-semibold tracking-tight text-foreground">
{t(section.title)}
</h4>
</div>

<div className="space-y-2 p-4">
{section.data.map((item: any, idx: any) => (
<div key={idx} className="flex items-center justify-between">
<span className="text-sm text-gray-500">{t(item.label)}:</span>
<Badge variant="secondary" className="font-mono">
{t(item.value)}
</Badge>
<div key={idx} className="flex items-start justify-between gap-4">
<span className="text-sm text-muted-foreground">
{t(item.label)}:
</span>
<span className="max-w-[60%] text-right text-sm font-medium text-foreground">
{item.value}
</span>
</div>
))}
</div>
Expand Down
Loading
Loading