Skip to content

Commit 5a85fb9

Browse files
mskorokhodovn1ru4l
andauthored
Feat/lab settings (#7943)
Co-authored-by: Laurin <laurinquast@googlemail.com>
1 parent d7e7025 commit 5a85fb9

11 files changed

Lines changed: 439 additions & 204 deletions

File tree

packages/libraries/laboratory/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,17 @@
2929
"peerDependencies": {
3030
"@tanstack/react-form": "^1.23.8",
3131
"date-fns": "^4.1.0",
32-
"graphql-ws": "^6.0.6",
3332
"lucide-react": "^0.548.0",
3433
"lz-string": "^1.5.0",
3534
"react": "^18.0.0 || ^19.0.0",
3635
"react-dom": "^18.0.0 || ^19.0.0",
36+
"subscriptions-transport-ws": "^0.11.0",
3737
"tslib": "^2.8.1",
3838
"zod": "^4.1.12"
3939
},
4040
"dependencies": {
4141
"@base-ui/react": "^1.1.0",
42+
"@graphql-tools/url-loader": "^9.1.0",
4243
"radix-ui": "^1.4.3",
4344
"react-zoom-pan-pinch": "^3.7.0",
4445
"uuid": "^13.0.0"
@@ -98,7 +99,6 @@
9899
"eslint-plugin-react-refresh": "^0.4.26",
99100
"globals": "^16.5.0",
100101
"graphql": "^16.12.0",
101-
"graphql-ws": "^6.0.6",
102102
"lodash": "^4.18.1",
103103
"lucide-react": "^0.548.0",
104104
"lz-string": "^1.5.0",
@@ -114,6 +114,7 @@
114114
"react-shadow": "^20.6.0",
115115
"rollup-plugin-typescript2": "^0.36.0",
116116
"sonner": "^2.0.7",
117+
"subscriptions-transport-ws": "^0.11.0",
117118
"tailwind-merge": "^3.4.0",
118119
"tailwindcss": "^4.1.18",
119120
"tailwindcss-scoped-preflight": "^3.5.7",

packages/libraries/laboratory/src/components/laboratory/laboratory.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ const LaboratoryContent = () => {
402402
>
403403
Preflight Script
404404
</DropdownMenuItem>
405-
{/* <DropdownMenuSeparator />
405+
<DropdownMenuSeparator />
406406
<DropdownMenuItem
407407
onSelect={() => {
408408
const tab =
@@ -416,7 +416,7 @@ const LaboratoryContent = () => {
416416
}}
417417
>
418418
Settings
419-
</DropdownMenuItem> */}
419+
</DropdownMenuItem>
420420
</DropdownMenuContent>
421421
</DropdownMenu>
422422
<TooltipContent side="right">Settings</TooltipContent>
@@ -514,7 +514,10 @@ export const Laboratory = (
514514
const pluginsApi = usePlugins(props);
515515
const testsApi = useTests(props);
516516
const tabsApi = useTabs(props);
517-
const endpointApi = useEndpoint(props);
517+
const endpointApi = useEndpoint({
518+
...props,
519+
settingsApi,
520+
});
518521
const collectionsApi = useCollections({
519522
...props,
520523
tabsApi,

packages/libraries/laboratory/src/components/laboratory/operation.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ export const Response = ({ historyItem }: { historyItem?: LaboratoryHistoryReque
392392
)}
393393
{historyItem ? (
394394
<div className="ml-auto flex items-center gap-2">
395-
{historyItem?.status && (
395+
{!!historyItem?.status && (
396396
<Badge
397397
className={cn('bg-green-400/10 text-green-500', {
398398
'bg-red-400/10 text-red-500': isError,
@@ -542,16 +542,31 @@ export const Query = (props: {
542542
return;
543543
}
544544

545-
const status = response.status;
545+
const extensionsResponse = (response.extensions?.response as {
546+
status: number;
547+
headers: Record<string, string>;
548+
}) ?? {
549+
status: 0,
550+
headers: {},
551+
};
552+
553+
delete response.extensions?.request;
554+
delete response.extensions?.response;
555+
556+
if (Object.keys(response.extensions ?? {}).length === 0) {
557+
delete response.extensions;
558+
}
559+
560+
const status = extensionsResponse.status;
546561
const duration = performance.now() - startTime;
547-
const responseText = await response.text();
562+
const responseText = JSON.stringify(response, null, 2);
548563
const size = responseText.length;
549564

550565
const newItemHistory = addHistory({
551566
status,
552567
duration,
553568
size,
554-
headers: JSON.stringify(Object.fromEntries(response.headers.entries()), null, 2),
569+
headers: JSON.stringify(extensionsResponse.headers, null, 2),
555570
operation,
556571
preflightLogs: result?.logs ?? [],
557572
response: responseText,

packages/libraries/laboratory/src/components/laboratory/settings.tsx

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ const settingsFormSchema = z.object({
1818
protocol: z.enum(['SSE', 'GRAPHQL_SSE', 'WS', 'LEGACY_WS']),
1919
}),
2020
introspection: z.object({
21-
queryName: z.string().optional(),
2221
method: z.enum(['GET', 'POST']).optional(),
2322
schemaDescription: z.boolean().optional(),
2423
}),
@@ -87,8 +86,12 @@ export const Settings = () => {
8786
<Input
8887
type="number"
8988
name={field.name}
90-
value={field.state.value}
91-
onChange={e => field.handleChange(Number(e.target.value))}
89+
value={field.state.value ?? ''}
90+
onChange={e =>
91+
field.handleChange(
92+
e.target.value === '' ? undefined : Number(e.target.value),
93+
)
94+
}
9295
/>
9396
</Field>
9497
);
@@ -102,8 +105,12 @@ export const Settings = () => {
102105
<Input
103106
type="number"
104107
name={field.name}
105-
value={field.state.value}
106-
onChange={e => field.handleChange(Number(e.target.value))}
108+
value={field.state.value ?? ''}
109+
onChange={e =>
110+
field.handleChange(
111+
e.target.value === '' ? undefined : Number(e.target.value),
112+
)
113+
}
107114
/>
108115
</Field>
109116
);
@@ -115,7 +122,7 @@ export const Settings = () => {
115122
<Field className="flex-row items-center">
116123
<Switch
117124
className="!w-8"
118-
checked={field.state.value}
125+
checked={field.state.value ?? false}
119126
onCheckedChange={field.handleChange}
120127
/>
121128
<FieldLabel htmlFor={field.name}>Use GET for queries</FieldLabel>
@@ -175,20 +182,6 @@ export const Settings = () => {
175182
</CardHeader>
176183
<CardContent>
177184
<FieldGroup>
178-
<form.Field name="introspection.queryName">
179-
{field => {
180-
return (
181-
<Field>
182-
<FieldLabel htmlFor={field.name}>Query name</FieldLabel>
183-
<Input
184-
name={field.name}
185-
value={field.state.value}
186-
onChange={e => field.handleChange(e.target.value)}
187-
/>
188-
</Field>
189-
);
190-
}}
191-
</form.Field>
192185
<form.Field name="introspection.method">
193186
{field => {
194187
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
@@ -219,7 +212,7 @@ export const Settings = () => {
219212
<Field className="flex-row items-center">
220213
<Switch
221214
className="!w-8"
222-
checked={field.state.value}
215+
checked={field.state.value ?? false}
223216
onCheckedChange={field.handleChange}
224217
/>
225218
<FieldLabel htmlFor={field.name}>Schema description</FieldLabel>

packages/libraries/laboratory/src/lib/endpoint.ts

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { useCallback, useEffect, useMemo, useState } from 'react';
22
import {
33
buildClientSchema,
4-
getIntrospectionQuery,
54
GraphQLSchema,
5+
introspectionFromSchema,
66
type IntrospectionQuery,
77
} from 'graphql';
88
import { toast } from 'sonner';
9-
import z from 'zod';
9+
// import z from 'zod';
1010
import { asyncInterval } from '@/lib/utils';
11+
import { SubscriptionProtocol, UrlLoader } from '@graphql-tools/url-loader';
12+
import type { LaboratorySettingsActions, LaboratorySettingsState } from './settings';
1113

1214
export interface LaboratoryEndpointState {
1315
endpoint: string | null;
@@ -22,20 +24,11 @@ export interface LaboratoryEndpointActions {
2224
restoreDefaultEndpoint: () => void;
2325
}
2426

25-
const GraphQLResponseErrorSchema = z
26-
.object({
27-
errors: z.array(
28-
z.object({
29-
message: z.string(),
30-
}),
31-
),
32-
})
33-
.strict();
34-
3527
export const useEndpoint = (props: {
3628
defaultEndpoint?: string | null;
3729
onEndpointChange?: (endpoint: string | null) => void;
3830
defaultSchemaIntrospection?: IntrospectionQuery | null;
31+
settingsApi?: LaboratorySettingsState & LaboratorySettingsActions;
3932
}): LaboratoryEndpointState & LaboratoryEndpointActions => {
4033
const [endpoint, _setEndpoint] = useState<string | null>(props.defaultEndpoint ?? null);
4134
const [introspection, setIntrospection] = useState<IntrospectionQuery | null>(null);
@@ -52,6 +45,8 @@ export const useEndpoint = (props: {
5245
return introspection ? buildClientSchema(introspection) : null;
5346
}, [introspection]);
5447

48+
const loader = useMemo(() => new UrlLoader(), []);
49+
5550
const fetchSchema = useCallback(
5651
async (signal?: AbortSignal) => {
5752
if (endpoint === props.defaultEndpoint && props.defaultSchemaIntrospection) {
@@ -65,28 +60,37 @@ export const useEndpoint = (props: {
6560
}
6661

6762
try {
68-
const response = await fetch(endpoint, {
69-
signal,
70-
method: 'POST',
71-
body: JSON.stringify({
72-
query: getIntrospectionQuery(),
73-
}),
74-
headers: {
75-
'Content-Type': 'application/json',
76-
},
77-
}).then(r => r.json());
78-
79-
const parsedResponse = GraphQLResponseErrorSchema.safeParse(response);
80-
81-
if (parsedResponse.success) {
82-
throw new Error(parsedResponse.data.errors.map(e => e.message).join('\n'));
63+
const result = await loader.load(endpoint, {
64+
subscriptionsEndpoint: endpoint,
65+
subscriptionsProtocol:
66+
(props.settingsApi?.settings.subscriptions.protocol as SubscriptionProtocol) ??
67+
SubscriptionProtocol.GRAPHQL_SSE,
68+
credentials: props.settingsApi?.settings.fetch.credentials,
69+
specifiedByUrl: true,
70+
directiveIsRepeatable: true,
71+
inputValueDeprecation: true,
72+
retry: props.settingsApi?.settings.fetch.retry,
73+
timeout: props.settingsApi?.settings.fetch.timeout,
74+
useGETForQueries: props.settingsApi?.settings.fetch.useGETForQueries,
75+
exposeHTTPDetailsInExtensions: true,
76+
descriptions: props.settingsApi?.settings.introspection.schemaDescription ?? false,
77+
method: props.settingsApi?.settings.introspection.method ?? 'POST',
78+
fetch: (input: string | URL | Request, init?: RequestInit) =>
79+
fetch(input, {
80+
...init,
81+
signal,
82+
}),
83+
});
84+
85+
if (result.length === 0) {
86+
throw new Error('Failed to fetch schema');
8387
}
8488

85-
if (response.error && typeof response.error === 'string') {
86-
throw new Error(response.error);
89+
if (!result[0].schema) {
90+
throw new Error('Failed to fetch schema');
8791
}
8892

89-
setIntrospection(response.data as IntrospectionQuery);
93+
setIntrospection(introspectionFromSchema(result[0].schema));
9094
} catch (error: unknown) {
9195
if (
9296
error &&
@@ -104,7 +108,12 @@ export const useEndpoint = (props: {
104108
throw error;
105109
}
106110
},
107-
[endpoint],
111+
[
112+
endpoint,
113+
props.settingsApi?.settings.fetch.timeout,
114+
props.settingsApi?.settings.introspection.method,
115+
props.settingsApi?.settings.introspection.schemaDescription,
116+
],
108117
);
109118

110119
const shouldPollSchema = useMemo(() => {
@@ -129,6 +138,7 @@ export const useEndpoint = (props: {
129138
5000,
130139
intervalController.signal,
131140
);
141+
132142
return () => {
133143
intervalController.abort();
134144
};

0 commit comments

Comments
 (0)