DashboardViewer.tsx2.6 KBView on GitHub 'use client';
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useTRPC } from '@/providers/query-provider';
import { cn } from '@/lib/utils';
import { applyComputations } from '../lib/data';
import { isMockSource, type DashboardSpec } from '../types/dashboard';
import { SourcesProvider, type ResolvedSource } from './context';
import { LayoutNode } from './blocks';
interface DashboardViewerProps {
spec: DashboardSpec;
/** Execution context for query params (userId, dateFrom, …). Ignored by mock sources. */
queryContext?: Record<string, string>;
className?: string;
}
/**
* Resolves a dashboard spec's data sources and renders its layout tree.
*
* Mock sources resolve inline (no network). Query sources call
* `dashboard.execute` with a 5-minute cache. The set of sources is fixed for a
* given spec, so the per-source hooks below have a stable order — callers
* should key this component by `spec.id` when the spec can change.
*/
export function DashboardViewer({ spec, queryContext = {}, className }: DashboardViewerProps) {
const trpc = useTRPC();
const entries = Object.entries(spec.dataSources);
const sources: Record<string, ResolvedSource> = {};
for (const [name, source] of entries) {
if (isMockSource(source)) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const rows = useMemo(() => applyComputations(source.mock, source.computations), [source]);
sources[name] = { rows, isLoading: false };
} else {
// Positional params, in spec order — bound as $2…$n on the server ($1 is
// always the authenticated user id).
const queryParams = (source.queryParams ?? []).map((p) => queryContext[p.sourceField] ?? '');
// eslint-disable-next-line react-hooks/rules-of-hooks
const result = useQuery({
...trpc.dashboard.execute.queryOptions({
specId: spec.id,
source: name,
query: source.query,
queryParams,
}),
staleTime: 5 * 60 * 1000,
refetchOnWindowFocus: false,
});
sources[name] = {
rows: applyComputations(
(result.data?.rows as Record<string, unknown>[]) ?? [],
source.computations,
),
isLoading: result.isLoading,
error: result.error,
};
}
}
return (
<SourcesProvider value={sources}>
<div className={cn('flex flex-col gap-4', className)}>
{spec.title && <h2 className="text-lg font-semibold">{spec.title}</h2>}
<LayoutNode node={spec.layout} />
</div>
</SourcesProvider>
);
}