Prometheus: Add e2e tests for decoupling (#80333)

* update selectors for prom

* add selector to switch component, needs id instead of testid

* add testid and ids to Prom settings

* add e2e tests for prom config

* add config to editor test

* export select function

* start query editor spec

* clean up describe

* add selectors for general query editor

* add selectors to components in options in best locations

* wrap header switch in id because component doesn't accept testid nor id

* add id to wrap legend components in one selector

* update selector in shared folder component, note to change in shared library

* update selector in shared folder component, note to change in shared library

* add notes for selectors in shared folder

* add tests and file for query editor

* add selectors for metrics browser in code editor

* add selector to component to open metrics browser

* add selectors to components within the metrics browser

* add tests for metrics browser and stub resource calls

* add selectors to query builder components

* add e2e tests for query builder

* generic query builder test with hints

* add selectors for more code editor parts

* add test for code and update selector

* fix tests with selector

* remove shared folder changes and use data-testid where possible

* remove unused import

* share getResources

* create variable query editor selectors

* add selectors to the variable query editor

* add e2e tests for the Prometheus variable query editor

* fix test function

* refactor add data source method

* add annotation selectors

* add selectors to annotation components

* add annotation e2e tests

* commit for yarn i18n:extract error in drone
This commit is contained in:
Brendan O'Handley 2024-01-22 08:42:24 -06:00 committed by GitHub
parent 2210ed50b4
commit 639bf3036d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 782 additions and 62 deletions

View File

@ -0,0 +1,62 @@
import { e2e } from '../../utils';
/**
* Create a Prom data source
*/
export function createPromDS(dataSourceID: string, name: string): void {
// login
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true);
// select the prometheus DS
e2e.pages.AddDataSource.visit();
e2e.pages.AddDataSource.dataSourcePluginsV2(dataSourceID)
.scrollIntoView()
.should('be.visible') // prevents flakiness
.click();
// add url for DS to save without error
e2e.components.DataSource.Prometheus.configPage.connectionSettings().type('http://prom-url:9090');
// name the DS
e2e.pages.DataSource.name().clear();
e2e.pages.DataSource.name().type(name);
e2e.pages.DataSource.saveAndTest().click();
}
export function getResources() {
cy.intercept(/__name__/g, metricResponse);
cy.intercept(/metadata/g, metadataResponse);
cy.intercept(/labels/g, labelsResponse);
}
const metricResponse = {
status: 'success',
data: ['metric1', 'metric2'],
};
const metadataResponse = {
status: 'success',
data: {
metric1: [
{
type: 'counter',
help: 'metric1 help',
unit: '',
},
],
metric2: [
{
type: 'counter',
help: 'metric2 help',
unit: '',
},
],
},
};
const labelsResponse = {
status: 'success',
data: ['__name__', 'action', 'active', 'backend'],
};

View File

@ -0,0 +1,75 @@
import { selectors } from '@grafana/e2e-selectors';
import { e2e } from '../utils';
import { addDashboard } from '../utils/flows';
import { createPromDS, getResources } from './helpers/prometheus-helpers';
const DATASOURCE_ID = 'Prometheus';
const DATASOURCE_NAME = 'aprometheusAnnotationDS';
/**
* Click dashboard settings and then the variables tab
*
*/
function navigateToAnnotations() {
e2e.components.PageToolbar.item('Dashboard settings').click();
e2e.components.Tab.title('Annotations').click();
}
function addPrometheusAnnotation(annotationName: string) {
e2e.pages.Dashboard.Settings.Annotations.List.addAnnotationCTAV2().click();
getResources();
e2e.pages.Dashboard.Settings.Annotations.Settings.name().clear().type(annotationName);
e2e.components.DataSourcePicker.container().should('be.visible').click();
cy.contains(DATASOURCE_NAME).scrollIntoView().should('be.visible').click();
}
describe('Prometheus annotations', () => {
beforeEach(() => {
createPromDS(DATASOURCE_ID, DATASOURCE_NAME);
});
it('should navigate to variable query editor', () => {
const annotationName = 'promAnnotation';
addDashboard();
navigateToAnnotations();
addPrometheusAnnotation(annotationName);
e2e.components.DataSource.Prometheus.queryEditor.code.metricsBrowser
.openButton()
.contains('Metrics browser')
.click();
e2e.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.selectMetric().should('exist').type('met');
e2e.components.DataSource.Prometheus.queryEditor.code.metricsBrowser
.metricList()
.should('exist')
.contains('metric1')
.click();
e2e.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.useQuery().should('exist').click();
e2e.components.DataSource.Prometheus.queryEditor.code.queryField().should('exist').contains('metric1');
// check for other parts of the annotations
// min step
cy.get(`#${selectors.components.DataSource.Prometheus.annotations.minStep}`);
// title
e2e.components.DataSource.Prometheus.annotations.title().scrollIntoView().should('exist');
// tags
e2e.components.DataSource.Prometheus.annotations.tags().scrollIntoView().should('exist');
// text
e2e.components.DataSource.Prometheus.annotations.text().scrollIntoView().should('exist');
// series value as timestamp
e2e.components.DataSource.Prometheus.annotations.seriesValueAsTimestamp().scrollIntoView().should('exist');
e2e.pages.Dashboard.Settings.Annotations.NewAnnotation.previewInDashboard().click();
// check that annotation exists
cy.get('body').contains(annotationName);
});
});

View File

