mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
enhancement: lab to fetch schema if no introspection provided in inte… (#7888)
This commit is contained in:
parent
d3e0ef500f
commit
574a5d823e
6 changed files with 151 additions and 50 deletions
7
.changeset/loud-mammals-clean.md
Normal file
7
.changeset/loud-mammals-clean.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
'@graphql-hive/laboratory': patch
|
||||
'@graphql-hive/render-laboratory': patch
|
||||
---
|
||||
|
||||
If schema introspection isn't provided as property to Laboratory, lab will start interval to fetch
|
||||
schema every second.
|
||||
|
|
@ -791,8 +791,6 @@ export const Builder = (props: {
|
|||
});
|
||||
}, [schema, deferredSearchValue, isSearchActive, tabValue]);
|
||||
|
||||
console.log(searchResult);
|
||||
|
||||
const visiblePaths = isSearchActive ? (searchResult?.visiblePaths ?? null) : null;
|
||||
const forcedOpenPaths =
|
||||
isSearchActive && deferredSearchValue.includes('.')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } from 'react';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import { MonacoGraphQLAPI } from 'monaco-graphql/esm/api.js';
|
||||
import { initializeMode } from 'monaco-graphql/initializeMode';
|
||||
import MonacoEditor, { loader } from '@monaco-editor/react';
|
||||
import { useLaboratory } from './context';
|
||||
|
|
@ -128,34 +129,44 @@ const EditorInner = forwardRef<EditorHandle, EditorProps>((props, ref) => {
|
|||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
||||
const { introspection, endpoint, theme } = useLaboratory();
|
||||
const [typescriptReady, setTypescriptReady] = useState(!!monaco.languages.typescript);
|
||||
const apiRef = useRef<MonacoGraphQLAPI | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (introspection) {
|
||||
const api = initializeMode({
|
||||
schemas: [
|
||||
if (apiRef.current) {
|
||||
apiRef.current.setSchemaConfig([
|
||||
{
|
||||
introspectionJSON: introspection,
|
||||
uri: `schema_${endpoint}.graphql`,
|
||||
},
|
||||
],
|
||||
diagnosticSettings:
|
||||
props.uri && props.variablesUri
|
||||
? {
|
||||
validateVariablesJSON: {
|
||||
[props.uri.toString()]: [props.variablesUri.toString()],
|
||||
},
|
||||
jsonDiagnosticSettings: {
|
||||
allowComments: true, // allow json, parse with a jsonc parser to make requests
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
]);
|
||||
} else {
|
||||
apiRef.current = initializeMode({
|
||||
schemas: [
|
||||
{
|
||||
introspectionJSON: introspection,
|
||||
uri: `schema_${endpoint}.graphql`,
|
||||
},
|
||||
],
|
||||
diagnosticSettings:
|
||||
props.uri && props.variablesUri
|
||||
? {
|
||||
validateVariablesJSON: {
|
||||
[props.uri.toString()]: [props.variablesUri.toString()],
|
||||
},
|
||||
jsonDiagnosticSettings: {
|
||||
allowComments: true, // allow json, parse with a jsonc parser to make requests
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
api.setCompletionSettings({
|
||||
__experimental__fillLeafsOnComplete: true,
|
||||
});
|
||||
apiRef.current.setCompletionSettings({
|
||||
__experimental__fillLeafsOnComplete: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [introspection, props.uri?.toString(), props.variablesUri?.toString()]);
|
||||
}, [endpoint, introspection, props.uri?.toString(), props.variablesUri?.toString()]);
|
||||
|
||||
useEffect(() => {
|
||||
void (async function () {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import {
|
|||
type IntrospectionQuery,
|
||||
} from 'graphql';
|
||||
import { toast } from 'sonner';
|
||||
import z from 'zod';
|
||||
import { asyncInterval } from '@/lib/utils';
|
||||
|
||||
export interface LaboratoryEndpointState {
|
||||
endpoint: string | null;
|
||||
|
|
@ -20,6 +22,16 @@ 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;
|
||||
|
|
@ -40,35 +52,87 @@ export const useEndpoint = (props: {
|
|||
return introspection ? buildClientSchema(introspection) : null;
|
||||
}, [introspection]);
|
||||
|
||||
const fetchSchema = useCallback(async () => {
|
||||
if (endpoint === props.defaultEndpoint && props.defaultSchemaIntrospection) {
|
||||
setIntrospection(props.defaultSchemaIntrospection);
|
||||
const fetchSchema = useCallback(
|
||||
async (signal?: AbortSignal) => {
|
||||
if (endpoint === props.defaultEndpoint && props.defaultSchemaIntrospection) {
|
||||
setIntrospection(props.defaultSchemaIntrospection);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!endpoint) {
|
||||
setIntrospection(null);
|
||||
return;
|
||||
}
|
||||
|
||||
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'));
|
||||
}
|
||||
|
||||
if (response.error && typeof response.error === 'string') {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
setIntrospection(response.data as IntrospectionQuery);
|
||||
} catch (error: unknown) {
|
||||
if (
|
||||
error &&
|
||||
typeof error === 'object' &&
|
||||
'message' in error &&
|
||||
typeof error.message === 'string'
|
||||
) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
toast.error('Failed to fetch schema');
|
||||
}
|
||||
|
||||
setIntrospection(null);
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[endpoint],
|
||||
);
|
||||
|
||||
const shouldPollSchema = useMemo(() => {
|
||||
return endpoint !== props.defaultEndpoint || !props.defaultSchemaIntrospection;
|
||||
}, [endpoint, props.defaultEndpoint, props.defaultSchemaIntrospection]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldPollSchema || !endpoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!endpoint) {
|
||||
setIntrospection(null);
|
||||
return;
|
||||
}
|
||||
const intervalController = new AbortController();
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
query: getIntrospectionQuery(),
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(r => r.json());
|
||||
|
||||
setIntrospection(response.data as IntrospectionQuery);
|
||||
} catch {
|
||||
toast.error('Failed to fetch schema');
|
||||
setIntrospection(null);
|
||||
return;
|
||||
}
|
||||
}, [endpoint]);
|
||||
void asyncInterval(
|
||||
async () => {
|
||||
try {
|
||||
await fetchSchema(intervalController.signal);
|
||||
} catch {
|
||||
intervalController.abort();
|
||||
}
|
||||
},
|
||||
5000,
|
||||
intervalController.signal,
|
||||
);
|
||||
return () => {
|
||||
intervalController.abort();
|
||||
};
|
||||
}, [shouldPollSchema, fetchSchema]);
|
||||
|
||||
const restoreDefaultEndpoint = useCallback(() => {
|
||||
if (props.defaultEndpoint) {
|
||||
|
|
@ -77,10 +141,10 @@ export const useEndpoint = (props: {
|
|||
}, [props.defaultEndpoint]);
|
||||
|
||||
useEffect(() => {
|
||||
if (endpoint) {
|
||||
if (endpoint && !shouldPollSchema) {
|
||||
void fetchSchema();
|
||||
}
|
||||
}, [endpoint, fetchSchema]);
|
||||
}, [endpoint, fetchSchema, shouldPollSchema]);
|
||||
|
||||
return {
|
||||
endpoint,
|
||||
|
|
|
|||
|
|
@ -348,8 +348,6 @@ export async function getOperationHash(
|
|||
operation: Pick<LaboratoryOperation, 'query' | 'variables'>,
|
||||
) {
|
||||
try {
|
||||
console.log(operation.query, operation.variables);
|
||||
|
||||
const canonicalQuery = print(parse(operation.query));
|
||||
const canonicalVariables = '';
|
||||
const canonical = `${canonicalQuery}\n${canonicalVariables}`;
|
||||
|
|
|
|||
|
|
@ -17,3 +17,26 @@ export function splitIdentifier(input: string): string[] {
|
|||
.split(/\s+/)
|
||||
.map(w => w.toLowerCase());
|
||||
}
|
||||
|
||||
export async function asyncInterval(
|
||||
fn: () => Promise<void>,
|
||||
delay: number,
|
||||
signal?: AbortSignal,
|
||||
): Promise<void> {
|
||||
while (!signal?.aborted) {
|
||||
await fn();
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timer = setTimeout(resolve, delay);
|
||||
|
||||
signal?.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
clearTimeout(timer);
|
||||
reject(new DOMException('Aborted', 'AbortError'));
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue