3
0
mirror of https://github.com/grafana/grafana.git synced 2025-02-25 18:55:37 -06:00

Panel: Show multiple errors info in the inspector ()

This commit is contained in:
Andres Martinez Gotor 2023-03-08 16:11:38 +01:00 committed by GitHub
parent 3292cb86ae
commit 15aae5e8a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 207 additions and 24 deletions

View File

@ -54,6 +54,7 @@ const resWithError = {
results: {
A: {
error: 'Hello Error',
status: 400,
frames: [
{
schema: {
@ -354,12 +355,14 @@ describe('Query Response parser', () => {
{
"message": "Hello Error",
"refId": "A",
"status": 400,
}
`);
expect(res.errors).toEqual([
{
message: 'Hello Error',
refId: 'A',
status: 400,
},
]);

View File

@ -33,6 +33,7 @@ export interface DataResponse {
error?: string;
refId?: string;
frames?: DataFrameJSON[];
status?: number;
// Legacy TSDB format...
series?: TimeSeries[];
@ -86,12 +87,13 @@ export function toDataQueryResponse(
rsp.error = {
refId: dr.refId,
message: dr.error,
status: dr.status,
};
}
if (rsp.errors) {
rsp.errors.push({ refId: dr.refId, message: dr.error });
rsp.errors.push({ refId: dr.refId, message: dr.error, status: dr.status });
} else {
rsp.errors = [{ refId: dr.refId, message: dr.error }];
rsp.errors = [{ refId: dr.refId, message: dr.error, status: dr.status }];
}
rsp.state = LoadingState.Error;
}

View File

@ -50,7 +50,10 @@ export const InspectContent = ({
return null;
}
const error = data?.error;
let errors = data?.errors;
if (!errors?.length && data?.error) {
errors = [data.error];
}
// Validate that the active tab is actually valid and allowed
let activeTab = currentTab;
@ -102,7 +105,7 @@ export const InspectContent = ({
{activeTab === InspectTab.JSON && (
<InspectJSONTab panel={panel} dashboard={dashboard} data={data} onClose={onClose} />
)}
{activeTab === InspectTab.Error && <InspectErrorTab error={error} />}
{activeTab === InspectTab.Error && <InspectErrorTab errors={errors} />}
{data && activeTab === InspectTab.Stats && <InspectStatsTab data={data} timeZone={dashboard.getTimezone()} />}
{data && activeTab === InspectTab.Query && (
<QueryInspector panel={panel} data={data.series} onRefreshQuery={() => panel.refresh()} />

View File

@ -1,4 +1,4 @@
import { act, render, screen } from '@testing-library/react';
import { act, fireEvent, render, screen } from '@testing-library/react';
import React, { FC } from 'react';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
@ -15,6 +15,7 @@ import {
PanelProps,
TimeRange,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { getTimeSrv, TimeSrv, setTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { PanelQueryRunner } from '../../../query/state/PanelQueryRunner';
@ -174,6 +175,64 @@ describe('PanelEditorTableView', () => {
width: 100,
});
});
it('should render an error', async () => {
const { rerender, props, subject, store } = setupTestContext({});
// only render the panel when loading is done
act(() => {
subject.next({ state: LoadingState.Loading, series: [], timeRange: getDefaultTimeRange() });
subject.next({
state: LoadingState.Error,
series: [],
errors: [{ message: 'boom!' }],
timeRange: getDefaultTimeRange(),
});
});
const newProps = { ...props, isInView: true };
rerender(
<Provider store={store}>
<PanelEditorTableView {...newProps} />
</Provider>
);
const button = screen.getByRole('button', { name: selectors.components.Panels.Panel.headerCornerInfo('error') });
expect(button).toBeInTheDocument();
await act(async () => {
fireEvent.focus(button);
});
expect(await screen.findByText('boom!')).toBeInTheDocument();
});
it('should render a description for multiple errors', async () => {
const { rerender, props, subject, store } = setupTestContext({});
// only render the panel when loading is done
act(() => {
subject.next({ state: LoadingState.Loading, series: [], timeRange: getDefaultTimeRange() });
subject.next({
state: LoadingState.Error,
series: [],
errors: [{ message: 'boom 1!' }, { message: 'boom 2!' }],
timeRange: getDefaultTimeRange(),
});
});
const newProps = { ...props, isInView: true };
rerender(
<Provider store={store}>
<PanelEditorTableView {...newProps} />
</Provider>
);
const button = screen.getByRole('button', { name: selectors.components.Panels.Panel.headerCornerInfo('error') });
expect(button).toBeInTheDocument();
await act(async () => {
fireEvent.focus(button);
});
expect(await screen.findByText('Multiple errors found. Click for more details')).toBeInTheDocument();
});
});
const TestPanelComponent: FC<PanelProps> = () => <div>Plugin Panel to Render</div>;

View File

@ -49,11 +49,17 @@ export function PanelEditorTableView({ width, height, panel, dashboard }: Props)
if (!data) {
return null;
}
const errorMessage = data?.errors
? data.errors.length > 1
? 'Multiple errors found. Click for more details'
: data.errors[0].message
: data?.error?.message;
return (
<PanelChrome width={width} height={height} padding="none">
{(innerWidth, innerHeight) => (
<>
<PanelHeaderCorner panel={panel} error={data?.error?.message} />
<PanelHeaderCorner panel={panel} error={errorMessage} />
<PanelRenderer
title="Raw data"
pluginId="table"

View File

@ -1,10 +1,11 @@
import { act, render, screen } from '@testing-library/react';
import { act, fireEvent, render, screen } from '@testing-library/react';
import React, { FC } from 'react';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
import { ReplaySubject } from 'rxjs';
import { EventBusSrv, getDefaultTimeRange, LoadingState, PanelData, PanelPlugin, PanelProps } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { PanelQueryRunner } from '../../query/state/PanelQueryRunner';
import { setTimeSrv, TimeSrv } from '../services/TimeSrv';
@ -98,6 +99,46 @@ describe('PanelStateWrapper', () => {
expect(screen.getByText(/plugin panel to render/i)).toBeInTheDocument();
});
});
describe('when there are error(s)', () => {
[
{ errors: [{ message: 'boom!' }], expectedMessage: 'boom!' },
{
errors: [{ message: 'boom!' }, { message: 'boom2!' }],
expectedMessage: 'Multiple errors found. Click for more details',
},
].forEach((scenario) => {
it(`then it should show the error message: ${scenario.expectedMessage}`, async () => {
const { rerender, props, subject, store } = setupTestContext({});
act(() => {
subject.next({ state: LoadingState.Loading, series: [], timeRange: getDefaultTimeRange() });
subject.next({
state: LoadingState.Error,
series: [],
errors: scenario.errors,
timeRange: getDefaultTimeRange(),
});
});
const newProps = { ...props, isInView: true };
rerender(
<Provider store={store}>
<PanelStateWrapper {...newProps} />
</Provider>
);
const button = screen.getByRole('button', {
name: selectors.components.Panels.Panel.headerCornerInfo('error'),
});
expect(button).toBeInTheDocument();
await act(async () => {
fireEvent.focus(button);
});
expect(await screen.findByText(scenario.expectedMessage)).toBeInTheDocument();
});
});
});
});
const TestPanelComponent: FC<PanelProps> = () => <div>Plugin Panel to Render</div>;

View File

@ -300,8 +300,14 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
}
break;
case LoadingState.Error:
const { error } = data;
if (error) {
const { error, errors } = data;
if (errors?.length) {
if (errors.length === 1) {
errorMessage = errors[0].message;
} else {
errorMessage = 'Multiple errors found. Click for more details';
}
} else if (error) {
if (errorMessage !== error.message) {
errorMessage = error.message;
}

View File

@ -26,7 +26,10 @@ type Props = DispatchProps & ConnectedProps<typeof connector>;
export function ExploreQueryInspector(props: Props) {
const { loading, width, onClose, queryResponse, timeZone } = props;
const dataFrames = queryResponse?.series || [];
const error = queryResponse?.error;
let errors = queryResponse?.errors;
if (!errors?.length && queryResponse?.error) {
errors = [queryResponse.error];
}
useEffect(() => {
reportInteraction('grafana_explore_query_inspector_opened');
@ -69,12 +72,12 @@ export function ExploreQueryInspector(props: Props) {
};
const tabs = [statsTab, queryTab, jsonTab, dataTab];
if (error) {
if (errors?.length) {
const errorTab: TabConfig = {
label: 'Error',
value: 'error',
icon: 'exclamation-triangle',
content: <InspectErrorTab error={error} />,
content: <InspectErrorTab errors={errors} />,
};
tabs.push(errorTab);
}

View File

@ -16,7 +16,7 @@ describe('InspectErrorTab', () => {
error: 'my error',
},
};
render(<InspectErrorTab error={error} />);
render(<InspectErrorTab errors={[error]} />);
expect(screen.getByText('This is an error')).toBeInTheDocument();
expect(screen.getByText('error:')).toBeInTheDocument();
expect(screen.getByText('"my error"')).toBeInTheDocument();
@ -27,7 +27,7 @@ describe('InspectErrorTab', () => {
message:
'{ "error": { "code": "BadRequest", "message": "Please provide below info when asking for support.", "details": [] } }',
};
const { container } = render(<InspectErrorTab error={error} />);
const { container } = render(<InspectErrorTab errors={[error]} />);
expect(container.childElementCount).toEqual(1);
expect(screen.getByText('code:')).toBeInTheDocument();
expect(screen.getByText('"BadRequest"')).toBeInTheDocument();
@ -39,7 +39,7 @@ describe('InspectErrorTab', () => {
message:
'400 BadRequest, Error from Azure: { "error": { "code": "BadRequest", "message": "Please provide below info when asking for support.", "details": [] } }',
};
const { container } = render(<InspectErrorTab error={error} />);
const { container } = render(<InspectErrorTab errors={[error]} />);
expect(container.childElementCount).toEqual(2);
expect(screen.getByRole('heading', { name: '400 BadRequest, Error from Azure:' })).toBeInTheDocument();
expect(screen.getByText('code:')).toBeInTheDocument();
@ -55,7 +55,7 @@ describe('InspectErrorTab', () => {
const error = {
message: errMsg,
};
render(<InspectErrorTab error={error} />);
render(<InspectErrorTab errors={[error]} />);
expect(screen.queryByRole('heading')).toBeNull();
expect(screen.getByText(errMsg)).toBeInTheDocument();
});
@ -65,9 +65,48 @@ describe('InspectErrorTab', () => {
const error = {
status: 400,
};
const { container } = render(<InspectErrorTab error={error} />);
const { container } = render(<InspectErrorTab errors={[error]} />);
expect(container.childElementCount).toEqual(1);
expect(screen.getByText('status:')).toBeInTheDocument();
expect(screen.getByText('400')).toBeInTheDocument();
});
it('should return a message along with a status', () => {
const error = {
status: 400,
message: 'This is an error',
};
render(<InspectErrorTab errors={[error]} />);
expect(screen.getByText(/This is an error/)).toBeInTheDocument();
expect(screen.getByText(/Status: 400/)).toBeInTheDocument();
});
it('should return a JSON encoded object along with a status', () => {
const error = {
status: 400,
message:
'{ "error": { "code": "BadRequest", "message": "Please provide below info when asking for support.", "details": [] } }',
};
render(<InspectErrorTab errors={[error]} />);
expect(screen.getByText('"BadRequest"')).toBeInTheDocument();
expect(screen.getByText(/Status: 400/)).toBeInTheDocument();
});
it('should return multiple errors', () => {
const errors = [
{
status: 400,
message: 'This is one error',
},
{
status: 401,
message: 'This is another error',
},
];
render(<InspectErrorTab errors={errors} />);
expect(screen.getByText(/This is one error/)).toBeInTheDocument();
expect(screen.getByText(/Status: 400/)).toBeInTheDocument();
expect(screen.getByText(/This is another error/)).toBeInTheDocument();
expect(screen.getByText(/Status: 401/)).toBeInTheDocument();
});
});

View File

@ -1,10 +1,10 @@
import React from 'react';
import { DataQueryError } from '@grafana/data';
import { JSONFormatter } from '@grafana/ui';
import { Alert, JSONFormatter } from '@grafana/ui';
interface InspectErrorTabProps {
error?: DataQueryError;
errors?: DataQueryError[];
}
const parseErrorMessage = (message: string): { msg: string; json?: any } => {
@ -20,10 +20,7 @@ const parseErrorMessage = (message: string): { msg: string; json?: any } => {
}
};
export const InspectErrorTab = ({ error }: InspectErrorTabProps) => {
if (!error) {
return null;
}
function renderError(error: DataQueryError) {
if (error.data) {
return (
<>
@ -35,15 +32,39 @@ export const InspectErrorTab = ({ error }: InspectErrorTabProps) => {
if (error.message) {
const { msg, json } = parseErrorMessage(error.message);
if (!json) {
return <div>{msg}</div>;
return (
<>
{error.status && <>Status: {error.status}. Message: </>}
{msg}
</>
);
} else {
return (
<>
{msg !== '' && <h3>{msg}</h3>}
{error.status && <>Status: {error.status}</>}
<JSONFormatter json={json} open={5} />
</>
);
}
}
return <JSONFormatter json={error} open={2} />;
}
export const InspectErrorTab = ({ errors }: InspectErrorTabProps) => {
if (!errors?.length) {
return null;
}
if (errors.length === 1) {
return renderError(errors[0]);
}
return (
<>
{errors.map((error, index) => (
<Alert title={error.refId || `Query ${index + 1}`} severity="error" key={index}>
{renderError(error)}
</Alert>
))}
</>
);
};