@ -0,0 +1,116 @@
import { selectors } from '@grafana/e2e-selectors';
import { e2e } from '../utils';
const DATASOURCE_ID = 'Prometheus';
const DATASOURCE_TYPED_NAME = 'PrometheusDatasourceInstance';
describe('Prometheus config', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true);
e2e.pages.AddDataSource.visit();
e2e.pages.AddDataSource.dataSourcePluginsV2(DATASOURCE_ID)
.scrollIntoView()
.should('be.visible') // prevents flakiness
.click();
});
it('should have a connection settings component', () => {
e2e.components.DataSource.Prometheus.configPage.connectionSettings().should('be.visible');
});
it('should have a managed alerts component', () => {
cy.get(`#${selectors.components.DataSource.Prometheus.configPage.manageAlerts}`).scrollIntoView().should('exist');
});
it('should have a scrape interval component', () => {
e2e.components.DataSource.Prometheus.configPage.scrapeInterval().scrollIntoView().should('exist');
});
it('should have a query timeout component', () => {
e2e.components.DataSource.Prometheus.configPage.queryTimeout().scrollIntoView().should('exist');
});
it('should have a default editor component', () => {
e2e.components.DataSource.Prometheus.configPage.defaultEditor().scrollIntoView().should('exist');
});
it('should save the default editor when navigating to explore', () => {
e2e.components.DataSource.Prometheus.configPage.defaultEditor().scrollIntoView().should('exist').click();
selectOption('Code');
e2e.components.DataSource.Prometheus.configPage.connectionSettings().type('http://prom-url:9090');
e2e.pages.DataSource.name().clear();
e2e.pages.DataSource.name().type(DATASOURCE_TYPED_NAME);
e2e.pages.DataSource.saveAndTest().click();
e2e.pages.Explore.visit();
e2e.components.DataSourcePicker.container().should('be.visible').click();
cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click();
const monacoLoadingText = 'Loading...';
e2e.components.QueryField.container().should('be.visible').should('have.text', monacoLoadingText);
e2e.components.QueryField.container().should('be.visible').should('not.have.text', monacoLoadingText);
});
it('should have a disable metric lookup component', () => {
cy.get(`#${selectors.components.DataSource.Prometheus.configPage.disableMetricLookup}`)
.scrollIntoView()
.should('exist');
});
it('should have a prometheus type component', () => {
e2e.components.DataSource.Prometheus.configPage.prometheusType().scrollIntoView().should('exist');
});
it('should allow a user to add the version when the Prom type is selected', () => {
e2e.components.DataSource.Prometheus.configPage.prometheusType().scrollIntoView().should('exist').click();
selectOption('Prometheus');
e2e.components.DataSource.Prometheus.configPage.prometheusVersion().scrollIntoView().should('exist');
});
it('should have a cache level component', () => {
e2e.components.DataSource.Prometheus.configPage.cacheLevel().scrollIntoView().should('exist');
});
it('should have an incremental querying component', () => {
cy.get(`#${selectors.components.DataSource.Prometheus.configPage.incrementalQuerying}`)
.scrollIntoView()
.should('exist');
});
it('should allow a user to select a query overlap window when incremental querying is selected', () => {
cy.get(`#${selectors.components.DataSource.Prometheus.configPage.incrementalQuerying}`)
.scrollIntoView()
.should('exist')
.check({ force: true });
e2e.components.DataSource.Prometheus.configPage.queryOverlapWindow().scrollIntoView().should('exist');
});
it('should have a disable recording rules component', () => {
cy.get(`#${selectors.components.DataSource.Prometheus.configPage.disableRecordingRules}`)
.scrollIntoView()
.should('exist');
});
it('should have a custom query parameters component', () => {
e2e.components.DataSource.Prometheus.configPage.customQueryParameters().scrollIntoView().should('exist');
});
it('should have an http method component', () => {
e2e.components.DataSource.Prometheus.configPage.httpMethod().scrollIntoView().should('exist');
});
// exemplars tested in exemplar.spec
});
export function selectOption(option: string) {
cy.get("[aria-label='Select option']").contains(option).should('be.visible').click();
}

View File

