@flanksource/clicky-ui
Version:
Flanksource Clicky UI — React component library built on shadcn/ui with light/dark and density theming.
219 lines (218 loc) • 9.83 kB
JavaScript
import { jsx, jsxs } from "react/jsx-runtime";
import { useState, useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { Button } from "../components/button.js";
import { MethodBadge } from "../data/MethodBadge.js";
import { Modal } from "../overlay/Modal.js";
import { filterOperationsByDomain, findListEndpoint, findDetailEndpointForList } from "./classify.js";
import { filterOperationsBySurface, findSurfaceListOperation, findSurfaceDetailOperation, findSurfaceEntityActions } from "./clickyMetadata.js";
import { ExecutionResult } from "./ExecutionResult.js";
import { FilterForm } from "./FilterForm.js";
import { packParameterValues } from "./formMetadata.js";
import { useOperations } from "./useOperations.js";
function defaultRenderError(err, title) {
const message = err instanceof Error ? err.message : String(err ?? "");
return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-destructive/40 bg-destructive/5 p-4 text-sm text-destructive", children: [
/* @__PURE__ */ jsx("div", { className: "font-medium", children: title }),
message && /* @__PURE__ */ jsx("div", { className: "mt-1 whitespace-pre-wrap text-xs opacity-80", children: message })
] });
}
function OperationEntityPage({
id,
definition,
entities,
client,
renderLink,
allOperations = false,
operationIdPrefix,
listOperationId,
detailOperationId,
surfaceKey,
backHref,
backLabel = "Back",
renderError = defaultRenderError,
commandRuntime
}) {
const { operations, isLoading } = useOperations(client);
const [activeAction, setActiveAction] = useState(null);
const [actionResult, setActionResult] = useState(null);
const [actionError, setActionError] = useState("");
const [isExecutingAction, setIsExecutingAction] = useState(false);
const surfaceOps = useMemo(
() => filterOperationsBySurface(operations, surfaceKey),
[operations, surfaceKey]
);
const useSurfaceMetadata = surfaceOps.length > 0;
const domainOps = useMemo(() => {
if (useSurfaceMetadata) {
return surfaceOps;
}
return (allOperations ? operations : filterOperationsByDomain(operations, entities)).filter(
(op) => operationIdPrefix ? (op.operation.operationId ?? "").startsWith(operationIdPrefix) : true
);
}, [allOperations, entities, operationIdPrefix, operations, surfaceOps, useSurfaceMetadata]);
const listEndpoint = useMemo(() => {
if (useSurfaceMetadata) {
return findSurfaceListOperation(domainOps, surfaceKey);
}
const explicitList = listOperationId ? domainOps.find((op) => op.method === "get" && op.operation.operationId === listOperationId) : void 0;
return explicitList ?? findListEndpoint(domainOps, entities);
}, [domainOps, entities, listOperationId, surfaceKey, useSurfaceMetadata]);
const resolvedDetailEndpoint = useMemo(() => {
if (useSurfaceMetadata) {
return findSurfaceDetailOperation(domainOps, surfaceKey);
}
return detailOperationId ? domainOps.find(
(op) => op.method === "get" && op.operation.operationId === detailOperationId
) : findDetailEndpointForList(domainOps, listEndpoint);
}, [detailOperationId, domainOps, listEndpoint, surfaceKey, useSurfaceMetadata]);
const idParameterName = useMemo(
() => {
var _a, _b;
return ((_b = (_a = resolvedDetailEndpoint == null ? void 0 : resolvedDetailEndpoint.operation.parameters) == null ? void 0 : _a.find((param) => param.in === "path")) == null ? void 0 : _b.name) ?? "id";
},
[resolvedDetailEndpoint]
);
const detailValues = useMemo(() => id ? { [idParameterName]: id } : {}, [id, idParameterName]);
const actionOps = useMemo(() => {
if (useSurfaceMetadata) {
return findSurfaceEntityActions(domainOps, surfaceKey);
}
return resolvedDetailEndpoint == null ? [] : domainOps.filter(
(op) => op.method !== "get" && op.path.startsWith(`${resolvedDetailEndpoint.path}/`)
);
}, [domainOps, resolvedDetailEndpoint, surfaceKey, useSurfaceMetadata]);
const detailQuery = useQuery({
queryKey: ["entity-detail", definition.key, id, resolvedDetailEndpoint == null ? void 0 : resolvedDetailEndpoint.path],
queryFn: async () => client.executeCommand(
resolvedDetailEndpoint.path,
resolvedDetailEndpoint.method,
detailValues,
{ Accept: "application/json+clicky" }
),
enabled: !!resolvedDetailEndpoint && !!id,
staleTime: 3e4,
retry: 0
});
const backLink = backHref == null ? null : renderLink ? renderLink({
to: backHref,
className: "text-sm text-primary underline-offset-4 hover:underline",
children: backLabel
}) : /* @__PURE__ */ jsx("a", { href: backHref, className: "text-sm text-primary underline-offset-4 hover:underline", children: backLabel });
async function executeAction(values) {
if (!activeAction) return;
setIsExecutingAction(true);
setActionError("");
try {
const response = await client.executeCommand(
activeAction.path,
activeAction.method,
packParameterValues(values, activeAction.operation.parameters ?? []),
{ Accept: "application/json+clicky" }
);
setActionResult(response);
await detailQuery.refetch();
} catch (err) {
setActionResult(null);
setActionError(err instanceof Error ? err.message : String(err ?? "Unknown error"));
} finally {
setIsExecutingAction(false);
}
}
if (isLoading) {
return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "Loading entity…" });
}
if (!resolvedDetailEndpoint || !id) {
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
backLink,
/* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "Unknown entity detail route." })
] });
}
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-4", children: [
/* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-2", children: [
backLink,
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
/* @__PURE__ */ jsx(MethodBadge, { method: resolvedDetailEndpoint.method }),
/* @__PURE__ */ jsx("code", { className: "rounded-md bg-muted px-2 py-1 text-sm", children: resolvedDetailEndpoint.path })
] }),
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsxs("h1", { className: "text-2xl font-semibold tracking-tight", children: [
definition.title,
": ",
id
] }),
/* @__PURE__ */ jsx("p", { className: "mt-1 max-w-3xl text-sm text-muted-foreground", children: definition.description })
] })
] }),
actionOps.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex shrink-0 flex-wrap justify-end gap-2", children: actionOps.map((op) => /* @__PURE__ */ jsx(
Button,
{
type: "button",
variant: "outline",
size: "sm",
onClick: () => {
setActiveAction(op);
setActionResult(null);
setActionError("");
},
children: op.operation.summary || op.operation.operationId || op.path
},
`${op.method}:${op.path}`
)) })
] }),
detailQuery.isLoading ? /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "Loading detail…" }) : detailQuery.isError ? renderError(detailQuery.error, `Failed to load ${resolvedDetailEndpoint.path}`) : /* @__PURE__ */ jsxs("section", { className: "rounded-xl border bg-card p-4", children: [
/* @__PURE__ */ jsx("h2", { className: "text-lg font-medium", children: "Entity detail" }),
/* @__PURE__ */ jsx(
ExecutionResult,
{
response: detailQuery.data ?? null,
...commandRuntime ? { commandRuntime } : {}
}
)
] }),
/* @__PURE__ */ jsx(
Modal,
{
open: activeAction != null,
onClose: () => setActiveAction(null),
title: (activeAction == null ? void 0 : activeAction.operation.summary) || (activeAction == null ? void 0 : activeAction.operation.operationId) || "Action",
size: "lg",
children: activeAction && /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
/* @__PURE__ */ jsx(MethodBadge, { method: activeAction.method }),
/* @__PURE__ */ jsx("code", { className: "rounded-md bg-muted px-2 py-1 text-sm", children: activeAction.path })
] }),
/* @__PURE__ */ jsx(
FilterForm,
{
client,
path: activeAction.path,
method: activeAction.method,
parameters: activeAction.operation.parameters ?? [],
lockedValues: { [idParameterName]: id },
hideLocked: true,
enableLookup: false,
submitLabel: "Execute request",
submittingLabel: "Executing…",
isSubmitting: isExecutingAction,
onSubmit: executeAction
}
),
actionError ? /* @__PURE__ */ jsx("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive", children: actionError }) : actionResult ? /* @__PURE__ */ jsx(
ExecutionResult,
{
response: actionResult,
className: "mt-0",
...commandRuntime ? { commandRuntime } : {}
}
) : null
] })
}
)
] });
}
export {
OperationEntityPage
};
//# sourceMappingURL=OperationEntityPage.js.map