UI: dashboards split
This commit is contained in:
@@ -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<Omit<ChartConfig, 'layout'> | null>(null);
|
||||
const { addChart } = useDashboardStore();
|
||||
const [selectedDashboardId, setSelectedDashboardId] = useState<string>('');
|
||||
const { dashboards, addChart, loadDashboards } = useDashboardStore();
|
||||
const { currentProject } = useProjectStore();
|
||||
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]) : [];
|
||||
|
||||
useEffect(() => {
|
||||
if (currentProject) {
|
||||
loadDashboards(currentProject.id);
|
||||
}
|
||||
}, [currentProject, loadDashboards]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dashboards.length > 0 && !selectedDashboardId) {
|
||||
setSelectedDashboardId(dashboards[0].id);
|
||||
}
|
||||
}, [dashboards, selectedDashboardId]);
|
||||
|
||||
const buildPendingChart = (): Omit<ChartConfig, 'layout'> => {
|
||||
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) {
|
||||
</Dialog>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={handleAddToDashboard} disabled={objectRows.length === 0}>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={handleAddToDashboard} disabled={objectRows.length === 0 || dashboards.length === 0}>
|
||||
<LayoutDashboard className="h-3.5 w-3.5 mr-1.5" />
|
||||
Add to Dashboard
|
||||
</Button>
|
||||
@@ -206,11 +220,26 @@ export function InlineVisualizationCard({ viz }: InlineVisualizationCardProps) {
|
||||
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('confirmAddToDashboard')}</DialogTitle>
|
||||
<DialogTitle>{t('pinChartToDashboard')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('confirmAddChartToDashboardDesc')}
|
||||
{t('selectDashboardToPin')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<label className="text-sm font-medium mb-2 block">{t('dashboardMenu')}</label>
|
||||
<Select value={selectedDashboardId} onValueChange={(val) => { if (val) setSelectedDashboardId(val); }}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('selectDashboard')}>
|
||||
{dashboards.find(d => d.id === selectedDashboardId)?.name || t('selectDashboard')}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{dashboards.map(d => (
|
||||
<SelectItem key={d.id} value={d.id}>{d.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -221,8 +250,8 @@ export function InlineVisualizationCard({ viz }: InlineVisualizationCardProps) {
|
||||
>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleConfirmAdd}>
|
||||
{t('confirmAdd')}
|
||||
<Button onClick={handleConfirmAdd} disabled={!selectedDashboardId}>
|
||||
{t('submit')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -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<string[]>([]);
|
||||
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 (
|
||||
<div className="px-3 pt-6">
|
||||
<div className="flex items-center justify-between mb-2 px-1 group">
|
||||
<div className="text-xs font-semibold text-zinc-500 flex items-center gap-1 uppercase tracking-wider">
|
||||
<div className="px-3 pt-4 pb-1">
|
||||
<div className="flex items-center justify-between px-1 group">
|
||||
<div className="text-[14px] font-semibold text-zinc-500 flex items-center gap-1">
|
||||
{title}
|
||||
<span>({count})</span>
|
||||
</div>
|
||||
@@ -140,7 +129,49 @@ function Section({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-0.5 mt-2">
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="px-3 pb-2">
|
||||
<div className="space-y-0 mt-1">
|
||||
{items.map((item) => {
|
||||
const displayTitle = item.metadata?.title || item.key.replace("api:", "");
|
||||
const isActive = activeKey === item.key;
|
||||
@@ -149,11 +180,11 @@ function Section({
|
||||
return (
|
||||
<div
|
||||
key={item.key}
|
||||
className={`w-full h-9 px-2 text-left rounded-md text-[14px] flex items-center justify-between group transition-colors cursor-pointer ${
|
||||
className={`w-full h-8 px-2 text-left rounded-md text-[14px] flex items-center justify-between group transition-colors cursor-pointer ${
|
||||
isActive && !isSelectionMode ? 'bg-zinc-100 text-zinc-900 font-medium' : 'text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900'
|
||||
} ${isSelected ? 'bg-indigo-50/50 text-indigo-700' : ''}`}
|
||||
onClick={(e) => isSelectionMode ? toggleSelect(item.key, e) : onSelect(item.key)}
|
||||
>
|
||||
onClick={(e) => isSelectionMode ? toggleSelect(item.key, e) : onSelect(item.key)}
|
||||
>
|
||||
<div className="truncate pr-2 flex-1 flex items-center gap-1.5 min-w-0">
|
||||
{isSelectionMode ? (
|
||||
<span
|
||||
@@ -242,7 +273,7 @@ function Section({
|
||||
<span>{t('deleteSession')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -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 (
|
||||
<div className="px-3 pt-4">
|
||||
<div className="flex items-center justify-between mb-1.5 px-1 group">
|
||||
<div className="text-[14px] font-semibold text-zinc-500 flex items-center gap-1">
|
||||
{title}
|
||||
<span>({count})</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onCreate}
|
||||
className="text-[10px] font-medium px-1.5 py-0.5 hover:bg-zinc-200 rounded text-zinc-500 transition-colors opacity-0 group-hover:opacity-100 flex items-center gap-0.5"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
{t('new')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-0 mt-1">
|
||||
{items.map((item) => {
|
||||
const isActive = activeId === item.id;
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`w-full h-8 px-2 text-left rounded-md text-[14px] flex items-center justify-between group transition-colors cursor-pointer ${
|
||||
isActive ? 'bg-zinc-100 text-zinc-900 font-medium' : 'text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900'
|
||||
}`}
|
||||
onClick={() => onSelect(item.id)}
|
||||
>
|
||||
<div className="truncate pr-2 flex-1 flex items-center gap-1.5 min-w-0">
|
||||
<span className="w-4 shrink-0 flex items-center justify-center">
|
||||
<LayoutDashboard className="h-3.5 w-3.5 text-zinc-400" />
|
||||
</span>
|
||||
<span className="truncate">{item.name}</span>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger onClick={(e) => 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">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-32">
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onRename(item.id, item.name);
|
||||
}}
|
||||
>
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
<span>{t('rename')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDelete(item.id);
|
||||
}}
|
||||
className="text-red-600 focus:text-red-600 focus:bg-red-50"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
<span>{t('deleteSession')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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<HTMLDivElement>(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<string[]>([]);
|
||||
const [archivedSelectionMode, setArchivedSelectionMode] = useState(false);
|
||||
const [archivedSelectedKeys, setArchivedSelectedKeys] = useState<string[]>([]);
|
||||
|
||||
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<SessionInfo[]>("/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 (
|
||||
<div className="h-full min-h-0 flex flex-col bg-zinc-50/30 border-r border-zinc-200 relative">
|
||||
{/* Header */}
|
||||
@@ -453,63 +627,103 @@ function SidebarBody() {
|
||||
<div className="w-8" />
|
||||
</div>
|
||||
|
||||
<div className="px-3 pt-4 space-y-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full justify-start h-10 px-3 rounded-lg bg-zinc-200/50 hover:bg-zinc-200 text-zinc-900 font-medium"
|
||||
onClick={() => navigate("/dashboard")}
|
||||
>
|
||||
<LayoutDashboard className="h-4.5 w-4.5 mr-2 text-zinc-600" />
|
||||
{t('dashboardMenu')}
|
||||
</Button>
|
||||
<div className="flex-none">
|
||||
<DashboardSection
|
||||
title={t('dashboards') || 'Dashboards'}
|
||||
count={filteredDashboards.length}
|
||||
items={filteredDashboards.map(d => ({ id: d.id, name: d.name }))}
|
||||
onSelect={handleSelectDashboard}
|
||||
onDelete={handleDashboardDelete}
|
||||
onRename={openDashboardRenameDialog}
|
||||
onCreate={handleCreateDashboard}
|
||||
activeId={location.pathname === "/dashboard" ? activeDashboardId : null}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start h-10 px-3 rounded-lg border-zinc-200 bg-white hover:bg-zinc-50 text-zinc-600 font-medium"
|
||||
onClick={handleNewThread}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{t('newThread')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="px-3 pt-4 mb-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start h-10 px-3 rounded-lg border-zinc-200 bg-white hover:bg-zinc-50 text-zinc-600 font-medium text-[14px]"
|
||||
onClick={handleNewThread}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{t('newThread')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1 min-h-0">
|
||||
<div className="px-3 pt-4">
|
||||
<div className="px-3 pt-2">
|
||||
<div className="relative">
|
||||
<Search className="h-4 w-4 text-zinc-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<Input
|
||||
value={sessionFilter}
|
||||
onChange={(e) => 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]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Section
|
||||
title={t('threads')}
|
||||
count={activeSessions.length}
|
||||
items={activeSessions}
|
||||
onSelect={handleSelectSession}
|
||||
onDelete={handleDeleteSession}
|
||||
onBatchDelete={handleBatchDelete}
|
||||
onRename={openRenameDialog}
|
||||
onTogglePinned={handleTogglePinned}
|
||||
onToggleArchived={handleToggleArchived}
|
||||
activeKey={activeSessionKey}
|
||||
/>
|
||||
<Section
|
||||
title={t('archivedThreads')}
|
||||
count={archivedSessions.length}
|
||||
items={archivedSessions}
|
||||
onSelect={handleSelectSession}
|
||||
onDelete={handleDeleteSession}
|
||||
onBatchDelete={handleBatchDelete}
|
||||
onRename={openRenameDialog}
|
||||
onTogglePinned={handleTogglePinned}
|
||||
onToggleArchived={handleToggleArchived}
|
||||
activeKey={activeSessionKey}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 flex flex-col mt-2">
|
||||
<div className="flex-1 min-h-0 flex flex-col">
|
||||
<SectionHeader
|
||||
title={t('threads')}
|
||||
count={activeSessions.length}
|
||||
isSelectionMode={activeSelectionMode}
|
||||
setIsSelectionMode={setActiveSelectionMode}
|
||||
selectedKeys={activeSelectedKeys}
|
||||
setSelectedKeys={setActiveSelectedKeys}
|
||||
items={activeSessions}
|
||||
onBatchDelete={handleBatchDelete}
|
||||
/>
|
||||
<ScrollArea className="flex-1 min-h-0">
|
||||
<Section
|
||||
items={activeSessions}
|
||||
onSelect={handleSelectSession}
|
||||
onDelete={handleDeleteSession}
|
||||
onBatchDelete={handleBatchDelete}
|
||||
onRename={openRenameDialog}
|
||||
onTogglePinned={handleTogglePinned}
|
||||
onToggleArchived={handleToggleArchived}
|
||||
activeKey={activeSessionKey}
|
||||
isSelectionMode={activeSelectionMode}
|
||||
setIsSelectionMode={setActiveSelectionMode}
|
||||
selectedKeys={activeSelectedKeys}
|
||||
setSelectedKeys={setActiveSelectedKeys}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
{archivedSessions.length > 0 && (
|
||||
<div className="h-[35%] min-h-[150px] shrink-0 border-t border-zinc-200 bg-zinc-50/50 flex flex-col">
|
||||
<SectionHeader
|
||||
title={t('archivedThreads')}
|
||||
count={archivedSessions.length}
|
||||
isSelectionMode={archivedSelectionMode}
|
||||
setIsSelectionMode={setArchivedSelectionMode}
|
||||
selectedKeys={archivedSelectedKeys}
|
||||
setSelectedKeys={setArchivedSelectedKeys}
|
||||
items={archivedSessions}
|
||||
onBatchDelete={handleBatchDelete}
|
||||
/>
|
||||
<ScrollArea className="flex-1 min-h-0">
|
||||
<Section
|
||||
items={archivedSessions}
|
||||
onSelect={handleSelectSession}
|
||||
onDelete={handleDeleteSession}
|
||||
onBatchDelete={handleBatchDelete}
|
||||
onRename={openRenameDialog}
|
||||
onTogglePinned={handleTogglePinned}
|
||||
onToggleArchived={handleToggleArchived}
|
||||
activeKey={activeSessionKey}
|
||||
isSelectionMode={archivedSelectionMode}
|
||||
setIsSelectionMode={setArchivedSelectionMode}
|
||||
selectedKeys={archivedSelectedKeys}
|
||||
setSelectedKeys={setArchivedSelectedKeys}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Dialog open={renameDialogOpen} onOpenChange={setRenameDialogOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
@@ -536,6 +750,31 @@ function SidebarBody() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={dashboardRenameDialogOpen} onOpenChange={setDashboardRenameDialogOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('renameDashboard')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<Input
|
||||
value={newDashboardName}
|
||||
onChange={(e) => setNewDashboardName(e.target.value)}
|
||||
placeholder={t('enterNewDashboardName')}
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleDashboardRename();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDashboardRenameDialogOpen(false)}>{t('cancel')}</Button>
|
||||
<Button className="bg-indigo-600 hover:bg-indigo-700 text-white" onClick={handleDashboardRename}>{t('save')}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<div className="p-4 border-t border-zinc-200 mt-auto relative" ref={menuRef}>
|
||||
<div className="flex items-center justify-between text-zinc-600">
|
||||
<button
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } 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, Download, LayoutDashboard, Loader2 } from "lucide-react";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
@@ -16,10 +17,23 @@ export function VisualizationPanel() {
|
||||
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 [selectedDashboardId, setSelectedDashboardId] = useState<string>('');
|
||||
const { dashboards, addChart, loadDashboards } = useDashboardStore();
|
||||
const { currentProject } = useProjectStore();
|
||||
const { currentData, currentSQL, currentChartSpec, currentChartInfo, isLoading, error } = useVisualizationStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentProject) {
|
||||
loadDashboards(currentProject.id);
|
||||
}
|
||||
}, [currentProject, loadDashboards]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dashboards.length > 0 && !selectedDashboardId) {
|
||||
setSelectedDashboardId(dashboards[0].id);
|
||||
}
|
||||
}, [dashboards, selectedDashboardId]);
|
||||
|
||||
const buildPendingChart = (): Omit<ChartConfig, 'layout'> | null => {
|
||||
if (!currentData || !currentSQL) return null;
|
||||
if (view === "table") {
|
||||
@@ -54,8 +68,8 @@ export function VisualizationPanel() {
|
||||
};
|
||||
|
||||
const handleConfirmAdd = () => {
|
||||
if (!pendingChart || !currentProject) return;
|
||||
addChart(pendingChart, currentProject.id);
|
||||
if (!pendingChart || !currentProject || !selectedDashboardId) return;
|
||||
addChart(pendingChart, selectedDashboardId, currentProject.id);
|
||||
setConfirmOpen(false);
|
||||
setPendingChart(null);
|
||||
};
|
||||
@@ -127,7 +141,7 @@ export function VisualizationPanel() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={handleAddToDashboard}>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={handleAddToDashboard} disabled={dashboards.length === 0}>
|
||||
<LayoutDashboard className="h-3.5 w-3.5 mr-1.5" />
|
||||
Add to Dashboard
|
||||
</Button>
|
||||
@@ -208,11 +222,26 @@ export function VisualizationPanel() {
|
||||
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('confirmAddToDashboard')}</DialogTitle>
|
||||
<DialogTitle>{t('pinChartToDashboard')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('confirmAddChartToDashboardDesc')}
|
||||
{t('selectDashboardToPin')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<label className="text-sm font-medium mb-2 block">{t('dashboardMenu')}</label>
|
||||
<Select value={selectedDashboardId} onValueChange={(val) => { if (val) setSelectedDashboardId(val); }}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('selectDashboard')}>
|
||||
{dashboards.find(d => d.id === selectedDashboardId)?.name || t('selectDashboard')}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{dashboards.map(d => (
|
||||
<SelectItem key={d.id} value={d.id}>{d.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -223,7 +252,7 @@ export function VisualizationPanel() {
|
||||
>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleConfirmAdd}>{t('confirmAdd')}</Button>
|
||||
<Button onClick={handleConfirmAdd} disabled={!selectedDashboardId}>{t('submit')}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user