Retry button on introspection errors

This commit is contained in:
Gregory Schier
2023-06-12 13:20:42 -07:00
parent e02954f396
commit 5dae591c79
8 changed files with 62 additions and 14 deletions

View File

@@ -8,7 +8,7 @@
}, },
"package": { "package": {
"productName": "Yaak", "productName": "Yaak",
"version": "2023.0.17" "version": "2023.0.18"
}, },
"tauri": { "tauri": {
"windows": [], "windows": [],

View File

@@ -6,6 +6,7 @@ import type { HttpRequest } from '../lib/models';
import { Button } from './core/Button'; import { Button } from './core/Button';
import type { EditorProps } from './core/Editor'; import type { EditorProps } from './core/Editor';
import { Editor, formatGraphQL } from './core/Editor'; import { Editor, formatGraphQL } from './core/Editor';
import { FormattedError } from './core/FormattedError';
import { Separator } from './core/Separator'; import { Separator } from './core/Separator';
import { useDialog } from './DialogContext'; import { useDialog } from './DialogContext';
@@ -24,7 +25,7 @@ interface GraphQLBody {
export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEditorProps }: Props) { export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEditorProps }: Props) {
const editorViewRef = useRef<EditorView>(null); const editorViewRef = useRef<EditorView>(null);
const { schema, isLoading, error } = useIntrospectGraphQL(baseRequest); const { schema, isLoading, error, refetch } = useIntrospectGraphQL(baseRequest);
const { query, variables } = useMemo<GraphQLBody>(() => { const { query, variables } = useMemo<GraphQLBody>(() => {
if (defaultValue === undefined) { if (defaultValue === undefined) {
return { query: '', variables: {} }; return { query: '', variables: {} };
@@ -89,7 +90,25 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
dialog.show({ dialog.show({
title: 'Introspection Failed', title: 'Introspection Failed',
size: 'sm', size: 'sm',
render: () => <div className="whitespace-pre-wrap">{error}</div>, id: 'introspection-failed',
render: () => (
<div className="whitespace-pre-wrap">
<FormattedError>{error ?? 'unknown'}</FormattedError>
<div className="w-full mt-3">
<Button
onClick={() => {
dialog.hide('introspection-failed');
refetch();
}}
className="ml-auto"
color="secondary"
size="sm"
>
Try Again
</Button>
</div>
</div>
),
}); });
}} }}
> >

View File

@@ -1,6 +1,7 @@
import { useRouteError } from 'react-router-dom'; import { useRouteError } from 'react-router-dom';
import { useAppRoutes } from '../hooks/useAppRoutes'; import { useAppRoutes } from '../hooks/useAppRoutes';
import { Button } from './core/Button'; import { Button } from './core/Button';
import { FormattedError } from './core/FormattedError';
import { Heading } from './core/Heading'; import { Heading } from './core/Heading';
import { VStack } from './core/Stacks'; import { VStack } from './core/Stacks';
@@ -14,9 +15,7 @@ export default function RouteError() {
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<VStack space={5} className="max-w-[30rem] !h-auto"> <VStack space={5} className="max-w-[30rem] !h-auto">
<Heading>Route Error 🔥</Heading> <Heading>Route Error 🔥</Heading>
<pre className="text-sm select-auto cursor-text bg-gray-100 p-3 rounded whitespace-normal"> <FormattedError>{message}</FormattedError>
{message}
</pre>
<VStack space={2}> <VStack space={2}>
<Button <Button
color="primary" color="primary"

View File

@@ -0,0 +1,11 @@
interface Props {
children: string;
}
export function FormattedError({ children }: Props) {
return (
<pre className="text-sm select-auto cursor-text bg-gray-100 p-3 rounded whitespace-normal border border-red-500 border-dashed">
{children}
</pre>
);
}

View File

@@ -1,5 +1,5 @@
import type { IntrospectionQuery } from 'graphql'; import type { IntrospectionQuery } from 'graphql';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useLocalStorage } from 'react-use'; import { useLocalStorage } from 'react-use';
import { buildClientSchema, getIntrospectionQuery } from '../components/core/Editor'; import { buildClientSchema, getIntrospectionQuery } from '../components/core/Editor';
import { minPromiseMillis } from '../lib/minPromiseMillis'; import { minPromiseMillis } from '../lib/minPromiseMillis';
@@ -17,6 +17,7 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) {
// Debounce the request because it can change rapidly and we don't // Debounce the request because it can change rapidly and we don't
// want to send so too many requests. // want to send so too many requests.
const request = useDebouncedValue(baseRequest); const request = useDebouncedValue(baseRequest);
const [refetchKey, setRefetchKey] = useState<number>(0);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string>(); const [error, setError] = useState<string>();
const [introspection, setIntrospection] = useLocalStorage<IntrospectionQuery>( const [introspection, setIntrospection] = useLocalStorage<IntrospectionQuery>(
@@ -63,12 +64,16 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) {
runIntrospection(); // Run immediately runIntrospection(); // Run immediately
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [request.id, request.method]); }, [request.id, request.url, request.method, refetchKey]);
const refetch = useCallback(() => {
setRefetchKey((k) => k + 1);
}, []);
const schema = useMemo( const schema = useMemo(
() => (introspection ? buildClientSchema(introspection) : undefined), () => (introspection ? buildClientSchema(introspection) : undefined),
[introspection], [introspection],
); );
return { schema, isLoading, error }; return { schema, isLoading, error, refetch };
} }

View File

@@ -3,7 +3,7 @@ import type { HttpResponse } from './models';
export async function getResponseBodyText(response: HttpResponse): Promise<string | null> { export async function getResponseBodyText(response: HttpResponse): Promise<string | null> {
if (response.body) { if (response.body) {
const uint8Array = Uint8Array.of(...response.body); const uint8Array = Uint8Array.from(response.body);
return new TextDecoder().decode(uint8Array); return new TextDecoder().decode(uint8Array);
} }
if (response.bodyPath) { if (response.bodyPath) {
@@ -14,7 +14,7 @@ export async function getResponseBodyText(response: HttpResponse): Promise<strin
export async function getResponseBodyBlob(response: HttpResponse): Promise<Uint8Array | null> { export async function getResponseBodyBlob(response: HttpResponse): Promise<Uint8Array | null> {
if (response.body) { if (response.body) {
return Uint8Array.of(...response.body); return Uint8Array.from(response.body);
} }
if (response.bodyPath) { if (response.bodyPath) {
return readBinaryFile(response.bodyPath); return readBinaryFile(response.bodyPath);

View File

@@ -8,9 +8,9 @@ const darkTheme: AppTheme = {
appearance: 'dark', appearance: 'dark',
layers: { layers: {
root: { root: {
blackPoint: 0.09, blackPoint: 0.2,
colors: { colors: {
gray: '#9b8ebe', gray: '#6b5b98',
red: '#ff417b', red: '#ff417b',
orange: '#fd9014', orange: '#fd9014',
yellow: '#e8d13f', yellow: '#e8d13f',

View File

@@ -29,7 +29,21 @@ module.exports = {
}, },
fontFamily: { fontFamily: {
"mono": ["JetBrains Mono", "Menlo", "monospace"], "mono": ["JetBrains Mono", "Menlo", "monospace"],
"sans": ["Inter", "sans-serif"] "sans": [
"Inter",
"-apple-system",
"BlinkMacSystemFont",
"Segoe UI",
"Roboto",
"Oxygen-Sans",
"Ubuntu",
"Cantarell",
"Helvetica Neue",
"sans-serif",
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol",
],
}, },
fontSize: { fontSize: {
'3xs': "0.6rem", '3xs': "0.6rem",