feat: add SQS + EventBridge
This commit is contained in:
@@ -4,10 +4,16 @@ import { ChevronDown, ChevronUp, Info, Loader2, Plus, RefreshCw, Trash2, X } fro
|
||||
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";
|
||||
@@ -39,6 +45,12 @@ import { CreateRdsInstanceForm } from "@/components/CreateRdsInstanceForm";
|
||||
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 { LambdaInvocationHistoryPanel } from "@/components/LambdaInvocationHistoryPanel";
|
||||
import { LambdaCreatePanel } from "@/components/LambdaCreatePanel";
|
||||
import { bucketDeleteWarning } from "@/lib/bucketDeletion";
|
||||
|
||||
interface DynamicResourceViewProps {
|
||||
cloud: CloudProvider;
|
||||
@@ -73,6 +85,7 @@ export function DynamicResourceView({
|
||||
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 handleDatabaseTabChange = (tab: "instances" | "snapshots") => {
|
||||
@@ -138,8 +151,94 @@ export function DynamicResourceView({
|
||||
};
|
||||
|
||||
const createMut = useMutation({
|
||||
mutationFn: (values: Record<string, unknown>) =>
|
||||
createCloudResource(cloud, service, values),
|
||||
mutationFn: async (values: Record<string, unknown>) => {
|
||||
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 (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) => {
|
||||
setSelected(resource);
|
||||
setCreateOpen(false);
|
||||
@@ -150,15 +249,25 @@ export function DynamicResourceView({
|
||||
});
|
||||
|
||||
const deleteMut = useMutation({
|
||||
mutationFn: (resource: CloudResource) =>
|
||||
deleteCloudResource(cloud, service, resource.id),
|
||||
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);
|
||||
}
|
||||
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({
|
||||
@@ -234,6 +343,13 @@ export function DynamicResourceView({
|
||||
}
|
||||
}, [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),
|
||||
@@ -280,8 +396,9 @@ export function DynamicResourceView({
|
||||
if (!trimmed && !field.required) {
|
||||
continue;
|
||||
}
|
||||
const initialStr =
|
||||
initialVal !== undefined && initialVal !== null ? String(initialVal).trim() : "";
|
||||
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;
|
||||
}
|
||||
@@ -623,6 +740,14 @@ export function DynamicResourceView({
|
||||
}}
|
||||
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)}
|
||||
/>
|
||||
) : (
|
||||
<DynamicFormRenderer
|
||||
key={`${cloud}:${service}:create`}
|
||||
@@ -658,9 +783,14 @@ export function DynamicResourceView({
|
||||
schema.updateFields &&
|
||||
schema.updateFields.length > 0 &&
|
||||
schema.actions.includes("update")
|
||||
? (resource) => {
|
||||
setEditingResource(resource);
|
||||
setUpdateError(null);
|
||||
? 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),
|
||||
@@ -709,11 +839,31 @@ export function DynamicResourceView({
|
||||
/>
|
||||
)}
|
||||
{service === "serverless" && (
|
||||
<ServerlessInvokePanel
|
||||
cloud={cloud}
|
||||
resource={activeSelected}
|
||||
runtimeReachable={canUseRuntime}
|
||||
/>
|
||||
<>
|
||||
<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}
|
||||
/>
|
||||
{cloud === "aws" && activeSelected?.type === "lambda" && (
|
||||
<LambdaInvocationHistoryPanel cloud={cloud} functionName={activeSelected.name} runtimeReachable={canUseRuntime}/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{service === "apigateway" && (
|
||||
<ApiGatewayRoutePanel
|
||||
@@ -803,6 +953,35 @@ export function DynamicResourceView({
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user