feat(dashboard) - Resizable and repositionable widgets.
This commit is contained in:
parent
7253cbc89c
commit
7b372e75da
11 changed files with 744 additions and 391 deletions
|
|
@ -9,7 +9,8 @@ import {
|
|||
Layers,
|
||||
List,
|
||||
} from 'lucide-react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { Responsive, WidthProvider } from 'react-grid-layout';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
|
|
@ -22,8 +23,11 @@ import WidgetConfigModal from '@/components/dashboard/WidgetConfigModal';
|
|||
import WidgetDisplay from '@/components/dashboard/WidgetDisplay';
|
||||
import { useDashboard } from '@/lib/hooks/useDashboard';
|
||||
import { Widget, WidgetConfig } from '@/lib/types/widget';
|
||||
import { DASHBOARD_CONSTRAINTS } from '@/lib/constants/dashboard';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const ResponsiveGridLayout = WidthProvider(Responsive);
|
||||
|
||||
const DashboardPage = () => {
|
||||
const {
|
||||
widgets,
|
||||
|
|
@ -37,17 +41,21 @@ const DashboardPage = () => {
|
|||
importDashboard,
|
||||
settings,
|
||||
updateSettings,
|
||||
getLayouts,
|
||||
updateLayouts,
|
||||
} = useDashboard();
|
||||
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [editingWidget, setEditingWidget] = useState<Widget | null>(null);
|
||||
const hasAutoRefreshed = useRef(false);
|
||||
|
||||
// Memoize the ResponsiveGridLayout to prevent re-renders
|
||||
const ResponsiveGrid = useMemo(() => ResponsiveGridLayout, []);
|
||||
|
||||
// Auto-refresh stale widgets when dashboard loads (only once)
|
||||
useEffect(() => {
|
||||
if (!isLoading && widgets.length > 0 && !hasAutoRefreshed.current) {
|
||||
hasAutoRefreshed.current = true;
|
||||
|
||||
refreshAllWidgets();
|
||||
}
|
||||
}, [isLoading, widgets, refreshAllWidgets]);
|
||||
|
|
@ -119,6 +127,25 @@ const DashboardPage = () => {
|
|||
updateSettings({ parallelLoading: !settings.parallelLoading });
|
||||
};
|
||||
|
||||
// Handle layout changes from react-grid-layout
|
||||
const handleLayoutChange = (layout: any, layouts: any) => {
|
||||
updateLayouts(layouts);
|
||||
};
|
||||
|
||||
// Memoize grid children to prevent unnecessary re-renders
|
||||
const gridChildren = useMemo(() => {
|
||||
return widgets.map((widget) => (
|
||||
<div key={widget.id}>
|
||||
<WidgetDisplay
|
||||
widget={widget}
|
||||
onEdit={handleEditWidget}
|
||||
onDelete={handleDeleteWidget}
|
||||
onRefresh={handleRefreshWidget}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
}, [widgets]);
|
||||
|
||||
// Empty state component
|
||||
const EmptyDashboard = () => (
|
||||
<div className="col-span-2 flex justify-center items-center min-h-[400px]">
|
||||
|
|
@ -224,22 +251,23 @@ const DashboardPage = () => {
|
|||
) : widgets.length === 0 ? (
|
||||
<EmptyDashboard />
|
||||
) : (
|
||||
<div
|
||||
className="grid gap-6 auto-rows-min"
|
||||
style={{
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))',
|
||||
}}
|
||||
<ResponsiveGrid
|
||||
className="layout"
|
||||
layouts={getLayouts()}
|
||||
breakpoints={DASHBOARD_CONSTRAINTS.GRID_BREAKPOINTS}
|
||||
cols={DASHBOARD_CONSTRAINTS.GRID_COLUMNS}
|
||||
rowHeight={DASHBOARD_CONSTRAINTS.GRID_ROW_HEIGHT}
|
||||
margin={DASHBOARD_CONSTRAINTS.GRID_MARGIN}
|
||||
containerPadding={DASHBOARD_CONSTRAINTS.GRID_CONTAINER_PADDING}
|
||||
onLayoutChange={handleLayoutChange}
|
||||
isDraggable={true}
|
||||
isResizable={true}
|
||||
compactType="vertical"
|
||||
preventCollision={false}
|
||||
draggableHandle=".widget-drag-handle"
|
||||
>
|
||||
{widgets.map((widget) => (
|
||||
<WidgetDisplay
|
||||
key={widget.id}
|
||||
widget={widget}
|
||||
onEdit={handleEditWidget}
|
||||
onDelete={handleDeleteWidget}
|
||||
onRefresh={handleRefreshWidget}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{gridChildren}
|
||||
</ResponsiveGrid>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
/* React Grid Layout styles */
|
||||
@import "react-grid-layout/css/styles.css";
|
||||
@import "react-resizable/css/styles.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
@layer base {
|
||||
.overflow-hidden-scrollable {
|
||||
-ms-overflow-style: none;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
AlertCircle,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
GripVertical,
|
||||
} from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import MarkdownRenderer from '@/components/MarkdownRenderer';
|
||||
|
|
@ -49,14 +50,24 @@ const WidgetDisplay = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<Card className="flex flex-col h-fit">
|
||||
<CardHeader className="pb-3">
|
||||
<Card className="flex flex-col h-full w-full">
|
||||
<CardHeader className="pb-3 flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg font-medium truncate">
|
||||
{widget.title}
|
||||
</CardTitle>
|
||||
<div className="flex items-center space-x-2 flex-1 min-w-0">
|
||||
{/* Drag Handle */}
|
||||
<div
|
||||
className="widget-drag-handle flex-shrink-0 p-1 rounded hover:bg-light-secondary dark:hover:bg-dark-secondary cursor-move transition-colors"
|
||||
title="Drag to move widget"
|
||||
>
|
||||
<GripVertical size={16} className="text-gray-400 dark:text-gray-500" />
|
||||
</div>
|
||||
|
||||
<CardTitle className="text-lg font-medium truncate">
|
||||
{widget.title}
|
||||
</CardTitle>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center space-x-2 flex-shrink-0">
|
||||
{/* Last updated date with refresh frequency tooltip */}
|
||||
<span
|
||||
className="text-xs text-gray-500 dark:text-gray-400"
|
||||
|
|
@ -81,43 +92,45 @@ const WidgetDisplay = ({
|
|||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex-1 max-h-[50vh] overflow-y-auto">
|
||||
{widget.isLoading ? (
|
||||
<div className="flex items-center justify-center py-8 text-gray-500 dark:text-gray-400">
|
||||
<RefreshCw size={20} className="animate-spin mr-2" />
|
||||
<span>Loading content...</span>
|
||||
</div>
|
||||
) : widget.error ? (
|
||||
<div className="flex items-start space-x-2 p-3 bg-red-50 dark:bg-red-900/20 rounded border border-red-200 dark:border-red-800">
|
||||
<AlertCircle
|
||||
size={16}
|
||||
className="text-red-500 mt-0.5 flex-shrink-0"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-red-800 dark:text-red-300">
|
||||
Error Loading Content
|
||||
</p>
|
||||
<p className="text-xs text-red-600 dark:text-red-400 mt-1">
|
||||
{widget.error}
|
||||
</p>
|
||||
<CardContent className="flex-1 overflow-hidden">
|
||||
<div className="h-full overflow-y-auto">
|
||||
{widget.isLoading ? (
|
||||
<div className="flex items-center justify-center py-8 text-gray-500 dark:text-gray-400">
|
||||
<RefreshCw size={20} className="animate-spin mr-2" />
|
||||
<span>Loading content...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : widget.content ? (
|
||||
<div className="prose prose-sm dark:prose-invert">
|
||||
<MarkdownRenderer content={widget.content} thinkOverlay={true} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center py-8 text-gray-500 dark:text-gray-400">
|
||||
<div className="text-center">
|
||||
<p className="text-sm">No content yet</p>
|
||||
<p className="text-xs mt-1">Click refresh to load content</p>
|
||||
) : widget.error ? (
|
||||
<div className="flex items-start space-x-2 p-3 bg-red-50 dark:bg-red-900/20 rounded border border-red-200 dark:border-red-800">
|
||||
<AlertCircle
|
||||
size={16}
|
||||
className="text-red-500 mt-0.5 flex-shrink-0"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-red-800 dark:text-red-300">
|
||||
Error Loading Content
|
||||
</p>
|
||||
<p className="text-xs text-red-600 dark:text-red-400 mt-1">
|
||||
{widget.error}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : widget.content ? (
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||
<MarkdownRenderer content={widget.content} thinkOverlay={true} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center py-8 text-gray-500 dark:text-gray-400">
|
||||
<div className="text-center">
|
||||
<p className="text-sm">No content yet</p>
|
||||
<p className="text-xs mt-1">Click refresh to load content</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
{/* Collapsible footer with sources and actions */}
|
||||
<div className="bg-light-secondary/30 dark:bg-dark-secondary/30">
|
||||
<div className="bg-light-secondary/30 dark:bg-dark-secondary/30 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => setIsFooterExpanded(!isFooterExpanded)}
|
||||
className="w-full px-4 py-2 flex items-center space-x-2 text-xs text-gray-500 dark:text-gray-400 hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors"
|
||||
|
|
|
|||
44
src/lib/constants/dashboard.ts
Normal file
44
src/lib/constants/dashboard.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// Dashboard-wide constants and constraints
|
||||
export const DASHBOARD_CONSTRAINTS = {
|
||||
// Grid layout constraints
|
||||
WIDGET_MIN_WIDTH: 2, // Minimum columns
|
||||
WIDGET_MAX_WIDTH: 12, // Maximum columns (full width)
|
||||
WIDGET_MIN_HEIGHT: 2, // Minimum rows
|
||||
WIDGET_MAX_HEIGHT: 20, // Maximum rows
|
||||
|
||||
// Default widget sizing
|
||||
DEFAULT_WIDGET_WIDTH: 6, // Half width by default
|
||||
DEFAULT_WIDGET_HEIGHT: 4, // Standard height
|
||||
|
||||
// Grid configuration
|
||||
GRID_COLUMNS: {
|
||||
lg: 12,
|
||||
md: 10,
|
||||
sm: 6,
|
||||
xs: 4,
|
||||
xxs: 2,
|
||||
},
|
||||
|
||||
GRID_BREAKPOINTS: {
|
||||
lg: 1200,
|
||||
md: 996,
|
||||
sm: 768,
|
||||
xs: 480,
|
||||
xxs: 0,
|
||||
},
|
||||
|
||||
GRID_ROW_HEIGHT: 60,
|
||||
GRID_MARGIN: [16, 16] as [number, number],
|
||||
GRID_CONTAINER_PADDING: [0, 0] as [number, number],
|
||||
} as const;
|
||||
|
||||
// Responsive constraints - adjust max width based on breakpoint
|
||||
export const getResponsiveConstraints = (breakpoint: keyof typeof DASHBOARD_CONSTRAINTS.GRID_COLUMNS) => {
|
||||
const maxCols = DASHBOARD_CONSTRAINTS.GRID_COLUMNS[breakpoint];
|
||||
return {
|
||||
minW: DASHBOARD_CONSTRAINTS.WIDGET_MIN_WIDTH,
|
||||
maxW: Math.min(DASHBOARD_CONSTRAINTS.WIDGET_MAX_WIDTH, maxCols),
|
||||
minH: DASHBOARD_CONSTRAINTS.WIDGET_MIN_HEIGHT,
|
||||
maxH: DASHBOARD_CONSTRAINTS.WIDGET_MAX_HEIGHT,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Widget, WidgetConfig } from '@/lib/types/widget';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { Widget, WidgetConfig, WidgetLayout } from '@/lib/types/widget';
|
||||
import {
|
||||
DashboardState,
|
||||
DashboardConfig,
|
||||
DashboardLayouts,
|
||||
GridLayoutItem,
|
||||
DASHBOARD_STORAGE_KEYS,
|
||||
} from '@/lib/types/dashboard';
|
||||
import { WidgetCache } from '@/lib/types/cache';
|
||||
import { DASHBOARD_CONSTRAINTS, getResponsiveConstraints } from '@/lib/constants/dashboard';
|
||||
|
||||
// Helper function to request location permission and get user's location
|
||||
const requestLocationPermission = async (): Promise<string | undefined> => {
|
||||
|
|
@ -81,6 +85,10 @@ interface UseDashboardReturn {
|
|||
refreshWidget: (id: string, forceRefresh?: boolean) => Promise<void>;
|
||||
refreshAllWidgets: (forceRefresh?: boolean) => Promise<void>;
|
||||
|
||||
// Layout management
|
||||
updateLayouts: (layouts: DashboardLayouts) => void;
|
||||
getLayouts: () => DashboardLayouts;
|
||||
|
||||
// Storage management
|
||||
exportDashboard: () => Promise<string>;
|
||||
importDashboard: (configJson: string) => Promise<void>;
|
||||
|
|
@ -108,11 +116,24 @@ export const useDashboard = (): UseDashboardReturn => {
|
|||
const savedWidgets = localStorage.getItem(DASHBOARD_STORAGE_KEYS.WIDGETS);
|
||||
const widgets: Widget[] = savedWidgets ? JSON.parse(savedWidgets) : [];
|
||||
|
||||
// Convert date strings back to Date objects
|
||||
widgets.forEach((widget) => {
|
||||
// Convert date strings back to Date objects and ensure layout exists
|
||||
widgets.forEach((widget, index) => {
|
||||
if (widget.lastUpdated) {
|
||||
widget.lastUpdated = new Date(widget.lastUpdated);
|
||||
}
|
||||
|
||||
// Migration: Add default layout if missing
|
||||
if (!widget.layout) {
|
||||
const defaultLayout: WidgetLayout = {
|
||||
x: (index % 2) * 6, // Alternate between columns
|
||||
y: Math.floor(index / 2) * 4, // Stack rows
|
||||
w: DASHBOARD_CONSTRAINTS.DEFAULT_WIDGET_WIDTH,
|
||||
h: DASHBOARD_CONSTRAINTS.DEFAULT_WIDGET_HEIGHT,
|
||||
isDraggable: true,
|
||||
isResizable: true,
|
||||
};
|
||||
widget.layout = defaultLayout;
|
||||
}
|
||||
});
|
||||
|
||||
// Load settings
|
||||
|
|
@ -167,6 +188,44 @@ export const useDashboard = (): UseDashboardReturn => {
|
|||
}, [state.settings]);
|
||||
|
||||
const addWidget = useCallback((config: WidgetConfig) => {
|
||||
// Find the next available position in the grid
|
||||
const getNextPosition = () => {
|
||||
const existingWidgets = state.widgets;
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
|
||||
// Simple algorithm: try to place in first available spot
|
||||
for (let row = 0; row < 20; row++) {
|
||||
for (let col = 0; col < 12; col += 6) { // Start with half-width widgets
|
||||
const position = { x: col, y: row };
|
||||
const hasCollision = existingWidgets.some(widget =>
|
||||
widget.layout.x < position.x + 6 &&
|
||||
widget.layout.x + widget.layout.w > position.x &&
|
||||
widget.layout.y < position.y + 3 &&
|
||||
widget.layout.y + widget.layout.h > position.y
|
||||
);
|
||||
|
||||
if (!hasCollision) {
|
||||
return { x: position.x, y: position.y };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: place at bottom
|
||||
const maxY = Math.max(0, ...existingWidgets.map(w => w.layout.y + w.layout.h));
|
||||
return { x: 0, y: maxY };
|
||||
};
|
||||
|
||||
const position = getNextPosition();
|
||||
const defaultLayout: WidgetLayout = {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
w: DASHBOARD_CONSTRAINTS.DEFAULT_WIDGET_WIDTH,
|
||||
h: DASHBOARD_CONSTRAINTS.DEFAULT_WIDGET_HEIGHT,
|
||||
isDraggable: true,
|
||||
isResizable: true,
|
||||
};
|
||||
|
||||
const newWidget: Widget = {
|
||||
...config,
|
||||
id: Date.now().toString() + Math.random().toString(36).substr(2, 9),
|
||||
|
|
@ -174,20 +233,26 @@ export const useDashboard = (): UseDashboardReturn => {
|
|||
isLoading: false,
|
||||
content: null,
|
||||
error: null,
|
||||
layout: config.layout || defaultLayout,
|
||||
};
|
||||
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
widgets: [...prev.widgets, newWidget],
|
||||
}));
|
||||
}, []);
|
||||
}, [state.widgets]);
|
||||
|
||||
const updateWidget = useCallback((id: string, config: WidgetConfig) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
widgets: prev.widgets.map((widget) =>
|
||||
widget.id === id
|
||||
? { ...widget, ...config, id } // Preserve the ID
|
||||
? {
|
||||
...widget,
|
||||
...config,
|
||||
id, // Preserve the ID
|
||||
layout: config.layout || widget.layout, // Preserve existing layout if not provided
|
||||
}
|
||||
: widget,
|
||||
),
|
||||
}));
|
||||
|
|
@ -436,6 +501,63 @@ export const useDashboard = (): UseDashboardReturn => {
|
|||
[],
|
||||
);
|
||||
|
||||
const getLayouts = useCallback((): DashboardLayouts => {
|
||||
const createBreakpointLayout = (breakpoint: keyof typeof DASHBOARD_CONSTRAINTS.GRID_COLUMNS) => {
|
||||
const constraints = getResponsiveConstraints(breakpoint);
|
||||
const maxCols = DASHBOARD_CONSTRAINTS.GRID_COLUMNS[breakpoint];
|
||||
|
||||
return state.widgets.map(widget => ({
|
||||
i: widget.id,
|
||||
x: widget.layout.x,
|
||||
y: widget.layout.y,
|
||||
w: Math.min(widget.layout.w, maxCols), // Constrain width to available columns
|
||||
h: widget.layout.h,
|
||||
minW: constraints.minW,
|
||||
maxW: constraints.maxW,
|
||||
minH: constraints.minH,
|
||||
maxH: constraints.maxH,
|
||||
static: widget.layout.static,
|
||||
isDraggable: widget.layout.isDraggable,
|
||||
isResizable: widget.layout.isResizable,
|
||||
}));
|
||||
};
|
||||
|
||||
return {
|
||||
lg: createBreakpointLayout('lg'),
|
||||
md: createBreakpointLayout('md'),
|
||||
sm: createBreakpointLayout('sm'),
|
||||
xs: createBreakpointLayout('xs'),
|
||||
xxs: createBreakpointLayout('xxs'),
|
||||
};
|
||||
}, [state.widgets]);
|
||||
|
||||
const updateLayouts = useCallback((layouts: DashboardLayouts) => {
|
||||
const updatedWidgets = state.widgets.map(widget => {
|
||||
// Use lg layout as the primary layout for position and size updates
|
||||
const newLayout = layouts.lg.find((layout: Layout) => layout.i === widget.id);
|
||||
if (newLayout) {
|
||||
return {
|
||||
...widget,
|
||||
layout: {
|
||||
x: newLayout.x,
|
||||
y: newLayout.y,
|
||||
w: newLayout.w,
|
||||
h: newLayout.h,
|
||||
static: newLayout.static || widget.layout.static,
|
||||
isDraggable: newLayout.isDraggable ?? widget.layout.isDraggable,
|
||||
isResizable: newLayout.isResizable ?? widget.layout.isResizable,
|
||||
},
|
||||
};
|
||||
}
|
||||
return widget;
|
||||
});
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
widgets: updatedWidgets,
|
||||
}));
|
||||
}, [state.widgets]);
|
||||
|
||||
return {
|
||||
// State
|
||||
widgets: state.widgets,
|
||||
|
|
@ -450,6 +572,10 @@ export const useDashboard = (): UseDashboardReturn => {
|
|||
refreshWidget,
|
||||
refreshAllWidgets,
|
||||
|
||||
// Layout management
|
||||
updateLayouts,
|
||||
getLayouts,
|
||||
|
||||
// Storage management
|
||||
exportDashboard,
|
||||
importDashboard,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// Dashboard configuration and state types
|
||||
import { Widget } from './widget';
|
||||
import { Widget, WidgetLayout } from './widget';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
|
||||
export interface DashboardConfig {
|
||||
widgets: Widget[];
|
||||
|
|
@ -19,9 +20,25 @@ export interface DashboardState {
|
|||
settings: DashboardConfig['settings'];
|
||||
}
|
||||
|
||||
// Layout item for react-grid-layout (extends WidgetLayout with required 'i' property)
|
||||
export interface GridLayoutItem extends WidgetLayout {
|
||||
i: string; // Widget ID
|
||||
}
|
||||
|
||||
// Layout configuration for responsive grid (compatible with react-grid-layout)
|
||||
export interface DashboardLayouts {
|
||||
lg: Layout[];
|
||||
md: Layout[];
|
||||
sm: Layout[];
|
||||
xs: Layout[];
|
||||
xxs: Layout[];
|
||||
[key: string]: Layout[]; // Index signature for react-grid-layout compatibility
|
||||
}
|
||||
|
||||
// Local storage keys
|
||||
export const DASHBOARD_STORAGE_KEYS = {
|
||||
WIDGETS: 'perplexica_dashboard_widgets',
|
||||
SETTINGS: 'perplexica_dashboard_settings',
|
||||
CACHE: 'perplexica_dashboard_cache',
|
||||
LAYOUTS: 'perplexica_dashboard_layouts',
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,17 @@ export interface Source {
|
|||
type: 'Web Page' | 'HTTP Data';
|
||||
}
|
||||
|
||||
// Grid layout properties for widgets (only position and size data that should be persisted)
|
||||
export interface WidgetLayout {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
static?: boolean;
|
||||
isDraggable?: boolean;
|
||||
isResizable?: boolean;
|
||||
}
|
||||
|
||||
export interface WidgetConfig {
|
||||
id?: string;
|
||||
title: string;
|
||||
|
|
@ -14,6 +25,7 @@ export interface WidgetConfig {
|
|||
refreshFrequency: number;
|
||||
refreshUnit: 'minutes' | 'hours';
|
||||
tool_names?: string[];
|
||||
layout?: WidgetLayout;
|
||||
}
|
||||
|
||||
export interface Widget extends WidgetConfig {
|
||||
|
|
@ -22,4 +34,5 @@ export interface Widget extends WidgetConfig {
|
|||
isLoading: boolean;
|
||||
content: string | null;
|
||||
error: string | null;
|
||||
layout: WidgetLayout;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue