Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve: error handling for scripts #4082

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
67 changes: 60 additions & 7 deletions packages/bruno-app/src/components/CodeEditor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,28 @@ if (!SERVER_RENDERED) {
};
}

const JS_LINT_OPTIONS = {
esversion: 11,
expr: true,
asi: true,
undef: true,
browser: true,
devel: true,
predef: {
'bru': false,
'req': false,
'res': false,
'test': false,
'expect': false
}
};

const DEFAULT_LINT_OPTIONS = {
esversion: 11,
expr: true,
asi: true
};

export default class CodeEditor extends React.Component {
constructor(props) {
super(props);
Expand All @@ -127,12 +149,10 @@ export default class CodeEditor extends React.Component {
this.cachedValue = props.value || '';
this.variables = {};
this.searchResultsCountElementId = 'search-results-count';

this.lintOptions = {
esversion: 11,
expr: true,
asi: true
};

// Set lint options based on mode
this.lintOptions = this.props.mode === 'javascript' ?
JS_LINT_OPTIONS : DEFAULT_LINT_OPTIONS;
}

componentDidMount() {
Expand Down Expand Up @@ -263,8 +283,37 @@ export default class CodeEditor extends React.Component {
}
return found;
});
CodeMirror.registerHelper('lint', 'javascript', function (text) {
const found = [];
if (!window.JSHINT) {
if (window.console) {
window.console.error('Error: window.JSHINT not defined, CodeMirror JavaScript linting cannot run.');
}
return found;
}

// Run JSHint with predefined Bruno globals
if (!window.JSHINT(text, JS_LINT_OPTIONS)) {
// Get JSHint errors and add them to CodeMirror
window.JSHINT.errors.forEach(function(err) {
// Skip if null error
if (!err) return;

found.push({
from: CodeMirror.Pos(err.line - 1, err.character - 1),
to: CodeMirror.Pos(err.line - 1, err.character),
message: err.reason,
severity: err.code && err.code.startsWith('W') ? 'warning' : 'error'
});
});
}
return found;
});
if (editor) {
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
if (this.props.mode === 'javascript') {
editor.setOption('lint', this.lintOptions);
}
editor.on('change', this._onEdit);
this.addOverlay();
}
Expand Down Expand Up @@ -357,7 +406,11 @@ export default class CodeEditor extends React.Component {

_onEdit = () => {
if (!this.ignoreChangeEvent && this.editor) {
this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false);
if (this.props.mode === 'javascript') {
this.editor.setOption('lint', this.lintOptions);
} else {
this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false);
}
this.cachedValue = this.editor.getValue();
if (this.props.onEdit) {
this.props.onEdit(this.cachedValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ const ContentIndicator = () => {
);
};

const ErrorContentIndicator = () => {
return (
<sup className="ml-[.125rem] opacity-80 font-medium text-red-500">
<DotIcon width="10" ></DotIcon>
</sup>
);
};

const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
Expand Down Expand Up @@ -136,7 +144,11 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
</div>
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
Script
{(script.req || script.res) && <ContentIndicator />}
{(script.req || script.res) && (
item.hasPreRequestError || item.hasPostResponseError ?
<ErrorContentIndicator /> :
<ContentIndicator />
)}
</div>
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
Assert
Expand Down
43 changes: 21 additions & 22 deletions packages/bruno-app/src/components/ResponsePane/QueryResult/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@ import classnames from 'classnames';
import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common';
import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror';
import QueryResultPreview from './QueryResultPreview';

import StyledWrapper from './StyledWrapper';
import { useState } from 'react';
import { useMemo } from 'react';
import { useEffect } from 'react';
import { useState, useMemo, useEffect } from 'react';
import { useTheme } from 'providers/Theme/index';
import { uuid } from 'utils/common/index';

Expand Down Expand Up @@ -143,24 +140,26 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
) : null}
</div>
) : (
<>
<QueryResultPreview
previewTab={previewTab}
data={data}
dataBuffer={dataBuffer}
formattedData={formattedData}
item={item}
contentType={contentType}
mode={mode}
collection={collection}
allowedPreviewModes={allowedPreviewModes}
disableRunEventListener={disableRunEventListener}
displayedTheme={displayedTheme}
/>
{queryFilterEnabled && (
<QueryResultFilter filter={filter} onChange={debouncedResultFilterOnChange} mode={mode} />
)}
</>
<div className="h-full flex flex-col">
<div className="flex-1 relative">
<QueryResultPreview
previewTab={previewTab}
data={data}
dataBuffer={dataBuffer}
formattedData={formattedData}
item={item}
contentType={contentType}
mode={mode}
collection={collection}
allowedPreviewModes={allowedPreviewModes}
disableRunEventListener={disableRunEventListener}
displayedTheme={displayedTheme}
/>
{queryFilterEnabled && (
<QueryResultFilter filter={filter} onChange={debouncedResultFilterOnChange} mode={mode} />
)}
</div>
</div>
)}
</StyledWrapper>
);
Expand Down
53 changes: 53 additions & 0 deletions packages/bruno-app/src/components/ResponsePane/StyledWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,59 @@ const StyledWrapper = styled.div`
.all-tests-passed {
color: ${(props) => props.theme.colors.text.green} !important;
}

.script-error {
border-left: 4px solid ${(props) => props.theme.colors.text.danger};
border-top: 1px solid transparent;
border-right: 1px solid transparent;
border-bottom: 1px solid transparent;
border-radius: 0.375rem;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
max-height: 200px;
min-height: 70px;
overflow-y: auto;
background-color: ${(props) => props.theme.bg === '#1e1e1e' ? 'rgba(40, 40, 40, 0.5)' : 'rgba(250, 250, 250, 0.9)'};

.error-icon-container {
margin-top: 0.125rem;
padding: 0.375rem;
border-radius: 9999px;
background-color: ${(props) => props.theme.bg === '#1e1e1e' ? 'rgba(40, 40, 40, 0.8)' : 'rgba(240, 240, 240, 0.8)'};

svg {
color: ${(props) => props.theme.colors.text.danger};
}
}

.close-button {
opacity: 0.7;
transition: opacity 0.2s;

&:hover {
opacity: 1;
}

svg {
color: ${(props) => props.theme.text};
}
}

.error-title {
font-size: 0.75rem;
font-weight: 600;
margin-bottom: 0.375rem;
color: ${(props) => props.theme.colors.text.danger};
}

.error-message {
font-family: monospace;
font-size: 0.6875rem;
line-height: 1.25rem;
white-space: pre-wrap;
word-break: break-all;
color: ${(props) => props.theme.text};
}
}
`;

