mirror of
https://github.com/ferdzo/iotDashboard.git
synced 2026-04-05 17:16:26 +00:00
Introduced GPT/AI service, added dashboard widgets, drag and drop, export import.
This commit is contained in:
4
frontend/src/hooks/index.ts
Normal file
4
frontend/src/hooks/index.ts
Normal 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'
|
||||
127
frontend/src/hooks/useDashboardConfig.ts
Normal file
127
frontend/src/hooks/useDashboardConfig.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
54
frontend/src/hooks/useDeviceMetrics.ts
Normal file
54
frontend/src/hooks/useDeviceMetrics.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
75
frontend/src/hooks/useTelemetrySeries.ts
Normal file
75
frontend/src/hooks/useTelemetrySeries.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user