@flanksource/clicky-ui
Version:
Flanksource Clicky UI — React component library built on shadcn/ui with light/dark and density theming.
283 lines (282 loc) • 11 kB
JavaScript
import { jsxs, jsx } from "react/jsx-runtime";
import { useMemo, useEffect } from "react";
import { ThemeSwitcher } from "../components/theme-switcher.js";
import { ApiExplorer } from "./ApiExplorer.js";
import { getClickySurfaces, makeSurfaceDefinition } from "./clickyMetadata.js";
import { OperationCatalog } from "./OperationCatalog.js";
import { OperationCommandPage } from "./OperationCommandPage.js";
import { OperationEntityPage } from "./OperationEntityPage.js";
import { useOperations } from "./useOperations.js";
function EntityExplorerApp({
client,
pathname,
renderLink,
basePath = "",
showApiExplorer = true,
apiExplorerMode = "scalar",
apiExplorerUrl,
apiExplorerConfiguration
}) {
const { spec } = useOperations(client);
const surfaces = useMemo(() => getClickySurfaces(spec), [spec]);
const defaultSurface = useMemo(
() => surfaces.find((surface) => !surface.admin) ?? surfaces[0],
[surfaces]
);
const relativePath = useMemo(() => stripBasePath(pathname, basePath), [basePath, pathname]);
const route = useMemo(() => parseExplorerRoute(relativePath), [relativePath]);
const resolvedRoute = useMemo(() => resolveRouteSurface(route, surfaces), [route, surfaces]);
const commandRuntime = useMemo(
() => ({
client,
hrefForCommand: (resolved) => buildCommandHref(basePath, resolved),
onNavigate: (resolved) => {
const href = buildCommandHref(basePath, resolved);
if (!href || typeof window === "undefined") return;
window.history.pushState(window.history.state, "", href);
window.dispatchEvent(new PopStateEvent("popstate"));
}
}),
[basePath, client]
);
const { initialValues: commandInitialValues, autoRun: commandAutoRun } = useMemo(
() => resolvedRoute.kind === "command" ? readCommandQueryParams() : { initialValues: {}, autoRun: false },
[resolvedRoute.kind, pathname]
);
const explorerDefinition = useMemo(
() => ({
key: "explorer",
title: "API Explorer",
description: "Every operation exposed by the current OpenAPI spec."
}),
[]
);
useEffect(() => {
if (typeof window === "undefined" || defaultSurface == null || relativePath !== "/" || window.location.pathname !== withBasePath(basePath, "/")) {
return;
}
const target = withBasePath(basePath, `/${defaultSurface.key}`);
window.history.replaceState(window.history.state, "", target);
window.dispatchEvent(new PopStateEvent("popstate"));
}, [basePath, defaultSurface, relativePath]);
return /* @__PURE__ */ jsxs("div", { className: "flex h-full", children: [
/* @__PURE__ */ jsxs("aside", { className: "flex w-64 shrink-0 flex-col border-r border-border bg-muted/30 p-4", children: [
/* @__PURE__ */ jsxs("div", { className: "mb-6", children: [
/* @__PURE__ */ jsx("div", { className: "text-sm font-semibold", children: (spec == null ? void 0 : spec.info.title) ?? "Clicky Explorer" }),
/* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: "Metadata-driven entity explorer" })
] }),
/* @__PURE__ */ jsxs("nav", { className: "flex flex-col gap-1", children: [
surfaces.map(
(surface) => renderLink({
key: surface.key,
to: withBasePath(basePath, `/${surface.key}`),
className: navLinkClassName(isSurfaceRouteActive(resolvedRoute, surface.key)),
children: surface.title
})
),
showApiExplorer && renderLink({
key: explorerDefinition.key,
to: withBasePath(basePath, "/explorer"),
className: navLinkClassName(
resolvedRoute.kind === "explorer" || resolvedRoute.kind === "command"
),
children: explorerDefinition.title
})
] }),
/* @__PURE__ */ jsx("div", { className: "mt-auto pt-4", children: /* @__PURE__ */ jsx(ThemeSwitcher, {}) })
] }),
/* @__PURE__ */ jsx("main", { className: "flex-1 overflow-auto p-6", children: resolvedRoute.kind === "surface" ? resolvedRoute.surface ? /* @__PURE__ */ jsx(
OperationCatalog,
{
definition: makeSurfaceDefinition(resolvedRoute.surface),
entities: [resolvedRoute.surface.entity],
client,
renderLink,
surfaceKey: resolvedRoute.surface.key,
commandRuntime
}
) : /* @__PURE__ */ jsx(UnknownSurface, { surfaceKey: resolvedRoute.surfaceKey }) : resolvedRoute.kind === "entity" ? resolvedRoute.surface ? /* @__PURE__ */ jsx(
OperationEntityPage,
{
definition: makeSurfaceDefinition(resolvedRoute.surface),
entities: [resolvedRoute.surface.entity],
client,
renderLink,
surfaceKey: resolvedRoute.surface.key,
backHref: withBasePath(basePath, `/${resolvedRoute.surface.key}`),
backLabel: `Back to ${resolvedRoute.surface.title}`,
commandRuntime,
...resolvedRoute.id ? { id: resolvedRoute.id } : {}
}
) : /* @__PURE__ */ jsx(UnknownSurface, { surfaceKey: resolvedRoute.surfaceKey }) : resolvedRoute.kind === "command" ? /* @__PURE__ */ jsx(
OperationCommandPage,
{
client,
backHref: withBasePath(basePath, "/explorer"),
backLabel: "Back to API Explorer",
renderLink,
commandRuntime,
initialValues: commandInitialValues,
autoRun: commandAutoRun,
...resolvedRoute.operationId ? { operationId: resolvedRoute.operationId } : {}
}
) : resolvedRoute.kind === "explorer" ? apiExplorerMode === "scalar" ? /* @__PURE__ */ jsx(
ApiExplorer,
{
...apiExplorerUrl ? { openApiUrl: apiExplorerUrl } : {},
...apiExplorerConfiguration ? { configuration: apiExplorerConfiguration } : {}
}
) : /* @__PURE__ */ jsx(
OperationCatalog,
{
definition: explorerDefinition,
entities: [],
allOperations: true,
client,
renderLink,
commandRuntime
}
) : defaultSurface == null ? /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "Loading surfaces…" }) : /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "Redirecting…" }) })
] });
}
function parseExplorerRoute(pathname) {
const normalized = trimTrailingSlash(pathname) || "/";
if (normalized === "/") {
return { kind: "root" };
}
const segments = normalized.replace(/^\/+/, "").split("/");
if (segments[0] === "explorer") {
return { kind: "explorer" };
}
if (segments[0] === "commands") {
return segments[1] ? { kind: "command", operationId: segments[1] } : { kind: "command" };
}
const surfaceKey = segments[0] ?? "";
if (segments[1]) {
return {
kind: "entity",
surfaceKey,
id: decodeURIComponent(segments[1])
};
}
return { kind: "surface", surfaceKey };
}
function resolveRouteSurface(route, surfaces) {
if (route.kind !== "surface" && route.kind !== "entity") {
return route;
}
const surface = surfaces.find((item) => item.key === route.surfaceKey);
return surface ? { ...route, surface } : route;
}
function isSurfaceRouteActive(route, surfaceKey) {
return route.kind === "surface" && route.surfaceKey === surfaceKey || route.kind === "entity" && route.surfaceKey === surfaceKey;
}
function navLinkClassName(active) {
return [
"rounded-md px-2 py-1.5 text-sm transition-colors",
active ? "bg-accent text-accent-foreground" : "text-foreground hover:bg-accent"
].join(" ");
}
function stripBasePath(pathname, basePath) {
const base = trimTrailingSlash(basePath);
if (!base || base === "/") {
return trimTrailingSlash(pathname) || "/";
}
if (pathname === base) {
return "/";
}
if (pathname.startsWith(`${base}/`)) {
return trimTrailingSlash(pathname.slice(base.length)) || "/";
}
return trimTrailingSlash(pathname) || "/";
}
function withBasePath(basePath, pathname) {
const base = trimTrailingSlash(basePath);
if (!base || base === "/") {
return pathname;
}
return `${base}${pathname}`;
}
function trimTrailingSlash(value) {
if (!value) return "";
return value.length > 1 ? value.replace(/\/+$/, "") : value;
}
function UnknownSurface({ surfaceKey }) {
return /* @__PURE__ */ jsxs("div", { className: "p-6 text-sm text-muted-foreground", children: [
"Unknown surface: ",
/* @__PURE__ */ jsx("code", { children: surfaceKey })
] });
}
const AUTORUN_QUERY_PARAM = "__autoRun";
const ARG_QUERY_PREFIX = "__arg";
function buildCommandHref(basePath, resolved) {
var _a, _b;
const commandId = ((_a = resolved.operation) == null ? void 0 : _a.operation.operationId) ?? resolved.request.command;
if (!commandId) return void 0;
const meta = (_b = resolved.operation) == null ? void 0 : _b.operation["x-clicky"];
const args = resolved.request.args ?? [];
const flags = resolved.request.flags ?? {};
if ((meta == null ? void 0 : meta.verb) === "get" && (meta == null ? void 0 : meta.scope) === "entity" && meta.surface && args[0]) {
const search2 = new URLSearchParams();
for (const [key, value] of Object.entries(flags)) {
if (value !== void 0 && value !== "") {
search2.set(key, value);
}
}
const query2 = search2.toString();
const suffix2 = query2 ? `?${query2}` : "";
return withBasePath(
basePath,
`/${encodeURIComponent(meta.surface)}/${encodeURIComponent(args[0])}${suffix2}`
);
}
const search = new URLSearchParams();
args.forEach((value, index) => {
if (value !== void 0 && value !== "") {
search.set(`${ARG_QUERY_PREFIX}${index}`, value);
}
});
for (const [key, value] of Object.entries(flags)) {
if (value !== void 0 && value !== "") {
search.set(key, value);
}
}
if (resolved.request.autoRun) {
search.set(AUTORUN_QUERY_PARAM, "1");
}
const query = search.toString();
const suffix = query ? `?${query}` : "";
return withBasePath(basePath, `/commands/${encodeURIComponent(commandId)}${suffix}`);
}
function readCommandQueryParams() {
if (typeof window === "undefined") {
return { initialValues: {}, autoRun: false };
}
const search = new URLSearchParams(window.location.search);
const initialValues = {};
let autoRun = false;
const positional = [];
for (const [key, value] of search.entries()) {
if (key === AUTORUN_QUERY_PARAM) {
autoRun = value === "1" || value === "true";
continue;
}
if (key.startsWith(ARG_QUERY_PREFIX)) {
const index = Number.parseInt(key.slice(ARG_QUERY_PREFIX.length), 10);
if (Number.isFinite(index) && index >= 0) {
positional[index] = value;
}
continue;
}
initialValues[key] = value;
}
const packedArgs = positional.filter((value) => value !== void 0).join(" ");
if (packedArgs !== "") {
initialValues.args = packedArgs;
}
return { initialValues, autoRun };
}
export {
EntityExplorerApp
};
//# sourceMappingURL=EntityExplorerApp.js.map