export default StyledWrapper;
73 changes: 71 additions & 2 deletions packages/bruno-app/src/components/ResponsePane/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import find from 'lodash/find';
import classnames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
Expand All @@ -16,12 +16,22 @@ import TestResultsLabel from './TestResultsLabel';
import StyledWrapper from './StyledWrapper';
import ResponseSave from 'src/components/ResponsePane/ResponseSave';
import ResponseClear from 'src/components/ResponsePane/ResponseClear';
import { IconAlertCircle, IconX } from '@tabler/icons';
import ToolHint from 'components/ToolHint';

const ResponsePane = ({ rightPaneWidth, item, collection }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const isLoading = ['queued', 'sending'].includes(item.requestState);
const [showErrorCard, setShowErrorCard] = useState(true);

// Reset showErrorCard when a new error occurs
useEffect(() => {
if (item?.hasPostResponseError) {
setShowErrorCard(true);
}
}, [item?.postResponseErrorMessage]);

const selectTab = (tab) => {
dispatch(
Expand All @@ -34,6 +44,63 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {

const response = item.response || {};

const renderScriptError = () => {
if (!item?.hasPostResponseError) return null;

if (showErrorCard) {
return (
<div className="script-error mt-4">
<div className="flex items-start gap-3 px-4 py-3">
<div className="error-icon-container flex-shrink-0">
<IconAlertCircle size={14} strokeWidth={1.5} />
</div>
<div className="flex-1 min-w-0">
<div className="error-title">
Script Execution Error
</div>
<div className="error-message">
{item.postResponseErrorMessage}
</div>
</div>
<div
className="close-button flex-shrink-0 cursor-pointer"
onClick={() => setShowErrorCard(false)}
>
<IconX size={16} strokeWidth={1.5} />
</div>
</div>
</div>
);
}

return null;
};

const renderErrorIcon = () => {
if (!item?.hasPostResponseError || showErrorCard) return null;

const toolhintId = `script-error-icon-${item.uid}`;

return (
<>
<div
id={toolhintId}
className="cursor-pointer ml-2"
onClick={() => setShowErrorCard(true)}
>
<div className="flex items-center text-red-400">
<IconAlertCircle size={16} strokeWidth={1.5} className="stroke-current" />
</div>
</div>
<ToolHint
toolhintId={toolhintId}
text="Script execution error occurred"
place="bottom"
/>
</>
);
};

const getTabPanel = (tab) => {
switch (tab) {
case 'response': {
Expand Down Expand Up @@ -117,6 +184,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
</div>
{!isLoading ? (
<div className="flex flex-grow justify-end items-center">
{renderErrorIcon()}
<ResponseClear item={item} collection={collection} />
<ResponseSave item={item} />
<StatusCode status={response.status} />
Expand All @@ -126,9 +194,10 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
) : null}
</div>
<section
className={`flex flex-grow relative pl-3 pr-4 ${focusedTab.responsePaneTab === 'response' ? '' : 'mt-4'}`}
className={`flex flex-col flex-grow relative pl-3 pr-4 ${focusedTab.responsePaneTab === 'response' ? '' : 'mt-4'}`}
>
{isLoading ? <Overlay item={item} collection={collection} /> : null}
{item?.hasPostResponseError && renderScriptError()}
{getTabPanel(focusedTab.responsePaneTab)}
</section>
</StyledWrapper>
Expand Down
Loading