1
- import { useCallback , useState } from 'react' ;
1
+ import { useCallback , useEffect , useState } from 'react' ;
2
2
import { useForm } from 'react-hook-form' ;
3
3
import { useMutation , useQuery } from 'urql' ;
4
4
import { z } from 'zod' ;
@@ -21,7 +21,7 @@ import { Switch } from '@/components/v2';
21
21
import { FragmentType , graphql , useFragment } from '@/gql' ;
22
22
import { useNotifications } from '@/lib/hooks' ;
23
23
import { zodResolver } from '@hookform/resolvers/zod' ;
24
- import { CheckIcon , Cross2Icon , UpdateIcon } from '@radix-ui/react-icons' ;
24
+ import { CheckIcon , Cross2Icon , ReloadIcon , UpdateIcon } from '@radix-ui/react-icons' ;
25
25
26
26
const ExternalCompositionStatus_TestQuery = graphql ( `
27
27
query ExternalCompositionStatus_TestQuery($selector: TestExternalSchemaCompositionInput!) {
@@ -73,14 +73,20 @@ const ExternalCompositionForm_ProjectFragment = graphql(`
73
73
}
74
74
` ) ;
75
75
76
+ enum TestState {
77
+ LOADING ,
78
+ ERROR ,
79
+ SUCCESS ,
80
+ }
81
+
76
82
const ExternalCompositionStatus = ( {
77
83
projectSlug,
78
84
organizationSlug,
79
85
} : {
80
86
projectSlug : string ;
81
87
organizationSlug : string ;
82
88
} ) => {
83
- const [ query ] = useQuery ( {
89
+ const [ { data , error : gqlError , fetching } , executeTestQuery ] = useQuery ( {
84
90
query : ExternalCompositionStatus_TestQuery ,
85
91
variables : {
86
92
selector : {
@@ -90,33 +96,84 @@ const ExternalCompositionStatus = ({
90
96
} ,
91
97
requestPolicy : 'network-only' ,
92
98
} ) ;
99
+ const error = gqlError ?. message ?? data ?. testExternalSchemaComposition ?. error ?. message ;
100
+ const testState = fetching
101
+ ? TestState . LOADING
102
+ : error
103
+ ? TestState . ERROR
104
+ : data ?. testExternalSchemaComposition ?. ok ?. externalSchemaComposition ?. endpoint
105
+ ? TestState . SUCCESS
106
+ : null ;
107
+
108
+ const [ hidden , setHidden ] = useState < boolean > ( ) ;
109
+
110
+ useEffect ( ( ) => {
111
+ // only hide the success icon after the duration
112
+ if ( testState !== TestState . SUCCESS ) return ;
113
+ const timerId = setTimeout ( ( ) => {
114
+ if ( testState === TestState . SUCCESS ) {
115
+ setHidden ( false ) ;
116
+ }
117
+ } , 5000 ) ;
93
118
94
- const error = query . error ?. message ?? query . data ?. testExternalSchemaComposition ?. error ?. message ;
119
+ return ( ) => {
120
+ clearTimeout ( timerId ) ;
121
+ } ;
122
+ } , [ testState ] ) ;
95
123
96
124
return (
97
125
< TooltipProvider delayDuration = { 100 } >
98
- { query . fetching ? (
126
+ { testState === TestState . LOADING ? (
99
127
< Tooltip >
100
128
< TooltipTrigger >
101
- < UpdateIcon className = "size-5 animate-spin text-gray-500" />
129
+ < UpdateIcon
130
+ className = "size-5 animate-spin cursor-default text-gray-500"
131
+ onClick = { e => e . preventDefault ( ) }
132
+ />
102
133
</ TooltipTrigger >
103
- < TooltipContent side = "right " > Connecting...</ TooltipContent >
134
+ < TooltipContent side = "left " > Connecting...</ TooltipContent >
104
135
</ Tooltip >
105
- ) : null }
106
- { error ? (
136
+ ) : (
107
137
< Tooltip >
108
138
< TooltipTrigger >
109
- < Cross2Icon className = "size-5 text-red-500" />
139
+ < ReloadIcon
140
+ className = "size-5"
141
+ onClick = { e => {
142
+ e . preventDefault ( ) ;
143
+ setHidden ( true ) ;
144
+ executeTestQuery ( ) ;
145
+ } }
146
+ />
147
+ </ TooltipTrigger >
148
+ < TooltipContent side = "top" className = "mr-1" >
149
+ Execute test
150
+ </ TooltipContent >
151
+ </ Tooltip >
152
+ ) }
153
+ { testState === TestState . ERROR ? (
154
+ < Tooltip defaultOpen >
155
+ < TooltipTrigger >
156
+ < Cross2Icon
157
+ className = "size-5 cursor-default text-red-500"
158
+ onClick = { e => e . preventDefault ( ) }
159
+ />
110
160
</ TooltipTrigger >
111
- < TooltipContent side = "right" > { error } </ TooltipContent >
161
+ < TooltipContent side = "right" className = "max-w-sm" >
162
+ { error }
163
+ </ TooltipContent >
112
164
</ Tooltip >
113
165
) : null }
114
- { query . data ?. testExternalSchemaComposition ?. ok ?. externalSchemaComposition ?. endpoint ? (
166
+ { testState === TestState . SUCCESS && ! hidden ? (
115
167
< Tooltip >
116
168
< TooltipTrigger >
117
- < CheckIcon className = "size-5 text-green-500" />
169
+ < CheckIcon
170
+ className = "size-5 cursor-default text-green-500"
171
+ onClick = { e => e . preventDefault ( ) }
172
+ />
118
173
</ TooltipTrigger >
119
- < TooltipContent side = "right" > Service is available</ TooltipContent >
174
+ < TooltipContent side = "right" className = "max-w-sm" >
175
+ Service is available
176
+ </ TooltipContent >
120
177
</ Tooltip >
121
178
) : null }
122
179
</ TooltipProvider >
@@ -148,6 +205,8 @@ const ExternalCompositionForm = ({
148
205
project : FragmentType < typeof ExternalCompositionForm_ProjectFragment > ;
149
206
organization : FragmentType < typeof ExternalCompositionForm_OrganizationFragment > ;
150
207
endpoint ?: string ;
208
+ isNativeCompositionEnabled ?: boolean ;
209
+ onClickDisable ?: ( ) => void ;
151
210
} ) => {
152
211
const project = useFragment ( ExternalCompositionForm_ProjectFragment , props . project ) ;
153
212
const organization = useFragment (
@@ -176,10 +235,11 @@ const ExternalCompositionForm = ({
176
235
} ,
177
236
} ) . then ( result => {
178
237
if ( result . data ?. enableExternalSchemaComposition ?. ok ) {
179
- notify ( 'External composition enabled' , 'success' ) ;
180
238
const endpoint =
181
239
result . data ?. enableExternalSchemaComposition ?. ok . externalSchemaComposition ?. endpoint ;
182
240
241
+ notify ( 'External composition enabled.' , 'success' ) ;
242
+
183
243
if ( endpoint ) {
184
244
form . reset (
185
245
{
@@ -230,10 +290,10 @@ const ExternalCompositionForm = ({
230
290
< FormItem >
231
291
< FormLabel > HTTP Endpoint</ FormLabel >
232
292
< FormDescription > A POST request will be sent to that endpoint</ FormDescription >
233
- < div className = "flex w-full max-w-sm items-center space-x-2" >
293
+ < div className = "flex w-full items-center space-x-2" >
234
294
< FormControl >
235
295
< Input
236
- className = "w-96 shrink-0"
296
+ className = "max-w-md shrink-0"
237
297
placeholder = "Endpoint"
238
298
type = "text"
239
299
autoComplete = "off"
@@ -265,7 +325,7 @@ const ExternalCompositionForm = ({
265
325
</ FormDescription >
266
326
< FormControl >
267
327
< Input
268
- className = "w-96 "
328
+ className = "w-full max-w-md "
269
329
placeholder = "Secret"
270
330
type = "password"
271
331
autoComplete = "off"
@@ -279,10 +339,26 @@ const ExternalCompositionForm = ({
279
339
{ mutation . error && (
280
340
< div className = "mt-2 text-xs text-red-500" > { mutation . error . message } </ div >
281
341
) }
282
- < div >
342
+ { props . isNativeCompositionEnabled ? (
343
+ < DocsNote warn className = "mt-0 max-w-2xl" >
344
+ Native Federation v2 composition is currently enabled. Until native composition is
345
+ disabled, External Schema Composition won't have any effect.
346
+ </ DocsNote >
347
+ ) : null }
348
+ < div className = "flex flex-row items-center gap-x-8" >
283
349
< Button type = "submit" disabled = { form . formState . isSubmitting } >
284
- Save
350
+ Save Configuration
285
351
</ Button >
352
+ { props . onClickDisable ? (
353
+ < Button
354
+ variant = "destructive"
355
+ type = "button"
356
+ disabled = { mutation . fetching || form . formState . isSubmitting }
357
+ onClick = { props . onClickDisable }
358
+ >
359
+ Disable External Composition
360
+ </ Button >
361
+ ) : null }
286
362
</ div >
287
363
</ form >
288
364
</ Form >
@@ -403,27 +479,25 @@ export const ExternalCompositionSettings = (props: {
403
479
) }
404
480
</ div >
405
481
</ CardTitle >
482
+ < CardDescription className = "max-w-2xl" >
483
+ For advanced users, you can configure an endpoint for external schema compositions. This
484
+ can be used to implement custom composition logic.
485
+ </ CardDescription >
406
486
< CardDescription >
407
- < ProductUpdatesLink href = "#native -composition" >
408
- Enable native Apollo Federation v2 support in Hive
487
+ < ProductUpdatesLink href = "https://the-guild.dev/graphql/hive/docs/features/external-schema -composition" >
488
+ Read about external schema composition in our documentation.
409
489
</ ProductUpdatesLink >
410
490
</ CardDescription >
411
491
</ CardHeader >
412
492
413
493
< CardContent >
414
- { isNativeCompositionEnabled && isEnabled ? (
415
- < DocsNote warn className = { isFormVisible ? 'mb-6 mt-0' : '' } >
416
- It appears that Native Federation v2 Composition is activated and will be used instead.
417
- < br />
418
- External composition won't have any effect.
419
- </ DocsNote >
420
- ) : null }
421
-
422
494
{ isFormVisible ? (
423
495
< ExternalCompositionForm
424
496
project = { project }
425
497
organization = { organization }
426
498
endpoint = { externalCompositionConfig ?. endpoint }
499
+ isNativeCompositionEnabled = { isNativeCompositionEnabled }
500
+ onClickDisable = { ( ) => handleSwitch ( false ) }
427
501
/>
428
502
) : (
429
503
< Button disabled = { mutation . fetching } onClick = { ( ) => handleSwitch ( true ) } >
0 commit comments