@ -0,0 +1,182 @@
import { selectors } from '@grafana/e2e-selectors';
import { e2e } from '../utils';
import { getResources } from './helpers/prometheus-helpers';
const DATASOURCE_ID = 'Prometheus';
type editorType = 'Code' | 'Builder';
/**
* Login, create and save a Prometheus data source, navigate to code or builder
*
* @param editorType 'Code' or 'Builder'
*/
function navigateToEditor(editorType: editorType, name: string): void {
// login
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true);
// select the prometheus DS
e2e.pages.AddDataSource.visit();
e2e.pages.AddDataSource.dataSourcePluginsV2(DATASOURCE_ID)
.scrollIntoView()
.should('be.visible') // prevents flakiness
.click();
// choose default editor
e2e.components.DataSource.Prometheus.configPage.defaultEditor().scrollIntoView().should('exist').click();
selectOption(editorType);
// add url for DS to save without error
e2e.components.DataSource.Prometheus.configPage.connectionSettings().type('http://prom-url:9090');
// name the DS
e2e.pages.DataSource.name().clear();
e2e.pages.DataSource.name().type(name);
e2e.pages.DataSource.saveAndTest().click();
// visit explore
e2e.pages.Explore.visit();
// choose the right DS
e2e.components.DataSourcePicker.container().should('be.visible').click();
cy.contains(name).scrollIntoView().should('be.visible').click();
}
describe('Prometheus query editor', () => {
it('should have a kickstart component', () => {
navigateToEditor('Code', 'prometheus');
e2e.components.QueryBuilder.queryPatterns().scrollIntoView().should('exist');
});
it('should have an explain component', () => {
navigateToEditor('Code', 'prometheus');
e2e.components.DataSource.Prometheus.queryEditor.explain().scrollIntoView().should('exist');
});
it('should have an editor toggle component', () => {
navigateToEditor('Code', 'prometheus');
e2e.components.DataSource.Prometheus.queryEditor.editorToggle().scrollIntoView().should('exist');
});
it('should have an options component with legend, format, step, type and exemplars', () => {
navigateToEditor('Code', 'prometheus');
// open options
e2e.components.DataSource.Prometheus.queryEditor.options().scrollIntoView().should('exist').click();
// check options
e2e.components.DataSource.Prometheus.queryEditor.legend().scrollIntoView().should('exist');
e2e.components.DataSource.Prometheus.queryEditor.format().scrollIntoView().should('exist');
cy.get(`#${selectors.components.DataSource.Prometheus.queryEditor.step}`).scrollIntoView().should('exist');
e2e.components.DataSource.Prometheus.queryEditor.type().scrollIntoView().should('exist');
cy.get(`#${selectors.components.DataSource.Prometheus.queryEditor.exemplars}`).scrollIntoView().should('exist');
});
describe('Code editor', () => {
it('navigates to the code editor with editor type as code', () => {
navigateToEditor('Code', 'prometheusCode');
});
it('navigates to the code editor and opens the metrics browser with metric search, labels, label values, and all components', () => {
navigateToEditor('Code', 'prometheusCode');
getResources();
e2e.components.DataSource.Prometheus.queryEditor.code.queryField().should('exist');
e2e.components.DataSource.Prometheus.queryEditor.code.metricsBrowser
.openButton()
.contains('Metrics browser')
.click();
e2e.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.selectMetric().should('exist');
e2e.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.labelNamesFilter().should('exist');
e2e.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.labelValuesFilter().should('exist');
e2e.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.useQuery().should('exist');
e2e.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.useAsRateQuery().should('exist');
e2e.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.validateSelector().should('exist');
e2e.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.clear().should('exist');
});
it('selects a metric in the metrics browser and uses the query', () => {
navigateToEditor('Code', 'prometheusCode');
getResources();
e2e.components.DataSource.Prometheus.queryEditor.code.metricsBrowser
.openButton()
.contains('Metrics browser')
.click();
e2e.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.selectMetric().should('exist').type('met');
e2e.components.DataSource.Prometheus.queryEditor.code.metricsBrowser
.metricList()
.should('exist')
.contains('metric1')
.click();
e2e.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.useQuery().should('exist').click();
e2e.components.DataSource.Prometheus.queryEditor.code.queryField().should('exist').contains('metric1');
});
});
describe('Query builder', () => {
it('navigates to the query builder with editor type as code', () => {
navigateToEditor('Builder', 'prometheusBuilder');
});
it('the query builder contains metric select, label filters and operations', () => {
navigateToEditor('Builder', 'prometheusBuilder');
getResources();
e2e.components.DataSource.Prometheus.queryEditor.builder.metricSelect().should('exist');
e2e.components.QueryBuilder.labelSelect().should('exist');
e2e.components.QueryBuilder.matchOperatorSelect().should('exist');
e2e.components.QueryBuilder.valueSelect().should('exist');
});
it('can select a metric and provide a hint', () => {
navigateToEditor('Builder', 'prometheusBuilder');
getResources();
e2e.components.DataSource.Prometheus.queryEditor.builder.metricSelect().should('exist').click();
selectOption('metric1');
e2e.components.DataSource.Prometheus.queryEditor.builder.hints().contains('hint: add rate');
});
it('should have the metrics explorer opened via the metric select', () => {
navigateToEditor('Builder', 'prometheusBuilder');
getResources();
e2e.components.DataSource.Prometheus.queryEditor.builder.metricSelect().should('exist').click();
selectOption('Metrics explorer');
e2e.components.DataSource.Prometheus.queryEditor.builder.metricsExplorer().should('exist');
});
// NEED TO COMPLETE QUEY ADVISOR WORK OR FIGURE OUT HOW TO ENABLE EXPERIMENTAL FEATURE TOGGLES
// it('should have a query advisor when enabled with feature toggle', () => {
// cy.window().then((win) => {
// win.localStorage.setItem('grafana.featureToggles', 'prometheusPromQAIL=0');
// navigateToEditor('Builder', 'prometheusBuilder');
// getResources();
// e2e.components.DataSource.Prometheus.queryEditor.builder.queryAdvisor().should('exist');
// });
// });
});
});
function selectOption(option: string) {
cy.get("[aria-label='Select option']").contains(option).should('be.visible').click();
}

View File

