diff --git a/src/app/(dashboard)/pipelines/[id]/metrics/page.tsx b/src/app/(dashboard)/pipelines/[id]/metrics/page.tsx index 7118b01..45517b4 100644 --- a/src/app/(dashboard)/pipelines/[id]/metrics/page.tsx +++ b/src/app/(dashboard)/pipelines/[id]/metrics/page.tsx @@ -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 }, @@ -116,6 +117,18 @@ export default function PipelineMetricsPage() { )} + + {/* Pipeline Logs */} + + + Logs + + +
+ +
+
+
); } diff --git a/src/app/(dashboard)/pipelines/[id]/page.tsx b/src/app/(dashboard)/pipelines/[id]/page.tsx index db87144..49bb9bd 100644 --- a/src/app/(dashboard)/pipelines/[id]/page.tsx +++ b/src/app/(dashboard)/pipelines/[id]/page.tsx @@ -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"; @@ -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 }> @@ -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); @@ -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; + // Merge component metrics into flow node data useEffect(() => { const components = componentMetricsQuery.data?.components; @@ -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) @@ -417,6 +436,11 @@ function PipelineBuilderInner({ pipelineId }: { pipelineId: string }) { )} + {logsOpen && ( +
+ +
+ )} void; + logsOpen?: boolean; + onToggleLogs?: () => void; + hasRecentErrors?: boolean; processStatus?: ProcessStatusValue | null; } @@ -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); @@ -317,6 +324,26 @@ export function FlowToolbar({ )} + {onToggleLogs && ( + + + + + {logsOpen ? "Hide logs" : "Show logs"} + + )} + diff --git a/src/components/pipeline/pipeline-logs.tsx b/src/components/pipeline/pipeline-logs.tsx index 53f5b9c..9d7e9b8 100644 --- a/src/components/pipeline/pipeline-logs.tsx +++ b/src/components/pipeline/pipeline-logs.tsx @@ -166,8 +166,12 @@ export function PipelineLogs({ pipelineId, nodeId }: PipelineLogsProps) { {log.level} {" "} - [{log.node.name}] - {" "} + {log.node?.name && ( + <> + [{log.node.name}] + {" "} + + )} {log.message} ))} diff --git a/src/server/routers/pipeline.ts b/src/server/routers/pipeline.ts index 4841761..346110c 100644 --- a/src/server/routers/pipeline.ts +++ b/src/server/routers/pipeline.ts @@ -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 = { pipelineId }; @@ -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,