update metrics layout.

add support for layout change in dashboard.
This commit is contained in:
Fizer Khan
2025-12-05 21:35:38 +05:30
parent 3348c73897
commit 21d720a9b4
4 changed files with 176 additions and 93 deletions
+1 -1
View File
@@ -964,7 +964,7 @@ function AlertsPageContent() {
setIsEditChannelOpen(false);
}
}}>
<DialogContent size="full" onClose={() => {
<DialogContent size="full" className="sm:max-w-5xl" onClose={() => {
setIsCreateChannelOpen(false);
setIsEditChannelOpen(false);
}}>
@@ -477,6 +477,42 @@ function ConsumerDetailContent() {
</select>
</div>
{/* Stats Cards - Moved to top */}
<div className="grid gap-4 md:grid-cols-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Pending</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatNumber(consumer.num_pending || 0)}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Ack Pending</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatNumber(consumer.num_ack_pending || 0)}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Redelivered</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatNumber(consumer.num_redelivered || 0)}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Waiting</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatNumber(consumer.num_waiting || 0)}</div>
</CardContent>
</Card>
</div>
{/* Consumer Lag Chart */}
<Card>
<CardHeader>
@@ -569,42 +605,6 @@ function ConsumerDetailContent() {
)}
</CardContent>
</Card>
{/* Quick Stats */}
<div className="grid gap-4 md:grid-cols-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Pending</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatNumber(consumer.num_pending || 0)}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Ack Pending</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatNumber(consumer.num_ack_pending || 0)}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Redelivered</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatNumber(consumer.num_redelivered || 0)}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Waiting</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatNumber(consumer.num_waiting || 0)}</div>
</CardContent>
</Card>
</div>
</div>
)}
</div>
+135 -52
View File
@@ -20,6 +20,8 @@ import {
PieChart,
X,
CheckCircle2,
Edit,
MoreVertical,
} from 'lucide-react';
import Link from 'next/link';
import { api } from '@/lib/api';
@@ -41,6 +43,13 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { DashboardWidget } from '@/components/dashboard/dashboard-widget';
// Widget types
@@ -77,6 +86,7 @@ export default function DashboardBuilderPage() {
const [newWidgetCluster, setNewWidgetCluster] = useState('');
const [newWidgetMetric, setNewWidgetMetric] = useState('');
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [isEditingLayout, setIsEditingLayout] = useState(false);
const { data: dashboardData, isLoading } = useQuery({
queryKey: ['dashboard', dashboardId],
@@ -101,16 +111,30 @@ export default function DashboardBuilderPage() {
const [saveSuccess, setSaveSuccess] = useState(false);
const saveMutation = useMutation({
mutationFn: () =>
api.dashboards.update(dashboardId, {
mutationFn: async (updatedWidgets?: Widget[]) => {
const widgetsToSave = updatedWidgets ?? widgets;
if (dashboardId === 'new') {
return api.dashboards.create({
name: dashboardName || 'New Dashboard',
description: dashboardDescription,
widgets: widgetsToSave,
});
}
return api.dashboards.update(dashboardId, {
name: dashboardName,
description: dashboardDescription,
widgets,
}),
onSuccess: () => {
widgets: widgetsToSave,
});
},
onSuccess: (data) => {
setHasUnsavedChanges(false);
setIsEditingLayout(false);
setSaveSuccess(true);
setTimeout(() => setSaveSuccess(false), 2000);
// If new dashboard was created, redirect to its page
if (dashboardId === 'new' && data?.dashboard?.id) {
router.replace(`/dashboards/${data.dashboard.id}`);
}
queryClient.invalidateQueries({ queryKey: ['dashboard', dashboardId] });
queryClient.invalidateQueries({ queryKey: ['dashboards'] });
},
@@ -135,13 +159,16 @@ export default function DashboardBuilderPage() {
},
};
setWidgets([...widgets, newWidget]);
setHasUnsavedChanges(true);
const updatedWidgets = [...widgets, newWidget];
setWidgets(updatedWidgets);
setShowAddWidget(false);
setSelectedWidgetType('');
setNewWidgetTitle('');
setNewWidgetCluster('');
setNewWidgetMetric('');
// Auto-save after adding widget
saveMutation.mutate(updatedWidgets);
};
const removeWidget = (widgetId: string) => {
@@ -175,24 +202,35 @@ export default function DashboardBuilderPage() {
</Button>
</Link>
<div className="flex-1">
<Input
value={dashboardName}
onChange={(e) => {
setDashboardName(e.target.value);
setHasUnsavedChanges(true);
}}
className="text-2xl font-bold border-none shadow-none px-0 h-auto focus-visible:ring-0"
placeholder="Dashboard Name"
/>
<Input
value={dashboardDescription}
onChange={(e) => {
setDashboardDescription(e.target.value);
setHasUnsavedChanges(true);
}}
className="text-sm text-muted-foreground border-none shadow-none px-0 h-auto focus-visible:ring-0"
placeholder="Add a description..."
/>
{isEditingLayout ? (
<>
<Input
value={dashboardName}
onChange={(e) => {
setDashboardName(e.target.value);
setHasUnsavedChanges(true);
}}
className="text-2xl font-bold border-none shadow-none px-0 h-auto focus-visible:ring-0"
placeholder="Dashboard Name"
/>
<Input
value={dashboardDescription}
onChange={(e) => {
setDashboardDescription(e.target.value);
setHasUnsavedChanges(true);
}}
className="text-sm text-muted-foreground border-none shadow-none px-0 h-auto focus-visible:ring-0"
placeholder="Add a description..."
/>
</>
) : (
<>
<h1 className="text-2xl font-bold">{dashboardName || 'Untitled Dashboard'}</h1>
{dashboardDescription && (
<p className="text-sm text-muted-foreground">{dashboardDescription}</p>
)}
</>
)}
</div>
</div>
<div className="flex gap-2">
@@ -200,21 +238,44 @@ export default function DashboardBuilderPage() {
<Plus className="h-4 w-4" />
Add Widget
</Button>
<Button
onClick={() => saveMutation.mutate()}
disabled={!hasUnsavedChanges || saveMutation.isPending}
variant={saveSuccess ? 'default' : 'default'}
className={saveSuccess ? 'bg-green-600 hover:bg-green-600' : ''}
>
{saveMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : saveSuccess ? (
<CheckCircle2 className="h-4 w-4" />
) : (
<Save className="h-4 w-4" />
)}
{saveMutation.isPending ? 'Saving...' : saveSuccess ? 'Saved!' : hasUnsavedChanges ? 'Save Changes' : 'Saved'}
</Button>
{isEditingLayout ? (
<>
<Button
variant="outline"
onClick={() => {
// Cancel editing - restore original data
if (dashboardData?.dashboard) {
setDashboardName(dashboardData.dashboard.name);
setDashboardDescription(dashboardData.dashboard.description || '');
setWidgets(dashboardData.dashboard.widgets || []);
}
setHasUnsavedChanges(false);
setIsEditingLayout(false);
}}
>
Cancel
</Button>
<Button
onClick={() => saveMutation.mutate()}
disabled={!hasUnsavedChanges || saveMutation.isPending}
className={saveSuccess ? 'bg-green-600 hover:bg-green-600' : ''}
>
{saveMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : saveSuccess ? (
<CheckCircle2 className="h-4 w-4" />
) : (
<Save className="h-4 w-4" />
)}
{saveMutation.isPending ? 'Saving...' : saveSuccess ? 'Saved!' : 'Save Changes'}
</Button>
</>
) : (
<Button variant="outline" onClick={() => setIsEditingLayout(true)}>
<Edit className="h-4 w-4" />
Edit Layout
</Button>
)}
</div>
</div>
@@ -243,28 +304,50 @@ export default function DashboardBuilderPage() {
gridColumn: `span ${widget.position.w}`,
}}
>
<Card className="group relative h-full">
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1 z-10">
<Card className={`relative h-full ${isEditingLayout ? 'group ring-2 ring-dashed ring-primary/30' : ''}`}>
{/* Widget controls - always visible, disabled during edit layout */}
<div className="absolute top-2 right-2 flex gap-1 z-10">
{isEditingLayout && (
<GripVertical className="h-4 w-4 text-muted-foreground cursor-move opacity-0 group-hover:opacity-100 mt-2 mr-1" />
)}
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => setShowWidgetConfig(widget)}
onClick={() => !isEditingLayout && setShowWidgetConfig(widget)}
disabled={isEditingLayout}
>
<Settings className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive"
onClick={() => removeWidget(widget.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
disabled={isEditingLayout}
>
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setShowWidgetConfig(widget)}>
<Settings className="h-4 w-4 mr-2" />
Configure
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive"
onClick={() => removeWidget(widget.id)}
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<CardHeader className="pb-2">
<div className="flex items-center gap-2">
<GripVertical className="h-4 w-4 text-muted-foreground cursor-move opacity-0 group-hover:opacity-100" />
<CardTitle className="text-sm font-medium">{widget.title}</CardTitle>
</div>
</CardHeader>
@@ -294,7 +294,7 @@ export default function SavedQueriesPage() {
{/* Create Query Dialog */}
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
<DialogContent className="sm:max-w-3xl">
<DialogContent className="sm:max-w-4xl">
<DialogHeader>
<DialogTitle>Create New Query</DialogTitle>
<DialogDescription>
@@ -314,7 +314,7 @@ export default function SavedQueriesPage() {
<Label>Query *</Label>
<Textarea
placeholder="Enter your query configuration (JSON format)..."
className="font-mono text-sm min-h-[300px]"
className="font-mono text-sm min-h-[150px]"
value={newQuery.query}
onChange={(e) => setNewQuery({ ...newQuery, query: e.target.value })}
/>
@@ -349,7 +349,7 @@ export default function SavedQueriesPage() {
{/* Edit Query Dialog */}
<Dialog open={!!editQuery} onOpenChange={() => setEditQuery(null)}>
<DialogContent className="sm:max-w-3xl">
<DialogContent className="sm:max-w-4xl">
<DialogHeader>
<DialogTitle>Edit Query</DialogTitle>
<DialogDescription>
@@ -370,7 +370,7 @@ export default function SavedQueriesPage() {
<Label>Query *</Label>
<Textarea
placeholder="Enter your query configuration (JSON format)..."
className="font-mono text-sm min-h-[300px]"
className="font-mono text-sm min-h-[150px]"
value={editQuery.query}
onChange={(e) => setEditQuery({ ...editQuery, query: e.target.value })}
/>