mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
Show output of preflight script (#6400)
This commit is contained in:
parent
fc89aeda4d
commit
d2a4387b64
7 changed files with 261 additions and 7 deletions
5
.changeset/cuddly-yaks-peel.md
Normal file
5
.changeset/cuddly-yaks-peel.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'hive': minor
|
||||
---
|
||||
|
||||
Display logs from the Preflight Script in Laboratory
|
||||
|
|
@ -303,4 +303,94 @@ describe('Execution', () => {
|
|||
cy.get('.graphiql-execute-button').click();
|
||||
cy.wait('@post');
|
||||
});
|
||||
|
||||
it('logs are visible when opened', () => {
|
||||
cy.dataCy('toggle-preflight-script').click();
|
||||
|
||||
cy.dataCy('preflight-script-modal-button').click();
|
||||
setMonacoEditorContents(
|
||||
'preflight-script-editor',
|
||||
dedent`
|
||||
console.info(1)
|
||||
console.warn(true)
|
||||
console.error('Fatal')
|
||||
throw new TypeError('Test')
|
||||
`,
|
||||
);
|
||||
cy.dataCy('preflight-script-modal-submit').click();
|
||||
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
}).as('post');
|
||||
|
||||
// shows no logs before executing
|
||||
cy.get('#preflight-script-logs button[data-cy="trigger"]').click({
|
||||
// it's because the button is not fully visible on the screen
|
||||
force: true,
|
||||
});
|
||||
cy.get('#preflight-script-logs [data-cy="logs"]').should(
|
||||
'contain',
|
||||
['No logs available', 'Execute a query to see logs'].join(''),
|
||||
);
|
||||
|
||||
cy.get('.graphiql-execute-button').click();
|
||||
cy.wait('@post');
|
||||
|
||||
cy.get('#preflight-script-logs [data-cy="logs"]').should(
|
||||
'contain',
|
||||
[
|
||||
'> start running script: > Start running script',
|
||||
'info: 1 (Line: 1, Column: 1)',
|
||||
'warn: true (Line: 2, Column: 1)',
|
||||
'error: Fatal (Line: 3, Column: 1)',
|
||||
'error: TypeError: Test (Line: 4, Column: 7)',
|
||||
'> preflight script failed: > Preflight script failed',
|
||||
].join(''),
|
||||
);
|
||||
});
|
||||
|
||||
it('logs are cleared when requested', () => {
|
||||
cy.dataCy('toggle-preflight-script').click();
|
||||
|
||||
cy.dataCy('preflight-script-modal-button').click();
|
||||
setMonacoEditorContents(
|
||||
'preflight-script-editor',
|
||||
dedent`
|
||||
console.info(1)
|
||||
console.warn(true)
|
||||
console.error('Fatal')
|
||||
throw new TypeError('Test')
|
||||
`,
|
||||
);
|
||||
cy.dataCy('preflight-script-modal-submit').click();
|
||||
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
}).as('post');
|
||||
cy.get('.graphiql-execute-button').click();
|
||||
cy.wait('@post');
|
||||
|
||||
// open logs
|
||||
cy.get('#preflight-script-logs button[data-cy="trigger"]').click({
|
||||
// it's because the button is not fully visible on the screen
|
||||
force: true,
|
||||
});
|
||||
cy.get('#preflight-script-logs [data-cy="logs"]').should(
|
||||
'contain',
|
||||
[
|
||||
'> start running script: > Start running script',
|
||||
'info: 1 (Line: 1, Column: 1)',
|
||||
'warn: true (Line: 2, Column: 1)',
|
||||
'error: Fatal (Line: 3, Column: 1)',
|
||||
'error: TypeError: Test (Line: 4, Column: 7)',
|
||||
'> preflight script failed: > Preflight script failed',
|
||||
].join(''),
|
||||
);
|
||||
|
||||
cy.get('#preflight-script-logs button[data-cy="erase-logs"]').click();
|
||||
cy.get('#preflight-script-logs [data-cy="logs"]').should(
|
||||
'contain',
|
||||
['No logs available', 'Execute a query to see logs'].join(''),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
"@radix-ui/react-alert-dialog": "1.1.4",
|
||||
"@radix-ui/react-avatar": "1.1.2",
|
||||
"@radix-ui/react-checkbox": "1.1.3",
|
||||
"@radix-ui/react-collapsible": "1.1.2",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.4",
|
||||
"@radix-ui/react-hover-card": "1.1.4",
|
||||
|
|
|
|||
9
packages/web/app/src/components/ui/collapsible.ts
Normal file
9
packages/web/app/src/components/ui/collapsible.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root;
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
|
|
@ -140,7 +140,7 @@ const PreflightScript_TargetFragment = graphql(`
|
|||
}
|
||||
`);
|
||||
|
||||
type LogRecord = LogMessage | { type: 'separator' };
|
||||
export type LogRecord = LogMessage | { type: 'separator' };
|
||||
|
||||
function safeParseJSON(str: string): Record<string, unknown> | null {
|
||||
try {
|
||||
|
|
@ -150,7 +150,7 @@ function safeParseJSON(str: string): Record<string, unknown> | null {
|
|||
}
|
||||
}
|
||||
|
||||
const enum PreflightWorkerState {
|
||||
export const enum PreflightWorkerState {
|
||||
running,
|
||||
ready,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import { ReactElement, useCallback, useMemo, useState } from 'react';
|
||||
import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { cx } from 'class-variance-authority';
|
||||
import clsx from 'clsx';
|
||||
import { GraphiQL } from 'graphiql';
|
||||
import { buildSchema } from 'graphql';
|
||||
import { ChevronDownIcon, EraserIcon } from 'lucide-react';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { useMutation, useQuery } from 'urql';
|
||||
import { Page, TargetLayout } from '@/components/layouts/target';
|
||||
import { ConnectLabModal } from '@/components/target/laboratory/connect-lab-modal';
|
||||
import { CreateOperationModal } from '@/components/target/laboratory/create-operation-modal';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import { DocsLink } from '@/components/ui/docs-note';
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -34,6 +36,7 @@ import { useSyncOperationState } from '@/lib/hooks/laboratory/use-sync-operation
|
|||
import { useOperationFromQueryString } from '@/lib/hooks/laboratory/useOperationFromQueryString';
|
||||
import { useResetState } from '@/lib/hooks/use-reset-state';
|
||||
import {
|
||||
LogRecord,
|
||||
preflightScriptPlugin,
|
||||
PreflightScriptProvider,
|
||||
usePreflightScript,
|
||||
|
|
@ -54,6 +57,7 @@ import 'graphiql/style.css';
|
|||
import '@graphiql/plugin-explorer/style.css';
|
||||
import { PromptManager, PromptProvider } from '@/components/ui/prompt';
|
||||
import { useRedirect } from '@/lib/access/common';
|
||||
import { captureException } from '@sentry/react';
|
||||
|
||||
const explorer = explorerPlugin();
|
||||
|
||||
|
|
@ -518,7 +522,7 @@ function LaboratoryPageContent(props: {
|
|||
.graphiql-dialog a {
|
||||
--color-primary: 40, 89%, 60% !important;
|
||||
}
|
||||
|
||||
|
||||
.graphiql-container {
|
||||
overflow: unset; /* remove default overflow */
|
||||
}
|
||||
|
|
@ -529,19 +533,19 @@ function LaboratoryPageContent(props: {
|
|||
line-height: 1.75rem !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
.graphiql-container,
|
||||
.graphiql-dialog,
|
||||
.CodeMirror-info {
|
||||
--color-base: 223, 70%, 3.9% !important;
|
||||
}
|
||||
|
||||
|
||||
.graphiql-tooltip,
|
||||
.graphiql-dropdown-content,
|
||||
.CodeMirror-lint-tooltip {
|
||||
background: #030711;
|
||||
}
|
||||
|
||||
|
||||
.graphiql-tab {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
@ -549,6 +553,30 @@ function LaboratoryPageContent(props: {
|
|||
.graphiql-sidebar > button.active {
|
||||
background-color: hsla(var(--color-neutral),var(--alpha-background-light))
|
||||
}
|
||||
|
||||
.graphiql-container .graphiql-footer {
|
||||
border: 0;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#preflight-script-logs {
|
||||
background-color: hsl(var(--color-base));
|
||||
border-radius: var(--border-radius-12);
|
||||
box-shadow: var(--popover-box-shadow);
|
||||
color: hsla(var(--color-neutral), var(--alpha-tertiary));
|
||||
}
|
||||
|
||||
#preflight-script-logs h2 {
|
||||
color: hsla(var(--color-neutral), var(--alpha-secondary));
|
||||
}
|
||||
|
||||
#preflight-script-logs button[data-state="open"] > h2 {
|
||||
color: hsl(var(--color-neutral));
|
||||
}
|
||||
|
||||
#preflight-script-logs > div {
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
`}</style>
|
||||
</Helmet>
|
||||
{!query.fetching && !query.stale && (
|
||||
|
|
@ -592,6 +620,16 @@ function LaboratoryPageContent(props: {
|
|||
</>
|
||||
)}
|
||||
</GraphiQL.Toolbar>
|
||||
<GraphiQL.Footer>
|
||||
<div>
|
||||
{preflightScript.isPreflightScriptEnabled ? (
|
||||
<PreflightScriptLogs
|
||||
logs={preflightScript.logs}
|
||||
onClear={preflightScript.clearLogs}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</GraphiQL.Footer>
|
||||
</GraphiQL>
|
||||
</PreflightScriptProvider>
|
||||
)}
|
||||
|
|
@ -655,3 +693,111 @@ function useApiTabValueState(graphqlEndpointUrl: string | null) {
|
|||
),
|
||||
] as const;
|
||||
}
|
||||
|
||||
function PreflightScriptLogs(props: { logs: LogRecord[]; onClear: () => void }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const consoleRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const consoleEl = consoleRef.current;
|
||||
consoleEl?.scroll({ top: consoleEl.scrollHeight, behavior: 'smooth' });
|
||||
}, [props.logs, isOpen]);
|
||||
|
||||
const logColor = {
|
||||
error: 'text-red-400',
|
||||
info: 'text-emerald-400',
|
||||
warn: 'text-yellow-400',
|
||||
log: '', // default
|
||||
};
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
className={cn('flex max-h-[200px] w-full flex-col overflow-hidden bg-[#030711]')}
|
||||
id="preflight-script-logs"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex shrink-0 items-center justify-between px-4 py-3',
|
||||
isOpen ? 'border-b' : 'border-b-0',
|
||||
)}
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex h-auto items-center gap-2 p-0 hover:bg-transparent"
|
||||
data-cy="trigger"
|
||||
>
|
||||
<ChevronDownIcon
|
||||
className={`size-4 text-gray-500 transition-transform ${
|
||||
isOpen ? 'rotate-0' : '-rotate-90'
|
||||
}`}
|
||||
/>
|
||||
<h2 className="text-[15px] font-normal">Preflight Script Logs</h2>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-cy="erase-logs"
|
||||
className={cn(
|
||||
'size-8 text-gray-500 hover:text-white',
|
||||
isOpen ? 'visible' : 'invisible',
|
||||
)}
|
||||
onClick={props.onClear}
|
||||
>
|
||||
<EraserIcon className="size-4" />
|
||||
<span className="sr-only">Clear logs</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<CollapsibleContent
|
||||
className="grow overflow-auto p-4 font-mono text-xs/[18px]"
|
||||
ref={consoleRef}
|
||||
data-cy="logs"
|
||||
>
|
||||
{props.logs.length === 0 ? (
|
||||
<div
|
||||
data-cy="empty-state"
|
||||
className="flex flex-col items-center justify-center text-gray-400"
|
||||
>
|
||||
<p>No logs available</p>
|
||||
<p>Execute a query to see logs</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{props.logs.map((log, index) => {
|
||||
if (typeof log !== 'string' && 'type' in log && log.type === 'separator') {
|
||||
return <hr key={index} className="my-2 border-dashed border-current" />;
|
||||
}
|
||||
|
||||
let logType: 'error' | 'warn' | 'info' | 'log' = 'log';
|
||||
let logMessage = '';
|
||||
|
||||
if (log instanceof Error) {
|
||||
logType = 'error';
|
||||
logMessage = `${log.name}: ${log.message}`;
|
||||
} else if (typeof log === 'string') {
|
||||
logType = log.split(':')[0].toLowerCase() as 'error' | 'warn' | 'info' | 'log';
|
||||
logMessage = log.substring(log.indexOf(':') + 1).trim();
|
||||
} else {
|
||||
captureException(new Error('Unexpected log type in Preflight Script Logs'), {
|
||||
extra: { log },
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index} className={logColor[logType] ?? ''}>
|
||||
{logType}: {logMessage}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1718,6 +1718,9 @@ importers:
|
|||
'@radix-ui/react-checkbox':
|
||||
specifier: 1.1.3
|
||||
version: 1.1.3(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-collapsible':
|
||||
specifier: 1.1.2
|
||||
version: 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-dialog':
|
||||
specifier: 1.1.4
|
||||
version: 1.1.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
|
|
|
|||
Loading…
Reference in a new issue