Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
ce97549
feat(tekton): Pipeline/PipelineRun/TaskRun with embedded DAG + live p…
jfillman Aug 30, 2026
0bc5201
fix(tekton): move task DAG to full view with real topology-style nodes
jfillman Aug 30, 2026
1e423fa
docs(tekton): update integration docs for the full-view DAG redesign
jfillman Aug 30, 2026
cb6f302
fix(tekton): add explicit back-link from TaskRun to its PipelineRun
jfillman Aug 30, 2026
c3bee3a
fix(tekton): sequential combined step logs + pod link on TaskRun
jfillman Aug 30, 2026
003b00e
fix(tekton): move back-to-PipelineRun link into the header chip row
jfillman Aug 30, 2026
be2eb26
fix(tekton): widen the step-name label in combined TaskRun logs
jfillman Aug 30, 2026
9ee91ea
feat(tekton): add a CI/CD tab — PipelineRun fleet view
jfillman Aug 30, 2026
3b4224c
fix(tekton): stop the DAG canvas from panning off-screen on scroll
jfillman Aug 30, 2026
b36b303
feat(tekton): sortable CI/CD table + GitOps-style header
jfillman Aug 30, 2026
4ee1297
fix(nav): keep a non-Resources peek's backdrop on further drill-downs
jfillman Aug 30, 2026
7ec447c
fix(nav): close a never-small peek outright instead of collapsing it
jfillman Aug 30, 2026
c447dbc
feat(tekton): add row actions to the CI/CD table
jfillman Aug 30, 2026
97743cf
fix(tekton): fix the DAG blanking (nodes hidden, edges missing) on li…
jfillman Aug 30, 2026
4149c4d
feat(tekton): bulk multi-select delete for the CI/CD table
jfillman Aug 30, 2026
5ffe14c
feat(tekton): make the CI/CD table's Pipeline column clickable
jfillman Aug 30, 2026
5f2144a
feat(tekton): widen ELK spacing + merge edges to fix overlapping DAG …
jfillman Sep 1, 2026
9f6cec9
feat(tekton): draggable DAG nodes with browser-local saved layout
jfillman Sep 1, 2026
8070a77
feat(tekton): background wash + ring on running DAG task cards
jfillman Sep 1, 2026
a9201cc
fix(tekton): solid running-task background, drop the opacity pulse
jfillman Sep 1, 2026
e8444df
fix(tekton): compute transitive reduction of the pipeline task DAG
jfillman Sep 4, 2026
8326759
fix(tekton): include spec.finally tasks in the DAG and task counts
jfillman Sep 4, 2026
b0d40b8
fix(tekton): aggregate matrix TaskRun statuses instead of keeping the…
jfillman Sep 4, 2026
3850536
fix(tekton): show a skipped task as skipped, not pending forever
jfillman Sep 4, 2026
72d68db
fix(nav): seed Tekton kinds into the pre-discovery plural-to-kind map
jfillman Sep 4, 2026
35d8aa0
fix(tekton): show an error state in the CI/CD fleet, not a false empt…
jfillman Sep 4, 2026
bb86f8f
fix(tekton): read the real step container name; poll running logs; fi…
jfillman Sep 4, 2026
f80954f
fix(nav): close a never-small CI/CD peek on browser Back, not collaps…
jfillman Sep 4, 2026
f336685
style(tekton): drop before/after comments; expand TaskRun Parameters/…
jfillman Sep 4, 2026
1175d94
fix(tekton): reset stale peekOpenedFullRef on close; unknown no longe…
jfillman Sep 4, 2026
21e5c8a
fix(tekton): count finally tasks in the Pipeline status badge
jfillman Sep 4, 2026
523aae3
fix(workload): keep the Logs tab visible for TaskRun/PipelineRun
jfillman Sep 6, 2026
1440418
fix(tekton): keep a DAG status update that lands during layout
jfillman Oct 7, 2026
db47ee3
fix(tekton): show a cancelled TaskRun as cancelled, not failed
jfillman Oct 7, 2026
a16a757
fix(tekton): stop polling TaskRuns that have settled
jfillman Oct 7, 2026
223e95d
fix(cicd): wait for cluster sync and add CI/CD to the command palette
jfillman Oct 7, 2026
f16e5c2
fix(cicd): show no value instead of a false zero on summary tiles
jfillman Oct 7, 2026
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ Upgrade impact also gets list-only access to CSIStorageCapacities, FlowSchemas,
| **GitOps (ArgoCD)** | Application, ApplicationSet, AppProject |
| **Argo Rollouts** | Rollout |
| **Argo Workflows** | Workflow, WorkflowTemplate |
| **Tekton Pipelines** | Pipeline, PipelineRun, TaskRun |
| **[Reflector](docs/integrations.md#configmap-and-secret-reflection-reflector)** | ConfigMap/Secret reflection details, source and mirror relationships, recorded copy evidence |
| **cert-manager** | Certificate, CertificateRequest, Order, Challenge, Issuer, ClusterIssuer |
| **Gateway API** | Gateway, GatewayClass, HTTPRoute, GRPCRoute, TCPRoute, TLSRoute |
Expand Down
7 changes: 7 additions & 0 deletions deploy/helm/radar/files/integration-read-baseline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,13 @@ entries:
decision: existing
reason: "Covered by the existing cluster-read add-on and its existing collection/tier gates."
source: "pkg/topology/cluster_scoped_kinds.go; existing cloud-rbac-cluster-read.yaml"
- group: "tekton.dev"
resources: ["pipelines","pipelineruns","taskruns"]
scope: Namespaced
collection: "tekton"
decision: withhold
reason: "Pending review: PipelineRun and TaskRun specs carry parameter values and inline workspace or pod-template data that have not been assessed for credentials."
source: "internal/k8s/dynamic_cache.go (supportedCRDFallbacks)"
- group: "traefik.containo.us"
resources: ["ingressroutes","ingressroutetcps","ingressrouteudps","middlewaretcps","serverstransports","tlsoptions","tlsstores","traefikservices"]
scope: Namespaced
Expand Down
26 changes: 26 additions & 0 deletions docs/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,32 @@ See the main [README](../README.md#gitops) for the user-facing overview. This se

---

## Tekton Pipelines

[Tekton Pipelines](https://tekton.dev/) is a Kubernetes-native CI/CD building block: Pipelines declare a DAG of Tasks, PipelineRuns execute them, and TaskRuns carry out each individual Task.

### What Radar Shows

**Pipeline / PipelineRun full view:** The task-dependency graph — built from `spec.tasks[].runAfter` plus any implicit ordering inferred from `$(tasks.<name>.results.*)` references in params or `when` expressions — lives in the resource's fullscreen "full view" (the expand icon on the drawer), not the compact drawer. Layout uses the same ELK.js engine as the main Topology view, and task nodes mirror the Topology view's node cards (icon, kind label, status dot). For a PipelineRun, each node is colored live by its TaskRun's outcome (succeeded / failed / running / pending / skipped), read from `status.childReferences` (targets `tekton.dev/v1`; a task absent from `childReferences` hasn't been reached yet, not an error) — running tasks get a spinning icon and a pulsing card. The graph is deliberately the only progress indicator — a Pipeline's tasks form a real DAG with parallel branches, so a linear step list would misrepresent the concurrency the graph already shows. A task with a live TaskRun is clickable and opens that TaskRun's own page.

**Pipeline / PipelineRun compact drawer:** Run info, parameters, and workspaces only — no graph; the drawer has no room to render one legibly. A hint points to the full view for the task graph.

**TaskRun Detail View:** Per-step status (running / completed / waiting, with exit codes) with a **Logs** button per step that streams that step's container logs (Tekton names each step's container `step-<name>`), plus parameters and results.

**Resource Browser:** Pipeline rows show task count; PipelineRun/TaskRun rows show status, the referenced Pipeline/Task (resolver-based refs included), and duration.

Topology-graph integration (Pipeline/PipelineRun/TaskRun nodes in the main cluster Topology view) isn't wired up yet — the fullscreen task graph is self-contained and doesn't depend on it.

### Supported CRDs

| CRD | Group | Topology | Detail View | AI Summary |
|-----|-------|----------|-------------|------------|
| Pipeline | `tekton.dev/v1` | — | Yes | — |
| PipelineRun | `tekton.dev/v1` | — | Yes | — |
| TaskRun | `tekton.dev/v1` | — | Yes | — |

---

## Istio

[Istio](https://istio.io/) is the most widely adopted service mesh, providing traffic management, security (mTLS), and observability for microservices.
Expand Down
3 changes: 3 additions & 0 deletions internal/k8s/dynamic_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@ var supportedCRDFallbacks = []supportedCRDResource{
{Group: "argoproj.io", Versions: []string{"v1alpha1"}, Resource: "cronworkflows", Kind: "CronWorkflow", Namespaced: true},
{Group: "argoproj.io", Versions: []string{"v1alpha1"}, Resource: "workflowtemplates", Kind: "WorkflowTemplate", Namespaced: true},
{Group: "argoproj.io", Versions: []string{"v1alpha1"}, Resource: "clusterworkflowtemplates", Kind: "ClusterWorkflowTemplate", Namespaced: false},
{Group: "tekton.dev", Versions: []string{"v1", "v1beta1"}, Resource: "pipelines", Kind: "Pipeline", Namespaced: true},
{Group: "tekton.dev", Versions: []string{"v1", "v1beta1"}, Resource: "pipelineruns", Kind: "PipelineRun", Namespaced: true},
{Group: "tekton.dev", Versions: []string{"v1", "v1beta1"}, Resource: "taskruns", Kind: "TaskRun", Namespaced: true},
{Group: "cert-manager.io", Versions: []string{"v1"}, Resource: "certificates", Kind: "Certificate", Namespaced: true},
{Group: "cert-manager.io", Versions: []string{"v1"}, Resource: "issuers", Kind: "Issuer", Namespaced: true},
{Group: "cert-manager.io", Versions: []string{"v1"}, Resource: "clusterissuers", Kind: "ClusterIssuer", Namespaced: false},
Expand Down
15 changes: 15 additions & 0 deletions packages/k8s-ui/src/components/logs/LogCore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ interface LogCoreProps {
onClear?: () => void
toolbarExtra?: ToolbarExtraRenderer
showPodName?: boolean
/** Like showPodName, but labels each line with its source container instead — for a single-pod, multi-container combined view (e.g. a Tekton TaskRun's steps). */
showContainerName?: boolean
emptyMessage?: string
emptyCommand?: string | null
errorMessage?: string | null
Expand Down Expand Up @@ -145,6 +147,7 @@ export function LogCore({
onClear,
toolbarExtra,
showPodName = false,
showContainerName = false,
emptyMessage = 'No logs available',
emptyCommand,
errorMessage,
Expand Down Expand Up @@ -1032,6 +1035,7 @@ export function LogCore({
searchIsRegex={search.isRegex}
searchIsCaseSensitive={search.isCaseSensitive}
showPodName={showPodName}
showContainerName={showContainerName}
showTimestamp={showTimestamps}
tsFormat={tsFormat}
ansiEnabled={ansiEnabled}
Expand Down Expand Up @@ -1144,6 +1148,7 @@ interface LogLineProps {
searchIsRegex: boolean
searchIsCaseSensitive: boolean
showPodName: boolean
showContainerName: boolean
showTimestamp: boolean
tsFormat: TimestampFormat
ansiEnabled: boolean
Expand All @@ -1165,6 +1170,7 @@ function LogLine({
searchIsRegex,
searchIsCaseSensitive,
showPodName,
showContainerName,
showTimestamp,
tsFormat,
ansiEnabled,
Expand Down Expand Up @@ -1244,6 +1250,14 @@ function LogLine({
[{entry.sourceLabel || entry.pod.split('-').slice(-2).join('-')}]
</span>
)}
{showContainerName && entry.container && (
<span
className={`${palette.textTertiary} select-none pr-2 whitespace-nowrap min-w-[140px] max-w-[260px] truncate`}
title={entry.container}
>
[{entry.container}]
</span>
)}
<span className="flex-1 min-w-0">{contentElement}</span>
<button
onClick={handleCopy}
Expand All @@ -1262,6 +1276,7 @@ interface LogGroupItemProps {
searchIsRegex: boolean
searchIsCaseSensitive: boolean
showPodName: boolean
showContainerName: boolean
showTimestamp: boolean
tsFormat: TimestampFormat
ansiEnabled: boolean
Expand Down
85 changes: 85 additions & 0 deletions packages/k8s-ui/src/components/resources/ResourcesView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ import {
SEVERITY_DOT_COLOR,
healthColors,
} from './resource-utils'
import { buildPipelineTaskGraph, getTektonPipelineRunStatus, getTektonTaskRunStatus, tektonRefName } from './resource-utils-tekton'
import { getGenericResourceStatus } from './generic-status'
import { getIstioGatewayServerCount, getIstioGatewaySelectorString } from './resource-utils-istio'
import {
Expand Down Expand Up @@ -235,6 +236,7 @@ export const SKIP_FILTER_COLUMNS = new Set([
// status.summary. Listing them here stops the header reserving room for a
// button that can never render.
'critical', 'high', 'medium', 'low', 'pass', 'fail', 'warn', 'error', 'skip',
'tasks', 'pipeline', 'task',
])

// Namespace/node cardinality scales with cluster size, not kind enum size;
Expand Down Expand Up @@ -747,6 +749,28 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
{ key: 'template', label: 'Template', width: 'w-40' },
{ key: 'age', label: 'Age', width: 'w-24' },
],
pipelines: [
{ key: 'name', label: 'Name' },
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
{ key: 'tasks', label: 'Tasks', width: 'w-20', tooltip: 'Number of declared tasks' },
{ key: 'age', label: 'Age', width: 'w-24' },
],
pipelineruns: [
{ key: 'name', label: 'Name' },
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
{ key: 'status', label: 'Status', width: 'w-32' },
{ key: 'pipeline', label: 'Pipeline', width: 'w-40' },
{ key: 'duration', label: 'Duration', width: 'w-24' },
{ key: 'age', label: 'Age', width: 'w-24' },
],
taskruns: [
{ key: 'name', label: 'Name' },
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
{ key: 'status', label: 'Status', width: 'w-32' },
{ key: 'task', label: 'Task', width: 'w-40' },
{ key: 'duration', label: 'Duration', width: 'w-24' },
{ key: 'age', label: 'Age', width: 'w-24' },
],
certificates: [
{ key: 'name', label: 'Name' },
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
Expand Down Expand Up @@ -2975,6 +2999,9 @@ const CURATED_COLUMN_GROUPS: Record<string, readonly string[]> = {
containersources: ['sources.knative.dev'],
pingsources: ['sources.knative.dev'],
sinkbindings: ['sources.knative.dev'],
pipelines: ['tekton.dev'],
pipelineruns: ['tekton.dev'],
taskruns: ['tekton.dev'],
ingressroutes: ['traefik.io', 'traefik.containo.us'],
ingressroutetcps: ['traefik.io', 'traefik.containo.us'],
ingressrouteudps: ['traefik.io', 'traefik.containo.us'],
Expand Down Expand Up @@ -7201,6 +7228,12 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
return <WorkflowCell resource={resource} column={column} />
case 'cronworkflows':
return <CronWorkflowCell resource={resource} column={column} />
case 'pipelines':
return <PipelineCell resource={resource} column={column} />
case 'pipelineruns':
return <PipelineRunCell resource={resource} column={column} />
case 'taskruns':
return <TaskRunCell resource={resource} column={column} />
case 'certificates':
return <CertificateCell resource={resource} column={column} />
case 'persistentvolumes':
Expand Down Expand Up @@ -8986,6 +9019,58 @@ function CronWorkflowCell({ resource, column }: { resource: any; column: string
}
}

function PipelineCell({ resource, column }: { resource: any; column: string }) {
switch (column) {
case 'tasks':
// buildPipelineTaskGraph, not a bare spec.tasks.length, so finally
// tasks count toward the total — Tekton runs them and includes their
// outcome in the PipelineRun result same as any regular task.
return <span className="text-sm text-theme-text-secondary">{buildPipelineTaskGraph(resource.spec).length}</span>
default:
return <span className="text-sm text-theme-text-tertiary">-</span>
}
}

function tektonDuration(resource: any): number | null {
const start = resource.status?.startTime
if (!start) return null
const end = resource.status?.completionTime
return (end ? new Date(end).getTime() : Date.now()) - new Date(start).getTime()
}

function PipelineRunCell({ resource, column }: { resource: any; column: string }) {
switch (column) {
case 'status': {
const status = getTektonPipelineRunStatus(resource)
return <span className={clsx('badge', status.color)}>{status.text}</span>
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
}
case 'pipeline':
return <span className="text-sm text-theme-text-secondary">{tektonRefName(resource.spec?.pipelineRef)}</span>
case 'duration': {
const ms = tektonDuration(resource)
return <span className="text-sm text-theme-text-secondary">{ms !== null ? formatDuration(ms) : '-'}</span>
}
default:
return <span className="text-sm text-theme-text-tertiary">-</span>
}
}

function TaskRunCell({ resource, column }: { resource: any; column: string }) {
switch (column) {
case 'status': {
const status = getTektonTaskRunStatus(resource)
return <span className={clsx('badge', status.color)}>{status.text}</span>
}
case 'task':
return <span className="text-sm text-theme-text-secondary">{tektonRefName(resource.spec?.taskRef)}</span>
case 'duration': {
const ms = tektonDuration(resource)
return <span className="text-sm text-theme-text-secondary">{ms !== null ? formatDuration(ms) : '-'}</span>
}
default:
return <span className="text-sm text-theme-text-tertiary">-</span>
}
}

function PersistentVolumeCell({ resource, column }: { resource: any; column: string }) {
switch (column) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ describe('curated column ownership', () => {
// Brace-counting, not parsing: assert the extraction actually saw the whole
// table, or a silently-missed key would let the guard above pass vacuously.
it('extracted every curated key', () => {
expect(CURATED.length).toBe(239)
expect(OWNED.size).toBe(201)
expect(CURATED.length).toBe(242)
expect(OWNED.size).toBe(204)
expect(CURATED).toContain('pods')
expect(CURATED).toContain('crossplanemanagedresources')
expect([...OWNED]).toContain('awsmanagedcontrolplanes')
Expand Down Expand Up @@ -166,7 +166,7 @@ describe('every declaration resolves', () => {
.map(m => [m[1], m[2].split(',').map(g => g.trim().replace(/'/g, ''))] as const)

it('covers the whole ownership table', () => {
expect(DECLARED.length).toBe(201)
expect(DECLARED.length).toBe(204)
})

it.each(DECLARED)('%s is curated under each group it claims', (key, groups) => {
Expand Down
1 change: 1 addition & 0 deletions packages/k8s-ui/src/components/resources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export * from './resource-utils-prometheus'
export * from './resource-utils-trivy'
export * from './resource-utils-traefik'
export * from './resource-utils-velero'
export * from './resource-utils-tekton'
export { ResourcesView, ResourcesViewDataContext, hasCuratedColumns } from './ResourcesView'
export type { ResourceQueryResult, ExtraColumn, LargeListGuardState } from './ResourcesView'
export { ResourcesSidebar } from './ResourcesSidebar'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// @vitest-environment jsdom
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import type { TektonTaskNode } from '../resource-utils-tekton'

const layout = vi.hoisted(() => ({ resolve: (_: unknown) => {} }))

vi.mock('elkjs/lib/elk.bundled.js', () => ({
default: class {
layout() {
return new Promise((resolve) => { layout.resolve = resolve })
}
},
}))

// jsdom has no layout engine, so render each node's task status as text
// instead of going through React Flow's measured viewport.
vi.mock('@xyflow/react', async (importOriginal) => ({
...(await importOriginal<typeof import('@xyflow/react')>()),
ReactFlow: ({ nodes }: { nodes: Array<{ id: string; data: { task: TektonTaskNode } }> }) => (
<ul>
{nodes.map((n) => <li key={n.id} data-task={n.id}>{n.data.task.status}</li>)}
</ul>
),
ReactFlowProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
Controls: () => null,
Panel: () => null,
}))

const { PipelineDagView } = await import('./PipelineDagView')

let element: HTMLDivElement
let root: Root

beforeEach(() => {
element = document.createElement('div')
document.body.appendChild(element)
root = createRoot(element)
})

afterEach(async () => {
await act(async () => root.unmount())
element.remove()
})

const tasks = (status: TektonTaskNode['status']): TektonTaskNode[] => [
{ name: 'build', dependsOn: [], status },
{ name: 'test', dependsOn: ['build'], status: 'pending' },
]

it('keeps a status update that lands while the layout is still running', async () => {
await act(async () => root.render(<PipelineDagView tasks={tasks('running')} />))
await act(async () => root.render(<PipelineDagView tasks={tasks('succeeded')} />))

await act(async () => layout.resolve({ children: [{ id: 'build', x: 0, y: 0 }, { id: 'test', x: 300, y: 0 }] }))

expect(element.querySelector('[data-task="build"]')?.textContent).toBe('succeeded')
})
Loading
Loading