CI/CD - CodeBuild + CodeDeploy + CodePipeline
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
import {ExternalLink, Play, RefreshCw} from 'lucide-react'
|
||||
import {getCodePipelineState, listCodePipelineExecutions, startCodePipeline, type PipelineState} from '@/api/cloudProxyClient'
|
||||
import {formatDateTime} from '@/lib/format'
|
||||
import type {CloudResource} from '@/types/resource'
|
||||
|
||||
interface CodePipelinePanelProps { resource?: CloudResource; runtimeReachable: boolean }
|
||||
|
||||
export function CodePipelinePanel({resource, runtimeReachable}: CodePipelinePanelProps) {
|
||||
const client = useQueryClient()
|
||||
const enabled = Boolean(resource && runtimeReachable)
|
||||
const executionsKey = ['codepipeline-executions', resource?.id]
|
||||
const executions = useQuery({queryKey: executionsKey, queryFn: ({signal}) => listCodePipelineExecutions(resource!.id, signal), enabled, refetchInterval: (query) => query.state.data?.some((execution) => active(execution.status)) ? 2_000 : false})
|
||||
const state = useQuery({queryKey: ['codepipeline-state', resource?.id], queryFn: ({signal}) => getCodePipelineState(resource!.id, signal), enabled, refetchInterval: (query) => query.state.data && activeState(query.state.data) ? 2_000 : false})
|
||||
const start = useMutation({mutationFn: () => startCodePipeline(resource!.id), onSuccess: () => { void client.invalidateQueries({queryKey: executionsKey}); void client.invalidateQueries({queryKey: ['codepipeline-state', resource?.id]}) }})
|
||||
if (!resource) return null
|
||||
return <section className="table-panel">
|
||||
<div className="dynamic-stage-header"><div><p className="eyebrow">Pipeline execution</p><h3>Run and inspect stages</h3><p className="muted compact-text">Local actions: S3 source, CodeBuild, and S3 deploy.</p></div><div className="header-actions"><button className="button" type="button" disabled={!enabled || executions.isFetching || state.isFetching} onClick={() => { void executions.refetch(); void state.refetch() }}><RefreshCw size={13}/>Refresh</button><button className="button primary" type="button" disabled={!enabled || start.isPending} onClick={() => start.mutate()}><Play size={13}/>{start.isPending ? 'Starting' : 'Release change'}</button></div></div>
|
||||
{start.isError && <p className="error-text compact-text">{message(start.error, 'Unable to start pipeline execution.')}</p>}
|
||||
{(executions.isError || state.isError) && <p className="error-text compact-text">{message(executions.error ?? state.error, 'Unable to load pipeline execution state.')}</p>}
|
||||
<div className="workflow-execution-list"><div className="workflow-execution-list-heading"><h4>Execution history</h4><span>{executions.data?.length ?? 0}</span></div><div className="workflow-execution-list-table"><table className="table"><thead><tr><th>Execution</th><th>Status</th><th>Started</th><th>Updated</th></tr></thead><tbody>{(executions.data ?? []).map((execution) => <tr key={execution.id}><td><code>{execution.id}</code>{execution.statusSummary && <p className="muted compact-text">{execution.statusSummary}</p>}</td><td>{execution.status ?? '-'}</td><td>{formatDateTime(execution.startTime) ?? '-'}</td><td>{formatDateTime(execution.lastUpdateTime) ?? '-'}</td></tr>)}</tbody></table></div>{!executions.isLoading && executions.data?.length === 0 && <p className="muted compact-text">No executions yet.</p>}</div>
|
||||
<PipelineTimeline state={state.data} loading={state.isLoading}/>
|
||||
</section>
|
||||
}
|
||||
|
||||
function PipelineTimeline({state, loading}: {state?: PipelineState; loading: boolean}) {
|
||||
if (loading) return <p className="muted compact-text">Loading stage state...</p>
|
||||
if (!state) return null
|
||||
return <section className="workflow-execution-detail"><div className="dynamic-stage-header"><div><p className="eyebrow">Current pipeline state</p><h3>Stages and actions</h3></div></div><ol className="workflow-execution-history">{state.stages.map((stage) => <li key={stage.name}><div><strong>{stage.name}</strong><span>{stage.status ?? 'Unknown'}</span></div><time>{formatDateTime(stage.lastStatusChange) ?? '-'}</time>{stage.actions.map((action) => <div className="workflow-action" key={action.name}><strong>{action.name}</strong><span>{action.status ?? 'Unknown'}</span>{action.summary && <p className="muted compact-text">{action.summary}</p>}<ActionLinks action={action}/></div>)}</li>)}</ol></section>
|
||||
}
|
||||
|
||||
function ActionLinks({action}: {action: PipelineState['stages'][number]['actions'][number]}) {
|
||||
const links = [
|
||||
action.codeBuildProject ? {href: `/cloud-explorer/aws/codebuild?resource=${encodeURIComponent(action.codeBuildProject)}`, label: 'Open CodeBuild'} : undefined,
|
||||
action.codeDeployDeploymentId ? {href: `/cloud-explorer/aws/codedeploy?deployment=${encodeURIComponent(action.codeDeployDeploymentId)}`, label: 'Open CodeDeploy'} : undefined,
|
||||
action.externalExecutionUrl ? {href: action.externalExecutionUrl, label: 'Execution details'} : undefined,
|
||||
action.entityUrl ? {href: action.entityUrl, label: 'Action details'} : undefined,
|
||||
].filter((item): item is {href: string; label: string} => Boolean(item))
|
||||
return <>{links.map((link) => <a className="button compact" key={`${link.label}:${link.href}`} href={link.href}><ExternalLink size={12}/>{link.label}</a>)}</>
|
||||
}
|
||||
|
||||
function active(status?: string): boolean { return status === 'InProgress' || status === 'Stopping' }
|
||||
function activeState(state: PipelineState): boolean { return state.stages.some((stage) => active(stage.status) || stage.actions.some((action) => active(action.status))) }
|
||||
function message(error: unknown, fallback: string): string { return error instanceof Error ? error.message : fallback }
|
||||
Reference in New Issue
Block a user