UNPKG

@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
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