mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-16 22:16:49 +01:00
Compare commits
3 Commits
v2023.0.17
...
v2023.0.18
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c75abce09 | ||
|
|
4e15eb197f | ||
|
|
a7544b4f8c |
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "Yaak",
|
||||
"version": "2023.0.17"
|
||||
"version": "2023.0.18"
|
||||
},
|
||||
"tauri": {
|
||||
"windows": [],
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { HttpRequest } from '../lib/models';
|
||||
import { Button } from './core/Button';
|
||||
import type { EditorProps } from './core/Editor';
|
||||
import { Editor, formatGraphQL } from './core/Editor';
|
||||
import { FormattedError } from './core/FormattedError';
|
||||
import { Separator } from './core/Separator';
|
||||
import { useDialog } from './DialogContext';
|
||||
|
||||
@@ -24,8 +25,7 @@ interface GraphQLBody {
|
||||
|
||||
export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEditorProps }: Props) {
|
||||
const editorViewRef = useRef<EditorView>(null);
|
||||
const introspection = useIntrospectGraphQL(baseRequest);
|
||||
|
||||
const { schema, isLoading, error, refetch } = useIntrospectGraphQL(baseRequest);
|
||||
const { query, variables } = useMemo<GraphQLBody>(() => {
|
||||
if (defaultValue === undefined) {
|
||||
return { query: '', variables: {} };
|
||||
@@ -65,8 +65,8 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
|
||||
// Refetch the schema when the URL changes
|
||||
useEffect(() => {
|
||||
if (editorViewRef.current === null) return;
|
||||
updateSchema(editorViewRef.current, introspection.data);
|
||||
}, [introspection.data]);
|
||||
updateSchema(editorViewRef.current, schema);
|
||||
}, [schema]);
|
||||
|
||||
const dialog = useDialog();
|
||||
|
||||
@@ -81,22 +81,38 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
|
||||
placeholder="..."
|
||||
ref={editorViewRef}
|
||||
actions={
|
||||
(introspection.error || introspection.isLoading) && (
|
||||
(error || isLoading) && (
|
||||
<Button
|
||||
size="xs"
|
||||
color={introspection.error ? 'danger' : 'gray'}
|
||||
isLoading={introspection.isLoading}
|
||||
color={error ? 'danger' : 'gray'}
|
||||
isLoading={isLoading}
|
||||
onClick={() => {
|
||||
dialog.show({
|
||||
title: 'Introspection Failed',
|
||||
size: 'sm',
|
||||
id: 'introspection-failed',
|
||||
render: () => (
|
||||
<div className="whitespace-pre-wrap">{introspection.error?.message}</div>
|
||||
<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>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{introspection.error ? 'Introspection Failed' : 'Introspecting'}
|
||||
{error ? 'Introspection Failed' : 'Introspecting'}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useRouteError } from 'react-router-dom';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { Button } from './core/Button';
|
||||
import { FormattedError } from './core/FormattedError';
|
||||
import { Heading } from './core/Heading';
|
||||
import { VStack } from './core/Stacks';
|
||||
|
||||
@@ -14,9 +15,7 @@ export default function RouteError() {
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<VStack space={5} className="max-w-[30rem] !h-auto">
|
||||
<Heading>Route Error 🔥</Heading>
|
||||
<pre className="text-sm select-auto cursor-text bg-gray-100 p-3 rounded whitespace-normal">
|
||||
{message}
|
||||
</pre>
|
||||
<FormattedError>{message}</FormattedError>
|
||||
<VStack space={2}>
|
||||
<Button
|
||||
color="primary"
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
@apply shadow-lg bg-gray-50 rounded text-gray-700 border border-gray-200 z-50 pointer-events-auto text-[0.75rem];
|
||||
|
||||
&.cm-completionInfo-right {
|
||||
@apply ml-1;
|
||||
@apply ml-1 -mt-0.5 text-sm;
|
||||
}
|
||||
|
||||
&.cm-completionInfo-right-narrow {
|
||||
|
||||
11
src-web/components/core/FormattedError.tsx
Normal file
11
src-web/components/core/FormattedError.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { GraphQLSchema } from 'graphql';
|
||||
import type { IntrospectionQuery } from 'graphql';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import { buildClientSchema, getIntrospectionQuery } from '../components/core/Editor';
|
||||
import { minPromiseMillis } from '../lib/minPromiseMillis';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
@@ -13,18 +14,24 @@ const introspectionRequestBody = JSON.stringify({
|
||||
});
|
||||
|
||||
export function useIntrospectGraphQL(baseRequest: HttpRequest) {
|
||||
// Debounce the URL because it can change rapidly, and we don't
|
||||
// want to send so many requests.
|
||||
// Debounce the request because it can change rapidly and we don't
|
||||
// want to send so too many requests.
|
||||
const request = useDebouncedValue(baseRequest);
|
||||
const [refetchKey, setRefetchKey] = useState<number>(0);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>();
|
||||
const [introspection, setIntrospection] = useLocalStorage<IntrospectionQuery>(
|
||||
`introspection:${baseRequest.id}`,
|
||||
);
|
||||
|
||||
return useQuery<GraphQLSchema, Error>({
|
||||
queryKey: ['introspectGraphQL', { url: request.url, method: request.method }],
|
||||
refetchInterval: 1000 * 60, // Refetch every minute
|
||||
queryFn: async () => {
|
||||
const response = await minPromiseMillis(
|
||||
sendEphemeralRequest({ ...baseRequest, body: introspectionRequestBody }),
|
||||
700,
|
||||
);
|
||||
const introspectionInterval = useRef<NodeJS.Timeout>();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchIntrospection = async () => {
|
||||
setIsLoading(true);
|
||||
setError(undefined);
|
||||
const args = { ...baseRequest, body: introspectionRequestBody };
|
||||
const response = await minPromiseMillis(sendEphemeralRequest(args), 700);
|
||||
|
||||
if (response.error) {
|
||||
return Promise.reject(new Error(response.error));
|
||||
@@ -42,7 +49,31 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) {
|
||||
}
|
||||
|
||||
const { data } = JSON.parse(body);
|
||||
return buildClientSchema(data);
|
||||
},
|
||||
});
|
||||
setIntrospection(data);
|
||||
};
|
||||
|
||||
const runIntrospection = () => {
|
||||
fetchIntrospection()
|
||||
.catch((e) => setError(e.message))
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
|
||||
// Do it again on an interval
|
||||
clearInterval(introspectionInterval.current);
|
||||
introspectionInterval.current = setInterval(runIntrospection, 1000 * 60);
|
||||
runIntrospection(); // Run immediately
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [request.id, request.url, request.method, refetchKey]);
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
setRefetchKey((k) => k + 1);
|
||||
}, []);
|
||||
|
||||
const schema = useMemo(
|
||||
() => (introspection ? buildClientSchema(introspection) : undefined),
|
||||
[introspection],
|
||||
);
|
||||
|
||||
return { schema, isLoading, error, refetch };
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { HttpResponse } from './models';
|
||||
|
||||
export async function getResponseBodyText(response: HttpResponse): Promise<string | null> {
|
||||
if (response.body) {
|
||||
const uint8Array = Uint8Array.of(...response.body);
|
||||
const uint8Array = Uint8Array.from(response.body);
|
||||
return new TextDecoder().decode(uint8Array);
|
||||
}
|
||||
if (response.bodyPath) {
|
||||
@@ -14,7 +14,7 @@ export async function getResponseBodyText(response: HttpResponse): Promise<strin
|
||||
|
||||
export async function getResponseBodyBlob(response: HttpResponse): Promise<Uint8Array | null> {
|
||||
if (response.body) {
|
||||
return Uint8Array.of(...response.body);
|
||||
return Uint8Array.from(response.body);
|
||||
}
|
||||
if (response.bodyPath) {
|
||||
return readBinaryFile(response.bodyPath);
|
||||
|
||||
@@ -29,7 +29,21 @@ module.exports = {
|
||||
},
|
||||
fontFamily: {
|
||||
"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: {
|
||||
'3xs': "0.6rem",
|
||||
|
||||
Reference in New Issue
Block a user