pipeline dag visualization + Dashboard command center upgrade + command palette wiring. fixed repo pipeline page.

This commit is contained in:
2026-05-11 20:49:48 +02:00
parent 3838aa1f53
commit 4f2fb846dd
15 changed files with 1659 additions and 203 deletions
+166 -13
View File
@@ -1,37 +1,190 @@
import { useQuery } from '@tanstack/react-query'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { z } from 'zod'
import { api } from '../client'
import type { Pipeline } from '../../types/api'
import type { Pipeline, PipelineRun, RunDetail, StepLogs } from '../../types/api'
// ── Zod schemas ───────────────────────────────────────────────────────────────
const runStatusSchema = z.enum(['queued', 'running', 'succeeded', 'failed', 'cancelled'])
const pipelineSchema = z.object({
id: z.number(),
repoId: z.number(),
ref: z.string(),
status: z.enum(['pending', 'running', 'success', 'failure', 'cancelled']),
name: z.string(),
filePath: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
})
const pipelinesSchema = z.array(pipelineSchema)
const runSchema = z.object({
id: z.number(),
pipelineId: z.number(),
repoId: z.number(),
triggerRef: z.string(),
triggerSha: z.string(),
triggeredBy: z.string(),
status: runStatusSchema,
startedAt: z.string().nullable(),
finishedAt: z.string().nullable(),
createdAt: z.string(),
})
const stepSchema = z.object({
id: z.number(),
jobId: z.number(),
seq: z.number(),
name: z.string(),
runCmd: z.string(),
usesAction: z.string(),
status: runStatusSchema,
exitCode: z.number(),
startedAt: z.string().nullable(),
finishedAt: z.string().nullable(),
})
const jobSchema = z.object({
id: z.number(),
runId: z.number(),
name: z.string(),
image: z.string(),
needs: z.string(),
status: runStatusSchema,
startedAt: z.string().nullable(),
finishedAt: z.string().nullable(),
createdAt: z.string(),
steps: z.array(stepSchema),
})
const runDetailSchema = runSchema.extend({
jobs: z.array(jobSchema),
})
const stepLogSchema = z.object({
id: z.number(),
stepId: z.number(),
chunkIndex: z.number(),
content: z.string(),
createdAt: z.string(),
})
const stepLogsSchema = z.array(
stepSchema.extend({ logs: z.array(stepLogSchema) }),
)
// ── Queries ───────────────────────────────────────────────────────────────────
/** Pipeline definitions for a repo. */
export function usePipelines(owner: string, repo: string) {
return useQuery({
queryKey: ['repos', owner, repo, 'pipelines'],
queryFn: () =>
api.get<Pipeline[]>(`/api/v1/repos/${owner}/${repo}/pipelines`, pipelinesSchema),
api.get<Pipeline[]>(`/api/v1/repos/${owner}/${repo}/pipelines`, z.array(pipelineSchema)),
enabled: Boolean(owner && repo),
refetchInterval: 5000, // poll while pipelines may be running
})
}
export function usePipeline(owner: string, repo: string, runId: number) {
/** Pipeline runs for a repo, newest first. */
export function useRuns(owner: string, repo: string, limit = 30) {
return useQuery({
queryKey: ['repos', owner, repo, 'pipelines', runId],
queryKey: ['repos', owner, repo, 'runs', limit],
queryFn: () =>
api.get<Pipeline>(
`/api/v1/repos/${owner}/${repo}/pipelines/${runId}`,
pipelineSchema,
api.get<PipelineRun[]>(
`/api/v1/repos/${owner}/${repo}/runs?limit=${limit}`,
z.array(runSchema),
),
enabled: Boolean(owner && repo && runId),
enabled: Boolean(owner && repo),
refetchInterval: 8_000, // poll while runs may be active
})
}
/** Run detail: run + jobs (each with steps). */
export function useRunDetail(owner: string, repo: string, runId: number) {
return useQuery({
queryKey: ['repos', owner, repo, 'runs', runId],
queryFn: () =>
api.get<RunDetail>(
`/api/v1/repos/${owner}/${repo}/runs/${runId}`,
runDetailSchema,
),
enabled: Boolean(owner && repo && runId),
refetchInterval: (query) => {
const status = query.state.data?.status
return status === 'running' || status === 'queued' ? 3_000 : false
},
})
}
/** Step-level log chunks for a job. */
export function useJobLogs(owner: string, repo: string, runId: number, jobId: number) {
return useQuery({
queryKey: ['repos', owner, repo, 'runs', runId, 'jobs', jobId, 'logs'],
queryFn: () =>
api.get<StepLogs>(
`/api/v1/repos/${owner}/${repo}/runs/${runId}/jobs/${jobId}/logs`,
stepLogsSchema,
),
enabled: Boolean(owner && repo && runId && jobId),
refetchInterval: (query) => {
// Keep polling only while the job may still be running
const hasRunning = query.state.data?.some(s => s.status === 'running')
return hasRunning ? 2_000 : false
},
})
}
/** Recent pipeline runs across all repos owned by the current user. */
export function useRecentRuns(limit = 20) {
return useQuery({
queryKey: ['pipelines', 'runs', limit],
queryFn: () =>
api.get<RecentRun[]>(`/api/v1/pipelines/runs?limit=${limit}`, recentRunSchema),
refetchInterval: 10_000,
})
}
// ── Mutations ─────────────────────────────────────────────────────────────────
export function useCancelRun(owner: string, repo: string) {
const qc = useQueryClient()
return useMutation({
mutationFn: (runId: number) =>
api.post(`/api/v1/repos/${owner}/${repo}/runs/${runId}/cancel`, z.unknown(), undefined),
onSuccess: (_data, runId) => {
qc.invalidateQueries({ queryKey: ['repos', owner, repo, 'runs'] })
qc.invalidateQueries({ queryKey: ['repos', owner, repo, 'runs', runId] })
qc.invalidateQueries({ queryKey: ['pipelines', 'runs'] })
},
})
}
export function useRetryJob(owner: string, repo: string, runId: number) {
const qc = useQueryClient()
return useMutation({
mutationFn: (jobId: number) =>
api.post(
`/api/v1/repos/${owner}/${repo}/runs/${runId}/jobs/${jobId}/retry`,
z.unknown(),
undefined,
),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['repos', owner, repo, 'runs', runId] })
qc.invalidateQueries({ queryKey: ['repos', owner, repo, 'runs'] })
qc.invalidateQueries({ queryKey: ['pipelines', 'runs'] })
},
})
}
// ── Cross-repo recent run type ─────────────────────────────────────────────────
// Returned by GET /api/v1/pipelines/runs — extends PipelineRun with repo context.
export interface RecentRun extends PipelineRun {
repoName: string
ownerName: string
}
const recentRunSchema = z.array(
runSchema.extend({
repoName: z.string(),
ownerName: z.string(),
}),
)