@ -0,0 +1,122 @@
import { e2e } from '../utils';
import { addDashboard } from '../utils/flows';
import { createPromDS, getResources } from './helpers/prometheus-helpers';
const DATASOURCE_ID = 'Prometheus';
const DATASOURCE_NAME = 'prometheusVariableDS';
/**
* Click dashboard settings and then the variables tab
*/
function navigateToVariables() {
e2e.components.PageToolbar.item('Dashboard settings').click();
e2e.components.Tab.title('Variables').click();
}
/**
* Begin the process of adding a query type variable for a Prometheus data source
*
* @param variableName the name of the variable as a label of the variable dropdown
*/
function addPrometheusQueryVariable(variableName: string) {
e2e.pages.Dashboard.Settings.Variables.List.addVariableCTAV2().click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type(variableName);
e2e.components.DataSourcePicker.container().should('be.visible').click();
cy.contains(DATASOURCE_NAME).scrollIntoView().should('be.visible').click();
getResources();
}
/**
* Create a Prometheus variable and navigate to the query editor to check that it is available to use.
*
* @param variableName name the variable
* @param queryType query type of 'Label names', 'Label values', 'Metrics', 'Query result', 'Series query' or 'Classic query'. These types should be imported from the Prometheus library eventually but not now because we are in the process of decoupling the DS from core grafana.
*/
function variableFlowToQueryEditor(variableName: string, queryType: string) {
addDashboard();
navigateToVariables();
addPrometheusQueryVariable(variableName);
// select query type
e2e.components.DataSource.Prometheus.variableQueryEditor.queryType().click();
selectOption(queryType);
// apply the variable
e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click();
// close to return to dashboard
e2e.pages.Dashboard.Settings.Actions.close().click();
// add visualization
e2e.pages.AddDashboard.itemButton('Create new panel button').should('be.visible').click();
// close the data source picker modal
cy.get('[aria-label="Close"]').click();
// select prom data source from the data source list with the useful data-testid
e2e.components.DataSourcePicker.inputV2().click({ force: true }).type(`${DATASOURCE_NAME}{enter}`);
// confirm the variable exists in the correct input
// use the variable query type from the library in the future
switch (queryType) {
case 'Label names':
e2e.components.QueryBuilder.labelSelect().should('exist').click({ force: true });
selectOption(`${variableName}`);
case 'Label values':
e2e.components.QueryBuilder.valueSelect().should('exist').click({ force: true });
selectOption(`${variableName}`);
case 'Metrics':
e2e.components.DataSource.Prometheus.queryEditor.builder.metricSelect().should('exist').click({ force: true });
selectOption(`${variableName}`);
default:
// do nothing
}
}
describe('Prometheus variable query editor', () => {
beforeEach(() => {
createPromDS(DATASOURCE_ID, DATASOURCE_NAME);
});
it('should navigate to variable query editor', () => {
addDashboard();
navigateToVariables();
});
it('should select a query type for a Prometheus variable query', () => {
addDashboard();
navigateToVariables();
addPrometheusQueryVariable('labelsVariable');
// select query type
e2e.components.DataSource.Prometheus.variableQueryEditor.queryType().click();
selectOption('Label names');
});
it('should create a label names variable that is selectable in the label select in query builder', () => {
addDashboard();
navigateToVariables();
variableFlowToQueryEditor('labelnames', 'Label names');
});
it('should create a label values variable that is selectable in the label values select in query builder', () => {
addDashboard();
navigateToVariables();
variableFlowToQueryEditor('labelvalues', 'Label values');
});
it('should create a metric names variable that is selectable in the metric select in query builder', () => {
addDashboard();
navigateToVariables();
variableFlowToQueryEditor('metrics', 'Metrics');
});
});
function selectOption(option: string) {
cy.get("[aria-label='Select option']").contains(option).should('be.visible').click();
}

View File

