Files
floci-ui/packages/frontend/src/components/DynamicResourceView.tsx
T
Zakaria 5eee0b7e9b
CI / checks (pull_request) Successful in 28m25s
feat(api): add Athena workgroups and a query editor to the cloud explorer
Create and delete Athena workgroups (with a result folder, optionally
enforced), and run SQL in one: pick a database from the Glue Data Catalog,
preview a table, run with Ctrl+Enter, cancel a running query, read up to 1,000
rows and download the full result CSV from S3. Recent queries in the workgroup
can be reopened. TheCloud runs the SQL for real over the files a Glue table
points at. The Glue page links to the editor.
2026-10-25 18:10:33 -04:00

1431 lines
54 KiB
TypeScript

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 { HttpApiPanel } from "@/components/HttpApiPanel";
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 { CloudFrontPanel } from "@/components/CloudFrontPanel";
import { AutoScalingPanel } from "@/components/AutoScalingPanel";
import { FileSystemPanel } from "@/components/FileSystemPanel";
import { DataCatalogPanel } from "@/components/DataCatalogPanel";
import { QueryPanel } from "@/components/QueryPanel";
import { CertificatePanel } from "@/components/CertificatePanel";
import { PipePanel } from "@/components/PipePanel";
import { CloudWatchAlarmPanel } from "@/components/CloudWatchAlarmPanel";
import { CloudWatchMetricsExplorer } from "@/components/CloudWatchMetricsExplorer";
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<string, unknown>;
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<CloudResource | undefined>();
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<CloudResource[]>([]);
const [deleteConfirm, setDeleteConfirm] = useState(false);
const [bulkDeleteError, setBulkDeleteError] = useState<string | null>(null);
const [deleteWarning, setDeleteWarning] = useState<string | null>(null);
const openedResourceRef = useRef<string | undefined>(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<void>((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<string>();
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<CloudResource | null>(null);
const [updateError, setUpdateError] = useState<string | null>(null);
const [successToast, setSuccessToast] = useState<string | null>(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<string, unknown> }) =>
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<string, unknown> = {};
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<string, unknown>) {
if (!editingResource || !schemaQuery.data?.updateFields) return;
setUpdateError(null);
const diff: Record<string, unknown> = {};
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 (
<div className="dynamic-resource-view">
<TopbarServiceInfo onOpenInfo={onOpenInfo} />
<MskPanel runtimeReachable={cloudStatus?.runtime === "reachable"} />
</div>
);
}
if (cloud === "aws" && service === "msk-connect") {
return <div className="dynamic-resource-view"><TopbarServiceInfo onOpenInfo={onOpenInfo} /><MskConnectPanel runtimeReachable={cloudStatus?.runtime === "reachable"} /></div>;
}
if (schemaQuery.isLoading) {
return (
<div className="dynamic-resource-view">
<TopbarServiceInfo onOpenInfo={onOpenInfo} />
<div className="empty compact">
<h3>Loading schema</h3>
</div>
</div>
);
}
if (schemaQuery.isError || !schemaQuery.data) {
return (
<div className="dynamic-resource-view">
<TopbarServiceInfo onOpenInfo={onOpenInfo} />
<div className="cloud-coming-soon">
<div>
<p className="eyebrow">Coming Soon</p>
<h3>
{cloud.toUpperCase()} {service}
</h3>
<p className="muted">
The proxy already exposes this provider as a placeholder. No
adapter is registered yet.
</p>
</div>
<div className="coming-soon-grid">
<StatusTile
label="Cloud"
value={cloud.toUpperCase()}
state="placeholder"
/>
<StatusTile label="Service" value={service} state="placeholder" />
<StatusTile
label="Adapter"
value="Not registered"
state="pending"
/>
<StatusTile label="Runtime" value="Future" state="pending" />
</div>
</div>
</div>
);
}
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 (
<div className="dynamic-resource-view">
<TopbarServiceInfo onOpenInfo={onOpenInfo} />
<div
className={`resource-workbench${activeSelected && !showDatabaseSnapshots ? " with-inspector" : ""}`}
>
<section className="resource-main">
{isAwsDatabase && (
<div className="drawer-tabs" style={{ marginBottom: 12 }}>
<button
type="button"
className={`drawer-tab ${databaseTab === "instances" ? "active" : ""}`}
onClick={() => handleDatabaseTabChange("instances")}
>
Instances
</button>
<button
type="button"
className={`drawer-tab ${databaseTab === "snapshots" ? "active" : ""}`}
onClick={() => handleDatabaseTabChange("snapshots")}
>
Snapshots
</button>
</div>
)}
{showDatabaseSnapshots ? (
<DatabaseSnapshotsPanel
cloud={cloud}
instances={resources}
listCapability={listSnapshotCapability}
createCapability={createSnapshotCapability}
runtimeReachable={canUseRuntime}
/>
) : (
<section className="table-panel">
<div className="input-row resource-table-bar">
<div>
<p className="eyebrow">Resources</p>
<span className="muted">
{resources.length} normalized resources
</span>
</div>
<div className="resource-table-tools">
<div className="search-box">
<input
className="input"
value={search}
onChange={(event) => setSearch(event.target.value)}
placeholder="Filter resources"
/>
</div>
{schema.filters.find((filter) => filter.name === "state" && filter.type === "select") && (
<select
className="input"
aria-label="Filter by lifecycle state"
value={state}
onChange={(event) => setState(event.target.value)}
>
{schema.filters.find((filter) => filter.name === "state")?.options?.map((option) => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
)}
{service === "email" && (
clearConfirm ? (
<>
<button
className="button danger"
type="button"
disabled={!canUseRuntime || clearInboxMut.isPending}
onClick={() => clearInboxMut.mutate()}
>
<Trash2 size={14} />
{clearInboxMut.isPending ? "Clearing" : "Confirm clear"}
</button>
<button className="button" type="button" disabled={clearInboxMut.isPending} onClick={() => setClearConfirm(false)}>
Cancel
</button>
</>
) : (
<button
className="button danger"
type="button"
disabled={!canUseRuntime}
onClick={() => setClearConfirm(true)}
>
<Trash2 size={14} />
Clear inbox
</button>
)
)}
{canCreate && (
<button
className="button"
type="button"
disabled={!canCreateResource}
title={createCapability?.reason}
onClick={() => setCreateOpen((open) => !open)}
>
<Plus size={14} />
{createResourceLabel}
{createOpen ? (
<ChevronUp size={13} />
) : (
<ChevronDown size={13} />
)}
</button>
)}
<button
className="button"
type="button"
disabled={!canUseRuntime || resourcesQuery.isFetching}
onClick={() => resourcesQuery.refetch()}
>
<RefreshCw size={14} />
{resourcesQuery.isFetching ? "Loading" : "Refresh"}
</button>
{canDelete && (
deleteConfirm ? (
<>
{deleteImpactText && (
<span className="delete-impact" role="status">{deleteImpactText}</span>
)}
<button
className="button danger"
type="button"
disabled={!canUseRuntime || deleteMultipleMut.isPending}
onClick={() => deleteMultipleMut.mutate(selectedItems)}
>
<Trash2 size={14} />
{deleteMultipleMut.isPending
? "Deleting..."
: `Confirm Delete (${selectedItems.length})`}
</button>
<button
className="button"
type="button"
disabled={deleteMultipleMut.isPending}
onClick={() => setDeleteConfirm(false)}
>
Cancel
</button>
</>
) : (
<button
className="button danger"
type="button"
disabled={
!canUseRuntime ||
resourcesQuery.isFetching ||
deleteMultipleMut.isPending
}
onClick={() => {
if (!isDeleteMulti) {
setIsDeleteMulti(true);
} else if (selectedItems.length > 0) {
setDeleteConfirm(true);
} else {
setIsDeleteMulti(false);
setSelectedItems([]);
}
}}
>
<Trash2 size={14} />
{isDeleteMulti && selectedItems.length > 0
? `Delete Selected (${selectedItems.length})`
: isDeleteMulti
? "Cancel Selection"
: "Delete Multiple"}
</button>
)
)}
</div>
</div>
{service === "email" && clearInboxMut.isError && (
<p className="error-text compact-text" style={{ margin: "0 12px 10px" }}>
{clearInboxMut.error instanceof Error
? clearInboxMut.error.message
: "Unable to clear the inbox."}
</p>
)}
{bulkDeleteError && (
<p className="error-text compact-text" style={{ margin: "0 12px 10px" }}>
{bulkDeleteError}
</p>
)}
{canCreate && createOpen && (
<div className="resource-create-inline">
{/*
* 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" ? (
<LaunchInstanceForm
cloud={cloud}
selectedResource={activeSelected}
onSuccess={(resource) => {
setSelected(resource);
setCreateOpen(false);
}}
onCancel={() => setCreateOpen(false)}
/>
) : service === "lightsail" && cloud === "aws" ? (
<LightsailLaunchPanel
isSubmitting={createMut.isPending}
submitError={createMut.error instanceof Error ? createMut.error.message : null}
onCancel={() => setCreateOpen(false)}
onSubmit={(values) => createMut.mutate({values})}
/>
) : service === "database" && cloud === "aws" ? (
<CreateRdsInstanceForm
cloud={cloud}
onSuccess={(resource) => {
setSelected(resource);
setCreateOpen(false);
}}
onCancel={() => setCreateOpen(false)}
/>
) : service === "serverless" && cloud === "aws" ? (
<LambdaCreatePanel
schema={schema}
isSubmitting={createMut.isPending}
submitError={createMut.error instanceof Error ? createMut.error.message : null}
onCancel={() => setCreateOpen(false)}
onSubmit={(values) => createMut.mutate({values})}
/>
) : service === "loadbalancing" && cloud === "aws" ? (
<LoadBalancerPanel
isSubmitting={createMut.isPending}
submitError={createMut.error instanceof Error ? createMut.error.message : null}
onCancel={() => setCreateOpen(false)}
onSubmit={(values) => createMut.mutate({values})}
/>
) : service === "workflows" && cloud === "aws" ? (
<WorkflowCreatePanel
isSubmitting={createMut.isPending}
submitError={createMut.error instanceof Error ? createMut.error.message : null}
onCancel={() => setCreateOpen(false)}
onSubmit={(submission) => createMut.mutate(submission)}
/>
) : service === "iac" && cloud === "aws" ? (
<CloudFormationCreatePanel
isSubmitting={createMut.isPending}
submitError={createMut.error instanceof Error ? createMut.error.message : null}
onCancel={() => setCreateOpen(false)}
onSubmit={(values) => createMut.mutate({values})}
/>
) : service === "codedeploy" && cloud === "aws" ? (
<CodeDeployCreatePanel isSubmitting={createMut.isPending} submitError={createMut.error instanceof Error ? createMut.error.message : null} onCancel={() => setCreateOpen(false)} onSubmit={(values) => createMut.mutate({values})}/>
) : (
<>
<DynamicFormRenderer
key={`${cloud}:${service}:create`}
schema={schema}
isSubmitting={createMut.isPending}
submitLabel={createResourceLabel}
pendingLabel="Creating"
submitError={
createMut.error instanceof Error
? createMut.error.message
: null
}
onCancel={() => setCreateOpen(false)}
onSubmit={(values) => createMut.mutate({values})}
/>
{service === "elasticache" && cloud === "aws" && createMut.isPending && <DatabaseLaunchProgress name="cache cluster" description="Starting the cache container and preparing its local endpoint." />}
</>
)}
</div>
)}
{!(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,
})}
</section>
)}
{cloud === 'aws' && service === 'email' && <SesConsolePanel runtimeReachable={canUseRuntime}/>}
</section>
{activeSelected && !showDatabaseSnapshots && (
<div>
{cloud === "aws" && service === "workflows" && <button className="button compact workflow-open-studio" type="button" onClick={() => navigate(workflowStudioPath(activeSelected.id))}>Open Studio</button>}
<ResourceInspector
resource={activeSelected}
object={selectedObject}
cloud={cloud}
runtimeReachable={canUseRuntime}
serviceName={schema.displayName}
/>
</div>
)}
</div>
{service === "storage" && (
<>
<StorageObjectBrowser
cloud={cloud}
resource={selected}
capabilities={objectCapabilities}
runtimeReachable={canUseRuntime}
selectedObjectKey={selectedObject?.key}
onSelectObject={setSelectedObject}
/>
{cloud === "aws" && <S3BucketWebsitePanel resource={selected} runtimeReachable={canUseRuntime} />}
</>
)}
{service === "messaging" && cloud === "aws" && <SqsQueuePanel resource={activeSelected} runtimeReachable={canUseRuntime} />}
{service === "iac" && cloud === "aws" && <CloudFormationStackPanel resource={activeSelected} />}
{service === "ecr" && cloud === "aws" && (
<EcrImageBrowser resource={activeSelected} runtimeReachable={canUseRuntime} />
)}
{service === "targetgroups" && cloud === "aws" && (
<TargetGroupPanel resource={activeSelected} runtimeReachable={canUseRuntime} />
)}
{service === "loadbalancing" && cloud === "aws" && (
<LoadBalancerListenerPanel resource={activeSelected} runtimeReachable={canUseRuntime} />
)}
{service === "userpools" && cloud === "aws" && (
<CognitoUsersPanel resource={activeSelected} runtimeReachable={canUseRuntime} />
)}
{service === "dns" && cloud === "aws" && (
<Route53RecordsPanel resource={activeSelected} runtimeReachable={canUseRuntime} />
)}
{service === "certificates" && cloud === "aws" && (
<CertificatePanel resource={activeSelected} runtimeReachable={canUseRuntime} />
)}
{service === "pipes" && cloud === "aws" && (
<PipePanel resource={activeSelected} runtimeReachable={canUseRuntime} />
)}
{service === "autoscaling" && cloud === "aws" && (
<AutoScalingPanel resource={activeSelected} runtimeReachable={canUseRuntime} />
)}
{service === "filesystems" && cloud === "aws" && (
<FileSystemPanel resource={activeSelected} runtimeReachable={canUseRuntime} />
)}
{service === "datacatalog" && cloud === "aws" && (
<DataCatalogPanel resource={activeSelected} runtimeReachable={canUseRuntime} />
)}
{service === "query" && cloud === "aws" && (
<QueryPanel resource={activeSelected} runtimeReachable={canUseRuntime} />
)}
{service === "cdn" && cloud === "aws" && (
<CloudFrontPanel resource={activeSelected} runtimeReachable={canUseRuntime} />
)}
{service === "monitoring" && cloud === "aws" && (
<>
<CloudWatchAlarmPanel resource={activeSelected} runtimeReachable={canUseRuntime} />
<CloudWatchMetricsExplorer runtimeReachable={canUseRuntime} />
</>
)}
{service === "compute" && (
<ComputePanel
cloud={cloud}
resource={activeSelected}
runtimeReachable={runtimeReachable}
/>
)}
{service === "networking" && (
<NetworkingPanel
cloud={cloud}
resource={activeSelected}
runtimeReachable={runtimeReachable}
/>
)}
{service === "serverless" && (
<>
<ServerlessInvokePanel
cloud={cloud}
resource={activeSelected}
runtimeReachable={canUseRuntime}
/>
<S3LambdaTriggerPanel
cloud={cloud}
resource={activeSelected}
runtimeReachable={canUseRuntime}
/>
<ScheduledLambdaTriggerPanel
cloud={cloud}
resource={activeSelected}
runtimeReachable={canUseRuntime}
/>
<SqsLambdaTriggerPanel
cloud={cloud}
resource={activeSelected}
runtimeReachable={canUseRuntime}
/>
<KinesisPanel cloud={cloud} resource={activeSelected} runtimeReachable={canUseRuntime} mode="lambda" />
{cloud === "aws" && activeSelected?.type === "lambda" && (
<LambdaInvocationHistoryPanel cloud={cloud} functionName={activeSelected.name} runtimeReachable={canUseRuntime}/>
)}
</>
)}
{service === "apigateway" && (activeSelected && ["http", "websocket"].includes(String(activeSelected.metadata.apigatewayService)) ? (
<HttpApiPanel
cloud={cloud}
resource={activeSelected}
runtimeReachable={canUseRuntime}
runtimeEndpoint={cloudStatus?.endpoint ?? undefined}
/>
) : (
<ApiGatewayRoutePanel
cloud={cloud}
resource={activeSelected}
runtimeReachable={canUseRuntime}
runtimeEndpoint={cloudStatus?.endpoint ?? undefined}
/>
))}
{service === "events" && (
<EventBridgeRulePanel
cloud={cloud}
resource={activeSelected}
runtimeReachable={canUseRuntime}
/>
)}
{service === "configuration" && cloud === "aws" && (
<AppConfigPanel
cloud={cloud}
resource={activeSelected}
runtimeReachable={canUseRuntime}
/>
)}
{service === "kms" && (
<KmsCryptoPanel
cloud={cloud}
resource={activeSelected}
runtimeReachable={canUseRuntime}
/>
)}
{service === "notifications" && (
<SnsTopicPanel
cloud={cloud}
resource={activeSelected}
runtimeReachable={canUseRuntime}
/>
)}
{service === "workflows" && cloud === "aws" && (
<WorkflowExecutionDebugger resource={activeSelected} runtimeReachable={canUseRuntime} />
)}
{service === "codebuild" && cloud === "aws" && (
<CodeBuildPanel resource={activeSelected} runtimeReachable={canUseRuntime} />
)}
{(service === "ecs" || service === "fargate") && cloud === "aws" && <EcsServicePanel resource={activeSelected} />}
{service === "codepipeline" && cloud === "aws" && (
<CodePipelinePanel resource={activeSelected} runtimeReachable={canUseRuntime} />
)}
{service === "codedeploy" && cloud === "aws" && (
<CodeDeployPanel resource={activeSelected} runtimeReachable={canUseRuntime} />
)}
{service === "kinesis" && cloud === "aws" && <KinesisPanel cloud={cloud} resource={activeSelected} runtimeReachable={canUseRuntime} mode="stream" />}
{service === "elasticache" && cloud === "aws" && <ElastiCacheManagementPanel runtimeReachable={canUseRuntime} />}
{service === "lightsail" && cloud === "aws" && <LightsailInstancePanel resource={activeSelected} runtimeReachable={canUseRuntime} onDeleted={() => setSelected(undefined)} />}
{editingResource && schema.updateFields && (
<div
className="modal-overlay"
onClick={() => {
if (!updateMut.isPending) setEditingResource(null);
}}
>
<div
className="create-table-modal"
style={{ width: 520, maxWidth: "90vw" }}
onClick={(e) => e.stopPropagation()}
>
<div
className="modal-header"
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<h3 style={{ margin: 0 }}>
Edit {schema.displayName}: {editingResource.name || editingResource.id}
</h3>
<button
className="icon-btn"
type="button"
disabled={updateMut.isPending}
onClick={() => setEditingResource(null)}
>
<X size={14} />
</button>
</div>
{service === "loadbalancing" && cloud === "aws" ? (
<LoadBalancerPanel
resource={editingResource}
isSubmitting={updateMut.isPending}
submitError={updateError}
onCancel={() => setEditingResource(null)}
onSubmit={handleEditSubmit}
/>
) : service === "iac" && cloud === "aws" ? <CloudFormationUpdatePanel initialTemplate={String(initialEditValues.templateBody ?? '')} isSubmitting={updateMut.isPending} submitError={updateError} onCancel={() => setEditingResource(null)} onSubmit={handleEditSubmit}/> : <DynamicFormRenderer
key={editingResource.id}
schema={schema}
fields={schema.updateFields}
initialValues={initialEditValues}
isSubmitting={updateMut.isPending}
submitLabel="Save Changes"
pendingLabel="Saving"
submitError={updateError}
onCancel={() => setEditingResource(null)}
onSubmit={handleEditSubmit}
/>}
</div>
</div>
)}
{successToast && (
<div
role="status"
style={{
position: "fixed",
bottom: 24,
right: 24,
background: "var(--accent, #3b82f6)",
color: "#fff",
padding: "10px 18px",
borderRadius: 6,
boxShadow: "0 4px 16px rgba(0,0,0,0.3)",
zIndex: 1000,
fontSize: 13,
fontWeight: 500,
}}
>
{successToast}
</div>
)}
{deleteWarning && (
<div
role="alert"
style={{
position: "fixed",
bottom: 24,
right: 24,
background: "var(--status-error, #dc2626)",
color: "#fff",
padding: "10px 18px",
borderRadius: 6,
boxShadow: "0 4px 16px rgba(0,0,0,0.3)",
zIndex: 1000,
fontSize: 13,
fontWeight: 500,
}}
>
<span>{deleteWarning}</span>
<button
className="icon-btn"
type="button"
aria-label="Dismiss warning"
onClick={() => setDeleteWarning(null)}
style={{ marginLeft: 10, color: "inherit" }}
>
<X size={14} />
</button>
</div>
)}
</div>
);
}
function TopbarServiceInfo({ onOpenInfo }: { onOpenInfo: () => void }) {
const [slot, setSlot] = useState<HTMLElement | null>(null);
useLayoutEffect(() => {
setSlot(document.getElementById("topbar-status"));
}, []);
if (!slot) return null;
return createPortal(
<button
className="icon-btn"
type="button"
onClick={onOpenInfo}
title="Service information"
>
<Info size={14} />
</button>,
slot,
);
}
function StatusTile({
label,
value,
state,
}: {
label: string;
value: string;
state: "placeholder" | "pending";
}) {
return (
<div className={`status-tile ${state}`}>
<span>{label}</span>
<strong>{value}</strong>
</div>
);
}
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 (
<RuntimeNotice
title="Loading service"
detail="Waiting for the proxy to describe this service and confirm its runtime."
state="pending"
/>
);
}
if (serviceAvailability !== "available") {
return (
<RuntimeNotice
title="Adapter coming soon"
detail={
serviceReason ??
`${schema.displayName} uses the same normalized schema, but a runtime adapter is not registered yet.`
}
state="pending"
/>
);
}
if (cloudStatus?.runtime === "unavailable") {
return (
<RuntimeNotice
title="Runtime unavailable"
detail={`${cloudStatus.endpoint ?? "Runtime endpoint"} is not reachable. Start the selected runtime before loading resources.`}
error={cloudStatus.error ?? undefined}
state="unavailable"
/>
);
}
if (resourcesError) {
return (
<RuntimeNotice
title={`Unable to load ${schema.displayName}.`}
detail={`The proxy could not fetch ${schema.displayName} resources from the runtime.`}
error={
resourcesError instanceof Error
? resourcesError.message
: "Unknown resource error"
}
state="unavailable"
isRetrying={isRetrying}
onRetry={onRetry}
/>
);
}
if (resourcesLoading) {
return (
<RuntimeNotice
title={`Loading ${schema.displayName}...`}
detail={`Reading ${schema.displayName} resources from the runtime.`}
state="pending"
showSpinner={true}
/>
);
}
return (
<ResourceTable
schema={schema}
resources={resources}
selectedId={selectedId}
deletingId={deletingId}
onSelect={onSelect}
onEdit={onEdit}
onDelete={onDelete}
dataPath={dataExplorerPath}
isDeleteMulti={isDeleteMulti}
selectedItems={selectedItems}
onToggleSelect={onToggleSelect}
onToggleSelectAll={onToggleSelectAll}
isAllSelected={isAllSelected}
/>
);
}
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 (
<div className={`runtime-notice ${state}`}>
{(showSpinner || isRetrying) && (
<Loader2
size={24}
className="spin"
style={{ marginBottom: 8, color: "var(--accent)" }}
/>
)}
<h3>{title}</h3>
<p>{detail}</p>
{error && <code>{error}</code>}
{onRetry && (
<button
className="button"
type="button"
style={{ marginTop: 12 }}
disabled={isRetrying}
onClick={() => { if (!isRetrying) onRetry(); }}
>
{isRetrying ? "Retrying\u2026" : "Retry"}
</button>
)}
</div>
);
}