Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions src/app/dashboard/[teamId]/_components/dashboard-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import { Skeleton } from "@/components/ui/skeleton";

import { DashboardMetricCard } from "./dashboard-metric-card";
import { usePipelineStatus } from "./pipeline-status-provider";
import { useDashboardCharts } from "./use-dashboard-charts";

/**
* Dashboard content - renders metric cards.
* Gets charts from PipelineStatusProvider context.
*/
export function DashboardContent() {
const { dashboardCharts, isLoading, isError } = usePipelineStatus();
interface DashboardContentProps {
teamId: string;
}

export function DashboardContent({ teamId }: DashboardContentProps) {
const { charts, isLoading, isError } = useDashboardCharts(teamId);

if (isLoading) {
return (
Expand All @@ -37,13 +37,13 @@ export function DashboardContent() {

return (
<div className="space-y-6">
{dashboardCharts.length > 0 && (
{charts.length > 0 && (
<p className="text-muted-foreground text-sm">
{`Showing ${dashboardCharts.length} metric${dashboardCharts.length === 1 ? "" : "s"}`}
{`Showing ${charts.length} metric${charts.length === 1 ? "" : "s"}`}
</p>
)}

{dashboardCharts.length === 0 ? (
{charts.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed py-16">
<div className="space-y-2 text-center">
<h3 className="text-lg font-semibold">No KPIs yet</h3>
Expand All @@ -55,8 +55,12 @@ export function DashboardContent() {
</div>
) : (
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{dashboardCharts.map((dc) => (
<DashboardMetricCard key={dc.id} dashboardChart={dc} />
{charts.map((dc) => (
<DashboardMetricCard
key={dc.id}
dashboardChart={dc}
teamId={teamId}
/>
))}
</div>
)}
Expand Down
148 changes: 91 additions & 57 deletions src/app/dashboard/[teamId]/_components/dashboard-metric-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import { toast } from "sonner";

import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
import {
Drawer,
DrawerContent,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import {
Tooltip,
TooltipContent,
Expand All @@ -17,60 +22,99 @@ import {
import { isDevMode } from "@/lib/dev-mode";
import type { ChartTransformResult } from "@/lib/metrics/transformer-types";
import { useConfirmation } from "@/providers/ConfirmationDialogProvider";
import { api } from "@/trpc/react";
import type { DashboardChartWithRelations } from "@/types/dashboard";

import { DashboardMetricChart } from "./dashboard-metric-chart";
import { DashboardMetricDrawer } from "./dashboard-metric-drawer";
import { usePipelineStatus } from "./pipeline-status-provider";
import { useDashboardCharts } from "./use-dashboard-charts";

interface DashboardMetricCardProps {
dashboardChart: DashboardChartWithRelations;
teamId: string;
}

/**
* Dashboard metric card component.
*
* Architecture:
* - Receives dashboardChart as prop
* - Uses PipelineStatusProvider for status and mutations
* - Simple isProcessing/getError API
*/
export function DashboardMetricCard({
dashboardChart,
teamId,
}: DashboardMetricCardProps) {
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const { confirm } = useConfirmation();
const pipeline = usePipelineStatus();
const utils = api.useUtils();
const { isProcessing, getError } = useDashboardCharts(teamId);

const metric = dashboardChart.metric;
const metricId = metric.id;
const isProcessing = pipeline.isProcessing(metricId);
const error = pipeline.getError(metricId);
const processing = isProcessing(metricId);
const error = getError(metricId);

const isIntegrationMetric = !!metric.integration?.providerId;
const chartTransform =
dashboardChart.chartConfig as ChartTransformResult | null;
const hasChartData = !!chartTransform?.chartData?.length;
const title = chartTransform?.title ?? metric.name;

// ---------------------------------------------------------------------------
// Mutations with optimistic updates
// ---------------------------------------------------------------------------
const setOptimisticProcessing = useCallback(
(id: string) => {
utils.dashboard.getDashboardCharts.setData({ teamId }, (old) =>
old?.map((dc) =>
dc.metric.id === id
? { ...dc, metric: { ...dc.metric, refreshStatus: "processing" } }
: dc,
),
);
},
[utils, teamId],
);

const refreshMutation = api.pipeline.refresh.useMutation({
onMutate: () => setOptimisticProcessing(metricId),
onSuccess: () => utils.dashboard.getDashboardCharts.invalidate({ teamId }),
onError: (err) =>
toast.error("Refresh failed", { description: err.message }),
});

const regenerateMutation = api.pipeline.regenerate.useMutation({
onMutate: () => setOptimisticProcessing(metricId),
onSuccess: () => utils.dashboard.getDashboardCharts.invalidate({ teamId }),
onError: (err) =>
toast.error("Regenerate failed", { description: err.message }),
});

const regenerateChartMutation = api.pipeline.regenerateChartOnly.useMutation({
onMutate: () => setOptimisticProcessing(metricId),
onSuccess: () => utils.dashboard.getDashboardCharts.invalidate({ teamId }),
onError: (err) =>
toast.error("Chart update failed", { description: err.message }),
});

const deleteMutation = api.metric.delete.useMutation({
onSuccess: () => utils.dashboard.getDashboardCharts.invalidate({ teamId }),
onError: (err) =>
toast.error("Delete failed", { description: err.message }),
});

const updateMutation = api.metric.update.useMutation({
onSuccess: () => utils.dashboard.getDashboardCharts.invalidate({ teamId }),
onError: (err) =>
toast.error("Update failed", { description: err.message }),
});

// ---------------------------------------------------------------------------
// Handlers
// ---------------------------------------------------------------------------
const handleRefresh = useCallback(
async (forceRebuild = false) => {
try {
if (!isIntegrationMetric || forceRebuild) {
await pipeline.regenerateMetric(metricId);
} else {
await pipeline.refreshMetric(metricId);
}
} catch (error) {
toast.error("Operation failed", {
description: error instanceof Error ? error.message : "Unknown error",
});
(forceRebuild = false) => {
if (!isIntegrationMetric || forceRebuild) {
regenerateMutation.mutate({ metricId });
} else {
refreshMutation.mutate({ metricId });
}
},
[metricId, isIntegrationMetric, pipeline],
[metricId, isIntegrationMetric, refreshMutation, regenerateMutation],
);

const handleDelete = useCallback(async () => {
Expand All @@ -82,51 +126,40 @@ export function DashboardMetricCard({
});

if (confirmed) {
await pipeline.deleteMetric(metricId);
deleteMutation.mutate({ id: metricId });
setIsDrawerOpen(false);
}
}, [metric.name, metricId, confirm, pipeline]);
}, [metric.name, metricId, confirm, deleteMutation]);

const handleUpdateMetric = useCallback(
async (name: string, description: string) => {
try {
await pipeline.updateMetric(metricId, {
name,
description: description || undefined,
});
} catch (error) {
console.error("Failed to update metric:", error);
toast.error("Failed to update metric", {
description: error instanceof Error ? error.message : "Unknown error",
});
}
(name: string, description: string) => {
updateMutation.mutate({
id: metricId,
name,
description: description || undefined,
});
},
[metricId, pipeline],
[metricId, updateMutation],
);

const handleRegenerateChart = useCallback(
async (chartType: string, cadence: Cadence, selectedDimension?: string) => {
try {
await pipeline.regenerateChart(metricId, {
chartType,
cadence,
selectedDimension,
});
} catch (error) {
toast.error("Failed to regenerate chart", {
description: error instanceof Error ? error.message : "Unknown error",
});
}
(chartType: string, cadence: Cadence, selectedDimension?: string) => {
regenerateChartMutation.mutate({
metricId,
chartType,
cadence,
selectedDimension,
});
},
[metricId, pipeline],
[metricId, regenerateChartMutation],
);

// ---------------------------------------------------------------------------
// Render
// ---------------------------------------------------------------------------
const cardContent = (
<div className="relative">
{error && !isProcessing && (
{error && !processing && (
<Tooltip>
<TooltipTrigger asChild>
<Badge
Expand Down Expand Up @@ -183,7 +216,7 @@ export function DashboardMetricCard({
goal={metric.goal}
goalProgress={dashboardChart.goalProgress}
valueLabel={dashboardChart.valueLabel}
isProcessing={isProcessing}
isProcessing={processing}
/>
</div>
);
Expand All @@ -193,12 +226,13 @@ export function DashboardMetricCard({
{cardContent}

<DrawerContent className="flex h-[90vh] max-h-[90vh] flex-col overflow-hidden">
<DrawerTitle className="sr-only">{metric.name} Settings</DrawerTitle>
<div className="min-h-0 w-full flex-1">
<DashboardMetricDrawer
dashboardChart={dashboardChart}
isProcessing={isProcessing}
isProcessing={processing}
error={error}
isDeleting={pipeline.isDeleting}
isDeleting={deleteMutation.isPending}
onRefresh={handleRefresh}
onUpdateMetric={handleUpdateMetric}
onDelete={handleDelete}
Expand Down
31 changes: 10 additions & 21 deletions src/app/dashboard/[teamId]/_components/dashboard-page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,11 @@ import { api } from "@/trpc/react";

import { DashboardContent } from "./dashboard-content";
import { DashboardSidebar } from "./dashboard-sidebar";
import { PipelineStatusProvider } from "./pipeline-status-provider";

interface DashboardPageClientProps {
teamId: string;
}

/**
* Dashboard page client component.
*
* Architecture:
* - PipelineStatusProvider owns the dashboard query with conditional refetch
* - DashboardContent consumes charts from context
* - Simple, single source of truth
*/
export function DashboardPageClient({ teamId }: DashboardPageClientProps) {
const {
data: integrations,
Expand Down Expand Up @@ -56,18 +47,16 @@ export function DashboardPageClient({ teamId }: DashboardPageClientProps) {
}

return (
<PipelineStatusProvider teamId={teamId}>
<div className="container mx-auto px-4 py-8 pt-24">
<div className="mb-8">
<h1 className="text-3xl font-bold tracking-tight">KPIs</h1>
<p className="text-muted-foreground mt-2">
Visualize and monitor your key metrics in one place
</p>
</div>

<DashboardContent />
<DashboardSidebar teamId={teamId} initialIntegrations={integrations} />
<div className="container mx-auto px-4 py-8 pt-24">
<div className="mb-8">
<h1 className="text-3xl font-bold tracking-tight">KPIs</h1>
<p className="text-muted-foreground mt-2">
Visualize and monitor your key metrics in one place
</p>
</div>
</PipelineStatusProvider>

<DashboardContent teamId={teamId} />
<DashboardSidebar teamId={teamId} initialIntegrations={integrations} />
</div>
);
}
Loading