mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: adds QueryRowErrors component, moves error display to QueryRow (#22438)
* Explore: adds QueryRowErrors component * Explore: updates QueryRow to use QueryRowErrors component * Explore: updates PromQueryField to remove error render * Explore: updates Elastic query field to remove error render * Explore: updates LokiQueryFieldForm to remove error render * Explore: updates QueryRow component - brings back passing errors down * Explore: removes QueryRowErrors component * Explore: updates ErrorContainer component - moves out data filtering * Explore: updates QueryRow component - changes QueryRowErrors to ErrorContainer * Explore: updates Explore component - adds error filtering for ErrorContainer * Explore: updates ErrorContainer and adds a basic test for it * Explore: updates Explore component props name and adds a basic render test * Explore: adds snapshots for Explore and ErrorContainer * Explore: adds a test for error render * Explore: adds a comment to Explore component explaining the way we filter non-query-row-specific errors * Explore: adds getFirstNonQueryRowSpecificError method to explore utilities * Explore: extracts getFirstNonQueryRowSpecificError method and slightly refactors Explore component * Explore: updates Explore component tests to cover non-query-row-specific errors
This commit is contained in:
parent
bbed213115
commit
688283a5cc
@ -541,3 +541,8 @@ export function getIntervals(range: TimeRange, lowLimit: string, resolution: num
|
||||
export function deduplicateLogRowsById(rows: LogRowModel[]) {
|
||||
return _.uniqBy(rows, 'uid');
|
||||
}
|
||||
|
||||
export const getFirstNonQueryRowSpecificError = (queryErrors?: DataQueryError[]) => {
|
||||
const refId = getValueWithRefId(queryErrors);
|
||||
return refId ? null : getFirstQueryErrorWithoutRefId(queryErrors);
|
||||
};
|
||||
|
36
public/app/features/explore/ErrorContainer.test.tsx
Normal file
36
public/app/features/explore/ErrorContainer.test.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { DataQueryError } from '@grafana/data';
|
||||
import { shallow } from 'enzyme';
|
||||
import { ErrorContainer } from './ErrorContainer';
|
||||
|
||||
const makeError = (propOverrides?: DataQueryError): DataQueryError => {
|
||||
const queryError: DataQueryError = {
|
||||
data: {
|
||||
message: 'Error data message',
|
||||
error: 'Error data content',
|
||||
},
|
||||
message: 'Error message',
|
||||
status: 'Error status',
|
||||
statusText: 'Error status text',
|
||||
refId: 'A',
|
||||
cancelled: false,
|
||||
};
|
||||
Object.assign(queryError, propOverrides);
|
||||
return queryError;
|
||||
};
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props = {
|
||||
queryError: makeError(propOverrides),
|
||||
};
|
||||
|
||||
const wrapper = shallow(<ErrorContainer {...props} />);
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
describe('ErrorContainer', () => {
|
||||
it('should render component', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -1,16 +1,13 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { DataQueryError } from '@grafana/data';
|
||||
import { FadeIn } from 'app/core/components/Animations/FadeIn';
|
||||
import { getFirstQueryErrorWithoutRefId, getValueWithRefId } from 'app/core/utils/explore';
|
||||
|
||||
interface Props {
|
||||
queryErrors?: DataQueryError[];
|
||||
export interface ErrorContainerProps {
|
||||
queryError?: DataQueryError;
|
||||
}
|
||||
|
||||
export const ErrorContainer: FunctionComponent<Props> = props => {
|
||||
const { queryErrors } = props;
|
||||
const refId = getValueWithRefId(queryErrors);
|
||||
const queryError = refId ? null : getFirstQueryErrorWithoutRefId(queryErrors);
|
||||
export const ErrorContainer: FunctionComponent<ErrorContainerProps> = props => {
|
||||
const { queryError } = props;
|
||||
const showError = queryError ? true : false;
|
||||
const duration = showError ? 100 : 10;
|
||||
const message = queryError ? queryError.message : null;
|
||||
|
165
public/app/features/explore/Explore.test.tsx
Normal file
165
public/app/features/explore/Explore.test.tsx
Normal file
@ -0,0 +1,165 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
DataSourceApi,
|
||||
LoadingState,
|
||||
ExploreMode,
|
||||
toUtc,
|
||||
DataQueryError,
|
||||
DataQueryRequest,
|
||||
CoreApp,
|
||||
} from '@grafana/data';
|
||||
import { getFirstNonQueryRowSpecificError } from 'app/core/utils/explore';
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Explore, ExploreProps } from './Explore';
|
||||
import { scanStopAction } from './state/actionTypes';
|
||||
import { toggleGraph } from './state/actions';
|
||||
import { Provider } from 'react-redux';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
const setup = (renderMethod: any, propOverrides?: object) => {
|
||||
const props: ExploreProps = {
|
||||
changeSize: jest.fn(),
|
||||
datasourceInstance: {
|
||||
meta: {
|
||||
metrics: true,
|
||||
logs: true,
|
||||
},
|
||||
components: {
|
||||
ExploreStartPage: {},
|
||||
},
|
||||
} as DataSourceApi,
|
||||
datasourceMissing: false,
|
||||
exploreId: ExploreId.left,
|
||||
initializeExplore: jest.fn(),
|
||||
initialized: true,
|
||||
modifyQueries: jest.fn(),
|
||||
update: {
|
||||
datasource: false,
|
||||
queries: false,
|
||||
range: false,
|
||||
mode: false,
|
||||
ui: false,
|
||||
},
|
||||
refreshExplore: jest.fn(),
|
||||
scanning: false,
|
||||
scanRange: {
|
||||
from: '0',
|
||||
to: '0',
|
||||
},
|
||||
scanStart: jest.fn(),
|
||||
scanStopAction: scanStopAction,
|
||||
setQueries: jest.fn(),
|
||||
split: false,
|
||||
queryKeys: [],
|
||||
initialDatasource: 'test',
|
||||
initialQueries: [],
|
||||
initialRange: {
|
||||
from: toUtc('2019-01-01 10:00:00'),
|
||||
to: toUtc('2019-01-01 16:00:00'),
|
||||
raw: {
|
||||
from: 'now-6h',
|
||||
to: 'now',
|
||||
},
|
||||
},
|
||||
mode: ExploreMode.Metrics,
|
||||
initialUI: {
|
||||
showingTable: false,
|
||||
showingGraph: false,
|
||||
showingLogs: false,
|
||||
},
|
||||
isLive: false,
|
||||
syncedTimes: false,
|
||||
updateTimeRange: jest.fn(),
|
||||
graphResult: [],
|
||||
loading: false,
|
||||
absoluteRange: {
|
||||
from: 0,
|
||||
to: 0,
|
||||
},
|
||||
showingGraph: false,
|
||||
showingTable: false,
|
||||
timeZone: 'UTC',
|
||||
onHiddenSeriesChanged: jest.fn(),
|
||||
toggleGraph: toggleGraph,
|
||||
queryResponse: {
|
||||
state: LoadingState.NotStarted,
|
||||
series: [],
|
||||
request: ({
|
||||
requestId: '1',
|
||||
dashboardId: 0,
|
||||
interval: '1s',
|
||||
panelId: 1,
|
||||
scopedVars: {
|
||||
apps: {
|
||||
value: 'value',
|
||||
},
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
refId: 'A',
|
||||
},
|
||||
],
|
||||
timezone: 'UTC',
|
||||
app: CoreApp.Explore,
|
||||
startTime: 0,
|
||||
} as unknown) as DataQueryRequest,
|
||||
error: {} as DataQueryError,
|
||||
timeRange: {
|
||||
from: toUtc('2019-01-01 10:00:00'),
|
||||
to: toUtc('2019-01-01 16:00:00'),
|
||||
raw: {
|
||||
from: 'now-6h',
|
||||
to: 'now',
|
||||
},
|
||||
},
|
||||
},
|
||||
originPanelId: 1,
|
||||
addQueryRow: jest.fn(),
|
||||
};
|
||||
|
||||
const store = configureStore();
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
return renderMethod(
|
||||
<Provider store={store}>
|
||||
<Explore {...props} />
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const setupErrors = (hasRefId?: boolean) => {
|
||||
return [
|
||||
{
|
||||
message: 'Error message',
|
||||
status: '400',
|
||||
statusText: 'Bad Request',
|
||||
refId: hasRefId ? 'A' : '',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
describe('Explore', () => {
|
||||
it('should render component', () => {
|
||||
const wrapper = setup(shallow);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should filter out a query-row-specific error when looking for non-query-row-specific errors', async () => {
|
||||
const queryErrors = setupErrors(true);
|
||||
const queryError = getFirstNonQueryRowSpecificError(queryErrors);
|
||||
expect(queryError).toBeNull();
|
||||
});
|
||||
|
||||
it('should not filter out a generic error when looking for non-query-row-specific errors', async () => {
|
||||
const queryErrors = setupErrors();
|
||||
const queryError = getFirstNonQueryRowSpecificError(queryErrors);
|
||||
expect(queryError).not.toBeNull();
|
||||
expect(queryError).toEqual({
|
||||
message: 'Error message',
|
||||
status: '400',
|
||||
statusText: 'Bad Request',
|
||||
refId: '',
|
||||
});
|
||||
});
|
||||
});
|
@ -50,6 +50,7 @@ import {
|
||||
getTimeRangeFromUrl,
|
||||
getTimeRange,
|
||||
lastUsedDatasourceKeyForOrgId,
|
||||
getFirstNonQueryRowSpecificError,
|
||||
} from 'app/core/utils/explore';
|
||||
import { Emitter } from 'app/core/utils/emitter';
|
||||
import { ExploreToolbar } from './ExploreToolbar';
|
||||
@ -72,7 +73,7 @@ const getStyles = stylesFactory(() => {
|
||||
};
|
||||
});
|
||||
|
||||
interface ExploreProps {
|
||||
export interface ExploreProps {
|
||||
changeSize: typeof changeSize;
|
||||
datasourceInstance: DataSourceApi;
|
||||
datasourceMissing: boolean;
|
||||
@ -294,6 +295,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
const StartPage = datasourceInstance?.components?.ExploreStartPage;
|
||||
const showStartPage = !queryResponse || queryResponse.state === LoadingState.NotStarted;
|
||||
|
||||
// gets an error without a refID, so non-query-row-related error, like a connection error
|
||||
const queryErrors = queryResponse.error ? [queryResponse.error] : undefined;
|
||||
const queryError = getFirstNonQueryRowSpecificError(queryErrors);
|
||||
|
||||
return (
|
||||
<div className={exploreClass} ref={this.getRef}>
|
||||
<ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} />
|
||||
@ -323,7 +328,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
<span className="btn-title">{'\xA0' + 'Query history'}</span>
|
||||
</button>
|
||||
</div>
|
||||
<ErrorContainer queryErrors={queryResponse.error ? [queryResponse.error] : undefined} />
|
||||
<ErrorContainer queryError={queryError} />
|
||||
<AutoSizer onResize={this.onResize} disableHeight>
|
||||
{({ width }) => {
|
||||
if (width === 0) {
|
||||
|
@ -26,6 +26,7 @@ import {
|
||||
import { ExploreItemState, ExploreId } from 'app/types/explore';
|
||||
import { Emitter } from 'app/core/utils/emitter';
|
||||
import { highlightLogsExpressionAction, removeQueryRowAction } from './state/actionTypes';
|
||||
import { ErrorContainer } from './ErrorContainer';
|
||||
|
||||
interface PropsFromParent {
|
||||
exploreId: ExploreId;
|
||||
@ -137,6 +138,7 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="query-row">
|
||||
<div className="query-row-field flex-shrink-1">
|
||||
{QueryField ? (
|
||||
@ -174,6 +176,8 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
|
||||
onClickRemoveButton={this.onClickRemoveButton}
|
||||
/>
|
||||
</div>
|
||||
{queryErrors.length > 0 && <ErrorContainer queryError={queryErrors[0]} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ErrorContainer should render component 1`] = `
|
||||
<Component
|
||||
duration={100}
|
||||
in={true}
|
||||
>
|
||||
<div
|
||||
className="alert-container"
|
||||
>
|
||||
<div
|
||||
className="alert-error alert"
|
||||
>
|
||||
<div
|
||||
className="alert-icon"
|
||||
>
|
||||
<i
|
||||
className="fa fa-exclamation-triangle"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="alert-body"
|
||||
>
|
||||
<div
|
||||
className="alert-title"
|
||||
>
|
||||
Error message
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Component>
|
||||
`;
|
147
public/app/features/explore/__snapshots__/Explore.test.tsx.snap
Normal file
147
public/app/features/explore/__snapshots__/Explore.test.tsx.snap
Normal file
@ -0,0 +1,147 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Explore should render component 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"store": Object {
|
||||
"dispatch": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
Symbol(observable): [Function],
|
||||
},
|
||||
"subscription": Subscription {
|
||||
"handleChangeWrapper": [Function],
|
||||
"listeners": Object {
|
||||
"notify": [Function],
|
||||
},
|
||||
"onStateChange": [Function],
|
||||
"parentSub": undefined,
|
||||
"store": Object {
|
||||
"dispatch": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
Symbol(observable): [Function],
|
||||
},
|
||||
"unsubscribe": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Explore
|
||||
absoluteRange={
|
||||
Object {
|
||||
"from": 0,
|
||||
"to": 0,
|
||||
}
|
||||
}
|
||||
addQueryRow={[MockFunction]}
|
||||
changeSize={[MockFunction]}
|
||||
datasourceInstance={
|
||||
Object {
|
||||
"components": Object {
|
||||
"ExploreStartPage": Object {},
|
||||
},
|
||||
"meta": Object {
|
||||
"logs": true,
|
||||
"metrics": true,
|
||||
},
|
||||
}
|
||||
}
|
||||
datasourceMissing={false}
|
||||
exploreId="left"
|
||||
graphResult={Array []}
|
||||
initialDatasource="test"
|
||||
initialQueries={Array []}
|
||||
initialRange={
|
||||
Object {
|
||||
"from": "2019-01-01T10:00:00.000Z",
|
||||
"raw": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"to": "2019-01-01T16:00:00.000Z",
|
||||
}
|
||||
}
|
||||
initialUI={
|
||||
Object {
|
||||
"showingGraph": false,
|
||||
"showingLogs": false,
|
||||
"showingTable": false,
|
||||
}
|
||||
}
|
||||
initializeExplore={[MockFunction]}
|
||||
initialized={true}
|
||||
isLive={false}
|
||||
loading={false}
|
||||
mode="Metrics"
|
||||
modifyQueries={[MockFunction]}
|
||||
onHiddenSeriesChanged={[MockFunction]}
|
||||
originPanelId={1}
|
||||
queryKeys={Array []}
|
||||
queryResponse={
|
||||
Object {
|
||||
"error": Object {},
|
||||
"request": Object {
|
||||
"app": "explore",
|
||||
"dashboardId": 0,
|
||||
"interval": "1s",
|
||||
"panelId": 1,
|
||||
"requestId": "1",
|
||||
"scopedVars": Object {
|
||||
"apps": Object {
|
||||
"value": "value",
|
||||
},
|
||||
},
|
||||
"startTime": 0,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"timezone": "UTC",
|
||||
},
|
||||
"series": Array [],
|
||||
"state": "NotStarted",
|
||||
"timeRange": Object {
|
||||
"from": "2019-01-01T10:00:00.000Z",
|
||||
"raw": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"to": "2019-01-01T16:00:00.000Z",
|
||||
},
|
||||
}
|
||||
}
|
||||
refreshExplore={[MockFunction]}
|
||||
scanRange={
|
||||
Object {
|
||||
"from": "0",
|
||||
"to": "0",
|
||||
}
|
||||
}
|
||||
scanStart={[MockFunction]}
|
||||
scanStopAction={[Function]}
|
||||
scanning={false}
|
||||
setQueries={[MockFunction]}
|
||||
showingGraph={false}
|
||||
showingTable={false}
|
||||
split={false}
|
||||
syncedTimes={false}
|
||||
timeZone="UTC"
|
||||
toggleGraph={[Function]}
|
||||
update={
|
||||
Object {
|
||||
"datasource": false,
|
||||
"mode": false,
|
||||
"queries": false,
|
||||
"range": false,
|
||||
"ui": false,
|
||||
}
|
||||
}
|
||||
updateTimeRange={[MockFunction]}
|
||||
/>
|
||||
</ContextProvider>
|
||||
`;
|
@ -59,7 +59,7 @@ class ElasticsearchQueryField extends React.PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { data, query } = this.props;
|
||||
const { query } = this.props;
|
||||
const { syntaxLoaded } = this.state;
|
||||
|
||||
return (
|
||||
@ -77,7 +77,6 @@ class ElasticsearchQueryField extends React.PureComponent<Props, State> {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{data && data.error ? <div className="prom-query-field-info text-error">{data.error.message}</div> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -137,7 +137,6 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
|
||||
render() {
|
||||
const {
|
||||
ExtraFieldElement,
|
||||
data,
|
||||
query,
|
||||
syntaxLoaded,
|
||||
logLabelOptions,
|
||||
@ -150,7 +149,6 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
|
||||
const hasLogLabels = logLabelOptions && logLabelOptions.length > 0;
|
||||
const chooserText = getChooserText(syntaxLoaded, hasLogLabels);
|
||||
const buttonDisabled = !(syntaxLoaded && hasLogLabels);
|
||||
const showError = data && data.error && data.error.refId === query.refId;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -183,11 +181,6 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
|
||||
</div>
|
||||
{ExtraFieldElement}
|
||||
</div>
|
||||
{showError ? (
|
||||
<div className="query-row-break">
|
||||
<div className="prom-query-field-info text-error">{data.error.message}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -292,12 +292,11 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
};
|
||||
|
||||
render() {
|
||||
const { data, query, ExtraFieldElement } = this.props;
|
||||
const { query, ExtraFieldElement } = this.props;
|
||||
const { metricsOptions, syntaxLoaded, hint } = this.state;
|
||||
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
|
||||
const chooserText = getChooserText(syntaxLoaded, metricsOptions);
|
||||
const buttonDisabled = !(syntaxLoaded && metricsOptions && metricsOptions.length > 0);
|
||||
const showError = data && data.error && data.error.refId === query.refId;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -324,11 +323,6 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
</div>
|
||||
{ExtraFieldElement}
|
||||
</div>
|
||||
{showError ? (
|
||||
<div className="query-row-break">
|
||||
<div className="prom-query-field-info text-error">{data.error.message}</div>
|
||||
</div>
|
||||
) : null}
|
||||
{hint ? (
|
||||
<div className="query-row-break">
|
||||
<div className="prom-query-field-info text-warning">
|
||||
|
Loading…
Reference in New Issue
Block a user