fix: dashboard persistent

This commit is contained in:
qixinbo
2026-03-15 18:11:26 +08:00
parent e60d8c0658
commit db841b18b9
3 changed files with 140 additions and 22 deletions
@@ -1,11 +1,11 @@
import { useState } from "react"; import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription, DialogFooter } from "@/components/ui/dialog";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Code, Table as TableIcon, BarChart as ChartIcon, LayoutDashboard } from "lucide-react"; import { Code, Table as TableIcon, BarChart as ChartIcon, LayoutDashboard } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { useDashboardStore } from "@/store/dashboardStore"; import { useDashboardStore, type ChartConfig } from "@/store/dashboardStore";
import type { ChartSpec } from "@/store/visualizationStore"; import type { ChartSpec } from "@/store/visualizationStore";
import { VegaChart } from "./VegaChart"; import { VegaChart } from "./VegaChart";
@@ -22,22 +22,37 @@ interface InlineVisualizationCardProps {
export function InlineVisualizationCard({ viz }: InlineVisualizationCardProps) { export function InlineVisualizationCard({ viz }: InlineVisualizationCardProps) {
const [view, setView] = useState<'table' | 'chart'>('chart'); const [view, setView] = useState<'table' | 'chart'>('chart');
const [confirmOpen, setConfirmOpen] = useState(false);
const [pendingChart, setPendingChart] = useState<Omit<ChartConfig, 'layout'> | null>(null);
const { addChart } = useDashboardStore(); const { addChart } = useDashboardStore();
const objectRows = viz.rows.filter((row) => row && typeof row === "object" && !Array.isArray(row)) as Record<string, unknown>[]; const objectRows = viz.rows.filter((row) => row && typeof row === "object" && !Array.isArray(row)) as Record<string, unknown>[];
const columns = objectRows.length > 0 ? Object.keys(objectRows[0]) : []; const columns = objectRows.length > 0 ? Object.keys(objectRows[0]) : [];
const handleAddToDashboard = () => { const buildPendingChart = (): Omit<ChartConfig, 'layout'> => {
const mark = viz.chartSpec?.mark; const mark = viz.chartSpec?.mark;
const markType = typeof mark === "string" ? mark : mark?.type; const markType = typeof mark === "string" ? mark : mark?.type;
const dashboardType = markType === "line" ? "line" : "bar"; const dashboardType = markType === "line" ? "line" : "bar";
addChart({ return {
id: Date.now().toString(), id: Date.now().toString(),
title: viz.chartSpec?.title || "Generated Analysis", title: viz.chartSpec?.title || "Generated Analysis",
type: dashboardType, type: dashboardType,
data: objectRows, data: objectRows,
sql: viz.sql, sql: viz.sql,
chartSpec: viz.chartSpec, chartSpec: viz.chartSpec,
}); };
};
const handleAddToDashboard = () => {
const chart = buildPendingChart();
setPendingChart(chart);
setConfirmOpen(true);
};
const handleConfirmAdd = () => {
if (!pendingChart) return;
addChart(pendingChart);
setConfirmOpen(false);
setPendingChart(null);
}; };
if (viz.error) { if (viz.error) {
@@ -128,6 +143,30 @@ export function InlineVisualizationCard({ viz }: InlineVisualizationCardProps) {
<div className="text-sm text-zinc-500"></div> <div className="text-sm text-zinc-500"></div>
)} )}
</CardContent> </CardContent>
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle> Dashboard</DialogTitle>
<DialogDescription>
Dashboard
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setConfirmOpen(false);
setPendingChart(null);
}}
>
</Button>
<Button onClick={handleConfirmAdd}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Card> </Card>
); );
} }
+44 -7
View File
@@ -1,33 +1,48 @@
import { useState } from "react"; import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription, DialogFooter } from "@/components/ui/dialog";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Code, Table as TableIcon, BarChart as ChartIcon, Download, LayoutDashboard, Loader2 } from "lucide-react"; import { Code, Table as TableIcon, BarChart as ChartIcon, Download, LayoutDashboard, Loader2 } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { useDashboardStore } from "@/store/dashboardStore"; import { useDashboardStore, type ChartConfig } from "@/store/dashboardStore";
import { useVisualizationStore } from "@/store/visualizationStore"; import { useVisualizationStore } from "@/store/visualizationStore";
import { VegaChart } from "./VegaChart"; import { VegaChart } from "./VegaChart";
export function VisualizationPanel() { export function VisualizationPanel() {
const [view, setView] = useState<'table' | 'chart'>('chart'); const [view, setView] = useState<'table' | 'chart'>('chart');
const [confirmOpen, setConfirmOpen] = useState(false);
const [pendingChart, setPendingChart] = useState<Omit<ChartConfig, 'layout'> | null>(null);
const { addChart } = useDashboardStore(); const { addChart } = useDashboardStore();
const { currentData, currentSQL, currentChartSpec, currentChartInfo, isLoading, error } = useVisualizationStore(); const { currentData, currentSQL, currentChartSpec, currentChartInfo, isLoading, error } = useVisualizationStore();
const handleAddToDashboard = () => { const buildPendingChart = (): Omit<ChartConfig, 'layout'> | null => {
if (!currentData || !currentSQL) return; if (!currentData || !currentSQL) return null;
const mark = currentChartSpec?.mark; const mark = currentChartSpec?.mark;
const markType = typeof mark === "string" ? mark : mark?.type; const markType = typeof mark === "string" ? mark : mark?.type;
const dashboardType = markType === "line" ? "line" : "bar"; const dashboardType = markType === "line" ? "line" : "bar";
addChart({ return {
id: Date.now().toString(), id: Date.now().toString(),
title: currentChartSpec?.title || 'Generated Analysis', title: currentChartSpec?.title || 'Generated Analysis',
type: dashboardType, type: dashboardType,
data: currentData, data: currentData,
sql: currentSQL, sql: currentSQL,
chartSpec: currentChartSpec, chartSpec: currentChartSpec,
}); };
alert("Added to Dashboard!"); };
const handleAddToDashboard = () => {
const chart = buildPendingChart();
if (!chart) return;
setPendingChart(chart);
setConfirmOpen(true);
};
const handleConfirmAdd = () => {
if (!pendingChart) return;
addChart(pendingChart);
setConfirmOpen(false);
setPendingChart(null);
}; };
if (isLoading) { if (isLoading) {
@@ -175,6 +190,28 @@ export function VisualizationPanel() {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle> Dashboard</DialogTitle>
<DialogDescription>
Dashboard
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setConfirmOpen(false);
setPendingChart(null);
}}
>
</Button>
<Button onClick={handleConfirmAdd}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div> </div>
); );
} }
+51 -9
View File
@@ -21,25 +21,67 @@ interface DashboardState {
updateLayout: (layouts: GridLayout[]) => void; updateLayout: (layouts: GridLayout[]) => void;
} }
const DASHBOARD_STORAGE_KEY = 'dashboard_charts_v1';
function loadChartsFromStorage(): ChartConfig[] {
if (typeof window === 'undefined') return [];
try {
const raw = window.localStorage.getItem(DASHBOARD_STORAGE_KEY);
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,
},
}));
} catch {
return [];
}
}
function saveChartsToStorage(charts: ChartConfig[]) {
if (typeof window === 'undefined') return;
window.localStorage.setItem(DASHBOARD_STORAGE_KEY, JSON.stringify(charts));
}
export const useDashboardStore = create<DashboardState>((set) => ({ export const useDashboardStore = create<DashboardState>((set) => ({
charts: [], charts: loadChartsFromStorage(),
addChart: (chart) => set((state) => { addChart: (chart) => set((state) => {
const colSize = 4;
const cols = 12 / colSize;
const index = state.charts.length;
const newLayout: GridLayout = { const newLayout: GridLayout = {
i: chart.id, i: chart.id,
x: (state.charts.length * 4) % 12, x: (index % cols) * colSize,
y: Infinity, y: Math.floor(index / cols) * 4,
w: 4, w: colSize,
h: 4, h: 4,
}; };
return { charts: [...state.charts, { ...chart, layout: newLayout }] }; const nextCharts = [...state.charts, { ...chart, layout: newLayout }];
saveChartsToStorage(nextCharts);
return { charts: nextCharts };
}), }),
removeChart: (id) => set((state) => ({ removeChart: (id) => set((state) => ({
charts: state.charts.filter((c) => c.id !== id), charts: (() => {
const nextCharts = state.charts.filter((c) => c.id !== id);
saveChartsToStorage(nextCharts);
return nextCharts;
})(),
})), })),
updateLayout: (layouts) => set((state) => ({ updateLayout: (layouts) => set((state) => {
charts: state.charts.map((chart) => { const nextCharts = state.charts.map((chart) => {
const layout = layouts.find((l) => l.i === chart.id); const layout = layouts.find((l) => l.i === chart.id);
return layout ? { ...chart, layout } : chart; return layout ? { ...chart, layout } : chart;
});
saveChartsToStorage(nextCharts);
return { charts: nextCharts };
}), }),
})),
})); }));