@@ -51,6 +51,7 @@ type GraphqlToolRouteParams = {
5151
5252const defaultGraphqlInput = ( ) : GraphqlConnectInput => ( {
5353 name : "My GraphQL Source" ,
54+ iconUrl : undefined ,
5455 endpoint : "https://example.com/graphql" ,
5556 defaultHeaders : null ,
5657 auth : {
@@ -61,6 +62,63 @@ const defaultGraphqlInput = (): GraphqlConnectInput => ({
6162const DEFAULT_BEARER_HEADER_NAME = "Authorization" ;
6263const DEFAULT_BEARER_PREFIX = "Bearer " ;
6364
65+ const 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+
79+ const isIpv4Address = ( value : string ) : boolean =>
80+ / ^ \d { 1 , 3 } (?: \. \d { 1 , 3 } ) { 3 } $ / . test ( value ) ;
81+
82+ const 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+
105+ const 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+
64122const presetString = (
65123 search : Record < string , unknown > ,
66124 key : string ,
@@ -196,6 +254,7 @@ function GraphqlSourceForm(props: {
196254} ) {
197255 const submitMutation = useExecutorMutation < GraphqlConnectInput , void > ( props . onSubmit ) ;
198256 const [ name , setName ] = useState ( props . initialValue . name ) ;
257+ const [ iconUrl , setIconUrl ] = useState ( props . initialValue . iconUrl ?? "" ) ;
199258 const [ endpoint , setEndpoint ] = useState ( props . initialValue . endpoint ) ;
200259 const [ headersText , setHeadersText ] = useState (
201260 stringifyStringMap ( props . initialValue . defaultHeaders ) ,
@@ -211,6 +270,7 @@ function GraphqlSourceForm(props: {
211270 bearerPrefixValue ( props . initialValue . auth ) ,
212271 ) ;
213272 const [ error , setError ] = useState < string | null > ( null ) ;
273+ const resolvedIconUrl = iconUrl . trim ( ) || getPreviewFaviconUrl ( endpoint ) ;
214274
215275 return (
216276 < div className = "space-y-6 rounded-lg border border-border bg-card p-6 text-sm ring-1 ring-foreground/[0.04]" >
@@ -223,6 +283,22 @@ function GraphqlSourceForm(props: {
223283 />
224284 </ div >
225285
286+ < div className = "grid gap-2" >
287+ < Label > Icon URL</ Label >
288+ < Input
289+ value = { iconUrl }
290+ onChange = { ( event ) => setIconUrl ( event . target . value ) }
291+ placeholder = "https://cdn.example.com/icon.png"
292+ className = "font-mono text-xs"
293+ />
294+ { resolvedIconUrl && (
295+ < div className = "flex items-center gap-2 text-xs text-muted-foreground" >
296+ < img src = { resolvedIconUrl } alt = "" className = "size-4 rounded-sm object-contain" />
297+ < span > { iconUrl . trim ( ) ? "Using override" : "Auto preview" } </ span >
298+ </ div >
299+ ) }
300+ </ div >
301+
226302 < div className = "grid gap-2" >
227303 < Label > Endpoint</ Label >
228304 < Input
@@ -305,6 +381,7 @@ function GraphqlSourceForm(props: {
305381 try {
306382 await submitMutation . mutateAsync ( {
307383 name : name . trim ( ) ,
384+ ...( iconUrl . trim ( ) ? { iconUrl : iconUrl . trim ( ) } : { } ) ,
308385 endpoint : endpoint . trim ( ) ,
309386 defaultHeaders : parseStringMap ( headersText ) ,
310387 auth : authFromSecretValue (
0 commit comments