@ -62,11 +62,83 @@ export const Components = {
},
Prometheus: {
configPage: {
connectionSettings: 'Data source connection URL',
connectionSettings: 'Data source connection URL', // aria-label in grafana experimental
manageAlerts: 'prometheus-alerts-manager', // id for switch component
scrapeInterval: 'data-testid scrape interval',
queryTimeout: 'data-testid query timeout',
defaultEditor: 'data-testid default editor',
disableMetricLookup: 'disable-metric-lookup', // id for switch component
prometheusType: 'data-testid prometheus type',
prometheusVersion: 'data-testid prometheus version',
cacheLevel: 'data-testid cache level',
incrementalQuerying: 'prometheus-incremental-querying', // id for switch component
queryOverlapWindow: 'data-testid query overlap window',
disableRecordingRules: 'disable-recording-rules', // id for switch component
customQueryParameters: 'data-testid custom query parameters',
httpMethod: 'data-testid http method',
exemplarsAddButton: 'data-testid Add exemplar config button',
internalLinkSwitch: 'data-testid Internal link switch',
},
queryEditor: {
// kickstart: '', see QueryBuilder queryPatterns below
explain: 'data-testid prometheus explain switch wrapper',
editorToggle: 'data-testid QueryEditorModeToggle', // wrapper for toggle
options: 'data-testid prometheus options', // wrapper for options group
legend: 'data-testid prometheus legend wrapper', // wrapper for multiple compomnents
format: 'data-testid prometheus format',
step: 'prometheus-step', // id for autosize component
type: 'data-testid prometheus type', //wrapper for radio button group
exemplars: 'prometheus-exemplars', // id for editor switch component
builder: {
// see QueryBuilder below for commented selectors
// labelSelect: 'data-testid Select label',
// valueSelect: 'data-testid Select value',
// matchOperatorSelect: 'data-testid Select match operator',
metricSelect: 'data-testid metric select',
hints: 'data-testid prometheus hints', // wrapper for hints component
metricsExplorer: 'data-testid metrics explorer',
queryAdvisor: 'data-testid query advisor',
},
code: {
queryField: 'data-testid prometheus query field',
metricsBrowser: {
openButton: 'data-testid open metrics browser',
selectMetric: 'data-testid select a metric',
metricList: 'data-testid metric list',
labelNamesFilter: 'data-testid label names filter',
labelValuesFilter: 'data-testid label values filter',
useQuery: 'data-testid use query',
useAsRateQuery: 'data-testid use as rate query',
validateSelector: 'data-testid validate selector',
clear: 'data-testid clear',
},
},
},
exemplarMarker: 'data-testid Exemplar marker',
variableQueryEditor: {
queryType: 'data-testid query type',
labelnames: {
metricRegex: 'data-testid label names metric regex',
},
labelValues: {
labelSelect: 'data-testid label values label select',
// metric select see queryEditor: builder for more context
// label select for metric filtering see queryEditor: builder for more context
},
metricNames: {
metricRegex: 'data-testid metric names metric regex',
},
varQueryResult: 'data-testid variable query result',
seriesQuery: 'data-testid prometheus series query',
classicQuery: 'data-testid prometheus classic query',
},
annotations: {
minStep: 'prometheus-annotation-min-step', // id for autosize input
title: 'data-testid prometheus annotation title',
tags: 'data-testid prometheus annotation tags',
text: 'data-testid prometheus annotation text',
seriesValueAsTimestamp: 'data-testid prometheus annotation series value as timestamp',
},
},
},
Menu: {

View File

@ -1,6 +1,7 @@
import React from 'react';
import { AnnotationQuery } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { EditorField, EditorRow, EditorRows, EditorSwitch, Space } from '@grafana/experimental';
import { AutoSizeInput, Input } from '@grafana/ui';
@ -56,6 +57,7 @@ export function AnnotationQueryEditor(props: Props) {
});
}}
defaultValue={query.interval}
id={selectors.components.DataSource.Prometheus.annotations.minStep}
/>
</EditorField>
</EditorRow>
@ -78,6 +80,7 @@ export function AnnotationQueryEditor(props: Props) {
titleFormat: event.currentTarget.value,
});
}}
data-testid={selectors.components.DataSource.Prometheus.annotations.title}
/>
</EditorField>
<EditorField label="Tags">
@ -91,6 +94,7 @@ export function AnnotationQueryEditor(props: Props) {
tagKeys: event.currentTarget.value,
});
}}
data-testid={selectors.components.DataSource.Prometheus.annotations.tags}
/>
</EditorField>
<EditorField
@ -109,6 +113,7 @@ export function AnnotationQueryEditor(props: Props) {
textFormat: event.currentTarget.value,
});
}}
data-testid={selectors.components.DataSource.Prometheus.annotations.text}
/>
</EditorField>
<EditorField
@ -125,6 +130,7 @@ export function AnnotationQueryEditor(props: Props) {
useValueForTime: event.currentTarget.value,
});
}}
data-testid={selectors.components.DataSource.Prometheus.annotations.seriesValueAsTimestamp}
/>
</EditorField>
</EditorRow>

View File

@ -2,6 +2,7 @@ import { cx } from '@emotion/css';
import React, { ReactNode } from 'react';
import { isDataFrame, QueryEditorProps, QueryHint, TimeRange, toLegacyResponseData } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { reportInteraction } from '@grafana/runtime';
import { Icon, Themeable2, withTheme2, clearButtonStyles } from '@grafana/ui';
import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider';
@ -230,6 +231,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
onClick={this.onClickChooserButton}
disabled={buttonDisabled}
type="button"
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.openButton}
>
{chooserText}
<Icon name={labelBrowserVisible ? 'angle-down' : 'angle-right'} />

View File

@ -3,6 +3,7 @@ import React, { ChangeEvent } from 'react';
import { FixedSizeList } from 'react-window';
import { GrafanaTheme2, TimeRange } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import {
Button,
HorizontalGroup,
@ -493,9 +494,14 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
onChange={this.onChangeMetricSearch}
aria-label="Filter expression for metric"
value={metricSearchTerm}
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.selectMetric}
/>
</div>
<div role="list" className={styles.valueListWrapper}>
<div
role="list"
className={styles.valueListWrapper}
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.metricList}
>
<FixedSizeList
height={Math.min(450, metricCount * LIST_ITEM_SIZE)}
itemCount={metricCount}
@ -537,6 +543,9 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
onChange={this.onChangeLabelSearch}
aria-label="Filter expression for label"
value={labelSearchTerm}
data-testid={
selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.labelNamesFilter
}
/>
</div>
{/* Using fixed height here to prevent jumpy layout */}
@ -564,6 +573,9 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
onChange={this.onChangeValueSearch}
aria-label="Filter expression for label values"
value={valueSearchTerm}
data-testid={
selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.labelValuesFilter
}
/>
</div>
<div className={styles.valueListArea} ref={this.valueListsRef}>
@ -625,10 +637,16 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
</div>
{validationStatus && <div className={styles.validationStatus}>{validationStatus}</div>}
<HorizontalGroup>
<Button aria-label="Use selector for query button" disabled={empty} onClick={this.onClickRunQuery}>
<Button
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.useQuery}
aria-label="Use selector for query button"
disabled={empty}
onClick={this.onClickRunQuery}
>
Use query
</Button>
<Button
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.useAsRateQuery}
aria-label="Use selector as metrics button"
variant="secondary"
disabled={empty}
@ -637,6 +655,7 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
Use as rate query
</Button>
<Button
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.validateSelector}
aria-label="Validate submit button"
variant="secondary"
disabled={empty}
@ -644,7 +663,12 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
>
Validate selector
</Button>
<Button aria-label="Selector clear button" variant="secondary" onClick={this.onClickClear}>
<Button
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.clear}
aria-label="Selector clear button"
variant="secondary"
onClick={this.onClickClear}
>
Clear
</Button>
<div className={cx(styles.status, (status || error) && styles.statusShowing)}>

