248 lines
7.3 KiB
TypeScript
248 lines
7.3 KiB
TypeScript
import {useEffect, useState} from "react";
|
|
import {ChevronDown, ChevronUp, Copy, Loader2, Play, Wand2} from "lucide-react";
|
|
import {useMutation} from "@tanstack/react-query";
|
|
import {invokeCloudResource} from "@/api/cloudProxyClient";
|
|
import type {ServerlessInvokeResult} from "@/api/cloudProxyClient";
|
|
import type {CloudProvider} from "@/types/cloud";
|
|
import type {CloudResource} from "@/types/resource";
|
|
import {ServiceIcon} from "@/components/serviceIcons";
|
|
import {LogsQueryPanel} from "./LogsQueryPanel";
|
|
|
|
interface ServerlessInvokePanelProps {
|
|
cloud: CloudProvider;
|
|
resource?: CloudResource;
|
|
runtimeReachable: boolean;
|
|
}
|
|
|
|
export function ServerlessInvokePanel({
|
|
cloud,
|
|
resource,
|
|
runtimeReachable,
|
|
}: ServerlessInvokePanelProps) {
|
|
const [payload, setPayload] = useState("{\n \n}");
|
|
const [invokeResult, setInvokeResult] = useState<ServerlessInvokeResult | null>(null);
|
|
const [showLog, setShowLog] = useState(false);
|
|
const [copied, setCopied] = useState(false);
|
|
const [validationError, setValidationError] = useState<string | null>(null);
|
|
const [logsRefreshKey, setLogsRefreshKey] = useState(0);
|
|
|
|
useEffect(() => {
|
|
setPayload("{\n \n}");
|
|
setInvokeResult(null);
|
|
setShowLog(false);
|
|
setCopied(false);
|
|
setValidationError(null);
|
|
setLogsRefreshKey(0);
|
|
}, [resource?.id]);
|
|
|
|
const isSupportedResource =
|
|
resource?.service === "serverless" &&
|
|
(resource.type === "lambda" ||
|
|
resource.type === "azure-function" ||
|
|
resource.type === "gcp-function");
|
|
|
|
const canInvoke = Boolean(resource && isSupportedResource && runtimeReachable);
|
|
const canSubmit = canInvoke && !validationError;
|
|
|
|
const providerLabel =
|
|
cloud === "aws"
|
|
? "Lambda function"
|
|
: cloud === "azure"
|
|
? "Azure Function"
|
|
: "Google Cloud Function";
|
|
|
|
const invokeMutation = useMutation({
|
|
mutationFn: () =>
|
|
invokeCloudResource(cloud, "serverless", resource!.id, payload),
|
|
onSuccess: (result) => {
|
|
setInvokeResult(result);
|
|
setShowLog(false);
|
|
setCopied(false);
|
|
setLogsRefreshKey((value) => value + 1);
|
|
},
|
|
});
|
|
|
|
const isInvokeError = Boolean(
|
|
invokeResult?.functionError || (invokeResult && invokeResult.statusCode >= 400),
|
|
);
|
|
|
|
const formatPayload = () => {
|
|
try {
|
|
const formatted = JSON.stringify(JSON.parse(payload), null, 2);
|
|
setPayload(formatted);
|
|
setValidationError(null);
|
|
} catch {
|
|
setValidationError("Payload must be valid JSON before formatting.");
|
|
}
|
|
};
|
|
|
|
const clearPayload = () => {
|
|
setPayload("{}");
|
|
setValidationError(null);
|
|
};
|
|
|
|
const copyResponse = async () => {
|
|
if (!invokeResult) return;
|
|
await navigator.clipboard.writeText(invokeResult.payload || "");
|
|
setCopied(true);
|
|
window.setTimeout(() => setCopied(false), 1200);
|
|
};
|
|
|
|
if (!resource || resource.service !== "serverless") {
|
|
return (
|
|
<section className="table-panel">
|
|
<div className="empty compact">
|
|
<h3>Select a serverless function</h3>
|
|
<p>
|
|
Select a Lambda function, Azure Function, or Google Cloud Function to
|
|
invoke it from Cloud Explorer.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<section className="table-panel">
|
|
<div className="dynamic-stage-header">
|
|
<div>
|
|
<p className="eyebrow">Serverless Actions</p>
|
|
<h3>
|
|
<ServiceIcon cloud={cloud} iconKey="serverless" size={18} />
|
|
Invoke {providerLabel}
|
|
</h3>
|
|
<p className="muted compact-text">
|
|
Send a JSON event payload to the selected serverless function.
|
|
</p>
|
|
</div>
|
|
<span className={`runtime-state ${canInvoke ? "ready" : "pending"}`}>
|
|
{canInvoke ? "Ready" : "Runtime unavailable"}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="resource-create-inline">
|
|
|
|
<div className="inspector-section-header">
|
|
<label className="metric-label" htmlFor="serverless-invoke-payload">
|
|
Event payload JSON
|
|
</label>
|
|
|
|
<button className="button" type="button" onClick={formatPayload}>
|
|
<Wand2 size={13} />
|
|
Format JSON
|
|
</button>
|
|
|
|
<button className="button" type="button" onClick={clearPayload}>
|
|
Clear
|
|
</button>
|
|
</div>
|
|
|
|
<textarea
|
|
id="serverless-invoke-payload"
|
|
className="json-editor serverless-payload-editor"
|
|
value={payload}
|
|
onChange={(event) => {
|
|
const value = event.target.value;
|
|
setPayload(value);
|
|
|
|
try {
|
|
JSON.parse(value);
|
|
setValidationError(null);
|
|
} catch {
|
|
setValidationError("Payload must be valid JSON.");
|
|
}
|
|
}}
|
|
spellCheck={false}
|
|
placeholder="{}"
|
|
style={{minHeight: 140}}
|
|
/>
|
|
{validationError && (
|
|
<p className="error-text compact-text">
|
|
{validationError}
|
|
</p>
|
|
)}
|
|
|
|
<button
|
|
className="button primary"
|
|
type="button"
|
|
disabled={!canSubmit || invokeMutation.isPending}
|
|
onClick={() => invokeMutation.mutate()}
|
|
>
|
|
{invokeMutation.isPending ? <Loader2 size={13} /> : <Play size={13} />}
|
|
{invokeMutation.isPending ? "Invoking" : "Invoke"}
|
|
</button>
|
|
|
|
{invokeMutation.isError && (
|
|
<p className="error-text compact-text">
|
|
{invokeMutation.error instanceof Error
|
|
? invokeMutation.error.message
|
|
: "Invocation failed"}
|
|
</p>
|
|
)}
|
|
|
|
{invokeResult && (
|
|
<div className="inspector-section">
|
|
<div className="inspector-section-header">
|
|
<p className="metric-label">Invocation Result</p>
|
|
<span className={`runtime-state ${isInvokeError ? "unavailable" : "ready"}`}>
|
|
HTTP {invokeResult.statusCode}
|
|
</span>
|
|
</div>
|
|
|
|
{invokeResult.executionDuration !== undefined && (
|
|
<p className="muted compact-text">
|
|
Completed in {invokeResult.executionDuration}ms
|
|
</p>
|
|
)}
|
|
|
|
{invokeResult.functionError && (
|
|
<p className="error-text compact-text">
|
|
{invokeResult.functionError}
|
|
</p>
|
|
)}
|
|
|
|
<button className="button" type="button" onClick={copyResponse}>
|
|
<Copy size={13} />
|
|
{copied ? "Copied" : "Copy response"}
|
|
</button>
|
|
|
|
<pre className={`invoke-result ${isInvokeError ? "error" : "success"}`}>
|
|
{tryFormatJson(invokeResult.payload) || "(empty)"}
|
|
</pre>
|
|
|
|
{invokeResult.logResult && (
|
|
<div>
|
|
<button
|
|
className="button"
|
|
type="button"
|
|
onClick={() => setShowLog((open) => !open)}
|
|
>
|
|
Log tail
|
|
{showLog ? <ChevronUp size={13} /> : <ChevronDown size={13} />}
|
|
</button>
|
|
{showLog && <div className="log-tail">{invokeResult.logResult}</div>}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{cloud === "aws" && invokeResult && (
|
|
<LogsQueryPanel
|
|
cloud={cloud}
|
|
logGroupName={`/aws/lambda/${resource.name}`}
|
|
runtimeReachable={runtimeReachable}
|
|
autoRunKey={logsRefreshKey}
|
|
/>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function tryFormatJson(raw: string): string {
|
|
try {
|
|
return JSON.stringify(JSON.parse(raw), null, 2);
|
|
} catch {
|
|
return raw;
|
|
}
|
|
}
|