mirror of
https://github.com/grafana/grafana.git
synced 2025-01-11 08:32:10 -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.", "2"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "3"]
|
[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": [
|
"public/app/features/annotations/events_processing.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
|
@ -1,15 +1,53 @@
|
|||||||
import { expect, test } from '@grafana/plugin-e2e';
|
import { expect, test } from '@grafana/plugin-e2e';
|
||||||
|
import { AlertVariant } from '@grafana/ui';
|
||||||
|
|
||||||
import { formatExpectError } from '../errors';
|
import {
|
||||||
import { successfulAnnotationQuery } from '../mocks/queries';
|
successfulAnnotationQueryWithData,
|
||||||
|
failedAnnotationQueryWithMultipleErrors,
|
||||||
|
successfulAnnotationQueryWithoutData,
|
||||||
|
failedAnnotationQuery,
|
||||||
|
} from '../mocks/queries';
|
||||||
|
|
||||||
test('annotation query data with mocked response', async ({ annotationEditPage, page }) => {
|
interface Scenario {
|
||||||
annotationEditPage.mockQueryDataResponse(successfulAnnotationQuery);
|
name: string;
|
||||||
await annotationEditPage.datasource.set('gdev-testdata');
|
mock: object;
|
||||||
await page.getByLabel('Scenario').last().fill('CSV Content');
|
text: string;
|
||||||
await page.keyboard.press('Tab');
|
severity: AlertVariant;
|
||||||
await expect(
|
status: number;
|
||||||
annotationEditPage.runQuery(),
|
}
|
||||||
formatExpectError('Expected annotation query to execute successfully')
|
|
||||||
).toBeOK();
|
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: {
|
results: {
|
||||||
Anno: {
|
Anno: {
|
||||||
status: 200,
|
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: {
|
Annotations: {
|
||||||
annotationsTypeInput: 'annotations-type-input',
|
annotationsTypeInput: 'annotations-type-input',
|
||||||
annotationsChoosePanelInput: 'choose-panels-input',
|
annotationsChoosePanelInput: 'choose-panels-input',
|
||||||
|
editor: {
|
||||||
|
testButton: 'data-testid annotations-test-button',
|
||||||
|
resultContainer: 'data-testid annotations-query-result-container',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Tooltip: {
|
Tooltip: {
|
||||||
container: 'data-testid tooltip',
|
container: 'data-testid tooltip',
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import React, { PureComponent, ReactElement } from 'react';
|
||||||
import React, { PureComponent } from 'react';
|
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -11,7 +10,8 @@ import {
|
|||||||
DataSourcePluginContextProvider,
|
DataSourcePluginContextProvider,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
} from '@grafana/data';
|
} 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 { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { PanelModel } from 'app/features/dashboard/state';
|
import { PanelModel } from 'app/features/dashboard/state';
|
||||||
@ -112,63 +112,85 @@ export default class StandardAnnotationQueryEditor extends PureComponent<Props,
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
renderStatus() {
|
getStatusSeverity(response: AnnotationQueryResponse): AlertVariant {
|
||||||
const { response, running } = this.state;
|
const { events, panelData } = response;
|
||||||
let rowStyle = 'alert-info';
|
|
||||||
let text = '...';
|
if (panelData?.errors || panelData?.error) {
|
||||||
let icon: IconName | undefined = undefined;
|
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) {
|
if (running || response?.panelData?.state === LoadingState.Loading || !response) {
|
||||||
text = 'loading...';
|
return <p>{'loading...'}</p>;
|
||||||
} 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)`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div
|
<p>
|
||||||
className={cx(
|
{events.length} events (from {frame?.fields.length} fields)
|
||||||
rowStyle,
|
</p>
|
||||||
css`
|
);
|
||||||
margin: 4px 0px;
|
}
|
||||||
padding: 4px;
|
|
||||||
display: flex;
|
renderStatus() {
|
||||||
justify-content: space-between;
|
const { response, running } = this.state;
|
||||||
align-items: center;
|
|
||||||
`
|
if (!response) {
|
||||||
)}
|
return null;
|
||||||
>
|
}
|
||||||
<div>
|
|
||||||
{icon && (
|
return (
|
||||||
<>
|
<>
|
||||||
<Icon name={icon} />
|
<Space v={2} />
|
||||||
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{text}
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
{running ? (
|
{running ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
<Button variant="secondary" size="xs" onClick={this.onRunQuery}>
|
<Button
|
||||||
TEST
|
data-testid={selectors.components.Annotations.editor.testButton}
|
||||||
|
variant="secondary"
|
||||||
|
size="xs"
|
||||||
|
onClick={this.onRunQuery}
|
||||||
|
>
|
||||||
|
Test annotation query
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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