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:
Erik Sundell 2024-03-18 14:26:56 +01:00 committed by GitHub
parent 5b085976bf
commit ebcca97052
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 187 additions and 65 deletions

View File

@ -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"]
],

View File

@ -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 });
});
}

View File

@ -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: [],
},
},
],
},
},
};

View File

@ -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',

View File

@ -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} />
&nbsp;
</>
)}
{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>
</>
);
}