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
13 changes: 13 additions & 0 deletions src/app/(dashboard)/pipelines/[id]/metrics/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { SummaryCards } from "@/components/metrics/summary-cards";
import { MetricsChart } from "@/components/metrics/component-chart";
import { PipelineLogs } from "@/components/pipeline/pipeline-logs";

const TIME_RANGES = [
{ label: "5m", minutes: 5 },
Expand Down Expand Up @@ -116,6 +117,18 @@ export default function PipelineMetricsPage() {
</Card>
</>
)}

{/* Pipeline Logs */}
<Card>
<CardHeader>
<CardTitle>Logs</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="h-[400px]">
<PipelineLogs pipelineId={params.id} />
</div>
</CardContent>
</Card>
</div>
);
}
26 changes: 25 additions & 1 deletion src/app/(dashboard)/pipelines/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import type { NodeMetricsData } from "@/stores/flow-store";
Expand Down Expand Up @@ -34,6 +34,7 @@ import { DeployDialog } from "@/components/flow/deploy-dialog";
import { SaveTemplateDialog } from "@/components/flow/save-template-dialog";
import { ConfirmDialog } from "@/components/confirm-dialog";
import { PipelineMetricsChart } from "@/components/pipeline/metrics-chart";
import { PipelineLogs } from "@/components/pipeline/pipeline-logs";

function aggregateProcessStatus(
statuses: Array<{ status: string }>
Expand Down Expand Up @@ -114,6 +115,7 @@ function PipelineBuilderInner({ pipelineId }: { pipelineId: string }) {
const [deleteOpen, setDeleteOpen] = useState(false);
const [undeployOpen, setUndeployOpen] = useState(false);
const [metricsOpen, setMetricsOpen] = useState(false);
const [logsOpen, setLogsOpen] = useState(false);

const loadGraph = useFlowStore((s) => s.loadGraph);
const isDirty = useFlowStore((s) => s.isDirty);
Expand Down Expand Up @@ -157,6 +159,20 @@ function PipelineBuilderInner({ pipelineId }: { pipelineId: string }) {
),
);

// Lightweight check for recent errors (for toolbar badge) — 24h window
const errorCheckSince = useMemo(
() => new Date(Date.now() - 24 * 60 * 60 * 1000),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const recentErrorsQuery = useQuery(
trpc.pipeline.logs.queryOptions(
{ pipelineId, levels: ["ERROR"], limit: 1, since: errorCheckSince },
{ enabled: !!isDeployed && !logsOpen, refetchInterval: 10000 },
),
);
const hasRecentErrors = (recentErrorsQuery.data?.items?.length ?? 0) > 0;
Comment on lines +168 to +174
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error badge query fetches the single most recent ERROR log with no time-window filter. If a pipeline had an error 3 months ago and has been healthy since, hasRecentErrors remains permanently true — the red dot will always appear, even though the issue is long resolved.

The logs tRPC endpoint does not currently support time-window parameters. To fix this, consider:

  1. Adding a since / after parameter to the logs endpoint (e.g., last 24h)
  2. Threading it through this query: { levels: ["ERROR"], limit: 1, since: new Date(Date.now() - 24*60*60*1000) }
  3. Or at minimum, documenting in the UI that the badge reflects "any historical error" rather than recent ones

Without a recency bound, the signal degrades into noise for long-running pipelines.


// Merge component metrics into flow node data
useEffect(() => {
const components = componentMetricsQuery.data?.components;
Expand Down Expand Up @@ -367,6 +383,9 @@ function PipelineBuilderInner({ pipelineId }: { pipelineId: string }) {
isDirty={isDirty}
metricsOpen={metricsOpen}
onToggleMetrics={() => setMetricsOpen((v) => !v)}
logsOpen={logsOpen}
onToggleLogs={() => setLogsOpen((v) => !v)}
hasRecentErrors={hasRecentErrors}
processStatus={
pipelineQuery.data?.nodeStatuses
? aggregateProcessStatus(pipelineQuery.data.nodeStatuses)
Expand Down Expand Up @@ -417,6 +436,11 @@ function PipelineBuilderInner({ pipelineId }: { pipelineId: string }) {
<PipelineMetricsChart pipelineId={pipelineId} />
</div>
)}
{logsOpen && (
<div className="h-[300px] shrink-0 border-t">
<PipelineLogs pipelineId={pipelineId} />
</div>
)}
<DeployDialog pipelineId={pipelineId} open={deployOpen} onOpenChange={setDeployOpen} />
<SaveTemplateDialog open={templateOpen} onOpenChange={setTemplateOpen} />
<ConfirmDialog
Expand Down
27 changes: 27 additions & 0 deletions src/components/flow/flow-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
BookTemplate,
History,
BarChart3,
ScrollText,
Settings,
} from "lucide-react";
import { toast } from "sonner";
Expand Down Expand Up @@ -61,6 +62,9 @@ interface FlowToolbarProps {
isDirty?: boolean;
metricsOpen?: boolean;
onToggleMetrics?: () => void;
logsOpen?: boolean;
onToggleLogs?: () => void;
hasRecentErrors?: boolean;
processStatus?: ProcessStatusValue | null;
}

Expand All @@ -87,6 +91,9 @@ export function FlowToolbar({
isDirty = false,
metricsOpen = false,
onToggleMetrics,
logsOpen = false,
onToggleLogs,
hasRecentErrors = false,
processStatus,
}: FlowToolbarProps) {
const globalConfig = useFlowStore((s) => s.globalConfig);
Expand Down Expand Up @@ -317,6 +324,26 @@ export function FlowToolbar({
</Tooltip>
)}

{onToggleLogs && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={logsOpen ? "secondary" : "ghost"}
size="sm"
onClick={onToggleLogs}
className="relative h-7 w-7 p-0"
aria-label="Toggle logs panel"
>
<ScrollText className="h-4 w-4" />
{hasRecentErrors && !logsOpen && (
<span className="absolute -right-0.5 -top-0.5 h-2 w-2 rounded-full bg-red-500" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>{logsOpen ? "Hide logs" : "Show logs"}</TooltipContent>
</Tooltip>
)}

<Popover>
<Tooltip>
<TooltipTrigger asChild>
Expand Down
8 changes: 6 additions & 2 deletions src/components/pipeline/pipeline-logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,12 @@ export function PipelineLogs({ pipelineId, nodeId }: PipelineLogsProps) {
{log.level}
</span>
{" "}
<span className="text-blue-400/70">[{log.node.name}]</span>
{" "}
{log.node?.name && (
<>
<span className="text-blue-400/70">[{log.node.name}]</span>
{" "}
</>
)}
<span className="text-gray-300">{log.message}</span>
</div>
))}
Expand Down
6 changes: 5 additions & 1 deletion src/server/routers/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -822,11 +822,12 @@ export const pipelineRouter = router({
limit: z.number().min(1).max(500).default(200),
levels: z.array(z.nativeEnum(LogLevel)).optional(),
nodeId: z.string().optional(),
since: z.date().optional(),
}),
)
.use(withTeamAccess("VIEWER"))
.query(async ({ input }) => {
const { pipelineId, cursor, limit, levels, nodeId } = input;
const { pipelineId, cursor, limit, levels, nodeId, since } = input;
const take = limit;

const where: Record<string, unknown> = { pipelineId };
Expand All @@ -836,6 +837,9 @@ export const pipelineRouter = router({
if (nodeId) {
where.nodeId = nodeId;
}
if (since) {
where.timestamp = { gte: since };
}

const items = await prisma.pipelineLog.findMany({
where,
Expand Down
Loading