import { BoxIcon } from "lucide-react"; import { useState } from "react"; import type { IntegrationPlugin } from "@executor-js/sdk/client"; import { getDomain } from "tldts"; // --------------------------------------------------------------------------- // IntegrationFavicon — renders a small favicon derived from an integration URL. // Falls back to a neutral icon if the URL is missing or the image fails to load. // --------------------------------------------------------------------------- export function integrationFaviconUrl(url: string | undefined, size: number): string | null { if (!url) return null; const domain = getDomain(url) ?? (URL.canParse(url) ? getDomain(new URL(url).hostname) : null); if (!domain) return null; return `https://www.google.com/s2/favicons?domain=${domain}&sz=${size * 2}`; } export function integrationLocalIconUrl(sourceId: string | undefined): string | null { if (sourceId !== "executor") return null; return "/favicon-32.png"; } const KIND_TO_PLUGIN_KEY: Record = { openapi: "openapi", mcp: "mcp", graphql: "graphql", googleDiscovery: "google", }; const normalizeUrl = (url: string | undefined): string | null => { if (!url) return null; try { const parsed = new URL(url); parsed.hash = ""; parsed.searchParams.sort(); return parsed.toString().replace(/\/$/, ""); } catch { return url.trim().replace(/\/$/, ""); } }; const googleApiServiceFromUrl = (url: string | undefined): string | null => { if (!url) return null; try { const parsed = new URL(url); const hostname = parsed.hostname.toLowerCase(); const segments = parsed.pathname.split("/").filter(Boolean); if ( hostname === "www.googleapis.com" && segments[0] === "discovery" && segments[2] === "apis" && segments[3] ) { return segments[3]; } if (hostname === "www.googleapis.com") return segments[0] ?? null; const suffix = ".googleapis.com"; if (hostname.endsWith(suffix)) { const service = hostname.slice(0, -suffix.length); return service.length > 0 ? service : null; } } catch { return null; } return null; }; const normalizeToken = (value: string | undefined): string => value?.toLowerCase().replace(/[^a-z0-9]+/g, "") ?? ""; const tokenVariants = (value: string | undefined): readonly string[] => { const tokens = new Set(); const normalized = normalizeToken(value); if (normalized.length > 0) tokens.add(normalized); const parts = value ?.toLowerCase() .split(/[^a-z0-9]+/g) .filter(Boolean) ?? []; const noise = new Set([ "api", "mcp", "openapi", "rest", "graphql", "web", "com", "net", "org", "dev", ]); const cleaned = parts.filter((part) => !noise.has(part)); const cleanedToken = cleaned.join(""); if (cleanedToken.length > 0) tokens.add(cleanedToken); const aliases: Record = { pscale: "planetscale", }; for (const part of cleaned) { tokens.add(part); const alias = aliases[part]; if (alias) tokens.add(alias); } return [...tokens]; }; export function integrationInferredUrl(source: { readonly id: string; readonly name?: string; }): string | null { const hostPattern = /^[a-z0-9][a-z0-9.-]*\.(?:app|co|com|dev|io|net|org)(?:\/.*)?$/i; const hostLike = [source.name, source.id.replaceAll("_", ".")] .filter((value): value is string => value != null && value.length > 0) .find((value) => hostPattern.test(value.trim())); if (hostLike) return `https://${hostLike.trim().replace(/^https?:\/\//i, "")}`; return null; } const tokenMatches = (sourceValue: string, presetValue: string): boolean => presetValue.length > 0 && sourceValue.length > 0 && (sourceValue === presetValue || sourceValue.includes(presetValue) || presetValue.includes(sourceValue)); export function integrationPresetIconUrl( source: { readonly id: string; readonly kind: string; readonly name?: string; readonly url?: string; }, integrationPlugins: readonly IntegrationPlugin[], ): string | null { const pluginKey = KIND_TO_PLUGIN_KEY[source.kind] ?? source.kind; const plugin = integrationPlugins.find((p) => p.key === pluginKey); const presets = plugin?.presets ?? []; const sourceUrl = normalizeUrl(source.url); const sourceGoogleService = googleApiServiceFromUrl(source.url); const sourceTokens = [...tokenVariants(source.id), ...tokenVariants(source.name)]; const preset = presets.find((p) => { const presetUrl = normalizeUrl(p.url); const presetGoogleService = googleApiServiceFromUrl(p.url); const presetTokens = [...tokenVariants(p.id), ...tokenVariants(p.name)]; return ( (sourceUrl !== null && presetUrl === sourceUrl) || (sourceGoogleService !== null && presetGoogleService === sourceGoogleService) || sourceTokens.some((sourceToken) => presetTokens.some((presetToken) => tokenMatches(sourceToken, presetToken)), ) ); }); return preset?.icon ?? null; } // Resolution cascade for the rendered favicon: first non-null, non-failed of an // explicit preset icon, the bundled local icon for a known source id, then a // favicon-service URL derived from the source URL. The built-in executor source // has no preset icon and no URL, so it resolves ONLY through the sourceId branch: // callers that drop sourceId fall through to the neutral BoxIcon placeholder. export function integrationFaviconSrc(args: { icon?: string | null; sourceId?: string; url?: string; size: number; failedSrcs?: readonly string[]; }): string | null { const failedSrcs = args.failedSrcs ?? []; return ( [ args.icon ?? null, integrationLocalIconUrl(args.sourceId), integrationFaviconUrl(args.url, args.size), ].find((candidate) => candidate !== null && !failedSrcs.includes(candidate)) ?? null ); } export function IntegrationFavicon({ icon, sourceId, url, size = 16, }: { icon?: string | null; sourceId?: string; url?: string; size?: number; }) { const [failedSrcs, setFailedSrcs] = useState([]); const src = integrationFaviconSrc({ icon, sourceId, url, size, failedSrcs }); if (!src) { return ( ); } return ( setFailedSrcs((current) => (current.includes(src) ? current : [...current, src])) } className="shrink-0 rounded-sm" style={{ width: size, height: size }} /> ); }