import { FullPageEmptyState } from '@/components/full-page-empty-state'; import { useOverviewOptions } from '@/components/overview/useOverviewOptions'; import { Button, LinkButton } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { createProjectTitle } from '@/utils/title'; import { LayoutPanelTopIcon, MoreHorizontal, PlusIcon, RotateCcw, ShareIcon, TrashIcon, } from 'lucide-react'; import { toast } from 'sonner'; import FullPageLoadingState from '@/components/full-page-loading-state'; import { GrafanaGrid, type Layout, useReportLayouts, } from '@/components/grafana-grid'; import { OverviewInterval } from '@/components/overview/overview-interval'; import { OverviewRange } from '@/components/overview/overview-range'; import { PageContainer } from '@/components/page-container'; import { PageHeader } from '@/components/page-header'; import { ReportItem, ReportItemSkeleton, } from '@/components/report/report-item'; import { handleErrorToastOptions, useTRPC } from '@/integrations/trpc/react'; import { pushModal, showConfirm } from '@/modals'; import { useMutation, useQuery } from '@tanstack/react-query'; import { createFileRoute, useRouter } from '@tanstack/react-router'; import { useCallback, useEffect, useState } from 'react'; export const Route = createFileRoute( '/_app/$organizationId/$projectId/dashboards_/$dashboardId', )({ component: Component, head: () => { return { meta: [ { title: createProjectTitle('Dashboard'), }, ], }; }, loader: async ({ context, params }) => { await Promise.all([ context.queryClient.prefetchQuery( context.trpc.dashboard.byId.queryOptions({ id: params.dashboardId, projectId: params.projectId, }), ), context.queryClient.prefetchQuery( context.trpc.report.list.queryOptions({ dashboardId: params.dashboardId, projectId: params.projectId, }), ), context.queryClient.prefetchQuery( context.trpc.project.getProjectWithClients.queryOptions({ projectId: params.projectId, }), ), context.queryClient.prefetchQuery( context.trpc.organization.get.queryOptions({ organizationId: params.organizationId, }), ), ]); }, pendingComponent: FullPageLoadingState, }); function Component() { const router = useRouter(); const { organizationId, dashboardId, projectId } = Route.useParams(); const trpc = useTRPC(); const { range, startDate, endDate, interval } = useOverviewOptions(); const dashboardQuery = useQuery( trpc.dashboard.byId.queryOptions({ id: dashboardId, projectId, }), ); const reportsQuery = useQuery( trpc.report.list.queryOptions({ dashboardId, projectId, }), ); const dashboardDeletion = useMutation( trpc.dashboard.delete.mutationOptions({ onError: handleErrorToastOptions({}), onSuccess() { toast('Dashboard deleted'); router.navigate({ to: '/$organizationId/$projectId/dashboards', params: { organizationId, projectId, }, }); }, }), ); const reports = reportsQuery.data ?? []; const dashboard = dashboardQuery.data; const [isGridReady, setIsGridReady] = useState(false); const [enableTransitions, setEnableTransitions] = useState(false); // Wait for initial render to ensure grid has proper dimensions useEffect(() => { if (reports.length > 0 && !isGridReady) { // Small delay to ensure container has rendered with proper width const timer = setTimeout(() => { setIsGridReady(true); // Enable transitions after initial render setTimeout(() => setEnableTransitions(true), 100); }, 0); return () => clearTimeout(timer); } }, [reports.length, isGridReady]); const reportDeletion = useMutation( trpc.report.delete.mutationOptions({ onError: handleErrorToastOptions({}), onSuccess() { reportsQuery.refetch(); toast('Report deleted'); }, }), ); const reportDuplicate = useMutation( trpc.report.duplicate.mutationOptions({ onError: handleErrorToastOptions({}), onSuccess() { reportsQuery.refetch(); toast('Report duplicated'); }, }), ); const updateLayout = useMutation( trpc.report.updateLayout.mutationOptions({ onError: handleErrorToastOptions({}), onSuccess() { // Silently refetch reports (which includes layouts) reportsQuery.refetch(); }, }), ); const resetLayout = useMutation( trpc.report.resetLayout.mutationOptions({ onError: handleErrorToastOptions({}), onSuccess() { toast('Layout reset to default'); reportsQuery.refetch(); }, }), ); // Convert reports to grid layout format for all breakpoints const layouts = useReportLayouts(reports); const handleLayoutChange = useCallback((newLayout: Layout[]) => { // This is called during dragging/resizing, we'll save on drag/resize stop }, []); const handleDragStop = useCallback( (newLayout: Layout[]) => { // Save each changed layout after drag stops newLayout.forEach((item) => { const report = reports.find((r) => r.id === item.i); if (report) { const oldLayout = report.layout; // Only update if layout actually changed if ( !oldLayout || oldLayout.x !== item.x || oldLayout.y !== item.y || oldLayout.w !== item.w || oldLayout.h !== item.h ) { updateLayout.mutate({ reportId: item.i, layout: { x: item.x, y: item.y, w: item.w, h: item.h, minW: item.minW ?? 3, minH: item.minH ?? 3, }, }); } } }); }, [reports, updateLayout], ); const handleResizeStop = useCallback( (newLayout: Layout[]) => { // Save each changed layout after resize stops newLayout.forEach((item) => { const report = reports.find((r) => r.id === item.i); if (report) { const oldLayout = report.layout; // Only update if layout actually changed if ( !oldLayout || oldLayout.x !== item.x || oldLayout.y !== item.y || oldLayout.w !== item.w || oldLayout.h !== item.h ) { updateLayout.mutate({ reportId: item.i, layout: { x: item.x, y: item.y, w: item.w, h: item.h, minW: item.minW ?? 3, minH: item.minH ?? 3, }, }); } } }); }, [reports, updateLayout], ); if (!dashboard) { return null; // Loading handled by suspense } return ( Create report Report pushModal('ShareDashboardModal', { dashboardId }) } > Share dashboard showConfirm({ title: 'Reset layout', text: 'Are you sure you want to reset the layout to default? This will clear all custom positioning and sizing.', onConfirm: () => resetLayout.mutate({ dashboardId, projectId }), }) } > Reset layout showConfirm({ title: 'Delete dashboard', text: 'Are you sure you want to delete this dashboard? All your reports will be deleted!', onConfirm: () => dashboardDeletion.mutate({ id: dashboardId }), }) } > Delete dashboard } /> {reports.length === 0 ? (

You can visualize your data with a report

Create report
) : !isGridReady || reportsQuery.isLoading ? (
) : ( {reports.map((report) => (
{ reportDeletion.mutate({ reportId }); }} onDuplicate={(reportId) => { reportDuplicate.mutate({ reportId }); }} />
))}
)}
); }