import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { ChevronDown, ChevronUp, Info, Loader2, Plus, RefreshCw, Trash2, X } from "lucide-react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { createCloudResource, createApiGatewayLambdaRoute, createEventBridgeLambdaTrigger, createS3LambdaTrigger, createSqsLambdaEventSourceMapping, clearEmailInbox, deleteCloudResource, getCloudResource, getServiceSchema, listCloudResources, listStorageObjects, updateCloudResource, } from "@/api/cloudProxyClient"; import { DynamicFormRenderer } from "@/components/DynamicFormRenderer"; import { ResourceInspector } from "@/components/ResourceInspector"; import { ResourceTable } from "@/components/ResourceTable"; import { StorageObjectBrowser } from "@/components/StorageObjectBrowser"; import { S3BucketWebsitePanel } from "@/components/S3BucketWebsitePanel"; import { ComputePanel, LaunchInstanceForm } from "@/components/ComputePanel"; import { NetworkingPanel } from "@/components/NetworkingPanel"; import { describeDeleteImpact } from "@/lib/deleteImpact"; import { getPath } from "@/lib/resourcePath"; import { capabilityEnabled, capabilityFor, normalizeCapabilities, withRuntimeState, withServiceAvailability, } from "@/lib/capabilities"; import type { CloudAvailability, CloudProvider, CloudServiceType, CloudStatus, } from "@/types/cloud"; import type { CloudResource, StorageObject } from "@/types/resource"; import type { ServiceSchema } from "@/types/schema"; import { ServerlessInvokePanel } from "@/components/ServerlessInvokePanel"; import { dataExplorerPath } from "@/lib/dataExplorer"; import { DatabaseSnapshotsPanel } from "@/components/DatabaseSnapshotsPanel"; import { CreateRdsInstanceForm } from "@/components/CreateRdsInstanceForm"; import { DatabaseLaunchProgress } from "@/components/DatabaseLaunchProgress"; import { AppConfigPanel } from "@/components/AppConfigPanel"; import { KmsCryptoPanel } from "@/components/KmsCryptoPanel"; import { ApiGatewayRoutePanel } from "@/components/ApiGatewayRoutePanel"; import { S3LambdaTriggerPanel } from "@/components/S3LambdaTriggerPanel"; import { ScheduledLambdaTriggerPanel } from "@/components/ScheduledLambdaTriggerPanel"; import { SqsLambdaTriggerPanel } from "@/components/SqsLambdaTriggerPanel"; import { SqsQueuePanel } from "@/components/SqsQueuePanel"; import { CloudFormationStackPanel } from "@/components/CloudFormationStackPanel"; import { CloudFormationCreatePanel } from "@/components/CloudFormationCreatePanel"; import { CloudFormationUpdatePanel } from "@/components/CloudFormationUpdatePanel"; import { EventBridgeRulePanel } from "@/components/EventBridgeRulePanel"; import { LambdaInvocationHistoryPanel } from "@/components/LambdaInvocationHistoryPanel"; import { LambdaCreatePanel } from "@/components/LambdaCreatePanel"; import { bucketDeleteWarning } from "@/lib/bucketDeletion"; import { SnsTopicPanel } from "@/components/SnsTopicPanel"; import { EcrImageBrowser } from "@/components/EcrImageBrowser"; import { LoadBalancerPanel } from "@/components/LoadBalancerPanel"; import { TargetGroupPanel } from "@/components/TargetGroupPanel"; import { LoadBalancerListenerPanel } from "@/components/LoadBalancerListenerPanel"; import { CognitoUsersPanel } from "@/components/CognitoUsersPanel"; import { Route53RecordsPanel } from "@/components/Route53RecordsPanel"; import { WorkflowExecutionDebugger } from "@/components/WorkflowExecutionDebugger"; import { CodeBuildPanel } from "@/components/CodeBuildPanel"; import { CodePipelinePanel } from "@/components/CodePipelinePanel"; import { MskPanel } from "@/components/MskPanel"; import { MskConnectPanel } from "@/components/MskConnectPanel"; import { EcsServicePanel } from "@/components/EcsServicePanel"; import {CodeDeployCreatePanel, CodeDeployPanel} from "@/components/CodeDeployPanel"; import {WorkflowCreatePanel} from "@/components/WorkflowCreatePanel"; import {KinesisPanel} from "@/components/KinesisPanel"; import {SesConsolePanel} from '@/components/SesConsolePanel' import {ElastiCacheManagementPanel} from '@/components/ElastiCacheManagementPanel' import {LightsailLaunchPanel} from '@/components/LightsailLaunchPanel' import {LightsailInstancePanel} from '@/components/LightsailInstancePanel' import {workflowCreationDestinationPath, type WorkflowCreationDestination} from "@/lib/workflowCreate"; import {workflowStudioPath} from "@/lib/workflowStudio"; import {useNavigate, useSearchParams} from "react-router-dom"; interface DynamicResourceViewProps { cloud: CloudProvider; service: CloudServiceType; serviceAvailability?: CloudAvailability; /** Server-supplied explanation, shown instead of generic coming-soon copy. */ serviceReason?: string; cloudStatus?: CloudStatus; statusLoading?: boolean; onOpenInfo: () => void; } const ELASTICACHE_CREATION_MINIMUM_MS = 5_000; interface CreateMutationVariables { values: Record; destination?: WorkflowCreationDestination; } export function DynamicResourceView({ cloud, service, serviceAvailability = "coming_soon", serviceReason, cloudStatus, statusLoading = false, onOpenInfo, }: DynamicResourceViewProps) { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const qc = useQueryClient(); const [search, setSearch] = useState(""); const [state, setState] = useState(""); const [databaseTab, setDatabaseTab] = useState<"instances" | "snapshots">("instances"); const [selected, setSelected] = useState(); const [selectedObject, setSelectedObject] = useState< StorageObject | undefined >(); const [createOpen, setCreateOpen] = useState(false); const [clearConfirm, setClearConfirm] = useState(false); const [isDeleteMulti, setIsDeleteMulti] = useState(false); const [selectedItems, setSelectedItems] = useState([]); const [deleteConfirm, setDeleteConfirm] = useState(false); const [bulkDeleteError, setBulkDeleteError] = useState(null); const [deleteWarning, setDeleteWarning] = useState(null); const openedResourceRef = useRef(undefined); useEffect(() => { if (cloud === "aws" && service === "compute") { setCreateOpen(true); } }, [cloud, service]); const handleDatabaseTabChange = (tab: "instances" | "snapshots") => { setDatabaseTab(tab); if (tab === "snapshots") { setSearch(""); } }; const resourcesKey = useMemo( () => ["cloud-resources", cloud, service, search, state], [cloud, service, search, state], ); const schemaQuery = useQuery({ queryKey: ["cloud-schema", cloud, service], queryFn: ({ signal }) => getServiceSchema(cloud, service, signal), enabled: !(cloud === "aws" && (service === "msk" || service === "msk-connect")), }); const resourcesQuery = useQuery({ queryKey: resourcesKey, queryFn: ({ signal }) => listCloudResources(cloud, service, search, signal, state), enabled: schemaQuery.isSuccess && serviceAvailability === "available" && cloudStatus?.runtime === "reachable", }); useEffect(() => { const resourceId = searchParams.get("resource"); if (!resourceId || !resourcesQuery.data) return; const key = `${cloud}:${service}:${resourceId}`; if (openedResourceRef.current === key) return; const resource = resourcesQuery.data.find((candidate) => candidate.id === resourceId); if (resource) setSelected(resource); openedResourceRef.current = key; }, [cloud, resourcesQuery.data, searchParams, service]); // Automatically reconcile selected items when resources query data changes useEffect(() => { setSelectedItems((prev) => { if (prev.length === 0) return prev; const currentIds = new Set((resourcesQuery.data || []).map((r) => r.id)); const next = prev.filter((item) => currentIds.has(item.id)); return next.length === prev.length ? prev : next; }); }, [resourcesQuery.data]); const selectMultiple = (select?: CloudResource | boolean) => { const currentResources = resourcesQuery.data || []; if (typeof select === "boolean" || select === undefined) { const isAllCurrentlySelected = currentResources.length > 0 && currentResources.every((r) => selectedItems.some((item) => item.id === r.id), ); const shouldSelectAll = typeof select === "boolean" ? select : !isAllCurrentlySelected; if (shouldSelectAll) { setSelectedItems(currentResources); } else { setSelectedItems([]); } } else { setSelectedItems((prev) => { const exists = prev.some((item) => item.id === select.id); return exists ? prev.filter((item) => item.id !== select.id) : [...prev, select]; }); } }; const createMut = useMutation({ mutationFn: async ({values}: CreateMutationVariables) => { const blueprintBucketName = typeof values.blueprintBucketName === "string" ? values.blueprintBucketName.trim() : ""; const blueprintApiName = typeof values.blueprintApiName === "string" ? values.blueprintApiName.trim() : ""; const blueprintRuleName = typeof values.blueprintRuleName === "string" ? values.blueprintRuleName.trim() : ""; const blueprintScheduleExpression = typeof values.blueprintScheduleExpression === "string" ? values.blueprintScheduleExpression.trim() : ""; const blueprintQueueName = typeof values.blueprintQueueName === "string" ? values.blueprintQueueName.trim() : ""; const blueprintSqsBatchSize = typeof values.blueprintSqsBatchSize === "number" ? values.blueprintSqsBatchSize : undefined; const lambdaValues = {...values}; delete lambdaValues.blueprintBucketName; delete lambdaValues.blueprintApiName; delete lambdaValues.blueprintApiPath; delete lambdaValues.blueprintApiMethod; delete lambdaValues.blueprintApiStage; delete lambdaValues.blueprintRuleName; delete lambdaValues.blueprintScheduleExpression; delete lambdaValues.blueprintQueueName; delete lambdaValues.blueprintSqsBatchSize; const resource = await createCloudResource(cloud, service, lambdaValues); if (cloud === "aws" && service === "elasticache") { await new Promise((resolve) => setTimeout(resolve, ELASTICACHE_CREATION_MINIMUM_MS)); return resource; } if (blueprintBucketName) { let bucketCreated = false; try { await createCloudResource(cloud, "storage", {bucketName: blueprintBucketName}); bucketCreated = true; await createS3LambdaTrigger(cloud, blueprintBucketName, { functionName: resource.name, events: ["s3:ObjectCreated:*"], }); return resource; } catch (error) { if (bucketCreated) await deleteCloudResource(cloud, "storage", blueprintBucketName); await deleteCloudResource(cloud, service, resource.id); throw error; } } if (blueprintRuleName) { try { await createEventBridgeLambdaTrigger(cloud, resource.name, { ruleName: blueprintRuleName, scheduleExpression: blueprintScheduleExpression, }); return resource; } catch (error) { await deleteCloudResource(cloud, service, resource.id); throw error; } } if (blueprintQueueName) { let queueId: string | undefined; try { const queue = await createCloudResource(cloud, "messaging", {queueName: blueprintQueueName}); queueId = queue.id; const queueArn = queue.metadata.arn; if (typeof queueArn !== "string" || !queueArn) { throw new Error("Created SQS queue did not return an ARN."); } await createSqsLambdaEventSourceMapping(cloud, resource.name, { queueArn, batchSize: blueprintSqsBatchSize, }); return resource; } catch (error) { const rollbacks = [deleteCloudResource(cloud, service, resource.id)]; if (queueId) rollbacks.push(deleteCloudResource(cloud, "messaging", queueId)); await Promise.allSettled(rollbacks); throw error; } } if (!blueprintApiName) return resource; let apiId: string | undefined; try { const api = await createCloudResource(cloud, "apigateway", {name: blueprintApiName}); apiId = api.id; const route = await createApiGatewayLambdaRoute(cloud, api.id, { functionName: resource.name, path: String(values.blueprintApiPath ?? ""), method: String(values.blueprintApiMethod ?? "GET"), stage: String(values.blueprintApiStage ?? "dev"), }); return {...resource, metadata: {...resource.metadata, apiGatewayId: api.id, apiGatewayInvokeUrl: route.invokeUrl}}; } catch (error) { if (apiId) await deleteCloudResource(cloud, "apigateway", apiId); await deleteCloudResource(cloud, service, resource.id); throw error; } }, onSuccess: (resource, variables) => { setSelected(resource); setCreateOpen(false); void qc.invalidateQueries({ queryKey: ["cloud-resources", cloud, service], }); if (cloud === "aws" && service === "workflows" && variables.destination === "studio") { const destinationPath = workflowCreationDestinationPath(variables.destination, resource.id); if (destinationPath) navigate(destinationPath); } }, }); const deleteMut = useMutation({ mutationFn: async (resource: CloudResource) => { if (cloud === "aws" && service === "storage") { const contents = await listStorageObjects(cloud, resource.id); const warning = bucketDeleteWarning(contents); if (warning) throw new Error(warning); } if (cloud === "aws" && service === "iac") { const current = await getCloudResource(cloud, service, resource.id); if (current?.metadata.enableTerminationProtection === true) throw new Error("Termination protection is enabled. Disable it from Stack actions before deleting this stack."); } await deleteCloudResource(cloud, service, resource.id); }, onSuccess: (_, resource) => { setDeleteWarning(null); if (selected?.id === resource.id) setSelected(undefined); setSelectedItems((prev) => prev.filter((item) => item.id !== resource.id)); void qc.invalidateQueries({ queryKey: ["cloud-resources", cloud, service], }); }, onError: (error) => { setDeleteWarning(error instanceof Error ? error.message : "Unable to delete resource."); }, }); const deleteMultipleMut = useMutation({ mutationFn: async (items: CloudResource[]) => { const results = await Promise.allSettled( items.map(async (item) => { await deleteCloudResource(cloud, service, item.id); return item.id; }), ); const succeededIds = new Set(); const failedItems: { id: string; error: unknown }[] = []; results.forEach((res, index) => { if (res.status === "fulfilled") { succeededIds.add(res.value); } else { failedItems.push({ id: items[index].id, error: res.reason }); } }); return { succeededIds, failedItems }; }, onSettled: () => { void qc.invalidateQueries({ queryKey: ["cloud-resources", cloud, service], }); }, onSuccess: ({ succeededIds, failedItems }) => { setDeleteConfirm(false); setSelectedItems((prev) => prev.filter((item) => !succeededIds.has(item.id)), ); if (selected && succeededIds.has(selected.id)) { setSelected(undefined); } if (failedItems.length > 0) { setBulkDeleteError( `Failed to delete ${failedItems.length} of ${ succeededIds.size + failedItems.length } selected resource(s).`, ); } else { setBulkDeleteError(null); setIsDeleteMulti(false); setSuccessToast( `Successfully deleted ${succeededIds.size} resource(s).`, ); } }, }); const clearInboxMut = useMutation({ mutationFn: () => clearEmailInbox(cloud), onSuccess: () => { setSelected(undefined); setClearConfirm(false); void qc.invalidateQueries({ queryKey: ["cloud-resources", cloud, service], }); }, }); const [editingResource, setEditingResource] = useState(null); const [updateError, setUpdateError] = useState(null); const [successToast, setSuccessToast] = useState(null); useEffect(() => { if (successToast) { const timer = setTimeout(() => setSuccessToast(null), 4000); return () => clearTimeout(timer); } }, [successToast]); useEffect(() => { if (deleteWarning) { const timer = setTimeout(() => setDeleteWarning(null), 5000); return () => clearTimeout(timer); } }, [deleteWarning]); const updateMut = useMutation({ mutationFn: ({ id, values }: { id: string; values: Record }) => updateCloudResource(cloud, service, id, values), onSuccess: (updatedResource) => { setEditingResource(null); setUpdateError(null); setSelected(updatedResource); setSuccessToast(`Successfully updated ${updatedResource.name || updatedResource.id}`); void qc.invalidateQueries({ queryKey: ["cloud-resources", cloud, service], }); }, onError: (err) => { setUpdateError(err instanceof Error ? err.message : "Failed to update resource"); }, }); const initialEditValues = useMemo(() => { if (!editingResource || !schemaQuery.data?.updateFields) return {}; const vals: Record = {}; for (const field of schemaQuery.data.updateFields) { const path = field.valuePath ?? field.name; const currentVal = getPath(editingResource, path); if (currentVal !== undefined && currentVal !== null) { vals[field.name] = currentVal; } } return vals; }, [editingResource, schemaQuery.data?.updateFields]); function handleEditSubmit(values: Record) { if (!editingResource || !schemaQuery.data?.updateFields) return; setUpdateError(null); const diff: Record = {}; for (const field of schemaQuery.data.updateFields) { const rawVal = values[field.name]; const initialVal = initialEditValues[field.name]; if (rawVal === undefined) continue; if (typeof rawVal === "string") { const trimmed = rawVal.trim(); if (!trimmed && !field.required) { continue; } const initialStr = field.type === 'key-value' && initialVal && typeof initialVal === 'object' ? JSON.stringify(Object.entries(initialVal).map(([key, value]) => ({key, value: String(value)}))) : initialVal !== undefined && initialVal !== null ? String(initialVal).trim() : ""; if (trimmed !== initialStr) { diff[field.name] = trimmed; } } else if (rawVal !== initialVal) { diff[field.name] = rawVal; } } if (Object.keys(diff).length === 0) { setUpdateError("No fields changed."); return; } updateMut.mutate({ id: editingResource.id, values: diff }); } useEffect(() => { setSelected(undefined); setSelectedObject(undefined); setCreateOpen(false); setClearConfirm(false); setDeleteConfirm(false); setBulkDeleteError(null); setEditingResource(null); setUpdateError(null); setSearch(""); setState(""); setDatabaseTab("instances"); setIsDeleteMulti(false); setSelectedItems([]); }, [cloud, service]); useEffect(() => { if (selectedItems.length === 0 && deleteConfirm) { setDeleteConfirm(false); } }, [selectedItems.length, deleteConfirm]); useEffect(() => { setSelectedObject(undefined); }, [selected?.id]); const adapterAvailable = serviceAvailability === "available"; if (cloud === "aws" && service === "msk") { return (
); } if (cloud === "aws" && service === "msk-connect") { return
; } if (schemaQuery.isLoading) { return (

Loading schema

); } if (schemaQuery.isError || !schemaQuery.data) { return (

Coming Soon

{cloud.toUpperCase()} {service}

The proxy already exposes this provider as a placeholder. No adapter is registered yet.

); } const schema = schemaQuery.data; const resources = resourcesQuery.data ?? []; const canCreate = schema.actions.includes("create"); const canDelete = schema.actions.includes("delete"); const deleteImpactText = describeDeleteImpact(schema.deleteImpact, selectedItems); const isAllSelected = resources.length > 0 && resources.every((r) => selectedItems.some((s) => s.id === r.id)); // Prefer the latest listed copy, so the inspector follows refreshes instead of the click-time snapshot. const activeSelected = selected?.cloud === cloud && selected.service === service ? resources.find((resource) => resource.id === selected.id) ?? selected : undefined; const runtimeReachable = cloudStatus?.runtime === "reachable"; const resourceCapabilityInputs = schema.capabilities?.resourceActions ?? schema.actions; const resourceCapabilities = withServiceAvailability( withRuntimeState( normalizeCapabilities(resourceCapabilityInputs), runtimeReachable, ), serviceAvailability, ); const objectCapabilities = withServiceAvailability( withRuntimeState( normalizeCapabilities(schema.capabilities?.objectActions), runtimeReachable, ), serviceAvailability, ); const createCapability = capabilityFor(resourceCapabilities, "create"); const createResourceLabel = createCapability?.label ?? "Create resource"; const databaseCapabilities = withServiceAvailability( withRuntimeState( normalizeCapabilities(schema.capabilities?.databaseActions), runtimeReachable, ), serviceAvailability, ); const listSnapshotCapability = capabilityFor( databaseCapabilities, "listSnapshots", ); const createSnapshotCapability = capabilityFor( databaseCapabilities, "createSnapshot", ); const canUseRuntime = runtimeReachable && adapterAvailable; const canCreateResource = canUseRuntime && capabilityEnabled(createCapability); const isAwsDatabase = cloud === "aws" && service === "database"; const showDatabaseSnapshots = isAwsDatabase && databaseTab === "snapshots"; return (
{isAwsDatabase && (
)} {showDatabaseSnapshots ? ( ) : (

Resources

{resources.length} normalized resources
setSearch(event.target.value)} placeholder="Filter resources" />
{schema.filters.find((filter) => filter.name === "state" && filter.type === "select") && ( )} {service === "email" && ( clearConfirm ? ( <> ) : ( ) )} {canCreate && ( )} {canDelete && ( deleteConfirm ? ( <> {deleteImpactText && ( {deleteImpactText} )} ) : ( ) )}
{service === "email" && clearInboxMut.isError && (

{clearInboxMut.error instanceof Error ? clearInboxMut.error.message : "Unable to clear the inbox."}

)} {bulkDeleteError && (

{bulkDeleteError}

)} {canCreate && createOpen && (
{/* * AWS only. LaunchInstanceForm is an EC2 form: it asks for an AMI id, * populates its dropdowns from the legacy /api/ec2 routes, and submits * imageId/instanceType. On any other cloud that is the wrong form * entirely — the Azure adapter rejects it with "resourceGroup is * required". Every other cloud falls through to DynamicFormRenderer, * which builds the right form from the adapter's own schema. */} {service === "compute" && cloud === "aws" ? ( { setSelected(resource); setCreateOpen(false); }} onCancel={() => setCreateOpen(false)} /> ) : service === "lightsail" && cloud === "aws" ? ( setCreateOpen(false)} onSubmit={(values) => createMut.mutate({values})} /> ) : service === "database" && cloud === "aws" ? ( { setSelected(resource); setCreateOpen(false); }} onCancel={() => setCreateOpen(false)} /> ) : service === "serverless" && cloud === "aws" ? ( setCreateOpen(false)} onSubmit={(values) => createMut.mutate({values})} /> ) : service === "loadbalancing" && cloud === "aws" ? ( setCreateOpen(false)} onSubmit={(values) => createMut.mutate({values})} /> ) : service === "workflows" && cloud === "aws" ? ( setCreateOpen(false)} onSubmit={(submission) => createMut.mutate(submission)} /> ) : service === "iac" && cloud === "aws" ? ( setCreateOpen(false)} onSubmit={(values) => createMut.mutate({values})} /> ) : service === "codedeploy" && cloud === "aws" ? ( setCreateOpen(false)} onSubmit={(values) => createMut.mutate({values})}/> ) : ( <> setCreateOpen(false)} onSubmit={(values) => createMut.mutate({values})} /> {service === "elasticache" && cloud === "aws" && createMut.isPending && } )}
)} {!(cloud === "aws" && service === "compute" && createOpen) && renderResourceSurface({ schema, resources, selectedId: activeSelected?.id, deletingId: deleteMut.variables?.id, cloudStatus, statusLoading, serviceAvailability, serviceReason, resourcesLoading: resourcesQuery.isLoading, resourcesError: resourcesQuery.error, isRetrying: resourcesQuery.isFetching, onSelect: setSelected, onEdit: schema.updateFields && schema.updateFields.length > 0 && schema.actions.includes("update") ? async (resource) => { try { const detailedResource = await getCloudResource(cloud, service, resource.id); setEditingResource(detailedResource); setUpdateError(null); } catch (error) { setUpdateError(error instanceof Error ? error.message : "Unable to load resource configuration"); } } : undefined, onDelete: (resource) => deleteMut.mutate(resource), onRetry: () => resourcesQuery.refetch(), isDeleteMulti, selectedItems, onToggleSelect: (resource) => selectMultiple(resource), onToggleSelectAll: () => selectMultiple(!isAllSelected), isAllSelected, })}
)} {cloud === 'aws' && service === 'email' && }
{activeSelected && !showDatabaseSnapshots && (
{cloud === "aws" && service === "workflows" && }
)}
{service === "storage" && ( <> {cloud === "aws" && } )} {service === "messaging" && cloud === "aws" && } {service === "iac" && cloud === "aws" && } {service === "ecr" && cloud === "aws" && ( )} {service === "targetgroups" && cloud === "aws" && ( )} {service === "loadbalancing" && cloud === "aws" && ( )} {service === "userpools" && cloud === "aws" && ( )} {service === "dns" && cloud === "aws" && ( )} {service === "compute" && ( )} {service === "networking" && ( )} {service === "serverless" && ( <> {cloud === "aws" && activeSelected?.type === "lambda" && ( )} )} {service === "apigateway" && ( )} {service === "events" && ( )} {service === "configuration" && cloud === "aws" && ( )} {service === "kms" && ( )} {service === "notifications" && ( )} {service === "workflows" && cloud === "aws" && ( )} {service === "codebuild" && cloud === "aws" && ( )} {(service === "ecs" || service === "fargate") && cloud === "aws" && } {service === "codepipeline" && cloud === "aws" && ( )} {service === "codedeploy" && cloud === "aws" && ( )} {service === "kinesis" && cloud === "aws" && } {service === "elasticache" && cloud === "aws" && } {service === "lightsail" && cloud === "aws" && setSelected(undefined)} />} {editingResource && schema.updateFields && (
{ if (!updateMut.isPending) setEditingResource(null); }} >
e.stopPropagation()} >

