Introduced GPT/AI service, added dashboard widgets, drag and drop, export import.

This commit is contained in:
2025-11-09 23:56:13 +01:00
parent 8e98f5ad7d
commit 7921049f56
36 changed files with 3498 additions and 403 deletions

View File

@@ -0,0 +1,4 @@
export { useTelemetrySeries } from './useTelemetrySeries'
export { useDeviceMetrics, useAllMetrics } from './useDeviceMetrics'
export { useDashboardConfig } from './useDashboardConfig'
export type { WidgetConfig, DashboardConfig, WidgetType } from './useDashboardConfig'

View File

@@ -0,0 +1,127 @@
import { useState, useEffect } from 'react'
export type WidgetType = 'line-chart' | 'gauge' | 'stat' | 'ai-insight' | 'bar-chart'
export interface WidgetConfig {
id: string
type: WidgetType
title: string
deviceIds: string[]
metricIds: string[]
timeframe: {
hours?: number
startTime?: string
endTime?: string
}
visualization?: {
colors?: string[]
showLegend?: boolean
showGrid?: boolean
height?: number
}
position?: {
x: number
y: number
w: number
h: number
}
}
export interface DashboardConfig {
widgets: WidgetConfig[]
layout: 'grid' | 'freeform'
refreshInterval?: number
}
const DEFAULT_CONFIG: DashboardConfig = {
widgets: [],
layout: 'grid',
refreshInterval: 30000,
}
const STORAGE_KEY = 'iot-dashboard-config'
/**
* Hook to manage dashboard configuration with localStorage persistence
*/
export function useDashboardConfig() {
const [config, setConfig] = useState<DashboardConfig>(() => {
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
return JSON.parse(stored) as DashboardConfig
}
} catch (error) {
console.error('Failed to load dashboard config:', error)
}
return DEFAULT_CONFIG
})
// Persist to localStorage whenever config changes
useEffect(() => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(config))
} catch (error) {
console.error('Failed to save dashboard config:', error)
}
}, [config])
const addWidget = (widget: WidgetConfig) => {
setConfig((prev) => ({
...prev,
widgets: [...prev.widgets, widget],
}))
}
const updateWidget = (id: string, updates: Partial<WidgetConfig>) => {
setConfig((prev) => ({
...prev,
widgets: prev.widgets.map((w) => (w.id === id ? { ...w, ...updates } : w)),
}))
}
const removeWidget = (id: string) => {
setConfig((prev) => ({
...prev,
widgets: prev.widgets.filter((w) => w.id !== id),
}))
}
const reorderWidgets = (widgets: WidgetConfig[]) => {
setConfig((prev) => ({
...prev,
widgets,
}))
}
const resetConfig = () => {
setConfig(DEFAULT_CONFIG)
}
const exportConfig = (): string => {
return JSON.stringify(config, null, 2)
}
const importConfig = (configOrJson: DashboardConfig | string) => {
try {
const imported = typeof configOrJson === 'string'
? JSON.parse(configOrJson) as DashboardConfig
: configOrJson
setConfig(imported)
} catch (error) {
console.error('Failed to import config:', error)
throw new Error('Invalid configuration')
}
}
return {
config,
addWidget,
updateWidget,
removeWidget,
reorderWidgets,
resetConfig,
exportConfig,
importConfig,
}
}

View File

@@ -0,0 +1,54 @@
import { useQuery } from '@tanstack/react-query'
import { devicesApi, telemetryApi } from '../api'
interface DeviceMetricsResult {
deviceId: string
deviceName: string
metrics: string[]
isLoading: boolean
error: Error | null
}
/**
* Hook to fetch available metrics for a specific device
*/
export function useDeviceMetrics(deviceId: string | undefined): DeviceMetricsResult {
const { data, isLoading, error } = useQuery({
queryKey: ['device', deviceId, 'metrics'],
queryFn: async () => {
if (!deviceId) return null
const response = await devicesApi.getMetrics(deviceId)
return response.data
},
enabled: !!deviceId,
staleTime: 60000, // Cache for 1 minute
})
return {
deviceId: data?.device_id || '',
deviceName: data?.device_name || '',
metrics: data?.metrics || [],
isLoading,
error: error as Error | null,
}
}
/**
* Hook to fetch all available metrics across all devices
*/
export function useAllMetrics() {
const { data, isLoading, error } = useQuery({
queryKey: ['telemetry', 'metrics'],
queryFn: async () => {
const response = await telemetryApi.getMetrics()
return response.data.metrics
},
staleTime: 60000,
})
return {
metrics: data || [],
isLoading,
error: error as Error | null,
}
}

View File

@@ -0,0 +1,75 @@
import { useQuery } from '@tanstack/react-query'
import { telemetryApi } from '../api'
import type { Telemetry } from '../types/api'
interface TelemetrySeriesParams {
deviceId?: string
metric?: string
hours?: number
startTime?: string
endTime?: string
limit?: number
enabled?: boolean
}
interface TelemetrySeries {
data: Telemetry[]
isLoading: boolean
isFetching: boolean
error: Error | null
refetch: () => void
}
/**
* Hook to fetch telemetry data series with flexible filtering
*/
export function useTelemetrySeries({
deviceId,
metric,
hours = 24,
startTime,
endTime,
limit = 1000,
enabled = true,
}: TelemetrySeriesParams): TelemetrySeries {
const { data, isLoading, isFetching, error, refetch } = useQuery({
queryKey: ['telemetry', 'series', { deviceId, metric, hours, startTime, endTime, limit }],
queryFn: async () => {
const params: {
device_id?: string
metric?: string
hours?: number
start_time?: string
end_time?: string
page_size: number
} = {
page_size: limit,
}
if (deviceId) params.device_id = deviceId
if (metric) params.metric = metric
if (startTime) params.start_time = startTime
if (endTime) params.end_time = endTime
if (!startTime && !endTime && hours) params.hours = hours
const response = await telemetryApi.query(params)
// Handle paginated response
if ('results' in response.data) {
return response.data.results
}
return response.data as Telemetry[]
},
enabled,
refetchInterval: 30000, // Refresh every 30 seconds
})
return {
data: data || [],
isLoading,
isFetching,
error: error as Error | null,
refetch,
}
}