History

refactor(plugins/react): deduplicate favicon URL resolution

Replace inline getPreviewFaviconUrl implementations in GraphQL, MCP, and OpenAPI form components with getFaviconUrlForRemoteUrl from shared module. Also applies useEffectEvent fix (openapi) and type annotation improvement (mcp).

Saatvik Arya committed 7 months ago

Commit 27ebe861 parent e1281843 files · +12 −178Browse files

Unified · Split

3 files
  1. plugins/graphql/react/components.tsx +2 −58
  2. plugins/mcp/react/components.tsx +3 −59
  3. plugins/openapi/react/components.tsx +7 −61
plugins/graphql/react/components.tsx+2−58
@@ -35,6 +35,7 @@ import { · 34 unmodified lines
35 graphqlHttpApiExtension,35 graphqlHttpApiExtension,
36} from "@executor/plugin-graphql-http";36} from "@executor/plugin-graphql-http";
37import {37import {
38 getFaviconUrlForRemoteUrl,
38 type GraphqlConnectInput,39 type GraphqlConnectInput,
39 type GraphqlConnectionAuth,40 type GraphqlConnectionAuth,
40} from "@executor/plugin-graphql-shared";41} from "@executor/plugin-graphql-shared";
@@ -62,63 +63,6 @@ const defaultGraphqlInput = (): GraphqlConnectInput => ({ · 21 unmodified lines
62const DEFAULT_BEARER_HEADER_NAME = "Authorization";63const DEFAULT_BEARER_HEADER_NAME = "Authorization";
63const DEFAULT_BEARER_PREFIX = "Bearer ";64const DEFAULT_BEARER_PREFIX = "Bearer ";
64 65
65const COMMON_COMPOUND_SUFFIXES = new Set([
66 "ac.uk",
67 "co.in",
68 "co.jp",
69 "co.nz",
70 "co.uk",
71 "com.au",
72 "com.br",
73 "com.mx",
74 "net.au",
75 "org.au",
76 "org.uk",
77]);
78
79const isIpv4Address = (value: string): boolean =>
80 /^\d{1,3}(?:\.\d{1,3}){3}$/.test(value);
81
82const toRegistrableDomain = (hostname: string): string | null => {
83 const normalized = hostname.trim().toLowerCase().replace(/^\.+|\.+$/g, "");
84 if (!normalized) {
85 return null;
86 }
87
88 if (normalized === "localhost" || isIpv4Address(normalized)) {
89 return normalized;
90 }
91
92 const parts = normalized.split(".").filter((part) => part.length > 0);
93 if (parts.length < 2) {
94 return null;
95 }
96
97 const suffix = parts.slice(-2).join(".");
98 if (parts.length >= 3 && COMMON_COMPOUND_SUFFIXES.has(suffix)) {
99 return parts.slice(-3).join(".");
100 }
101
102 return parts.slice(-2).join(".");
103};
104
105const getPreviewFaviconUrl = (value: string): string | null => {
106 const trimmed = value.trim();
107 if (!trimmed) {
108 return null;
109 }
110
111 try {
112 const url = new URL(trimmed);
113 const domain = toRegistrableDomain(url.hostname);
114 return domain
115 ? `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=32`
116 : null;
117 } catch {
118 return null;
119 }
120};
121
122const presetString = (66const presetString = (
123 search: Record<string, unknown>,67 search: Record<string, unknown>,
124 key: string,68 key: string,
@@ -270,7 +214,7 @@ function GraphqlSourceForm(props: { · 145 unmodified lines
270 bearerPrefixValue(props.initialValue.auth),214 bearerPrefixValue(props.initialValue.auth),
271 );215 );
272 const [error, setError] = useState<string | null>(null);216 const [error, setError] = useState<string | null>(null);
273 const resolvedIconUrl = iconUrl.trim() || getPreviewFaviconUrl(endpoint);217 const resolvedIconUrl = iconUrl.trim() || getFaviconUrlForRemoteUrl(endpoint);
274 218
275 return (219 return (
276 <div className="space-y-6 rounded-lg border border-border bg-card p-6 text-sm ring-1 ring-foreground/[0.04]">220 <div className="space-y-6 rounded-lg border border-border bg-card p-6 text-sm ring-1 ring-foreground/[0.04]">
plugins/mcp/react/components.tsx+3−59
@@ -30,6 +30,7 @@ import { · 29 unmodified lines
30 mcpHttpApiExtension,30 mcpHttpApiExtension,
31} from "@executor/plugin-mcp-http";31} from "@executor/plugin-mcp-http";
32import {32import {
33 getFaviconUrlForRemoteUrl,
33 type McpConnectInput,34 type McpConnectInput,
34 type McpConnectionAuth,35 type McpConnectionAuth,
35 type McpOAuthPopupResult,36 type McpOAuthPopupResult,
@@ -110,63 +111,6 @@ const presetJsonStringArray = ( · 74 unmodified lines
110 return value ? parseJsonStringArray(key, value) : null;111 return value ? parseJsonStringArray(key, value) : null;
111};112};
112 113
113const COMMON_COMPOUND_SUFFIXES = new Set([
114 "ac.uk",
115 "co.in",
116 "co.jp",
117 "co.nz",
118 "co.uk",
119 "com.au",
120 "com.br",
121 "com.mx",
122 "net.au",
123 "org.au",
124 "org.uk",
125]);
126
127const isIpv4Address = (value: string): boolean =>
128 /^\d{1,3}(?:\.\d{1,3}){3}$/.test(value);
129
130const toRegistrableDomain = (hostname: string): string | null => {
131 const normalized = hostname.trim().toLowerCase().replace(/^\.+|\.+$/g, "");
132 if (!normalized) {
133 return null;
134 }
135
136 if (normalized === "localhost" || isIpv4Address(normalized)) {
137 return normalized;
138 }
139
140 const parts = normalized.split(".").filter((part) => part.length > 0);
141 if (parts.length < 2) {
142 return null;
143 }
144
145 const suffix = parts.slice(-2).join(".");
146 if (parts.length >= 3 && COMMON_COMPOUND_SUFFIXES.has(suffix)) {
147 return parts.slice(-3).join(".");
148 }
149
150 return parts.slice(-2).join(".");
151};
152
153const getPreviewFaviconUrl = (value: string): string | null => {
154 const trimmed = value.trim();
155 if (!trimmed) {
156 return null;
157 }
158
159 try {
160 const url = new URL(trimmed);
161 const domain = toRegistrableDomain(url.hostname);
162 return domain
163 ? `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=32`
164 : null;
165 } catch {
166 return null;
167 }
168};
169
170const mcpInputFromSearch = (114const mcpInputFromSearch = (
171 search: Record<string, unknown>,115 search: Record<string, unknown>,
172): McpConnectInput => {116): McpConnectInput => {
@@ -347,7 +291,7 @@ function McpSourceForm(props: { · 174 unmodified lines
347 291
348 const isStdio = transportFields.transport === "stdio";292 const isStdio = transportFields.transport === "stdio";
349 const resolvedIconUrl = iconUrl.trim()293 const resolvedIconUrl = iconUrl.trim()
350 || (!isStdio ? getPreviewFaviconUrl(endpoint) : null);294 || (!isStdio ? getFaviconUrlForRemoteUrl(endpoint) : null);
351 295
352 const runOauth = async () => {296 const runOauth = async () => {
353 if (installation.status !== "ready") {297 if (installation.status !== "ready") {
@@ -371,7 +315,7 @@ function McpSourceForm(props: { · 17 unmodified lines
371 ).toString(),315 ).toString(),
372 };316 };
373 317
374 let started;318 let started: Awaited<ReturnType<typeof startOAuth>>;
375 try {319 try {
376 started = await startOAuth({320 started = await startOAuth({
377 path: {321 path: {
plugins/openapi/react/components.tsx+7−61
@@ -30,6 +30,9 @@ import { · 29 unmodified lines
30import {30import {
31 openApiHttpApiExtension,31 openApiHttpApiExtension,
32} from "@executor/plugin-openapi-http";32} from "@executor/plugin-openapi-http";
33import {
34 getFaviconUrlForRemoteUrl,
35} from "@executor/plugin-openapi-shared";
33import type {36import type {
34 OpenApiConnectInput,37 OpenApiConnectInput,
35 OpenApiPreviewRequest,38 OpenApiPreviewRequest,
@@ -37,7 +40,7 @@ import type { · 1 unmodified line
37 OpenApiPreviewResponse,40 OpenApiPreviewResponse,
38 OpenApiSourceConfigPayload,41 OpenApiSourceConfigPayload,
39} from "@executor/plugin-openapi-shared";42} from "@executor/plugin-openapi-shared";
40import { startTransition, useEffect, useState, type ReactNode } from "react";43import { startTransition, useEffect, useEffectEvent, useState, type ReactNode } from "react";
41 44
42type RouteToolSearch = SourceToolExplorerSearch;45type RouteToolSearch = SourceToolExplorerSearch;
43 46
@@ -54,63 +57,6 @@ const defaultOpenApiInput = (): OpenApiConnectInput => ({ · 10 unmodified lines
54const DEFAULT_BEARER_HEADER_NAME = "Authorization";57const DEFAULT_BEARER_HEADER_NAME = "Authorization";
55const DEFAULT_BEARER_PREFIX = "Bearer ";58const DEFAULT_BEARER_PREFIX = "Bearer ";
56 59
57const COMMON_COMPOUND_SUFFIXES = new Set([
58 "ac.uk",
59 "co.in",
60 "co.jp",
61 "co.nz",
62 "co.uk",
63 "com.au",
64 "com.br",
65 "com.mx",
66 "net.au",
67 "org.au",
68 "org.uk",
69]);
70
71const isIpv4Address = (value: string): boolean =>
72 /^\d{1,3}(?:\.\d{1,3}){3}$/.test(value);
73
74const toRegistrableDomain = (hostname: string): string | null => {
75 const normalized = hostname.trim().toLowerCase().replace(/^\.+|\.+$/g, "");
76 if (!normalized) {
77 return null;
78 }
79
80 if (normalized === "localhost" || isIpv4Address(normalized)) {
81 return normalized;
82 }
83
84 const parts = normalized.split(".").filter((part) => part.length > 0);
85 if (parts.length < 2) {
86 return null;
87 }
88
89 const suffix = parts.slice(-2).join(".");
90 if (parts.length >= 3 && COMMON_COMPOUND_SUFFIXES.has(suffix)) {
91 return parts.slice(-3).join(".");
92 }
93
94 return parts.slice(-2).join(".");
95};
96
97const getPreviewFaviconUrl = (value: string | null | undefined): string | null => {
98 const trimmed = value?.trim();
99 if (!trimmed) {
100 return null;
101 }
102
103 try {
104 const url = new URL(trimmed);
105 const domain = toRegistrableDomain(url.hostname);
106 return domain
107 ? `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=32`
108 : null;
109 } catch {
110 return null;
111 }
112};
113
114const presetString = (60const presetString = (
115 search: Record<string, unknown>,61 search: Record<string, unknown>,
116 key: string,62 key: string,
@@ -342,9 +288,9 @@ function OpenApiSourceForm(props: { · 225 unmodified lines
342 })288 })
343 );289 );
344 const submitMutation = useExecutorMutation<OpenApiConnectInput, void>(props.onSubmit);290 const submitMutation = useExecutorMutation<OpenApiConnectInput, void>(props.onSubmit);
345 const resolvedIconUrl = iconUrl.trim() || getPreviewFaviconUrl(baseUrl || specUrl);291 const resolvedIconUrl = iconUrl.trim() || getFaviconUrlForRemoteUrl(baseUrl || specUrl);
346 292
347 const runPreview = async (input: {293 const runPreview = useEffectEvent(async (input: {
348 mode: "auto" | "manual";294 mode: "auto" | "manual";
349 }) => {295 }) => {
350 const trimmedSpecUrl = specUrl.trim();296 const trimmedSpecUrl = specUrl.trim();
@@ -387,7 +333,7 @@ function OpenApiSourceForm(props: { · 36 unmodified lines
387 }333 }
388 setPreview(null);334 setPreview(null);
389 }335 }
390 };336 });
391 337
392 useEffect(() => {338 useEffect(() => {
393 const trimmedSpecUrl = specUrl.trim();339 const trimmedSpecUrl = specUrl.trim();