Files
floci-ui/packages/frontend/src/components/EcsServicePanel.tsx
T
2026-10-05 11:06:57 -04:00

52 lines
7.2 KiB
TypeScript

import type {CloudResource} from '@/types/resource'
import {formatDateTime} from '@/lib/format'
interface EcsServicePanelProps { resource?: CloudResource }
interface Deployment { id?: string; status?: string; taskDefinition?: string; desiredCount?: number; runningCount?: number; pendingCount?: number; createdAt?: string; updatedAt?: string }
interface Task { arn?: string; lastStatus?: string; desiredStatus?: string; healthStatus?: string; connectivity?: string; stoppedReason?: string }
interface Target { id?: string; port?: number; state?: string; reason?: string }
interface LoadBalancer { targetGroupArn?: string; targetType?: string | null; containerName?: string; containerPort?: number; targets?: Target[]; endpoints?: Array<{dnsName?: string; state?: string | null; scheme?: string | null}> }
export function EcsServicePanel({resource}: EcsServicePanelProps) {
if (!resource) return null
if (resource.type === 'ecs-cluster') return <EcsClusterDetails resource={resource}/>
if (resource.type === 'ecs-task-definition') return <EcsTaskDefinitionDetails resource={resource}/>
const deployments = array<Deployment>(resource.metadata.deployments)
const tasks = array<Task>(resource.metadata.tasks)
const loadBalancers = array<LoadBalancer>(resource.metadata.loadBalancers)
const inactive = resource.status === 'INACTIVE' || resource.metadata.inactive === true
const launchType = String(resource.metadata.launchType ?? 'EC2')
const network = object(resource.metadata.networkConfiguration)
return <section className="table-panel">
<div className="dynamic-stage-header"><div><p className="eyebrow">{launchType === 'FARGATE' ? 'Fargate service state' : 'EC2 service state'}</p><h3>{resource.name}</h3><p className="muted compact-text">Launch type: <strong>{launchType}</strong> | Deployment controller: <strong>{String(resource.metadata.deploymentController ?? 'ECS')}</strong> | Cluster: {String(resource.metadata.cluster ?? '-')} | <span>{inactive ? 'Inactive, not running' : `Running ${String(resource.metadata.runningCount ?? 0)} of ${String(resource.metadata.desiredCount ?? 0)}`}</span></p></div></div>
{network && <p className="muted compact-text">awsvpc networking: subnets {values(network.subnets)} | security groups {values(network.securityGroups)} | public IP {String(network.assignPublicIp ?? 'DISABLED')}</p>}
{launchType === 'FARGATE' && !network && <p className="muted compact-text">Fargate services require awsvpc networking. Attach an IP target group, not an instance target group, when using an ALB.</p>}
{inactive ? <p className="muted compact-text">This service is inactive. ECS may retain historical deployment counts while deletion settles; they are not shown as running tasks.</p> : <>
<h4>Tasks</h4>
{tasks.length ? <div className="workflow-execution-list-table"><table className="table"><thead><tr><th>Task</th><th>Last status</th><th>Health</th><th>Connectivity</th></tr></thead><tbody>{tasks.map((task, index) => <tr key={task.arn ?? index}><td><code>{task.arn ?? '-'}</code></td><td>{task.lastStatus ?? '-'}</td><td>{task.healthStatus ?? '-'}</td><td>{task.connectivity ?? '-'}</td></tr>)}</tbody></table></div> : <p className="muted compact-text">No running tasks reported for this service.</p>}
{loadBalancers.map((loadBalancer, index) => <div key={loadBalancer.targetGroupArn ?? index}><h4>ALB and target health</h4><p className="muted compact-text">Target group: <code>{loadBalancer.targetGroupArn ?? '-'}</code> | Target type: <strong>{loadBalancer.targetType ?? '-'}</strong>{launchType === 'FARGATE' && loadBalancer.targetType !== 'ip' ? ' (Fargate requires IP targets)' : ''}</p>{loadBalancer.endpoints?.map((endpoint, endpointIndex) => <p className="muted compact-text" key={endpoint.dnsName ?? endpointIndex}>ALB endpoint: <code>{endpoint.dnsName ?? '-'}</code> | {endpoint.scheme ?? '-'} | {endpoint.state ?? '-'}</p>)}<div className="workflow-execution-list-table"><table className="table"><thead><tr><th>Target</th><th>Health</th><th>Reason</th></tr></thead><tbody>{(loadBalancer.targets ?? []).map((target, targetIndex) => <tr key={`${target.id ?? targetIndex}:${target.port ?? ''}`}><td><code>{target.id ?? '-'}{target.port ? `:${target.port}` : ''}</code></td><td>{target.state ?? '-'}</td><td>{target.reason ?? '-'}</td></tr>)}</tbody></table></div>{!loadBalancer.targets?.length && <p className="muted compact-text">No target health reported by this target group.</p>}</div>)}
<h4>Deployments</h4>
<div className="workflow-execution-list-table"><table className="table"><thead><tr><th>Deployment</th><th>Status</th><th>Task definition</th><th>Running</th><th>Updated</th></tr></thead><tbody>{deployments.map((deployment, index) => <tr key={deployment.id ?? index}><td><code>{deployment.id ?? '-'}</code></td><td>{deployment.status ?? '-'}</td><td><code>{deployment.taskDefinition ?? '-'}</code></td><td>{deployment.runningCount ?? 0} / {deployment.desiredCount ?? 0}{deployment.pendingCount ? ` (${deployment.pendingCount} pending)` : ''}</td><td>{formatDateTime(deployment.updatedAt ?? deployment.createdAt) ?? '-'}</td></tr>)}</tbody></table></div>
{deployments.length === 0 && <p className="muted compact-text">No deployment state reported for this service.</p>}
</>}
</section>
}
function EcsClusterDetails({resource}: {resource: CloudResource}) {
return <section className="table-panel">
<div className="dynamic-stage-header"><div><p className="eyebrow">ECS cluster</p><h3>{resource.name}</h3><p className="muted compact-text">Status: <strong>{resource.status ?? '-'}</strong> | Services: {String(resource.metadata.activeServicesCount ?? 0)} | Running tasks: {String(resource.metadata.runningTasksCount ?? 0)} | Pending tasks: {String(resource.metadata.pendingTasksCount ?? 0)}</p></div></div>
<p className="muted compact-text">Registered container instances: {String(resource.metadata.registeredContainerInstancesCount ?? 0)}</p>
</section>
}
function EcsTaskDefinitionDetails({resource}: {resource: CloudResource}) {
return <section className="table-panel">
<div className="dynamic-stage-header"><div><p className="eyebrow">Fargate task definition</p><h3>{resource.name}</h3><p className="muted compact-text">CPU: <strong>{String(resource.metadata.cpu ?? '-')}</strong> | Memory: <strong>{String(resource.metadata.memory ?? '-')} MiB</strong> | Network mode: <strong>{String(resource.metadata.networkMode ?? '-')}</strong></p></div></div>
<div className="workflow-execution-list-table"><table className="table"><thead><tr><th>Container</th><th>Image</th><th>Port</th></tr></thead><tbody><tr><td>{String(resource.metadata.containerName ?? '-')}</td><td><code>{String(resource.metadata.image ?? '-')}</code></td><td>{String(resource.metadata.containerPort ?? '-')}</td></tr></tbody></table></div>
</section>
}
function array<T>(value: unknown): T[] { return Array.isArray(value) ? value as T[] : [] }
function object(value: unknown): Record<string, unknown> | null { return typeof value === 'object' && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null }
function values(value: unknown): string { return Array.isArray(value) && value.length ? value.map(String).join(', ') : '-' }