fix: dashboard persistent
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
}),
|
}),
|
||||||
})),
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user