Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1aa46ac
fix: legacy member role mapping for settings should not include delet…
n1ru4l Mar 18, 2026
0fd99db
feat: query plan
mskorokhodov Mar 20, 2026
bc6a84f
Merge branch 'feat/lab-query-plan' of github.com:graphql-hive/console…
mskorokhodov Mar 20, 2026
7f23520
Merge branch 'main' of github.com:graphql-hive/console into feat/lab-…
mskorokhodov Mar 23, 2026
a7505c2
Merge branch 'main' of github.com:graphql-hive/console into feat/lab-…
mskorokhodov Mar 30, 2026
0b91352
Merge branch 'main' of github.com:graphql-hive/console into feat/lab-…
mskorokhodov Mar 30, 2026
b71eb76
Merge branch 'main' of github.com:graphql-hive/console into feat/lab-…
mskorokhodov Mar 30, 2026
6493b7d
feat: lab settings
mskorokhodov Apr 2, 2026
1a7dbf0
enhancement: migrated lab operation to graphql-tools implementation
mskorokhodov Apr 3, 2026
55244e0
Merge branch 'main' of github.com:graphql-hive/console into feat/lab-…
mskorokhodov Apr 6, 2026
ca3cf53
fix: qp fit in view & text mode rendering
mskorokhodov Apr 6, 2026
586c363
fix: qp to show full node details (include condition and skip in fetc…
mskorokhodov Apr 7, 2026
7badd8a
Merge branch 'main' of github.com:graphql-hive/console into feat/lab-…
mskorokhodov Apr 7, 2026
9d9cb47
fix: remove extra logger
mskorokhodov Apr 14, 2026
841acba
Merge branch 'feat/lab-query-plan' of github.com:graphql-hive/console…
mskorokhodov Apr 14, 2026
be46f57
fix: safe parse of query plan
mskorokhodov Apr 14, 2026
0691304
Merge branch 'feat/lab-query-plan' of github.com:graphql-hive/console…
mskorokhodov Apr 14, 2026
ce3d72b
fix: linter errors
mskorokhodov Apr 14, 2026
669c442
added map of graphiql options to lab in render-laboratory
mskorokhodov Apr 14, 2026
152a348
removed fetchWithRetry fn that isn't in use
mskorokhodov Apr 14, 2026
978ee50
Merge branch 'main' of github.com:graphql-hive/console into feat/lab-…
mskorokhodov Apr 14, 2026
1960f7f
fix: to load schema using url loader as well
mskorokhodov Apr 14, 2026
9aa8cde
Merge branch 'main' of github.com:graphql-hive/console into feat/lab-…
mskorokhodov Apr 15, 2026
17e1c35
fix: removed query name option mapping from render-laboratory
mskorokhodov Apr 15, 2026
c800df7
Merge branch 'main' of github.com:graphql-hive/console into feat/lab-…
mskorokhodov Apr 15, 2026
2fb3be0
fix: remove graphql-sse and graphql-ws deps
mskorokhodov Apr 15, 2026
4edc0b8
pnpm lock fix
mskorokhodov Apr 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/libraries/laboratory/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,19 @@
"peerDependencies": {
"@tanstack/react-form": "^1.23.8",
"date-fns": "^4.1.0",
"graphql-sse": "^2.5.3",
"graphql-ws": "^6.0.6",
"lucide-react": "^0.548.0",
"lz-string": "^1.5.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"subscriptions-transport-ws": "^0.11.0",
"tslib": "^2.8.1",
"zod": "^4.1.12"
},
"dependencies": {
"@base-ui/react": "^1.1.0",
"@graphql-tools/url-loader": "^9.1.0",
"radix-ui": "^1.4.3",
"react-zoom-pan-pinch": "^3.7.0",
"uuid": "^13.0.0"
Expand Down Expand Up @@ -98,6 +101,7 @@
"eslint-plugin-react-refresh": "^0.4.26",
"globals": "^16.5.0",
"graphql": "^16.12.0",
"graphql-sse": "^2.5.3",
Comment thread
ardatan marked this conversation as resolved.
Outdated
"graphql-ws": "^6.0.6",
"lodash": "^4.18.1",
"lucide-react": "^0.548.0",
Expand All @@ -114,6 +118,7 @@
"react-shadow": "^20.6.0",
"rollup-plugin-typescript2": "^0.36.0",
"sonner": "^2.0.7",
"subscriptions-transport-ws": "^0.11.0",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"tailwindcss-scoped-preflight": "^3.5.7",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ const LaboratoryContent = () => {
>
Preflight Script
</DropdownMenuItem>
{/* <DropdownMenuSeparator />
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() => {
const tab =
Expand All @@ -416,7 +416,7 @@ const LaboratoryContent = () => {
}}
>
Settings
</DropdownMenuItem> */}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<TooltipContent side="right">Settings</TooltipContent>
Expand Down Expand Up @@ -514,7 +514,10 @@ export const Laboratory = (
const pluginsApi = usePlugins(props);
const testsApi = useTests(props);
const tabsApi = useTabs(props);
const endpointApi = useEndpoint(props);
const endpointApi = useEndpoint({
...props,
settingsApi,
});
const collectionsApi = useCollections({
...props,
tabsApi,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ export const Response = ({ historyItem }: { historyItem?: LaboratoryHistoryReque
)}
{historyItem ? (
<div className="ml-auto flex items-center gap-2">
{historyItem?.status && (
{!!historyItem?.status && (
<Badge
className={cn('bg-green-400/10 text-green-500', {
'bg-red-400/10 text-red-500': isError,
Expand Down Expand Up @@ -537,16 +537,31 @@ export const Query = (props: {
return;
}

const status = response.status;
const extensionsResponse = (response.extensions?.response as {
status: number;
headers: Record<string, string>;
}) ?? {
status: 0,
headers: {},
};

delete response.extensions?.request;
delete response.extensions?.response;

if (Object.keys(response.extensions ?? {}).length === 0) {
delete response.extensions;
}

const status = extensionsResponse.status;
const duration = performance.now() - startTime;
const responseText = await response.text();
const responseText = JSON.stringify(response, null, 2);
const size = responseText.length;

const newItemHistory = addHistory({
status,
duration,
size,
headers: JSON.stringify(Object.fromEntries(response.headers.entries()), null, 2),
headers: JSON.stringify(extensionsResponse.headers, null, 2),
operation,
preflightLogs: result?.logs ?? [],
response: responseText,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const settingsFormSchema = z.object({
protocol: z.enum(['SSE', 'GRAPHQL_SSE', 'WS', 'LEGACY_WS']),
}),
introspection: z.object({
queryName: z.string().optional(),
method: z.enum(['GET', 'POST']).optional(),
schemaDescription: z.boolean().optional(),
}),
Expand Down Expand Up @@ -87,8 +86,12 @@ export const Settings = () => {
<Input
type="number"
name={field.name}
value={field.state.value}
onChange={e => field.handleChange(Number(e.target.value))}
value={field.state.value ?? ''}
onChange={e =>
field.handleChange(
e.target.value === '' ? undefined : Number(e.target.value),
)
}
/>
</Field>
);
Expand All @@ -102,8 +105,12 @@ export const Settings = () => {
<Input
type="number"
name={field.name}
value={field.state.value}
onChange={e => field.handleChange(Number(e.target.value))}
value={field.state.value ?? ''}
onChange={e =>
field.handleChange(
e.target.value === '' ? undefined : Number(e.target.value),
)
}
/>
</Field>
);
Expand All @@ -115,7 +122,7 @@ export const Settings = () => {
<Field className="flex-row items-center">
<Switch
className="!w-8"
checked={field.state.value}
checked={field.state.value ?? false}
onCheckedChange={field.handleChange}
/>
<FieldLabel htmlFor={field.name}>Use GET for queries</FieldLabel>
Expand Down Expand Up @@ -175,20 +182,6 @@ export const Settings = () => {
</CardHeader>
<CardContent>
<FieldGroup>
<form.Field name="introspection.queryName">
{field => {
return (
<Field>
<FieldLabel htmlFor={field.name}>Query name</FieldLabel>
<Input
name={field.name}
value={field.state.value}
onChange={e => field.handleChange(e.target.value)}
/>
</Field>
);
}}
</form.Field>
<form.Field name="introspection.method">
{field => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
Expand Down Expand Up @@ -219,7 +212,7 @@ export const Settings = () => {
<Field className="flex-row items-center">
<Switch
className="!w-8"
checked={field.state.value}
checked={field.state.value ?? false}
onCheckedChange={field.handleChange}
/>
<FieldLabel htmlFor={field.name}>Schema description</FieldLabel>
Expand Down
72 changes: 41 additions & 31 deletions packages/libraries/laboratory/src/lib/endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
buildClientSchema,
getIntrospectionQuery,
GraphQLSchema,
introspectionFromSchema,
type IntrospectionQuery,
} from 'graphql';
import { toast } from 'sonner';
import z from 'zod';
// import z from 'zod';
import { asyncInterval } from '@/lib/utils';
import { SubscriptionProtocol, UrlLoader } from '@graphql-tools/url-loader';
import type { LaboratorySettingsActions, LaboratorySettingsState } from './settings';

export interface LaboratoryEndpointState {
endpoint: string | null;
Expand All @@ -22,20 +24,11 @@ export interface LaboratoryEndpointActions {
restoreDefaultEndpoint: () => void;
}

const GraphQLResponseErrorSchema = z
.object({
errors: z.array(
z.object({
message: z.string(),
}),
),
})
.strict();

export const useEndpoint = (props: {
defaultEndpoint?: string | null;
onEndpointChange?: (endpoint: string | null) => void;
defaultSchemaIntrospection?: IntrospectionQuery | null;
settingsApi?: LaboratorySettingsState & LaboratorySettingsActions;
}): LaboratoryEndpointState & LaboratoryEndpointActions => {
const [endpoint, _setEndpoint] = useState<string | null>(props.defaultEndpoint ?? null);
const [introspection, setIntrospection] = useState<IntrospectionQuery | null>(null);
Expand All @@ -52,6 +45,8 @@ export const useEndpoint = (props: {
return introspection ? buildClientSchema(introspection) : null;
}, [introspection]);

const loader = useMemo(() => new UrlLoader(), []);

const fetchSchema = useCallback(
async (signal?: AbortSignal) => {
if (endpoint === props.defaultEndpoint && props.defaultSchemaIntrospection) {
Expand All @@ -65,28 +60,37 @@ export const useEndpoint = (props: {
}

try {
const response = await fetch(endpoint, {
signal,
method: 'POST',
body: JSON.stringify({
query: getIntrospectionQuery(),
}),
headers: {
'Content-Type': 'application/json',
},
}).then(r => r.json());

const parsedResponse = GraphQLResponseErrorSchema.safeParse(response);

if (parsedResponse.success) {
throw new Error(parsedResponse.data.errors.map(e => e.message).join('\n'));
const result = await loader.load(endpoint, {
subscriptionsEndpoint: endpoint,
subscriptionsProtocol:
(props.settingsApi?.settings.subscriptions.protocol as SubscriptionProtocol) ??
SubscriptionProtocol.GRAPHQL_SSE,
credentials: props.settingsApi?.settings.fetch.credentials,
specifiedByUrl: true,
directiveIsRepeatable: true,
inputValueDeprecation: true,
retry: props.settingsApi?.settings.fetch.retry,
timeout: props.settingsApi?.settings.fetch.timeout,
useGETForQueries: props.settingsApi?.settings.fetch.useGETForQueries,
exposeHTTPDetailsInExtensions: true,
descriptions: props.settingsApi?.settings.introspection.schemaDescription ?? false,
method: props.settingsApi?.settings.introspection.method ?? 'POST',
fetch: (input: string | URL | Request, init?: RequestInit) =>
fetch(input, {
...init,
signal,
}),
});

if (result.length === 0) {
throw new Error('Failed to fetch schema');
}

if (response.error && typeof response.error === 'string') {
throw new Error(response.error);
if (!result[0].schema) {
throw new Error('Failed to fetch schema');
}

setIntrospection(response.data as IntrospectionQuery);
setIntrospection(introspectionFromSchema(result[0].schema));
} catch (error: unknown) {
if (
error &&
Expand All @@ -104,7 +108,12 @@ export const useEndpoint = (props: {
throw error;
}
},
[endpoint],
[
endpoint,
props.settingsApi?.settings.fetch.timeout,
props.settingsApi?.settings.introspection.method,
props.settingsApi?.settings.introspection.schemaDescription,
],
);

const shouldPollSchema = useMemo(() => {
Expand All @@ -129,6 +138,7 @@ export const useEndpoint = (props: {
5000,
intervalController.signal,
);

return () => {
intervalController.abort();
};
Expand Down
Loading
Loading