diff --git a/frontend/src/components/InlineVisualizationCard.tsx b/frontend/src/components/InlineVisualizationCard.tsx index b805d75..1aea1ce 100644 --- a/frontend/src/components/InlineVisualizationCard.tsx +++ b/frontend/src/components/InlineVisualizationCard.tsx @@ -1,7 +1,8 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription, DialogFooter } from "@/components/ui/dialog"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Code, Table as TableIcon, BarChart as ChartIcon, LayoutDashboard, Copy, Check } from "lucide-react"; import { ScrollArea } from "@/components/ui/scroll-area"; @@ -31,11 +32,24 @@ export function InlineVisualizationCard({ viz }: InlineVisualizationCardProps) { const [confirmOpen, setConfirmOpen] = useState(false); const [copied, setCopied] = useState(false); const [pendingChart, setPendingChart] = useState | null>(null); - const { addChart } = useDashboardStore(); + const [selectedDashboardId, setSelectedDashboardId] = useState(''); + const { dashboards, addChart, loadDashboards } = useDashboardStore(); const { currentProject } = useProjectStore(); const objectRows = viz.rows.filter((row) => row && typeof row === "object" && !Array.isArray(row)) as Record[]; const columns = objectRows.length > 0 ? Object.keys(objectRows[0]) : []; + useEffect(() => { + if (currentProject) { + loadDashboards(currentProject.id); + } + }, [currentProject, loadDashboards]); + + useEffect(() => { + if (dashboards.length > 0 && !selectedDashboardId) { + setSelectedDashboardId(dashboards[0].id); + } + }, [dashboards, selectedDashboardId]); + const buildPendingChart = (): Omit => { if (view === "table") { return { @@ -68,8 +82,8 @@ export function InlineVisualizationCard({ viz }: InlineVisualizationCardProps) { }; const handleConfirmAdd = () => { - if (!pendingChart || !currentProject) return; - addChart(pendingChart, currentProject.id); + if (!pendingChart || !currentProject || !selectedDashboardId) return; + addChart(pendingChart, selectedDashboardId, currentProject.id); setConfirmOpen(false); setPendingChart(null); }; @@ -165,7 +179,7 @@ export function InlineVisualizationCard({ viz }: InlineVisualizationCardProps) {
- @@ -206,11 +220,26 @@ export function InlineVisualizationCard({ viz }: InlineVisualizationCardProps) { - {t('confirmAddToDashboard')} + {t('pinChartToDashboard')} - {t('confirmAddChartToDashboardDesc')} + {t('selectDashboardToPin')} +
+ + +
-
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index adb1380..91dbb20 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -6,6 +6,8 @@ import { useState, useRef, useEffect } from "react"; import { Link, useNavigate, useLocation } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { useAuthStore } from "@/store/authStore"; +import { useProjectStore } from "@/store/projectStore"; +import { useDashboardStore } from "@/store/dashboardStore"; import { api } from "@/lib/api"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; @@ -23,39 +25,26 @@ interface SessionInfo { }; } -function Section({ +function SectionHeader({ title, count, + isSelectionMode, + setIsSelectionMode, + selectedKeys, + setSelectedKeys, items, - onSelect, - onDelete, - onRename, - onTogglePinned, - onToggleArchived, - onBatchDelete, - activeKey + onBatchDelete }: { title: string; count: number; + isSelectionMode: boolean; + setIsSelectionMode: (val: boolean) => void; + selectedKeys: string[]; + setSelectedKeys: (val: string[] | ((prev: string[]) => string[])) => void; items: SessionInfo[]; - onSelect: (key: string) => void; - onDelete: (key: string) => void; - onRename: (key: string, currentTitle: string) => void; - onTogglePinned: (key: string, pinned: boolean) => void; - onToggleArchived: (key: string, archived: boolean) => void; onBatchDelete: (keys: string[]) => void; - activeKey: string | null; }) { const { t } = useTranslation(); - const [selectedKeys, setSelectedKeys] = useState([]); - const [isSelectionMode, setIsSelectionMode] = useState(false); - - const toggleSelect = (key: string, e: React.MouseEvent) => { - e.stopPropagation(); - setSelectedKeys(prev => - prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key] - ); - }; const handleSelectAll = (e: React.MouseEvent) => { e.stopPropagation(); @@ -84,12 +73,12 @@ function Section({ if (!isSelectionMode) { setSelectedKeys([]); } - }, [isSelectionMode]); + }, [isSelectionMode, setSelectedKeys]); return ( -
-
-
+
+
+
{title} ({count})
@@ -140,7 +129,49 @@ function Section({ )}
-
+
+ ); +} + +function Section({ + items, + onSelect, + onDelete, + onRename, + onTogglePinned, + onToggleArchived, + onBatchDelete, + activeKey, + isSelectionMode, + setIsSelectionMode, + selectedKeys, + setSelectedKeys +}: { + items: SessionInfo[]; + onSelect: (key: string) => void; + onDelete: (key: string) => void; + onRename: (key: string, currentTitle: string) => void; + onTogglePinned: (key: string, pinned: boolean) => void; + onToggleArchived: (key: string, archived: boolean) => void; + onBatchDelete: (keys: string[]) => void; + activeKey: string | null; + isSelectionMode: boolean; + setIsSelectionMode: (val: boolean) => void; + selectedKeys: string[]; + setSelectedKeys: (val: string[] | ((prev: string[]) => string[])) => void; +}) { + const { t } = useTranslation(); + + const toggleSelect = (key: string, e: React.MouseEvent) => { + e.stopPropagation(); + setSelectedKeys(prev => + prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key] + ); + }; + + return ( +
+
{items.map((item) => { const displayTitle = item.metadata?.title || item.key.replace("api:", ""); const isActive = activeKey === item.key; @@ -149,11 +180,11 @@ function Section({ return (
isSelectionMode ? toggleSelect(item.key, e) : onSelect(item.key)} - > + onClick={(e) => isSelectionMode ? toggleSelect(item.key, e) : onSelect(item.key)} + >
{isSelectionMode ? ( {t('deleteSession')} - + )}
); @@ -252,10 +283,100 @@ function Section({ ); } +function DashboardSection({ + title, + count, + items, + onSelect, + onDelete, + onRename, + onCreate, + activeId +}: { + title: string; + count: number; + items: {id: string, name: string}[]; + onSelect: (id: string) => void; + onDelete: (id: string) => void; + onRename: (id: string, currentName: string) => void; + onCreate: () => void; + activeId: string | null; +}) { + const { t } = useTranslation(); + return ( +
+
+
+ {title} + ({count}) +
+ +
+
+ {items.map((item) => { + const isActive = activeId === item.id; + return ( +
onSelect(item.id)} + > +
+ + + + {item.name} +
+ + e.stopPropagation()} className="h-6 w-6 flex items-center justify-center rounded hover:bg-zinc-200 text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity outline-none"> + + + + { + e.preventDefault(); + e.stopPropagation(); + onRename(item.id, item.name); + }} + > + + {t('rename')} + + { + e.preventDefault(); + e.stopPropagation(); + onDelete(item.id); + }} + className="text-red-600 focus:text-red-600 focus:bg-red-50" + > + + {t('deleteSession')} + + + +
+ ); + })} +
+
+ ); +} + function SidebarBody() { const navigate = useNavigate(); const location = useLocation(); const { user, logout } = useAuthStore(); + const { currentProject } = useProjectStore(); + const { dashboards, activeDashboardId, loadDashboards, createDashboard, deleteDashboard, renameDashboard, setActiveDashboard } = useDashboardStore(); const { t, i18n } = useTranslation(); const [showUserMenu, setShowUserMenu] = useState(false); const menuRef = useRef(null); @@ -266,11 +387,26 @@ function SidebarBody() { const [renameDialogOpen, setRenameDialogOpen] = useState(false); const [sessionToRename, setSessionToRename] = useState<{key: string, title: string} | null>(null); const [newTitle, setNewTitle] = useState(""); - + + const [activeSelectionMode, setActiveSelectionMode] = useState(false); + const [activeSelectedKeys, setActiveSelectedKeys] = useState([]); + const [archivedSelectionMode, setArchivedSelectionMode] = useState(false); + const [archivedSelectedKeys, setArchivedSelectedKeys] = useState([]); + + const [dashboardRenameDialogOpen, setDashboardRenameDialogOpen] = useState(false); + const [dashboardToRename, setDashboardToRename] = useState<{id: string, name: string} | null>(null); + const [newDashboardName, setNewDashboardName] = useState(""); + // Try to parse active session from URL query const queryParams = new URLSearchParams(location.search); const activeSessionKey = queryParams.get("session") || "api:default"; + useEffect(() => { + if (currentProject) { + loadDashboards(currentProject.id); + } + }, [currentProject, loadDashboards]); + const fetchSessions = async () => { try { const data = await api.get("/nanobot/sessions"); @@ -440,6 +576,44 @@ function SidebarBody() { return title.includes(normalizedFilter); }); + const handleCreateDashboard = () => { + if (!currentProject) return; + if (dashboards.length >= 3) { + alert(t('dashboardLimitReached') || "You can only create up to 3 dashboards."); + return; + } + createDashboard(t('newDashboardNameDefault'), currentProject.id); + navigate(`/dashboard`); + }; + + const handleSelectDashboard = (id: string) => { + setActiveDashboard(id); + navigate(`/dashboard`); + }; + + const openDashboardRenameDialog = (id: string, name: string) => { + setDashboardToRename({ id, name }); + setNewDashboardName(name); + setDashboardRenameDialogOpen(true); + }; + + const handleDashboardRename = () => { + if (!currentProject || !dashboardToRename || !newDashboardName.trim()) return; + renameDashboard(dashboardToRename.id, newDashboardName.trim(), currentProject.id); + setDashboardRenameDialogOpen(false); + }; + + const handleDashboardDelete = (id: string) => { + if (!currentProject) return; + if (!window.confirm(t('confirmDeleteDashboard'))) return; + deleteDashboard(id, currentProject.id); + }; + + const filteredDashboards = dashboards.filter((d) => { + if (!normalizedFilter) return true; + return d.name.toLowerCase().includes(normalizedFilter); + }); + return (
{/* Header */} @@ -453,63 +627,103 @@ function SidebarBody() {
-
- +
+ ({ id: d.id, name: d.name }))} + onSelect={handleSelectDashboard} + onDelete={handleDashboardDelete} + onRename={openDashboardRenameDialog} + onCreate={handleCreateDashboard} + activeId={location.pathname === "/dashboard" ? activeDashboardId : null} + /> - -
+
+ +
- -
+
setSessionFilter(e.target.value)} placeholder={t('filterSessionName')} - className="pl-9 h-9 border-zinc-200 bg-white" + className="pl-9 h-9 border-zinc-200 bg-white text-[14px]" />
-
-
- +
+ +
+
+ + +
+ +
+ + {archivedSessions.length > 0 && ( +
+ + +
+ +
+ )} +
@@ -536,6 +750,31 @@ function SidebarBody() { + + + + {t('renameDashboard')} + +
+ setNewDashboardName(e.target.value)} + placeholder={t('enterNewDashboardName')} + autoFocus + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleDashboardRename(); + } + }} + /> +
+ + + + +
+
+
- @@ -208,11 +222,26 @@ export function VisualizationPanel() { - {t('confirmAddToDashboard')} + {t('pinChartToDashboard')} - {t('confirmAddChartToDashboardDesc')} + {t('selectDashboardToPin')} +
+ + +
- +
diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 986e869..6b24fdd 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -10,18 +10,18 @@ "unarchive": "Unarchive", "deleteSession": "Delete Session", "confirmDeleteSession": "Are you sure you want to delete this session?", - "confirmBatchDeleteSessions": "Are you sure you want to delete the selected {{count}} sessions?", - "filterSessionName": "Filter session name", - "renameSession": "Rename Session", - "enterNewSessionTitle": "Enter new session title", + "confirmBatchDeleteSessions": "Are you sure you want to delete the selected {{count}} threads?", + "filterSessionName": "Filter thread name", + "renameSession": "Rename thread", + "enterNewSessionTitle": "Enter new thread title", "save": "Save", "lobsterDataQA": "DataClaw", "skillCenter": "Skill Center", "projectManagement": "Project Management", - "dataSourceManagement": "Data Sources", - "personalSettings": "Settings", - "modelConfig": "Model Config", - "userManagement": "Users", + "dataSourceManagement": "Data Source Management", + "personalSettings": "Personal Settings", + "modelConfig": "Model Configuration", + "userManagement": "User Management", "logout": "Logout", "searchModel": "Search model...", "modelNotFound": "Model not found", @@ -204,5 +204,19 @@ "newThread": "New Thread", "threads": "THREADS", "archivedThreads": "ARCHIVED THREADS", - "defaultUser": "User" + "defaultUser": "Default User", + "searchSkills": "Search skills...", + "selectDashboard": "Select a dashboard", + "submit": "Submit", + "noDashboardsInCurrentProject": "No dashboards in current project", + "createDashboardToGetStarted": "Create a new dashboard from the sidebar to get started", + "confirmDeleteDashboard": "Are you sure you want to delete this dashboard?", + "renameDashboard": "Rename Dashboard", + "enterNewDashboardName": "Enter new dashboard name", + "newDashboardNameDefault": "New Dashboard", + "dashboardLimitReached": "You can only create up to 3 dashboards.", + "dashboards": "Dashboards", + "new": "New", + "pinChartToDashboard": "Pin chart to dashboard", + "selectDashboardToPin": "Select a dashboard to pin this chart to." } diff --git a/frontend/src/i18n/locales/zh.json b/frontend/src/i18n/locales/zh.json index acd221a..3cba2fc 100644 --- a/frontend/src/i18n/locales/zh.json +++ b/frontend/src/i18n/locales/zh.json @@ -1,5 +1,5 @@ { - "selectAllOrCancel": "全选/取消全选", + "selectAllOrCancel": "全选 / 取消全选", "invertSelection": "反选", "batchDelete": "批量删除", "cancel": "取消", @@ -11,6 +11,19 @@ "deleteSession": "删除会话", "confirmDeleteSession": "确定要删除这个会话吗?", "confirmBatchDeleteSessions": "确定要删除选中的 {{count}} 个会话吗?", + "confirmDeleteDashboard": "确定要删除此仪表盘吗?", + "renameDashboard": "重命名仪表盘", + "enterNewDashboardName": "输入新的仪表盘名称", + "newDashboardNameDefault": "新仪表盘", + "dashboardLimitReached": "最多只能创建 3 个仪表盘。", + "dashboards": "仪表盘", + "new": "新建", + "pinChartToDashboard": "固定图表到仪表盘", + "selectDashboardToPin": "选择一个仪表盘以固定此图表。", + "selectDashboard": "选择一个仪表盘", + "submit": "提交", + "noDashboardsInCurrentProject": "当前项目下没有仪表盘", + "createDashboardToGetStarted": "从侧边栏创建一个新仪表盘以开始", "filterSessionName": "过滤会话名称", "renameSession": "重命名会话", "enterNewSessionTitle": "输入新的会话标题", @@ -204,5 +217,6 @@ "newThread": "新会话", "threads": "会话", "archivedThreads": "已归档会话", - "defaultUser": "用户" + "defaultUser": "默认用户", + "searchSkills": "搜索技能..." } diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index f1be1f4..267533a 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -45,14 +45,20 @@ function inferChartKeys(data: Record[]) { export function Dashboard() { const { t } = useTranslation(); - const { charts, removeChart, updateLayout, loadCharts } = useDashboardStore(); + const { dashboards, activeDashboardId, removeChart, updateLayout, loadDashboards } = useDashboardStore(); const { currentProject } = useProjectStore(); useEffect(() => { if (currentProject) { - loadCharts(currentProject.id); + loadDashboards(currentProject.id); } - }, [currentProject, loadCharts]); + }, [currentProject, loadDashboards]); + + const activeDashboard = useMemo(() => { + return dashboards.find((d) => d.id === activeDashboardId) || dashboards[0] || null; + }, [dashboards, activeDashboardId]); + + const charts = activeDashboard?.charts || []; const ResponsiveGridLayout = useMemo( () => WidthProvider(Responsive as any) as any, @@ -64,7 +70,7 @@ export function Dashboard() { }), [charts]); const onLayoutChange = (currentLayout: any[]) => { - if (currentProject) { + if (currentProject && activeDashboard) { updateLayout( currentLayout.map((item) => ({ i: item.i, @@ -73,6 +79,7 @@ export function Dashboard() { w: item.w, h: item.h, })), + activeDashboard.id, currentProject.id ); } @@ -86,7 +93,16 @@ export function Dashboard() { ); } - if (charts.length === 0) { + if (dashboards.length === 0) { + return ( +
+

{t('noDashboardsInCurrentProject')}

+

{t('createDashboardToGetStarted')}

+
+ ); + } + + if (!activeDashboard || charts.length === 0) { return (

{t('noChartsInCurrentProject')}

@@ -97,7 +113,7 @@ export function Dashboard() { return (
-

Dashboard

+

{activeDashboard.name || t('dashboardMenu')}

removeChart(chart.id, currentProject.id)} + onClick={() => removeChart(chart.id, activeDashboard.id, currentProject.id)} > diff --git a/frontend/src/store/dashboardStore.ts b/frontend/src/store/dashboardStore.ts index bafee65..cf8ab11 100644 --- a/frontend/src/store/dashboardStore.ts +++ b/frontend/src/store/dashboardStore.ts @@ -14,80 +14,164 @@ export interface ChartConfig { layout: GridLayout; } -interface DashboardState { +export interface DashboardConfig { + id: string; + name: string; + createdAt: number; charts: ChartConfig[]; - addChart: (chart: Omit, projectId: number) => void; - removeChart: (id: string, projectId: number) => void; - updateLayout: (layouts: GridLayout[], projectId: number) => void; - loadCharts: (projectId: number) => void; } -const DASHBOARD_STORAGE_KEY_PREFIX = 'dashboard_charts_v1_project_'; +interface DashboardState { + dashboards: DashboardConfig[]; + activeDashboardId: string | null; + loadDashboards: (projectId: number) => void; + createDashboard: (name: string, projectId: number) => string; + deleteDashboard: (id: string, projectId: number) => void; + renameDashboard: (id: string, newName: string, projectId: number) => void; + setActiveDashboard: (id: string | null) => void; + addChart: (chart: Omit, dashboardId: string, projectId: number) => void; + removeChart: (chartId: string, dashboardId: string, projectId: number) => void; + updateLayout: (layouts: GridLayout[], dashboardId: string, projectId: number) => void; +} + +const DASHBOARD_STORAGE_KEY_PREFIX = 'dashboards_v2_project_'; function getStorageKey(projectId: number) { return `${DASHBOARD_STORAGE_KEY_PREFIX}${projectId}`; } -function loadChartsFromStorage(projectId: number): ChartConfig[] { +function loadDashboardsFromStorage(projectId: number): DashboardConfig[] { if (typeof window === 'undefined') return []; try { const raw = window.localStorage.getItem(getStorageKey(projectId)); - if (!raw) return []; - const parsed = JSON.parse(raw); - if (!Array.isArray(parsed)) return []; - return parsed - .filter((item): item is ChartConfig => Boolean(item?.id && item?.layout)) - .map((item) => ({ - ...item, - layout: { - i: item.layout.i, - x: Number.isFinite(item.layout.x) ? item.layout.x : 0, - y: Number.isFinite(item.layout.y) ? item.layout.y : 0, - w: Number.isFinite(item.layout.w) ? item.layout.w : 4, - h: Number.isFinite(item.layout.h) ? item.layout.h : 4, - }, - })); + if (raw) { + const parsed = JSON.parse(raw); + if (Array.isArray(parsed)) { + return parsed.map((d: any) => ({ + ...d, + charts: Array.isArray(d.charts) ? d.charts.map((item: any) => ({ + ...item, + layout: { + i: item.layout?.i || item.id, + x: Number.isFinite(item.layout?.x) ? item.layout.x : 0, + y: Number.isFinite(item.layout?.y) ? item.layout.y : 0, + w: Number.isFinite(item.layout?.w) ? item.layout.w : 4, + h: Number.isFinite(item.layout?.h) ? item.layout.h : 4, + }, + })) : [] + })); + } + } + + // Migration from v1 + const oldRaw = window.localStorage.getItem(`dashboard_charts_v1_project_${projectId}`); + if (oldRaw) { + const parsed = JSON.parse(oldRaw); + if (Array.isArray(parsed) && parsed.length > 0) { + const defaultDashboard: DashboardConfig = { + id: 'default', + name: 'Default Dashboard', + createdAt: Date.now(), + charts: parsed.map((item: any) => ({ + ...item, + layout: { + i: item.layout?.i || item.id, + x: Number.isFinite(item.layout?.x) ? item.layout.x : 0, + y: Number.isFinite(item.layout?.y) ? item.layout.y : 0, + w: Number.isFinite(item.layout?.w) ? item.layout.w : 4, + h: Number.isFinite(item.layout?.h) ? item.layout.h : 4, + }, + })), + }; + saveDashboardsToStorage([defaultDashboard], projectId); + return [defaultDashboard]; + } + } + return []; } catch { return []; } } -function saveChartsToStorage(charts: ChartConfig[], projectId: number) { +function saveDashboardsToStorage(dashboards: DashboardConfig[], projectId: number) { if (typeof window === 'undefined') return; - window.localStorage.setItem(getStorageKey(projectId), JSON.stringify(charts)); + window.localStorage.setItem(getStorageKey(projectId), JSON.stringify(dashboards)); } export const useDashboardStore = create((set) => ({ - charts: [], - loadCharts: (projectId) => { - set({ charts: loadChartsFromStorage(projectId) }); + dashboards: [], + activeDashboardId: null, + loadDashboards: (projectId) => { + const dashboards = loadDashboardsFromStorage(projectId); + set({ dashboards, activeDashboardId: dashboards.length > 0 ? dashboards[0].id : null }); }, - addChart: (chart, projectId) => set((state) => { - const colSize = 4; - const cols = 12 / colSize; - const index = state.charts.length; - const newLayout: GridLayout = { - i: chart.id, - x: (index % cols) * colSize, - y: Math.floor(index / cols) * 4, - w: colSize, - h: 4, - }; - const nextCharts = [...state.charts, { ...chart, layout: newLayout }]; - saveChartsToStorage(nextCharts, projectId); - return { charts: nextCharts }; - }), - removeChart: (id, projectId) => set((state) => { - const nextCharts = state.charts.filter((c) => c.id !== id); - saveChartsToStorage(nextCharts, projectId); - return { charts: nextCharts }; - }), - updateLayout: (layouts, projectId) => set((state) => { - const nextCharts = state.charts.map((chart) => { - const layout = layouts.find((l) => l.i === chart.id); - return layout ? { ...chart, layout } : chart; + createDashboard: (name, projectId) => { + const newId = Date.now().toString(); + set((state) => { + const newDashboard: DashboardConfig = { + id: newId, + name, + createdAt: Date.now(), + charts: [], + }; + const nextDashboards = [...state.dashboards, newDashboard]; + saveDashboardsToStorage(nextDashboards, projectId); + return { dashboards: nextDashboards, activeDashboardId: newId }; }); - saveChartsToStorage(nextCharts, projectId); - return { charts: nextCharts }; + return newId; + }, + deleteDashboard: (id, projectId) => set((state) => { + const nextDashboards = state.dashboards.filter((d) => d.id !== id); + saveDashboardsToStorage(nextDashboards, projectId); + return { + dashboards: nextDashboards, + activeDashboardId: state.activeDashboardId === id ? (nextDashboards.length > 0 ? nextDashboards[0].id : null) : state.activeDashboardId, + }; + }), + renameDashboard: (id, newName, projectId) => set((state) => { + const nextDashboards = state.dashboards.map((d) => d.id === id ? { ...d, name: newName } : d); + saveDashboardsToStorage(nextDashboards, projectId); + return { dashboards: nextDashboards }; + }), + setActiveDashboard: (id) => set({ activeDashboardId: id }), + addChart: (chart, dashboardId, projectId) => set((state) => { + const nextDashboards = state.dashboards.map((d) => { + if (d.id !== dashboardId) return d; + const colSize = 4; + const cols = 12 / colSize; + const index = d.charts.length; + const newLayout: GridLayout = { + i: chart.id, + x: (index % cols) * colSize, + y: Math.floor(index / cols) * 4, + w: colSize, + h: 4, + }; + return { ...d, charts: [...d.charts, { ...chart, layout: newLayout }] }; + }); + saveDashboardsToStorage(nextDashboards, projectId); + return { dashboards: nextDashboards }; + }), + removeChart: (chartId, dashboardId, projectId) => set((state) => { + const nextDashboards = state.dashboards.map((d) => { + if (d.id !== dashboardId) return d; + return { ...d, charts: d.charts.filter((c) => c.id !== chartId) }; + }); + saveDashboardsToStorage(nextDashboards, projectId); + return { dashboards: nextDashboards }; + }), + updateLayout: (layouts, dashboardId, projectId) => set((state) => { + const nextDashboards = state.dashboards.map((d) => { + if (d.id !== dashboardId) return d; + return { + ...d, + charts: d.charts.map((chart) => { + const layout = layouts.find((l) => l.i === chart.id); + return layout ? { ...chart, layout } : chart; + }) + }; + }); + saveDashboardsToStorage(nextDashboards, projectId); + return { dashboards: nextDashboards }; }), }));