View File

@ -1,6 +1,7 @@
import React, { FormEvent, useCallback, useEffect, useState } from 'react';
import { QueryEditorProps, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { InlineField, InlineFieldRow, Input, Select, TextArea } from '@grafana/ui';
import { PrometheusDatasource } from '../datasource';
@ -243,6 +244,7 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }:
value={qryType}
options={variableOptions}
width={25}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.queryType}
/>
</InlineField>
</InlineFieldRow>
@ -269,6 +271,7 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }:
width={25}
allowCustomValue
isClearable={true}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.labelValues.labelSelect}
/>
</InlineField>
</InlineFieldRow>
@ -303,6 +306,7 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }:
setLabelNamesMatch(e.currentTarget.value);
}}
width={25}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.labelnames.metricRegex}
/>
</InlineField>
</InlineFieldRow>
@ -329,6 +333,7 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }:
onMetricChange(e.currentTarget.value);
}}
width={25}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.metricNames.metricRegex}
/>
</InlineField>
</InlineFieldRow>
@ -358,6 +363,7 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }:
}
}}
cols={100}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.varQueryResult}
/>
</InlineField>
</InlineFieldRow>
@ -389,6 +395,7 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }:
}
}}
width={100}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.seriesQuery}
/>
</InlineField>
</InlineFieldRow>
@ -418,6 +425,7 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }:
}
}}
width={100}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.classicQuery}
/>
</InlineField>
</InlineFieldRow>

View File

@ -2,6 +2,7 @@ import { cx } from '@emotion/css';
import React from 'react';
import { DataSourceJsonData, DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { ConfigSubSection } from '@grafana/experimental';
import { config } from '@grafana/runtime';
import { InlineField, Switch, useTheme2 } from '@grafana/ui';
@ -53,6 +54,7 @@ export function AlertingSettingsOverhaul<T extends AlertingConfig>({
jsonData: { ...options.jsonData, manageAlerts: event!.currentTarget.checked },
})
}
id={selectors.components.DataSource.Prometheus.configPage.manageAlerts}
/>
</InlineField>
</div>

View File

