From c900ab72b932dba092322695284b275f42f321d8 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Wed, 20 May 2026 18:49:08 +0545 Subject: [PATCH 1/2] fix(arthas): use plugin UI SDK Arthas UI was constructing plugin operation URLs by hand, which can break when served through the Mission Control plugin UI proxy. Vendor the built plugin UI SDK as a local dependency, use its invoke helper for operations, and signal tab readiness after render. --- arthas/ui-src/package.json | 1 + arthas/ui-src/pnpm-lock.yaml | 9 ++ arthas/ui-src/src/lib/api.ts | 17 +--- arthas/ui-src/src/main.tsx | 3 + arthas/ui-src/src/vite-env.d.ts | 1 + .../vendor/plugin-ui-sdk/lib/index.d.ts | 10 ++ .../vendor/plugin-ui-sdk/lib/index.d.ts.map | 1 + .../ui-src/vendor/plugin-ui-sdk/lib/index.js | 96 +++++++++++++++++++ .../vendor/plugin-ui-sdk/lib/index.js.map | 1 + .../ui-src/vendor/plugin-ui-sdk/package.json | 37 +++++++ 10 files changed, 162 insertions(+), 14 deletions(-) create mode 100644 arthas/ui-src/src/vite-env.d.ts create mode 100644 arthas/ui-src/vendor/plugin-ui-sdk/lib/index.d.ts create mode 100644 arthas/ui-src/vendor/plugin-ui-sdk/lib/index.d.ts.map create mode 100644 arthas/ui-src/vendor/plugin-ui-sdk/lib/index.js create mode 100644 arthas/ui-src/vendor/plugin-ui-sdk/lib/index.js.map create mode 100644 arthas/ui-src/vendor/plugin-ui-sdk/package.json diff --git a/arthas/ui-src/package.json b/arthas/ui-src/package.json index e777f2a..9f4b38e 100644 --- a/arthas/ui-src/package.json +++ b/arthas/ui-src/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@flanksource/clicky-ui": "^0.2.1", + "@flanksource/plugin-ui-sdk": "file:vendor/plugin-ui-sdk", "@tanstack/react-query": "^5.80.7", "lucide-react": "^0.545.0", "preact": "^10.24.0" diff --git a/arthas/ui-src/pnpm-lock.yaml b/arthas/ui-src/pnpm-lock.yaml index 9fbef6d..73f180d 100644 --- a/arthas/ui-src/pnpm-lock.yaml +++ b/arthas/ui-src/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@flanksource/clicky-ui': specifier: ^0.2.1 version: 0.2.1(@tanstack/react-query@5.100.9(react@19.2.5))(@vue/compiler-sfc@3.5.33)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.2.4)(typescript@5.9.3) + '@flanksource/plugin-ui-sdk': + specifier: file:vendor/plugin-ui-sdk + version: file:vendor/plugin-ui-sdk '@tanstack/react-query': specifier: ^5.80.7 version: 5.100.9(react@19.2.5) @@ -366,6 +369,10 @@ packages: shiki: optional: true + '@flanksource/plugin-ui-sdk@file:vendor/plugin-ui-sdk': + resolution: {directory: vendor/plugin-ui-sdk, type: directory} + engines: {node: '>=20'} + '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -2481,6 +2488,8 @@ snapshots: - typescript - universal-cookie + '@flanksource/plugin-ui-sdk@file:vendor/plugin-ui-sdk': {} + '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 diff --git a/arthas/ui-src/src/lib/api.ts b/arthas/ui-src/src/lib/api.ts index e6aa725..745fd25 100644 --- a/arthas/ui-src/src/lib/api.ts +++ b/arthas/ui-src/src/lib/api.ts @@ -1,11 +1,6 @@ -export const PLUGIN_NAME = "arthas"; +import { invoke } from "@flanksource/plugin-ui-sdk"; -function operationURL(op: string, configID: string): string { - const base = window.location.pathname.replace(/\/ui\/.*$/, ""); - const url = new URL(base + "/operations/" + op, window.location.origin); - if (configID) url.searchParams.set("config_id", configID); - return url.toString(); -} +export const PLUGIN_NAME = "arthas"; export class OpError extends Error { readonly status: number; @@ -25,13 +20,7 @@ export async function callOp( op: string, params: Record = {}, ): Promise { - const configID = configIDFromURL(); - const res = await fetch(operationURL(op, configID), { - method: "POST", - headers: { "Content-Type": "application/json" }, - credentials: "same-origin", - body: JSON.stringify(params), - }); + const res = await invoke(op, params); if (!res.ok) { const text = await res.text(); let body: unknown = text; diff --git a/arthas/ui-src/src/main.tsx b/arthas/ui-src/src/main.tsx index 44fd3fc..1dcb1ce 100644 --- a/arthas/ui-src/src/main.tsx +++ b/arthas/ui-src/src/main.tsx @@ -1,6 +1,7 @@ import { render } from "preact"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ThemeProvider, DensityProvider } from "@flanksource/clicky-ui"; +import { ready } from "@flanksource/plugin-ui-sdk"; import { App } from "./App"; import { logBanner } from "./version"; import "./styles.css"; @@ -24,3 +25,5 @@ render( , root, ); + +ready(); diff --git a/arthas/ui-src/src/vite-env.d.ts b/arthas/ui-src/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/arthas/ui-src/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/arthas/ui-src/vendor/plugin-ui-sdk/lib/index.d.ts b/arthas/ui-src/vendor/plugin-ui-sdk/lib/index.d.ts new file mode 100644 index 0000000..de9388b --- /dev/null +++ b/arthas/ui-src/vendor/plugin-ui-sdk/lib/index.d.ts @@ -0,0 +1,10 @@ +export type QueryValue = string | number | boolean | null | undefined; +export type QueryParams = Record; +export type InvokeOptions = Omit & { + query?: QueryParams; +}; +export type StreamOptions = EventSourceInit; +export declare function invoke(operation: string, body?: unknown, options?: InvokeOptions): Promise; +export declare function stream(operation: string, query?: QueryParams, options?: StreamOptions): EventSource; +export declare function ready(): void; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/arthas/ui-src/vendor/plugin-ui-sdk/lib/index.d.ts.map b/arthas/ui-src/vendor/plugin-ui-sdk/lib/index.d.ts.map new file mode 100644 index 0000000..fb4c7fa --- /dev/null +++ b/arthas/ui-src/vendor/plugin-ui-sdk/lib/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC;AAEtE,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,UAAU,EAAE,CAAC,CAAC;AAE7E,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,GAAG;IACtD,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,eAAe,CAAC;AAS5C,wBAAsB,MAAM,CAC1B,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,OAAO,EACd,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,QAAQ,CAAC,CAmBnB;AAED,wBAAgB,MAAM,CACpB,SAAS,EAAE,MAAM,EACjB,KAAK,CAAC,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,aAAa,GACtB,WAAW,CAKb;AAED,wBAAgB,KAAK,IAAI,IAAI,CAE5B"} \ No newline at end of file diff --git a/arthas/ui-src/vendor/plugin-ui-sdk/lib/index.js b/arthas/ui-src/vendor/plugin-ui-sdk/lib/index.js new file mode 100644 index 0000000..944c9c9 --- /dev/null +++ b/arthas/ui-src/vendor/plugin-ui-sdk/lib/index.js @@ -0,0 +1,96 @@ +const readyMessage = { type: "mc.tab.ready" }; +export async function invoke(operation, body, options = {}) { + const { query, ...requestInit } = options; + const hasBody = body !== undefined; + const method = (requestInit.method ?? (hasBody ? "POST" : "GET")).toUpperCase(); + if (hasBody && (method === "GET" || method === "HEAD")) { + throw new Error(`plugin-ui-sdk: ${method} requests cannot include a body; use query params instead`); + } + const headers = new Headers(requestInit.headers); + const encodedBody = hasBody ? encodeBody(body, headers) : undefined; + return fetch(operationURL(operation, query), { + ...requestInit, + method, + credentials: requestInit.credentials ?? "same-origin", + headers, + body: encodedBody, + }); +} +export function stream(operation, query, options) { + if (typeof EventSource === "undefined") { + throw new Error("plugin-ui-sdk: EventSource is not available in this environment"); + } + return new EventSource(operationURL(operation, query), options); +} +export function ready() { + currentWindow().parent.postMessage(readyMessage, "*"); +} +function operationURL(operation, query) { + validateOperation(operation); + const win = currentWindow(); + const ctx = runtimeContext(win); + const url = new URL(`/api/plugins/${ctx.pluginRefSegment}/proxy/${encodeURIComponent(operation)}`, win.location.href); + url.searchParams.set("config_id", ctx.configId); + appendQuery(url.searchParams, query); + return `${url.pathname}${url.search}`; +} +function runtimeContext(win) { + const match = win.location.pathname.match(/^\/api\/plugins\/([^/]+)\/ui(?:\/|$)/); + if (!match) { + throw new Error("plugin-ui-sdk: expected to run under /api/plugins//ui"); + } + const configId = new URLSearchParams(win.location.search).get("config_id"); + if (!configId) { + throw new Error("plugin-ui-sdk: missing config_id in plugin UI URL"); + } + return { + pluginRefSegment: match[1], + configId, + }; +} +function validateOperation(operation) { + if (!operation.trim()) { + throw new Error("plugin-ui-sdk: operation is required"); + } + if (operation.includes("/")) { + throw new Error("plugin-ui-sdk: operation must be a single path segment"); + } +} +function appendQuery(searchParams, query) { + if (!query) + return; + for (const [key, value] of Object.entries(query)) { + if (key === "config_id") + continue; + const values = Array.isArray(value) ? value : [value]; + for (const item of values) { + if (item === null || item === undefined) + continue; + searchParams.append(key, String(item)); + } + } +} +function encodeBody(body, headers) { + if (isBodyInit(body)) + return body; + if (!headers.has("content-type")) { + headers.set("content-type", "application/json"); + } + return JSON.stringify(body); +} +function isBodyInit(value) { + return (typeof value === "string" || + (typeof Blob !== "undefined" && value instanceof Blob) || + (typeof FormData !== "undefined" && value instanceof FormData) || + (typeof URLSearchParams !== "undefined" && value instanceof URLSearchParams) || + (typeof ArrayBuffer !== "undefined" && value instanceof ArrayBuffer) || + (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(value)) || + (typeof ReadableStream !== "undefined" && value instanceof ReadableStream)); +} +function currentWindow() { + if (typeof window === "undefined") { + throw new Error("plugin-ui-sdk: window is not available in this environment"); + } + return window; +} +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/arthas/ui-src/vendor/plugin-ui-sdk/lib/index.js.map b/arthas/ui-src/vendor/plugin-ui-sdk/lib/index.js.map new file mode 100644 index 0000000..02027eb --- /dev/null +++ b/arthas/ui-src/vendor/plugin-ui-sdk/lib/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/arthas/ui-src/vendor/plugin-ui-sdk/package.json b/arthas/ui-src/vendor/plugin-ui-sdk/package.json new file mode 100644 index 0000000..df0ad1c --- /dev/null +++ b/arthas/ui-src/vendor/plugin-ui-sdk/package.json @@ -0,0 +1,37 @@ +{ + "name": "@flanksource/plugin-ui-sdk", + "version": "0.1.2", + "description": "Browser UI SDK for Mission Control plugin tabs", + "type": "module", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/flanksource/plugin-ui-sdk.git" + }, + "files": [ + "lib", + "README.md", + "LICENSE" + ], + "exports": { + ".": { + "types": "./lib/index.d.ts", + "import": "./lib/index.js" + } + }, + "types": "./lib/index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.json", + "check": "tsc -p tsconfig.json --noEmit", + "test": "vitest run", + "clean": "trash lib || true" + }, + "devDependencies": { + "typescript": "^5.8.3", + "vitest": "^3.1.4" + }, + "engines": { + "node": ">=20" + }, + "packageManager": "pnpm@10.11.0" +} From 7f2a2030eb8daabd2449646bab7a13067501f125 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Wed, 20 May 2026 18:52:11 +0545 Subject: [PATCH 2/2] fix(inspektor-gadget): use plugin UI SDK Inspektor Gadget UI was constructing plugin operation URLs by hand, which can break when served through the Mission Control plugin UI proxy. Vendor the built plugin UI SDK as a local dependency, use its invoke helper for operations, and signal tab readiness through the SDK. --- inspektor-gadget/ui-src/package.json | 1 + inspektor-gadget/ui-src/pnpm-lock.yaml | 9 ++ inspektor-gadget/ui-src/src/main.tsx | 15 +-- .../vendor/plugin-ui-sdk/lib/index.d.ts | 10 ++ .../vendor/plugin-ui-sdk/lib/index.d.ts.map | 1 + .../ui-src/vendor/plugin-ui-sdk/lib/index.js | 96 +++++++++++++++++++ .../vendor/plugin-ui-sdk/lib/index.js.map | 1 + .../ui-src/vendor/plugin-ui-sdk/package.json | 37 +++++++ 8 files changed, 158 insertions(+), 12 deletions(-) create mode 100644 inspektor-gadget/ui-src/vendor/plugin-ui-sdk/lib/index.d.ts create mode 100644 inspektor-gadget/ui-src/vendor/plugin-ui-sdk/lib/index.d.ts.map create mode 100644 inspektor-gadget/ui-src/vendor/plugin-ui-sdk/lib/index.js create mode 100644 inspektor-gadget/ui-src/vendor/plugin-ui-sdk/lib/index.js.map create mode 100644 inspektor-gadget/ui-src/vendor/plugin-ui-sdk/package.json diff --git a/inspektor-gadget/ui-src/package.json b/inspektor-gadget/ui-src/package.json index 6e809cf..71f4500 100644 --- a/inspektor-gadget/ui-src/package.json +++ b/inspektor-gadget/ui-src/package.json @@ -8,6 +8,7 @@ }, "dependencies": { "@flanksource/clicky-ui": "^0.2.1", + "@flanksource/plugin-ui-sdk": "file:vendor/plugin-ui-sdk", "@flanksource/icons": "^1.0.56", "@tanstack/react-query": "^5.90.11", "@vitejs/plugin-react": "^5.1.0", diff --git a/inspektor-gadget/ui-src/pnpm-lock.yaml b/inspektor-gadget/ui-src/pnpm-lock.yaml index e7d0911..b0e5f3c 100644 --- a/inspektor-gadget/ui-src/pnpm-lock.yaml +++ b/inspektor-gadget/ui-src/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@flanksource/icons': specifier: ^1.0.56 version: 1.0.58(react@19.2.5) + '@flanksource/plugin-ui-sdk': + specifier: file:vendor/plugin-ui-sdk + version: file:vendor/plugin-ui-sdk '@tanstack/react-query': specifier: ^5.90.11 version: 5.100.9(react@19.2.5) @@ -373,6 +376,10 @@ packages: peerDependencies: react: '*' + '@flanksource/plugin-ui-sdk@file:vendor/plugin-ui-sdk': + resolution: {directory: vendor/plugin-ui-sdk, type: directory} + engines: {node: '>=20'} + '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -2383,6 +2390,8 @@ snapshots: dependencies: react: 19.2.5 + '@flanksource/plugin-ui-sdk@file:vendor/plugin-ui-sdk': {} + '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 diff --git a/inspektor-gadget/ui-src/src/main.tsx b/inspektor-gadget/ui-src/src/main.tsx index f290165..e9b8feb 100644 --- a/inspektor-gadget/ui-src/src/main.tsx +++ b/inspektor-gadget/ui-src/src/main.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { createRoot } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { invoke as sdkInvoke, ready } from "@flanksource/plugin-ui-sdk"; import { Button, DataTable, @@ -169,18 +170,8 @@ function configId() { return new URLSearchParams(window.location.search).get("config_id") || ""; } -function hostOperationUrl(op: string) { - const params = new URLSearchParams(window.location.search); - const id = params.get("config_id") || ""; - return `/api/plugins/inspektor-gadget/operations/${op}${id ? `?config_id=${encodeURIComponent(id)}` : ""}`; -} - async function invoke(op: string, body: unknown = {}): Promise { - const res = await fetch(hostOperationUrl(op), { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body) - }); + const res = await sdkInvoke(op, body); if (!res.ok) { throw new Error(await res.text()); } @@ -229,7 +220,7 @@ function App() { useEffect(() => { refresh().catch((err) => setError(String(err))); - window.parent?.postMessage({ type: "mc.tab.ready" }, "*"); + ready(); }, []); useEffect(() => { diff --git a/inspektor-gadget/ui-src/vendor/plugin-ui-sdk/lib/index.d.ts b/inspektor-gadget/ui-src/vendor/plugin-ui-sdk/lib/index.d.ts new file mode 100644 index 0000000..de9388b --- /dev/null +++ b/inspektor-gadget/ui-src/vendor/plugin-ui-sdk/lib/index.d.ts @@ -0,0 +1,10 @@ +export type QueryValue = string | number | boolean | null | undefined; +export type QueryParams = Record; +export type InvokeOptions = Omit & { + query?: QueryParams; +}; +export type StreamOptions = EventSourceInit; +export declare function invoke(operation: string, body?: unknown, options?: InvokeOptions): Promise; +export declare function stream(operation: string, query?: QueryParams, options?: StreamOptions): EventSource; +export declare function ready(): void; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/inspektor-gadget/ui-src/vendor/plugin-ui-sdk/lib/index.d.ts.map b/inspektor-gadget/ui-src/vendor/plugin-ui-sdk/lib/index.d.ts.map new file mode 100644 index 0000000..fb4c7fa --- /dev/null +++ b/inspektor-gadget/ui-src/vendor/plugin-ui-sdk/lib/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC;AAEtE,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,UAAU,EAAE,CAAC,CAAC;AAE7E,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,GAAG;IACtD,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,eAAe,CAAC;AAS5C,wBAAsB,MAAM,CAC1B,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,OAAO,EACd,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,QAAQ,CAAC,CAmBnB;AAED,wBAAgB,MAAM,CACpB,SAAS,EAAE,MAAM,EACjB,KAAK,CAAC,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,aAAa,GACtB,WAAW,CAKb;AAED,wBAAgB,KAAK,IAAI,IAAI,CAE5B"} \ No newline at end of file diff --git a/inspektor-gadget/ui-src/vendor/plugin-ui-sdk/lib/index.js b/inspektor-gadget/ui-src/vendor/plugin-ui-sdk/lib/index.js new file mode 100644 index 0000000..944c9c9 --- /dev/null +++ b/inspektor-gadget/ui-src/vendor/plugin-ui-sdk/lib/index.js @@ -0,0 +1,96 @@ +const readyMessage = { type: "mc.tab.ready" }; +export async function invoke(operation, body, options = {}) { + const { query, ...requestInit } = options; + const hasBody = body !== undefined; + const method = (requestInit.method ?? (hasBody ? "POST" : "GET")).toUpperCase(); + if (hasBody && (method === "GET" || method === "HEAD")) { + throw new Error(`plugin-ui-sdk: ${method} requests cannot include a body; use query params instead`); + } + const headers = new Headers(requestInit.headers); + const encodedBody = hasBody ? encodeBody(body, headers) : undefined; + return fetch(operationURL(operation, query), { + ...requestInit, + method, + credentials: requestInit.credentials ?? "same-origin", + headers, + body: encodedBody, + }); +} +export function stream(operation, query, options) { + if (typeof EventSource === "undefined") { + throw new Error("plugin-ui-sdk: EventSource is not available in this environment"); + } + return new EventSource(operationURL(operation, query), options); +} +export function ready() { + currentWindow().parent.postMessage(readyMessage, "*"); +} +function operationURL(operation, query) { + validateOperation(operation); + const win = currentWindow(); + const ctx = runtimeContext(win); + const url = new URL(`/api/plugins/${ctx.pluginRefSegment}/proxy/${encodeURIComponent(operation)}`, win.location.href); + url.searchParams.set("config_id", ctx.configId); + appendQuery(url.searchParams, query); + return `${url.pathname}${url.search}`; +} +function runtimeContext(win) { + const match = win.location.pathname.match(/^\/api\/plugins\/([^/]+)\/ui(?:\/|$)/); + if (!match) { + throw new Error("plugin-ui-sdk: expected to run under /api/plugins//ui"); + } + const configId = new URLSearchParams(win.location.search).get("config_id"); + if (!configId) { + throw new Error("plugin-ui-sdk: missing config_id in plugin UI URL"); + } + return { + pluginRefSegment: match[1], + configId, + }; +} +function validateOperation(operation) { + if (!operation.trim()) { + throw new Error("plugin-ui-sdk: operation is required"); + } + if (operation.includes("/")) { + throw new Error("plugin-ui-sdk: operation must be a single path segment"); + } +} +function appendQuery(searchParams, query) { + if (!query) + return; + for (const [key, value] of Object.entries(query)) { + if (key === "config_id") + continue; + const values = Array.isArray(value) ? value : [value]; + for (const item of values) { + if (item === null || item === undefined) + continue; + searchParams.append(key, String(item)); + } + } +} +function encodeBody(body, headers) { + if (isBodyInit(body)) + return body; + if (!headers.has("content-type")) { + headers.set("content-type", "application/json"); + } + return JSON.stringify(body); +} +function isBodyInit(value) { + return (typeof value === "string" || + (typeof Blob !== "undefined" && value instanceof Blob) || + (typeof FormData !== "undefined" && value instanceof FormData) || + (typeof URLSearchParams !== "undefined" && value instanceof URLSearchParams) || + (typeof ArrayBuffer !== "undefined" && value instanceof ArrayBuffer) || + (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(value)) || + (typeof ReadableStream !== "undefined" && value instanceof ReadableStream)); +} +function currentWindow() { + if (typeof window === "undefined") { + throw new Error("plugin-ui-sdk: window is not available in this environment"); + } + return window; +} +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/inspektor-gadget/ui-src/vendor/plugin-ui-sdk/lib/index.js.map b/inspektor-gadget/ui-src/vendor/plugin-ui-sdk/lib/index.js.map new file mode 100644 index 0000000..02027eb --- /dev/null +++ b/inspektor-gadget/ui-src/vendor/plugin-ui-sdk/lib/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/inspektor-gadget/ui-src/vendor/plugin-ui-sdk/package.json b/inspektor-gadget/ui-src/vendor/plugin-ui-sdk/package.json new file mode 100644 index 0000000..df0ad1c --- /dev/null +++ b/inspektor-gadget/ui-src/vendor/plugin-ui-sdk/package.json @@ -0,0 +1,37 @@ +{ + "name": "@flanksource/plugin-ui-sdk", + "version": "0.1.2", + "description": "Browser UI SDK for Mission Control plugin tabs", + "type": "module", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/flanksource/plugin-ui-sdk.git" + }, + "files": [ + "lib", + "README.md", + "LICENSE" + ], + "exports": { + ".": { + "types": "./lib/index.d.ts", + "import": "./lib/index.js" + } + }, + "types": "./lib/index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.json", + "check": "tsc -p tsconfig.json --noEmit", + "test": "vitest run", + "clean": "trash lib || true" + }, + "devDependencies": { + "typescript": "^5.8.3", + "vitest": "^3.1.4" + }, + "engines": { + "node": ">=20" + }, + "packageManager": "pnpm@10.11.0" +}