mirror of
https://github.com/grafana/grafana.git
synced 2024-12-23 15:40:19 -06:00
Annotation query: Render query result in alert box (#83230)
* add alert to annotation result * cleanup * add tests * more refactoring * apply pr feedback * change severity * use toHaveAlert matcher
This commit is contained in:
parent
5b085976bf
commit
ebcca97052
@ -2320,9 +2320,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"]
|
||||
],
|
||||
"public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
||||
],
|
||||
"public/app/features/annotations/events_processing.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
|
@ -1,15 +1,53 @@
|
||||
import { expect, test } from '@grafana/plugin-e2e';
|
||||
import { AlertVariant } from '@grafana/ui';
|
||||
|
||||
import { formatExpectError } from '../errors';
|
||||
import { successfulAnnotationQuery } from '../mocks/queries';
|
||||
import {
|
||||
successfulAnnotationQueryWithData,
|
||||
failedAnnotationQueryWithMultipleErrors,
|
||||
successfulAnnotationQueryWithoutData,
|
||||
failedAnnotationQuery,
|
||||
} from '../mocks/queries';
|
||||
|
||||
test('annotation query data with mocked response', async ({ annotationEditPage, page }) => {
|
||||
annotationEditPage.mockQueryDataResponse(successfulAnnotationQuery);
|
||||
await annotationEditPage.datasource.set('gdev-testdata');
|
||||
await page.getByLabel('Scenario').last().fill('CSV Content');
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(
|
||||
annotationEditPage.runQuery(),
|
||||
formatExpectError('Expected annotation query to execute successfully')
|
||||
).toBeOK();
|
||||
});
|
||||
interface Scenario {
|
||||
name: string;
|
||||
mock: object;
|
||||
text: string;
|
||||
severity: AlertVariant;
|
||||
status: number;
|
||||
}
|
||||
|
||||
const scenarios: Scenario[] = [
|
||||
{ name: 'error', severity: 'error', mock: failedAnnotationQuery, text: 'Google API Error 400', status: 400 },
|
||||
{
|
||||
name: 'multiple errors',
|
||||
severity: 'error',
|
||||
mock: failedAnnotationQueryWithMultipleErrors,
|
||||
text: 'Google API Error 400Google API Error 401',
|
||||
status: 400,
|
||||
},
|
||||
{
|
||||
name: 'data',
|
||||
severity: 'success',
|
||||
mock: successfulAnnotationQueryWithData,
|
||||
text: '2 events (from 2 fields)',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
name: 'empty result',
|
||||
severity: 'warning',
|
||||
mock: successfulAnnotationQueryWithoutData,
|
||||
text: 'No events found',
|
||||
status: 200,
|
||||
},
|
||||
];
|
||||
|
||||
for (const scenario of scenarios) {
|
||||
test(`annotation query data with ${scenario.name}`, async ({ annotationEditPage, page }) => {
|
||||
annotationEditPage.mockQueryDataResponse(scenario.mock, scenario.status);
|
||||
await annotationEditPage.datasource.set('gdev-testdata');
|
||||
await page.getByLabel('Scenario').last().fill('CSV Content');
|
||||
await page.keyboard.press('Tab');
|
||||
await annotationEditPage.runQuery();
|
||||
await expect(annotationEditPage).toHaveAlert(scenario.severity, { hasText: scenario.text });
|
||||
});
|
||||
}
|
||||
|
@ -37,7 +37,32 @@ export const successfulDataQuery = {
|
||||
},
|
||||
};
|
||||
|
||||
export const successfulAnnotationQuery = {
|
||||
export const failedAnnotationQuery: object = {
|
||||
results: {
|
||||
Anno: {
|
||||
error: 'Google API Error 400',
|
||||
errorSource: '',
|
||||
status: 500,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const failedAnnotationQueryWithMultipleErrors: object = {
|
||||
results: {
|
||||
Anno1: {
|
||||
error: 'Google API Error 400',
|
||||
errorSource: '',
|
||||
status: 400,
|
||||
},
|
||||
Anno2: {
|
||||
error: 'Google API Error 401',
|
||||
errorSource: '',
|
||||
status: 401,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const successfulAnnotationQueryWithData: object = {
|
||||
results: {
|
||||
Anno: {
|
||||
status: 200,
|
||||
@ -75,3 +100,39 @@ export const successfulAnnotationQuery = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const successfulAnnotationQueryWithoutData: object = {
|
||||
results: {
|
||||
Anno: {
|
||||
status: 200,
|
||||
frames: [
|
||||
{
|
||||
schema: {
|
||||
refId: 'Anno',
|
||||
fields: [
|
||||
{
|
||||
name: 'time',
|
||||
type: 'time',
|
||||
typeInfo: {
|
||||
frame: 'time.Time',
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'col2',
|
||||
type: 'string',
|
||||
typeInfo: {
|
||||
frame: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
data: {
|
||||
values: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -529,6 +529,10 @@ export const Components = {
|
||||
Annotations: {
|
||||
annotationsTypeInput: 'annotations-type-input',
|
||||
annotationsChoosePanelInput: 'choose-panels-input',
|
||||
editor: {
|
||||
testButton: 'data-testid annotations-test-button',
|
||||
resultContainer: 'data-testid annotations-query-result-container',
|
||||
},
|
||||
},
|
||||
Tooltip: {
|
||||
container: 'data-testid tooltip',
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { PureComponent, ReactElement } from 'react';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import {
|
||||
@ -11,7 +10,8 @@ import {
|
||||
DataSourcePluginContextProvider,
|
||||
LoadingState,
|
||||
} from '@grafana/data';
|
||||
import { Button, Icon, IconName, Spinner } from '@grafana/ui';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Alert, AlertVariant, Button, Space, Spinner } from '@grafana/ui';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
@ -112,63 +112,85 @@ export default class StandardAnnotationQueryEditor extends PureComponent<Props,
|
||||
});
|
||||
};
|
||||
|
||||
renderStatus() {
|
||||
const { response, running } = this.state;
|
||||
let rowStyle = 'alert-info';
|
||||
let text = '...';
|
||||
let icon: IconName | undefined = undefined;
|
||||
getStatusSeverity(response: AnnotationQueryResponse): AlertVariant {
|
||||
const { events, panelData } = response;
|
||||
|
||||
if (panelData?.errors || panelData?.error) {
|
||||
return 'error';
|
||||
}
|
||||
|
||||
if (!events?.length) {
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
return 'success';
|
||||
}
|
||||
|
||||
renderStatusText(response: AnnotationQueryResponse, running: boolean | undefined): ReactElement {
|
||||
const { events, panelData } = response;
|
||||
|
||||
if (running || response?.panelData?.state === LoadingState.Loading || !response) {
|
||||
text = 'loading...';
|
||||
} else {
|
||||
const { events, panelData } = response;
|
||||
|
||||
if (panelData?.error) {
|
||||
rowStyle = 'alert-error';
|
||||
icon = 'exclamation-triangle';
|
||||
text = panelData.error.message ?? 'error';
|
||||
} else if (!events?.length) {
|
||||
rowStyle = 'alert-warning';
|
||||
icon = 'exclamation-triangle';
|
||||
text = 'No events found';
|
||||
} else {
|
||||
const frame = panelData?.series?.[0] ?? panelData?.annotations?.[0];
|
||||
|
||||
text = `${events.length} events (from ${frame?.fields.length} fields)`;
|
||||
}
|
||||
return <p>{'loading...'}</p>;
|
||||
}
|
||||
|
||||
if (panelData?.errors) {
|
||||
return (
|
||||
<>
|
||||
{panelData.errors.map((e, i) => (
|
||||
<p key={i}>{e.message}</p>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (panelData?.error) {
|
||||
return <p>{panelData.error.message ?? 'There was an error fetching data'}</p>;
|
||||
}
|
||||
|
||||
if (!events?.length) {
|
||||
return <p>No events found</p>;
|
||||
}
|
||||
|
||||
const frame = panelData?.series?.[0] ?? panelData?.annotations?.[0];
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
rowStyle,
|
||||
css`
|
||||
margin: 4px 0px;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
{icon && (
|
||||
<>
|
||||
<Icon name={icon} />
|
||||
|
||||
</>
|
||||
)}
|
||||
{text}
|
||||
</div>
|
||||
<p>
|
||||
{events.length} events (from {frame?.fields.length} fields)
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
renderStatus() {
|
||||
const { response, running } = this.state;
|
||||
|
||||
if (!response) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Space v={2} />
|
||||
<div>
|
||||
{running ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Button variant="secondary" size="xs" onClick={this.onRunQuery}>
|
||||
TEST
|
||||
<Button
|
||||
data-testid={selectors.components.Annotations.editor.testButton}
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
onClick={this.onRunQuery}
|
||||
>
|
||||
Test annotation query
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Space v={2} layout="block" />
|
||||
<Alert
|
||||
data-testid={selectors.components.Annotations.editor.resultContainer}
|
||||
severity={this.getStatusSeverity(response)}
|
||||
title="Query result"
|
||||
>
|
||||
{this.renderStatusText(response, running)}
|
||||
</Alert>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user