@ -8,6 +8,7 @@ import {
SelectableValue,
updateDatasourcePluginJsonDataOption,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { ConfigSubSection } from '@grafana/experimental';
import { getBackendSrv } from '@grafana/runtime/src';
import { InlineField, Input, Select, Switch, useTheme2 } from '@grafana/ui';
@ -202,6 +203,7 @@ export const PromSettings = (props: Props) => {
timeInterval: e.currentTarget.value,
})
}
data-testid={selectors.components.DataSource.Prometheus.configPage.scrapeInterval}
/>
{validateInput(validDuration.timeInterval, DURATION_REGEX, durationError)}
</>
@ -231,6 +233,7 @@ export const PromSettings = (props: Props) => {
queryTimeout: e.currentTarget.value,
})
}
data-testid={selectors.components.DataSource.Prometheus.configPage.queryTimeout}
/>
{validateInput(validDuration.queryTimeout, DURATION_REGEX, durationError)}
</>
@ -259,6 +262,7 @@ export const PromSettings = (props: Props) => {
}
onChange={onChangeHandler('defaultEditor', options, onOptionsChange)}
width={40}
data-testid={selectors.components.DataSource.Prometheus.configPage.defaultEditor}
/>
</InlineField>
</div>
@ -280,6 +284,7 @@ export const PromSettings = (props: Props) => {
<Switch
value={options.jsonData.disableMetricsLookup ?? false}
onChange={onUpdateDatasourceJsonDataOptionChecked(props, 'disableMetricsLookup')}
id={selectors.components.DataSource.Prometheus.configPage.disableMetricLookup}
/>
</InlineField>
</div>
@ -338,6 +343,7 @@ export const PromSettings = (props: Props) => {
}
)}
width={40}
data-testid={selectors.components.DataSource.Prometheus.configPage.prometheusType}
/>
</InlineField>
</div>
@ -365,6 +371,7 @@ export const PromSettings = (props: Props) => {
)}
onChange={onChangeHandler('prometheusVersion', options, onOptionsChange)}
width={40}
data-testid={selectors.components.DataSource.Prometheus.configPage.prometheusVersion}
/>
</InlineField>
</div>
@ -392,6 +399,7 @@ export const PromSettings = (props: Props) => {
value={
cacheValueOptions.find((o) => o.value === options.jsonData.cacheLevel) ?? PrometheusCacheLevel.Low
}
data-testid={selectors.components.DataSource.Prometheus.configPage.cacheLevel}
/>
</InlineField>
</div>
@ -416,6 +424,7 @@ export const PromSettings = (props: Props) => {
<Switch
value={options.jsonData.incrementalQuerying ?? false}
onChange={onUpdateDatasourceJsonDataOptionChecked(props, 'incrementalQuerying')}
id={selectors.components.DataSource.Prometheus.configPage.incrementalQuerying}
/>
</InlineField>
</div>
@ -447,6 +456,7 @@ export const PromSettings = (props: Props) => {
value={options.jsonData.incrementalQueryOverlapWindow ?? defaultPrometheusQueryOverlapWindow}
onChange={onChangeHandler('incrementalQueryOverlapWindow', options, onOptionsChange)}
spellCheck={false}
data-testid={selectors.components.DataSource.Prometheus.configPage.queryOverlapWindow}
/>
{validateInput(validDuration.incrementalQueryOverlapWindow, MULTIPLE_DURATION_REGEX, durationError)}
</>
@ -467,6 +477,7 @@ export const PromSettings = (props: Props) => {
<Switch
value={options.jsonData.disableRecordingRules ?? false}
onChange={onUpdateDatasourceJsonDataOptionChecked(props, 'disableRecordingRules')}
id={selectors.components.DataSource.Prometheus.configPage.disableRecordingRules}
/>
</InlineField>
</div>
@ -496,6 +507,7 @@ export const PromSettings = (props: Props) => {
onChange={onChangeHandler('customQueryParameters', options, onOptionsChange)}
spellCheck={false}
placeholder="Example: max_source_resolution=5m&timeout=10"
data-testid={selectors.components.DataSource.Prometheus.configPage.customQueryParameters}
/>
</InlineField>
</div>
@ -522,6 +534,7 @@ export const PromSettings = (props: Props) => {
options={httpOptions}
value={httpOptions.find((o) => o.value === options.jsonData.httpMethod)}
onChange={onChangeHandler('httpMethod', options, onOptionsChange)}
data-testid={selectors.components.DataSource.Prometheus.configPage.httpMethod}
/>
</InlineField>
</div>

View File

@ -4,6 +4,7 @@ import React, { RefCallback, useCallback, useState } from 'react';
import Highlighter from 'react-highlight-words';
import { GrafanaTheme2, SelectableValue, toOption } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { EditorField, EditorFieldGroup } from '@grafana/experimental';
import { config } from '@grafana/runtime';
import {
@ -188,6 +189,7 @@ export function MetricSelect({
{...props.innerProps}
ref={props.innerRef}
className={`${styles.customOptionWidth} metric-encyclopedia-open`}
aria-label="Select option"
onKeyDown={(e) => {
// if there is no metric and the m.e. is enabled, open the modal
if (e.code === 'Enter') {
@ -259,6 +261,7 @@ export function MetricSelect({
const asyncSelect = () => {
return (
<AsyncSelect
data-testid={selectors.components.DataSource.Prometheus.queryEditor.builder.metricSelect}
isClearable={variableEditor ? true : false}
inputId="prometheus-metric-select"
className={styles.select}

View File

@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import React, { useEffect, useState } from 'react';
import { DataSourceApi, PanelData } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { EditorRow } from '@grafana/experimental';
import { config } from '@grafana/runtime';
import { Drawer } from '@grafana/ui';
@ -111,14 +112,16 @@ export const PromQueryBuilder = React.memo<Props>((props) => {
<QueryAssistantButton llmAppEnabled={llmAppEnabled} metric={query.metric} setShowDrawer={setShowDrawer} />
</div>
)}
<QueryBuilderHints<PromVisualQuery>
datasource={datasource}
query={query}
onChange={onChange}
data={data}
queryModeller={promQueryModeller}
buildVisualQueryFromString={buildVisualQueryFromString}
/>
<div data-testid={selectors.components.DataSource.Prometheus.queryEditor.builder.hints}>
<QueryBuilderHints<PromVisualQuery>
datasource={datasource}
query={query}
onChange={onChange}
data={data}
queryModeller={promQueryModeller}
buildVisualQueryFromString={buildVisualQueryFromString}
/>
</div>
</OperationsEditorRow>
{showExplain && (
<OperationListExplained<PromVisualQuery>

View File

@ -1,6 +1,7 @@
import React, { SyntheticEvent } from 'react';
import { CoreApp, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { EditorField, EditorRow, EditorSwitch } from '@grafana/experimental';
import { AutoSizeInput, RadioButtonGroup, Select } from '@grafana/ui';
@ -61,56 +62,69 @@ export const PromQueryBuilderOptions = React.memo<Props>(({ query, app, onChange
return (
<EditorRow>
<QueryOptionGroup
title="Options"
collapsedInfo={getCollapsedInfo(query, formatOption.label!, queryTypeLabel, app)}
>
<PromQueryLegendEditor
legendFormat={query.legendFormat}
onChange={(legendFormat) => onChange({ ...query, legendFormat })}
onRunQuery={onRunQuery}
/>
<EditorField
label="Min step"
tooltip={
<>
An additional lower limit for the step parameter of the Prometheus query and for the{' '}
<code>$__interval</code> and <code>$__rate_interval</code> variables.
</>
}
<div data-testid={selectors.components.DataSource.Prometheus.queryEditor.options}>
<QueryOptionGroup
title="Options"
collapsedInfo={getCollapsedInfo(query, formatOption.label!, queryTypeLabel, app)}
>
<AutoSizeInput
type="text"
aria-label="Set lower limit for the step parameter"
placeholder={'auto'}
minWidth={10}
onCommitChange={onChangeStep}
defaultValue={query.interval}
<PromQueryLegendEditor
legendFormat={query.legendFormat}
onChange={(legendFormat) => onChange({ ...query, legendFormat })}
onRunQuery={onRunQuery}
/>
</EditorField>
<EditorField label="Format">
<Select value={formatOption} allowCustomValue onChange={onChangeFormat} options={FORMAT_OPTIONS} />
</EditorField>
<EditorField label="Type">
<RadioButtonGroup options={queryTypeOptions} value={queryTypeValue} onChange={onQueryTypeChange} />
</EditorField>
{shouldShowExemplarSwitch(query, app) && (
<EditorField label="Exemplars">
<EditorSwitch value={query.exemplar || false} onChange={onExemplarChange} />
</EditorField>
)}
{query.intervalFactor && query.intervalFactor > 1 && (
<EditorField label="Resolution">
<Select
aria-label="Select resolution"
isSearchable={false}
options={INTERVAL_FACTOR_OPTIONS}
onChange={onIntervalFactorChange}
value={INTERVAL_FACTOR_OPTIONS.find((option) => option.value === query.intervalFactor)}
<EditorField
label="Min step"
tooltip={
<>
An additional lower limit for the step parameter of the Prometheus query and for the{' '}
<code>$__interval</code> and <code>$__rate_interval</code> variables.
</>
}
>
<AutoSizeInput
id={selectors.components.DataSource.Prometheus.queryEditor.step}
type="text"
aria-label="Set lower limit for the step parameter"
placeholder={'auto'}
minWidth={10}
onCommitChange={onChangeStep}
defaultValue={query.interval}
/>
</EditorField>
)}
</QueryOptionGroup>
<EditorField label="Format">
<Select
data-testid={selectors.components.DataSource.Prometheus.queryEditor.format}
value={formatOption}
allowCustomValue
onChange={onChangeFormat}
options={FORMAT_OPTIONS}
/>
</EditorField>
<EditorField label="Type" data-testid={selectors.components.DataSource.Prometheus.queryEditor.type}>
<RadioButtonGroup options={queryTypeOptions} value={queryTypeValue} onChange={onQueryTypeChange} />
</EditorField>
{shouldShowExemplarSwitch(query, app) && (
<EditorField label="Exemplars">
<EditorSwitch
value={query.exemplar || false}
onChange={onExemplarChange}
id={selectors.components.DataSource.Prometheus.queryEditor.exemplars}
/>
</EditorField>
)}
{query.intervalFactor && query.intervalFactor > 1 && (
<EditorField label="Resolution">
<Select
aria-label="Select resolution"
isSearchable={false}
options={INTERVAL_FACTOR_OPTIONS}
onChange={onIntervalFactorChange}
value={INTERVAL_FACTOR_OPTIONS.find((option) => option.value === query.intervalFactor)}
/>
</EditorField>
)}
</QueryOptionGroup>
</div>
</EditorRow>
);
});

View File

@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { useStyles2 } from '@grafana/ui';
import PromQueryField from '../../components/PromQueryField';
@ -18,7 +19,10 @@ export function PromQueryCodeEditor(props: Props) {
const styles = useStyles2(getStyles);
return (
<div className={styles.wrapper}>
<div
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.queryField}
className={styles.wrapper}
>
<PromQueryField
datasource={datasource}
query={query}

View File

@ -123,7 +123,9 @@ export const PromQueryEditorSelector = React.memo<Props>((props) => {
>
Kick start your query
</Button>
<QueryHeaderSwitch label="Explain" value={explain} onChange={onShowExplainChange} />
<div data-testid={selectors.components.DataSource.Prometheus.queryEditor.explain}>
<QueryHeaderSwitch label="Explain" value={explain} onChange={onShowExplainChange} />
</div>
<FlexItem grow={1} />
{app !== CoreApp.Explore && app !== CoreApp.Correlations && (
<Button
@ -136,7 +138,9 @@ export const PromQueryEditorSelector = React.memo<Props>((props) => {
Run queries
</Button>
)}
<QueryEditorModeToggle mode={editorMode} onChange={onEditorModeChange} />
<div data-testid={selectors.components.DataSource.Prometheus.queryEditor.editorToggle}>
<QueryEditorModeToggle mode={editorMode} onChange={onEditorModeChange} />
</div>
</EditorHeader>
<Space v={0.5} />
<EditorRows>

View File

@ -1,6 +1,7 @@
import React, { useRef } from 'react';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { EditorField } from '@grafana/experimental';
import { Select, AutoSizeInput } from '@grafana/ui';
@ -64,6 +65,7 @@ export const PromQueryLegendEditor = React.memo<Props>(({ legendFormat, onChange
<EditorField
label="Legend"
tooltip="Series name override or template. Ex. {{hostname}} will be replaced with label value for hostname."
data-testid={selectors.components.DataSource.Prometheus.queryEditor.legend}
>
<>
{mode === LegendFormatMode.Custom && (

View File

@ -3,6 +3,7 @@ import debounce from 'debounce-promise';
import React, { useCallback, useEffect, useMemo, useReducer } from 'react';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import {
Input,
Modal,
@ -204,7 +205,10 @@ export const MetricsModal = (props: MetricsModalProps) => {
className={styles.modal}
>
<FeedbackLink feedbackUrl="https://forms.gle/DEMAJHoAMpe3e54CA" />
<div className={styles.inputWrapper}>
<div
className={styles.inputWrapper}
data-testid={selectors.components.DataSource.Prometheus.queryEditor.builder.metricsExplorer}
>
<div className={cx(styles.inputItem, styles.inputItemFirst)}>
<Input
autoFocus={true}

View File

@ -1,5 +1,6 @@
import React from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { reportInteraction } from '@grafana/runtime';
import { Button, Tooltip, useTheme2 } from '@grafana/ui';
@ -32,6 +33,7 @@ export function QueryAssistantButton(props: Props) {
setShowDrawer(true);
}}
disabled={!metric || !llmAppEnabled}
data-testid={selectors.components.DataSource.Prometheus.queryEditor.builder.queryAdvisor}
>
<img height={16} src={AI_Logo_color} alt="AI logo black and white" />
{'\u00A0'}Get query suggestions