small feat: svg animation for servers, codebuild, codePipleline etc...

This commit is contained in:
Zakaria
2026-10-15 10:25:16 -04:00
parent ca1e845cb6
commit 668911ed6e
29 changed files with 10034 additions and 117 deletions
@@ -1,5 +1,7 @@
import {useEffect, useState} from 'react'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {ExternalLink, Play, RefreshCw} from 'lucide-react'
import pipelineReleaseAnimation from '../../../../svgAsset/Pipleline.svg'
import {getCodePipelineState, listCodePipelineExecutions, startCodePipeline, type PipelineState} from '@/api/cloudProxyClient'
import {formatDateTime} from '@/lib/format'
import type {CloudResource} from '@/types/resource'
@@ -8,14 +10,20 @@ interface CodePipelinePanelProps { resource?: CloudResource; runtimeReachable: b
export function CodePipelinePanel({resource, runtimeReachable}: CodePipelinePanelProps) {
const client = useQueryClient()
const [releaseExecutionId, setReleaseExecutionId] = useState<string>()
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]}) }})
const executions = useQuery({queryKey: executionsKey, queryFn: ({signal}) => listCodePipelineExecutions(resource!.id, signal), enabled, refetchInterval: (query) => Boolean(releaseExecutionId) || 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) => Boolean(releaseExecutionId) || query.state.data && activeState(query.state.data) ? 2_000 : false})
const start = useMutation({mutationFn: () => startCodePipeline(resource!.id), onSuccess: ({id}) => { setReleaseExecutionId(id); void client.invalidateQueries({queryKey: executionsKey}); void client.invalidateQueries({queryKey: ['codepipeline-state', resource?.id]}) }})
useEffect(() => { setReleaseExecutionId(undefined) }, [resource?.id])
const releaseExecution = executions.data?.find((execution) => execution.id === releaseExecutionId)
const releaseInProgress = Boolean(releaseExecutionId && (!releaseExecution || active(releaseExecution.status)))
useEffect(() => { if (releaseExecutionId && releaseExecution && !active(releaseExecution.status)) setReleaseExecutionId(undefined) }, [releaseExecution, releaseExecutionId])
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 CodeDeployToECS for ECS EC2 and Fargate services.</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>
{releaseInProgress && <PipelineReleaseProgress executionId={releaseExecutionId} execution={releaseExecution}/>}
{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>
@@ -23,6 +31,17 @@ export function CodePipelinePanel({resource, runtimeReachable}: CodePipelinePane
</section>
}
function PipelineReleaseProgress({executionId, execution}: {executionId?: string; execution?: {status?: string; statusSummary?: string}}) {
return <div className="pipeline-release-progress" role="status" aria-live="polite">
<PipelineReleaseAnimation/>
<div><p className="eyebrow">Release in progress</p><h3>{execution?.statusSummary || execution?.status || 'Starting pipeline execution'}</h3><p className="muted compact-text">Pipeline execution <code>{executionId}</code> is moving through source, build, and deployment stages.</p></div>
</div>
}
function PipelineReleaseAnimation() {
return <img className="pipeline-release-art" src={pipelineReleaseAnimation} alt="Animated pipeline release icon"/>
}
function PipelineTimeline({state, loading}: {state?: PipelineState; loading: boolean}) {
if (loading) return <p className="muted compact-text">Loading stage state...</p>
if (!state) return null