Edit {schema.displayName}: {editingResource.name || editingResource.id}

{service === "loadbalancing" && cloud === "aws" ? ( setEditingResource(null)} onSubmit={handleEditSubmit} /> ) : service === "iac" && cloud === "aws" ? setEditingResource(null)} onSubmit={handleEditSubmit}/> : setEditingResource(null)} onSubmit={handleEditSubmit} />}
)} {successToast && (
{successToast}
)} {deleteWarning && (
{deleteWarning}
)}
); } function TopbarServiceInfo({ onOpenInfo }: { onOpenInfo: () => void }) { const [slot, setSlot] = useState(null); useLayoutEffect(() => { setSlot(document.getElementById("topbar-status")); }, []); if (!slot) return null; return createPortal( , slot, ); } function StatusTile({ label, value, state, }: { label: string; value: string; state: "placeholder" | "pending"; }) { return (
{label} {value}
); } function renderResourceSurface({ schema, resources, selectedId, deletingId, cloudStatus, statusLoading, serviceAvailability, serviceReason, resourcesLoading, resourcesError, isRetrying, onSelect, onEdit, onDelete, onRetry, isDeleteMulti, selectedItems, onToggleSelect, onToggleSelectAll, isAllSelected, }: { schema: ServiceSchema; resources: CloudResource[]; selectedId?: string; deletingId?: string; cloudStatus?: CloudStatus; statusLoading: boolean; serviceAvailability: CloudAvailability; serviceReason?: string; resourcesLoading: boolean; resourcesError: unknown; isRetrying: boolean; onSelect: (resource: CloudResource) => void; onEdit?: (resource: CloudResource) => void; onDelete: (resource: CloudResource) => void; onRetry?: () => void; isDeleteMulti?: boolean; selectedItems?: CloudResource[]; onToggleSelect?: (resource: CloudResource) => void; onToggleSelectAll?: () => void; isAllSelected?: boolean; }) { if (statusLoading) { return ( ); } if (serviceAvailability !== "available") { return ( ); } if (cloudStatus?.runtime === "unavailable") { return ( ); } if (resourcesError) { return ( ); } if (resourcesLoading) { return ( ); } return ( ); } function RuntimeNotice({ title, detail, error, state, showSpinner, isRetrying, onRetry, }: { title: string; detail: string; error?: string; state: "pending" | "unavailable"; showSpinner?: boolean; isRetrying?: boolean; onRetry?: () => void; }) { return (
{(showSpinner || isRetrying) && ( )}

{title}

{detail}

{error && {error}} {onRetry && ( )}
); }