Prometheus: Configuration page overhaul (#66198)

* organize layout, make design uniform, add doc link

* fix e2e test

* move overhauled config parts into prometheus code

* update tooltips with doc links

* clean component styles for section padding, top and bottom 32px

* make additional settings subsection headers h6

* use secondary gray for section descriptions

* fix merge issues

* change inlineswitch to switch only in prom settings because the other components are shared

* remove legacy formfield and input, replace with inlinefield and input from grafana-ui

* find more formfield and UI design fixes like changing inlineformlabel to inlinefield

* remove unused inline form label

* replace legacy duration validations with <FieldValidationMessage>

* fix styles secondary gray from theme

* change language, headings and datasource -> data source

* update alert setting styles with new component

* update prom url heading and tooltip

* update default editor tooltip and set builder as default editor

* update interval tooltip

* update prom type tooltip

* update custom query params tooltip

* update exemplar internal link tooltip

* fix inline form styling inconsistency

* allow for using the DataSourceHTTPSettings component without the connection string

* remove overhaul component, re-use dshtttps comp, and use connection input in config editor

* make tooltips interactive to click links

* consistent label width across the elements we can control for now, fix exemplar switch

* make connection url a component

* refactor onBlur validation

* remove unused component

* add tests for config validations

* add more meaningful health check

* fix e2e test

* fix e2e test

* fix e2e test

* add error handling for more url errors

* remove unnecessary conversion

* health check tests

* Clean up the health check

* health check unit tests

* health check unit tests improved

* make pretty for drone

* lint check go

* lint check go

* add required attr to connection component

* Update public/app/plugins/datasource/prometheus/configuration/Connection.tsx

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>

* fix read only issue for provisioned datasources

* validate multiple durations for incremental query setting

* use sentence case for headers

* use className consistently for styles

* add tests for url regex

* remove console logs

* only use query as healthcheck as the healthy api is not supported by Mimir

* fix exemplar e2e test

* remove overhaul prop from custom headers setting component

* remove connection section and use DatasourceHttpSettings connection with custom label and interactive tooltip

* add spaces back

* spaces

---------

Co-authored-by: ismail simsek <ismailsimsek09@gmail.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
Brendan O'Handley 2023-04-27 09:43:54 -04:00 committed by GitHub
parent 5c32925f9f
commit 0a9240aeba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 818 additions and 345 deletions

View File

@ -4753,7 +4753,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "19"],
[0, 0, 0, "Unexpected any. Specify a different type.", "20"],
[0, 0, 0, "Unexpected any. Specify a different type.", "21"],
[0, 0, 0, "Do not use any type assertions.", "22"],
[0, 0, 0, "Unexpected any. Specify a different type.", "22"],
[0, 0, 0, "Unexpected any. Specify a different type.", "23"],
[0, 0, 0, "Unexpected any. Specify a different type.", "24"],
[0, 0, 0, "Unexpected any. Specify a different type.", "25"],
@ -4762,9 +4762,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "28"],
[0, 0, 0, "Unexpected any. Specify a different type.", "29"],
[0, 0, 0, "Unexpected any. Specify a different type.", "30"],
[0, 0, 0, "Unexpected any. Specify a different type.", "31"],
[0, 0, 0, "Unexpected any. Specify a different type.", "32"],
[0, 0, 0, "Unexpected any. Specify a different type.", "33"]
[0, 0, 0, "Unexpected any. Specify a different type.", "31"]
],
"public/app/plugins/datasource/prometheus/language_provider.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],

View File

@ -4,7 +4,7 @@ const dataSourceName = 'PromExemplar';
const addDataSource = () => {
e2e.flows.addDataSource({
type: 'Prometheus',
expectedAlertMessage: 'Error reading Prometheus',
expectedAlertMessage: 'saved',
name: dataSourceName,
form: () => {
e2e.components.DataSource.Prometheus.configPage.exemplarsAddButton().click();

View File

@ -73,6 +73,7 @@ export const DataSourceHttpSettings = (props: HttpSettingsProps) => {
azureAuthSettings,
renderSigV4Editor,
secureSocksDSProxyEnabled,
connectionElements,
} = props;
let urlTooltip;
const [isAccessHelpVisible, setIsAccessHelpVisible] = useState(false);
@ -93,6 +94,7 @@ export const DataSourceHttpSettings = (props: HttpSettingsProps) => {
urlTooltip = (
<>
Your access method is <em>Browser</em>, this means the URL needs to be accessible from the browser.
{connectionElements?.tooltip}
</>
);
break;
@ -101,11 +103,12 @@ export const DataSourceHttpSettings = (props: HttpSettingsProps) => {
<>
Your access method is <em>Server</em>, this means the URL needs to be accessible from the grafana
backend/server.
{connectionElements?.tooltip}
</>
);
break;
default:
urlTooltip = 'Specify a complete HTTP URL (for example http://your_server:8080)';
urlTooltip = <>Specify a complete HTTP URL (for example http://your_server:8080) {connectionElements?.tooltip}</>;
}
const accessSelect = (
@ -143,14 +146,24 @@ export const DataSourceHttpSettings = (props: HttpSettingsProps) => {
const azureAuthEnabled: boolean =
(azureAuthSettings?.azureAuthSupported && azureAuthSettings.getAzureAuthEnabled(dataSourceConfig)) || false;
const connectionLabel = connectionElements?.label ? connectionElements?.label : 'URL';
return (
<div className="gf-form-group">
<>
<h3 className="page-heading">HTTP</h3>
<div className="gf-form-group">
<div className="gf-form">
<FormField label="URL" labelWidth={13} tooltip={urlTooltip} inputEl={urlInput} />
</div>
{defaultUrl && (
<div className="gf-form">
<FormField
interactive={connectionElements?.tooltip ? true : false}
label={connectionLabel}
labelWidth={13}
tooltip={urlTooltip}
inputEl={urlInput}
/>
</div>
)}
{showAccessOptions && (
<>

View File

@ -30,7 +30,7 @@ export interface HttpSettingsBaseProps<JSONData extends DataSourceJsonData = any
export interface HttpSettingsProps extends HttpSettingsBaseProps {
/** The default url for the data source */
defaultUrl: string;
defaultUrl?: string;
/** Show the http access help box */
showAccessOptions?: boolean;
/** Show the SigV4 auth toggle option */
@ -41,4 +41,9 @@ export interface HttpSettingsProps extends HttpSettingsBaseProps {
renderSigV4Editor?: React.ReactNode;
/** Show the Secure Socks Datasource Proxy toggle option */
secureSocksDSProxyEnabled?: boolean;
/** connection URL label and tooltip */
connectionElements?: {
label?: string;
tooltip?: React.ReactNode;
};
}

View File

@ -0,0 +1,93 @@
package prometheus
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/kindsys"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/tsdb/prometheus/kinds/dataquery"
"github.com/grafana/grafana/pkg/tsdb/prometheus/models"
)
const (
refID = "__healthcheck__"
)
var logger log.Logger = log.New("tsdb.prometheus")
func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult,
error) {
logger := logger.FromContext(ctx)
ds, err := s.getInstance(req.PluginContext)
// check that the datasource exists
if err != nil {
return getHealthCheckMessage(logger, "error getting datasource info", err)
}
if ds == nil {
return getHealthCheckMessage(logger, "", errors.New("invalid datasource info received"))
}
return healthcheck(ctx, req, ds)
}
func healthcheck(ctx context.Context, req *backend.CheckHealthRequest, i *instance) (*backend.CheckHealthResult, error) {
qm := models.QueryModel{
LegendFormat: "",
UtcOffsetSec: 0,
PrometheusDataQuery: dataquery.PrometheusDataQuery{
Expr: "1+1",
Instant: kindsys.Ptr(true),
RefId: refID,
},
}
b, _ := json.Marshal(&qm)
query := backend.DataQuery{
RefID: refID,
TimeRange: backend.TimeRange{
From: time.Unix(1, 0).UTC(),
To: time.Unix(4, 0).UTC(),
},
JSON: b,
}
resp, err := i.queryData.Execute(ctx, &backend.QueryDataRequest{
PluginContext: req.PluginContext,
Queries: []backend.DataQuery{query},
})
if err != nil {
return getHealthCheckMessage(logger, "Your configuration has been saved but there is an error.", err)
}
if resp.Responses[refID].Error != nil {
return getHealthCheckMessage(logger, "Your configuration has been saved but there is an error.",
errors.New(resp.Responses[refID].Error.Error()))
}
return getHealthCheckMessage(logger, "Successfully saved the configuration and queried the Prometheus API.", nil)
}
func getHealthCheckMessage(logger log.Logger, message string, err error) (*backend.CheckHealthResult, error) {
if err == nil {
return &backend.CheckHealthResult{
Status: backend.HealthStatusOk,
Message: message,
}, nil
}
logger.Warn("error performing prometheus healthcheck", "err", err.Error())
errorMessage := fmt.Sprintf("%s - %s", err.Error(), message)
return &backend.CheckHealthResult{
Status: backend.HealthStatusError,
Message: errorMessage,
}, nil
}

View File

@ -0,0 +1,125 @@
package prometheus
import (
"context"
"net/http"
"testing"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
sdkHttpClient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
)
type healthCheckProvider[T http.RoundTripper] struct {
httpclient.Provider
RoundTripper *T
}
type healthCheckSuccessRoundTripper struct {
}
type healthCheckFailRoundTripper struct {
}
func (rt *healthCheckSuccessRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return &http.Response{
Status: "200",
StatusCode: 200,
Header: nil,
Body: nil,
ContentLength: 0,
Request: req,
}, nil
}
func (rt *healthCheckFailRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return &http.Response{
Status: "400",
StatusCode: 400,
Header: nil,
Body: nil,
ContentLength: 0,
Request: req,
}, nil
}
func (provider *healthCheckProvider[T]) New(opts ...sdkHttpClient.Options) (*http.Client, error) {
client := &http.Client{}
provider.RoundTripper = new(T)
client.Transport = *provider.RoundTripper
return client, nil
}
func (provider *healthCheckProvider[T]) GetTransport(opts ...sdkHttpClient.Options) (http.RoundTripper, error) {
return *new(T), nil
}
func getMockProvider[T http.RoundTripper]() *healthCheckProvider[T] {
return &healthCheckProvider[T]{
RoundTripper: new(T),
}
}
func Test_healthcheck(t *testing.T) {
t.Run("should do a successful health check", func(t *testing.T) {
httpProvider := getMockProvider[*healthCheckSuccessRoundTripper]()
s := &Service{
im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, &setting.Cfg{}, &featuremgmt.FeatureManager{}, nil)),
}
req := &backend.CheckHealthRequest{
PluginContext: getPluginContext(),
Headers: nil,
}
res, err := s.CheckHealth(context.Background(), req)
assert.NoError(t, err)
assert.Equal(t, backend.HealthStatusOk, res.Status)
})
t.Run("should return an error for an unsuccessful health check", func(t *testing.T) {
httpProvider := getMockProvider[*healthCheckFailRoundTripper]()
s := &Service{
im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, &setting.Cfg{}, &featuremgmt.FeatureManager{}, nil)),
}
req := &backend.CheckHealthRequest{
PluginContext: getPluginContext(),
Headers: nil,
}
res, err := s.CheckHealth(context.Background(), req)
assert.NoError(t, err)
assert.Equal(t, backend.HealthStatusError, res.Status)
})
}
func getPluginContext() backend.PluginContext {
return backend.PluginContext{
OrgID: 0,
PluginID: "prometheus",
User: nil,
AppInstanceSettings: nil,
DataSourceInstanceSettings: getPromInstanceSettings(),
}
}
func getPromInstanceSettings() *backend.DataSourceInstanceSettings {
return &backend.DataSourceInstanceSettings{
ID: 0,
UID: "",
Type: "prometheus",
Name: "test-prometheus",
URL: "http://promurl:9090",
User: "",
Database: "",
BasicAuthEnabled: true,
BasicAuthUser: "admin",
JSONData: []byte("{}"),
DecryptedSecureJSONData: map[string]string{},
Updated: time.Time{},
}
}

View File

@ -179,6 +179,13 @@ func (s *QueryData) instantQuery(ctx context.Context, c *client.Client, q *model
}
}
// This is only for health check fall back scenario
if res.StatusCode != 200 && q.RefId == "__healthcheck__" {
return backend.DataResponse{
Error: fmt.Errorf(res.Status),
}
}
defer func() {
err := res.Body.Close()
if err != nil {

View File

@ -0,0 +1,56 @@
import React from 'react';
import { DataSourceJsonData, DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { InlineField, Switch, useTheme2 } from '@grafana/ui';
import { docsTip, overhaulStyles } from './ConfigEditor';
export interface Props<T extends DataSourceJsonData>
extends Pick<DataSourcePluginOptionsEditorProps<T>, 'options' | 'onOptionsChange'> {}
export interface AlertingConfig extends DataSourceJsonData {
manageAlerts?: boolean;
}
export function AlertingSettingsOverhaul<T extends AlertingConfig>({
options,
onOptionsChange,
}: Props<T>): JSX.Element {
const theme = useTheme2();
const styles = overhaulStyles(theme);
return (
<>
<h6 className="page-heading">Alerting</h6>
<div className="gf-form-group">
<div className="gf-form-inline">
<div className="gf-form">
<InlineField
labelWidth={30}
label="Manage alerts via Alerting UI"
disabled={options.readOnly}
tooltip={
<>
Manage alert rules for this data source. To manage other alerting resources, add an Alertmanager data
source. {docsTip()}
</>
}
interactive={true}
className={styles.switchField}
>
<Switch
value={options.jsonData.manageAlerts !== false}
onChange={(event) =>
onOptionsChange({
...options,
jsonData: { ...options.jsonData, manageAlerts: event!.currentTarget.checked },
})
}
/>
</InlineField>
</div>
</div>
</div>
</>
);
}

View File

@ -45,7 +45,7 @@ export const AzureAuthSettings = (props: HttpSettingsBaseProps) => {
return (
<>
<h6>Azure Authentication</h6>
<h6>Azure authentication</h6>
<AzureCredentialsForm
managedIdentityEnabled={config.azure.managedIdentityEnabled}
credentials={credentials}
@ -55,7 +55,7 @@ export const AzureAuthSettings = (props: HttpSettingsBaseProps) => {
/>
{overrideAudienceAllowed && (
<>
<h6>Azure Configuration</h6>
<h6>Azure configuration</h6>
<div className="gf-form-group">
<InlineFieldRow>
<InlineField labelWidth={26} label="Override AAD audience" disabled={dataSourceConfig.readOnly}>

View File

@ -0,0 +1,93 @@
import React from 'react';
import { FieldValidationMessage } from '@grafana/ui';
import { validateInput } from './ConfigEditor';
import { DURATION_REGEX, MULTIPLE_DURATION_REGEX } from './PromSettings';
const VALID_URL_REGEX = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
const error = <FieldValidationMessage>Value is not valid</FieldValidationMessage>;
// replaces promSettingsValidationEvents to display a <FieldValidationMessage> onBlur for duration input errors
describe('promSettings validateInput', () => {
it.each`
value | expected
${'1ms'} | ${true}
${'1M'} | ${true}
${'1w'} | ${true}
${'1d'} | ${true}
${'1h'} | ${true}
${'1m'} | ${true}
${'1s'} | ${true}
${'1y'} | ${true}
`(
"Single duration regex, when calling the rule with correct formatted value: '$value' then result should be '$expected'",
({ value, expected }) => {
expect(validateInput(value, DURATION_REGEX)).toBe(expected);
}
);
it.each`
value | expected
${'1M 2s'} | ${true}
${'1w 2d'} | ${true}
${'1d 2m'} | ${true}
${'1h 2m'} | ${true}
${'1m 2s'} | ${true}
`(
"Multiple duration regex, when calling the rule with correct formatted value: '$value' then result should be '$expected'",
({ value, expected }) => {
expect(validateInput(value, MULTIPLE_DURATION_REGEX)).toBe(expected);
}
);
it.each`
value | expected
${'1 ms'} | ${error}
${'1x'} | ${error}
${' '} | ${error}
${'w'} | ${error}
${'1.0s'} | ${error}
`(
"when calling the rule with incorrect formatted value: '$value' then result should be '$expected'",
({ value, expected }) => {
expect(validateInput(value, DURATION_REGEX)).toStrictEqual(expected);
}
);
it.each`
value | expected
${'frp://'} | ${error}
${'htp://'} | ${error}
${'httpss:??'} | ${error}
${'http@//'} | ${error}
${'http:||'} | ${error}
${'http://'} | ${error}
${'https://'} | ${error}
${'ftp://'} | ${error}
`(
"Url incorrect formatting, when calling the rule with correct formatted value: '$value' then result should be '$expected'",
({ value, expected }) => {
expect(validateInput(value, VALID_URL_REGEX)).toStrictEqual(expected);
}
);
it.each`
value | expected
${'ftp://example'} | ${true}
${'http://example'} | ${true}
${'https://example'} | ${true}
`(
"Url correct formatting, when calling the rule with correct formatted value: '$value' then result should be '$expected'",
({ value, expected }) => {
expect(validateInput(value, VALID_URL_REGEX)).toBe(expected);
}
);
it('should display a custom validation message', () => {
const invalidDuration = 'invalid';
const customMessage = 'This is invalid';
const errorWithCustomMessage = <FieldValidationMessage>{customMessage}</FieldValidationMessage>;
expect(validateInput(invalidDuration, DURATION_REGEX, customMessage)).toStrictEqual(errorWithCustomMessage);
});
});

View File

@ -1,19 +1,24 @@
import { css } from '@emotion/css';
import React, { useRef } from 'react';
import { SIGV4ConnectionConfig } from '@grafana/aws-sdk';
import { DataSourcePluginOptionsEditorProps, DataSourceSettings } from '@grafana/data';
import { AlertingSettings, DataSourceHttpSettings, Alert } from '@grafana/ui';
import { DataSourcePluginOptionsEditorProps, DataSourceSettings, GrafanaTheme2 } from '@grafana/data';
import { Alert, DataSourceHttpSettings, FieldValidationMessage, useTheme2 } from '@grafana/ui';
import { config } from 'app/core/config';
import { PromOptions } from '../types';
import { AlertingSettingsOverhaul } from './AlertingSettingsOverhaul';
import { AzureAuthSettings } from './AzureAuthSettings';
import { hasCredentials, setDefaultCredentials, resetCredentials } from './AzureCredentialsConfig';
import { PromSettings } from './PromSettings';
export const PROM_CONFIG_LABEL_WIDTH = 30;
export type Props = DataSourcePluginOptionsEditorProps<PromOptions>;
export const ConfigEditor = (props: Props) => {
const { options, onOptionsChange } = props;
// use ref so this is evaluated only first time it renders and the select does not disappear suddenly.
const showAccessOptions = useRef(props.options.access === 'direct');
@ -25,14 +30,16 @@ export const ConfigEditor = (props: Props) => {
azureSettingsUI: AzureAuthSettings,
};
const theme = useTheme2();
const styles = overhaulStyles(theme);
return (
<>
{options.access === 'direct' && (
<Alert title="Error" severity="error">
Browser access mode in the Prometheus datasource is no longer available. Switch to server access mode.
Browser access mode in the Prometheus data source is no longer available. Switch to server access mode.
</Alert>
)}
<DataSourceHttpSettings
defaultUrl="http://localhost:9090"
dataSourceConfig={options}
@ -42,11 +49,88 @@ export const ConfigEditor = (props: Props) => {
azureAuthSettings={azureAuthSettings}
renderSigV4Editor={<SIGV4ConnectionConfig {...props}></SIGV4ConnectionConfig>}
secureSocksDSProxyEnabled={config.secureSocksDSProxyEnabled}
connectionElements={{
label: 'Prometheus server URL',
tooltip: docsTip(),
}}
/>
<>
<hr className={styles.hrTopSpace} />
<h3 className={styles.sectionHeaderPadding}>Additional settings</h3>
<p className={`${styles.secondaryGrey} ${styles.subsectionText}`}>
Additional settings are optional settings that can be configured for more control over your data source.
</p>
<AlertingSettings<PromOptions> options={options} onOptionsChange={onOptionsChange} />
<AlertingSettingsOverhaul<PromOptions> options={options} onOptionsChange={onOptionsChange} />
<PromSettings options={options} onOptionsChange={onOptionsChange} />
<PromSettings options={options} onOptionsChange={onOptionsChange} />
</>
</>
);
};
/**
* Use this to return a url in a tooltip in a field. Don't forget to make the field interactive to be able to click on the tooltip
* @param url
* @returns
*/
export function docsTip(url?: string) {
const docsUrl = 'https://grafana.com/docs/grafana/latest/datasources/prometheus/#configure-the-data-source';
return (
<a href={url ? url : docsUrl} target="_blank" rel="noopener noreferrer">
Visit docs for more details here.
</a>
);
}
export const validateInput = (
input: string,
pattern: string | RegExp,
errorMessage?: string
): boolean | JSX.Element => {
const defaultErrorMessage = 'Value is not valid';
if (input && !input.match(pattern)) {
return <FieldValidationMessage>{errorMessage ? errorMessage : defaultErrorMessage}</FieldValidationMessage>;
} else {
return true;
}
};
export function overhaulStyles(theme: GrafanaTheme2) {
return {
additionalSettings: css`
margin-bottom: 25px;
`,
secondaryGrey: css`
color: ${theme.colors.secondary.text};
opacity: 65%;
`,
inlineError: css`
margin: 0px 0px 4px 245px;
`,
switchField: css`
align-items: center;
`,
sectionHeaderPadding: css`
padding-top: 32px;
`,
sectionBottomPadding: css`
padding-bottom: 28px;
`,
subsectionText: css`
font-size: 12px;
`,
hrBottomSpace: css`
margin-bottom: 56px;
`,
hrTopSpace: css`
margin-top: 50px;
`,
textUnderline: css`
text-decoration: underline;
`,
versionMargin: css`
margin-bottom: 12px;
`,
};
}

View File

@ -1,12 +1,13 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { DataSourcePicker } from '@grafana/runtime';
import { Button, InlineField, InlineSwitch, Input } from '@grafana/ui';
import { Button, InlineField, Input, Switch, useTheme2 } from '@grafana/ui';
import { ExemplarTraceIdDestination } from '../types';
import { docsTip, overhaulStyles, PROM_CONFIG_LABEL_WIDTH } from './ConfigEditor';
type Props = {
value: ExemplarTraceIdDestination;
onChange: (value: ExemplarTraceIdDestination) => void;
@ -17,38 +18,40 @@ type Props = {
export default function ExemplarSetting({ value, onChange, onDelete, disabled }: Props) {
const [isInternalLink, setIsInternalLink] = useState(Boolean(value.datasourceUid));
const theme = useTheme2();
const styles = overhaulStyles(theme);
return (
<div className="gf-form-group">
<InlineField label="Internal link" labelWidth={24} disabled={disabled}>
<InlineField
label="Internal link"
labelWidth={PROM_CONFIG_LABEL_WIDTH}
disabled={disabled}
tooltip={
<>
Enable this option if you have an internal link. When enabled, this reveals the data source selector. Select
the backend tracing data store for your exemplar data. {docsTip()}
</>
}
interactive={true}
className={styles.switchField}
>
<>
<InlineSwitch
<Switch
value={isInternalLink}
aria-label={selectors.components.DataSource.Prometheus.configPage.internalLinkSwitch}
onChange={(ev) => setIsInternalLink(ev.currentTarget.checked)}
/>
{!disabled && (
<Button
variant="destructive"
title="Remove link"
icon="times"
onClick={(event) => {
event.preventDefault();
onDelete();
}}
className={css`
margin-left: 8px;
`}
/>
)}
</>
</InlineField>
{isInternalLink ? (
<InlineField
label="Data source"
labelWidth={24}
tooltip="The data source the exemplar is going to navigate to."
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={<>The data source the exemplar is going to navigate to. {docsTip()}</>}
disabled={disabled}
interactive={true}
>
<DataSourcePicker
tracing={true}
@ -67,9 +70,10 @@ export default function ExemplarSetting({ value, onChange, onDelete, disabled }:
) : (
<InlineField
label="URL"
labelWidth={24}
tooltip="The URL of the trace backend the user would go to see its trace."
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={<>The URL of the trace backend the user would go to see its trace. {docsTip()}</>}
disabled={disabled}
interactive={true}
>
<Input
placeholder="https://example.com/${__value.raw}"
@ -89,9 +93,10 @@ export default function ExemplarSetting({ value, onChange, onDelete, disabled }:
<InlineField
label="URL Label"
labelWidth={24}
tooltip="Use to override the button label on the exemplar traceID field."
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={<>Use to override the button label on the exemplar traceID field. {docsTip()}</>}
disabled={disabled}
interactive={true}
>
<Input
placeholder="Go to example.com"
@ -108,9 +113,10 @@ export default function ExemplarSetting({ value, onChange, onDelete, disabled }:
</InlineField>
<InlineField
label="Label name"
labelWidth={24}
tooltip="The name of the field in the labels object that should be used to get the traceID."
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={<>The name of the field in the labels object that should be used to get the traceID. {docsTip()}</>}
disabled={disabled}
interactive={true}
>
<Input
placeholder="traceID"
@ -125,6 +131,19 @@ export default function ExemplarSetting({ value, onChange, onDelete, disabled }:
}
/>
</InlineField>
{!disabled && (
<InlineField label="Remove exemplar link" labelWidth={PROM_CONFIG_LABEL_WIDTH} disabled={disabled}>
<Button
variant="destructive"
title="Remove exemplar link"
icon="times"
onClick={(event) => {
event.preventDefault();
onDelete();
}}
/>
</InlineField>
)}
</div>
);
}

View File

@ -2,10 +2,11 @@ import { css } from '@emotion/css';
import React from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { Button } from '@grafana/ui';
import { Button, useTheme2 } from '@grafana/ui';
import { ExemplarTraceIdDestination } from '../types';
import { overhaulStyles } from './ConfigEditor';
import ExemplarSetting from './ExemplarSetting';
type Props = {
@ -15,9 +16,11 @@ type Props = {
};
export function ExemplarsSettings({ options, onChange, disabled }: Props) {
const theme = useTheme2();
const styles = overhaulStyles(theme);
return (
<>
<h3 className="page-heading">Exemplars</h3>
<div className={styles.sectionBottomPadding}>
<h6 className="page-heading">Exemplars</h6>
{options &&
options.map((option, index) => {
@ -57,8 +60,7 @@ export function ExemplarsSettings({ options, onChange, disabled }: Props) {
Add
</Button>
)}
{disabled && !options && <i>No exemplars configurations</i>}
</>
</div>
);
}

View File

@ -3,11 +3,10 @@ import React, { SyntheticEvent } from 'react';
import { Provider } from 'react-redux';
import { SelectableValue } from '@grafana/data';
import { EventsWithValidation } from '@grafana/ui';
import { configureStore } from '../../../../store/configureStore';
import { getValueFromEventItem, promSettingsValidationEvents, PromSettings } from './PromSettings';
import { getValueFromEventItem, PromSettings } from './PromSettings';
import { createDefaultConfigOptions } from './mocks';
describe('PromSettings', () => {
@ -38,58 +37,6 @@ describe('PromSettings', () => {
});
});
describe('promSettingsValidationEvents', () => {
const validationEvents = promSettingsValidationEvents;
it('should have one event handlers', () => {
expect(Object.keys(validationEvents).length).toEqual(1);
});
it('should have an onBlur handler', () => {
expect(validationEvents.hasOwnProperty(EventsWithValidation.onBlur)).toBe(true);
});
it('should have one rule', () => {
expect(validationEvents[EventsWithValidation.onBlur].length).toEqual(1);
});
describe('when calling the rule with an empty string', () => {
it('then it should return true', () => {
expect(validationEvents[EventsWithValidation.onBlur][0].rule('')).toBe(true);
});
});
it.each`
value | expected
${'1ms'} | ${true}
${'1M'} | ${true}
${'1w'} | ${true}
${'1d'} | ${true}
${'1h'} | ${true}
${'1m'} | ${true}
${'1s'} | ${true}
${'1y'} | ${true}
`(
"when calling the rule with correct formatted value: '$value' then result should be '$expected'",
({ value, expected }) => {
expect(validationEvents[EventsWithValidation.onBlur][0].rule(value)).toBe(expected);
}
);
it.each`
value | expected
${'1 ms'} | ${false}
${'1x'} | ${false}
${' '} | ${false}
${'w'} | ${false}
${'1.0s'} | ${false}
`(
"when calling the rule with incorrect formatted value: '$value' then result should be '$expected'",
({ value, expected }) => {
expect(validationEvents[EventsWithValidation.onBlur][0].rule(value)).toBe(expected);
}
);
});
describe('PromSettings component', () => {
const defaultProps = createDefaultConfigOptions();

View File

@ -1,24 +1,15 @@
import React, { SyntheticEvent } from 'react';
import React, { SyntheticEvent, useState } from 'react';
import semver from 'semver/preload';
import {
DataSourcePluginOptionsEditorProps,
DataSourceSettings as DataSourceSettingsType,
isValidDuration,
onUpdateDatasourceJsonDataOptionChecked,
SelectableValue,
updateDatasourcePluginJsonDataOption,
} from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime/src';
import {
EventsWithValidation,
InlineField,
InlineFormLabel,
InlineSwitch,
LegacyForms,
regexValidation,
Select,
} from '@grafana/ui';
import { InlineField, Input, Select, Switch, useTheme2 } from '@grafana/ui';
import config from '../../../../core/config';
import { useUpdateDatasource } from '../../../../features/datasources/state';
@ -27,11 +18,10 @@ import { QueryEditorMode } from '../querybuilder/shared/types';
import { defaultPrometheusQueryOverlapWindow } from '../querycache/QueryCache';
import { PrometheusCacheLevel, PromOptions } from '../types';
import { docsTip, overhaulStyles, PROM_CONFIG_LABEL_WIDTH, validateInput } from './ConfigEditor';
import { ExemplarsSettings } from './ExemplarsSettings';
import { PromFlavorVersions } from './PromFlavorVersions';
const { Input, FormField } = LegacyForms;
const httpOptions = [
{ value: 'POST', label: 'POST' },
{ value: 'GET', label: 'GET' },
@ -60,6 +50,13 @@ const prometheusFlavorSelectItems: PrometheusSelectItemsType = [
type Props = Pick<DataSourcePluginOptionsEditorProps<PromOptions>, 'options' | 'onOptionsChange'>;
// single duration input
export const DURATION_REGEX = /^$|^\d+(ms|[Mwdhmsy])$/;
// multiple duration input
export const MULTIPLE_DURATION_REGEX = /(\d+)(.+)/;
const durationError = 'Value is not valid, you can use number with time unit specifier: y, M, w, d, h, m, s';
/**
* Returns the closest version to what the user provided that we have in our PromFlavorVersions for the currently selected flavor
* Bugs: It will only reject versions that are a major release apart, so Mimir 2.x might get selected for Prometheus 2.8 if the user selects an incorrect flavor
@ -158,76 +155,132 @@ export const PromSettings = (props: Props) => {
options.jsonData.httpMethod = 'POST';
}
const theme = useTheme2();
const styles = overhaulStyles(theme);
type ValidDuration = {
timeInterval: string;
queryTimeout: string;
incrementalQueryOverlapWindow: string;
};
const [validDuration, updateValidDuration] = useState<ValidDuration>({
timeInterval: '',
queryTimeout: '',
incrementalQueryOverlapWindow: '',
});
return (
<>
<h6 className="page-heading">Interval behaviour</h6>
<div className="gf-form-group">
{/* Scrape interval */}
<div className="gf-form-inline">
<div className="gf-form">
<FormField
<InlineField
label="Scrape interval"
labelWidth={13}
inputEl={
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
This interval is how frequently Prometheus scrapes targets. Set this to the typical scrape and
evaluation interval configured in your Prometheus config file. If you set this to a greater value than
your Prometheus config file interval, Grafana will evaluate the data according to this interval and
you will see less data points. Defaults to 15s. {docsTip()}
</>
}
interactive={true}
disabled={options.readOnly}
>
<>
<Input
className="width-6"
className="width-20"
value={options.jsonData.timeInterval}
spellCheck={false}
placeholder="15s"
onChange={onChangeHandler('timeInterval', options, onOptionsChange)}
validationEvents={promSettingsValidationEvents}
disabled={options.readOnly}
onBlur={(e) => updateValidDuration({ ...validDuration, timeInterval: e.currentTarget.value })}
/>
}
tooltip="Set this to the typical scrape and evaluation interval configured in Prometheus. Defaults to 15s."
/>
{validateInput(validDuration.timeInterval, DURATION_REGEX, durationError)}
</>
</InlineField>
</div>
</div>
{/* Query Timeout */}
<div className="gf-form-inline">
<div className="gf-form">
<FormField
<InlineField
label="Query timeout"
labelWidth={13}
inputEl={
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={<>Set the Prometheus query timeout. {docsTip()}</>}
interactive={true}
disabled={options.readOnly}
>
<>
<Input
className="width-6"
className="width-20"
value={options.jsonData.queryTimeout}
onChange={onChangeHandler('queryTimeout', options, onOptionsChange)}
spellCheck={false}
placeholder="60s"
validationEvents={promSettingsValidationEvents}
disabled={options.readOnly}
onBlur={(e) => updateValidDuration({ ...validDuration, queryTimeout: e.currentTarget.value })}
/>
}
tooltip="Set the Prometheus query timeout."
/>
{validateInput(validDuration.queryTimeout, DURATION_REGEX, durationError)}
</>
</InlineField>
</div>
</div>
{/* HTTP Method */}
<div className="gf-form">
<InlineFormLabel
width={13}
tooltip="You can use either POST or GET HTTP method to query your Prometheus data source. POST is the recommended method as it allows bigger queries. Change this to GET if you have a Prometheus version older than 2.1 or if POST requests are restricted in your network."
>
HTTP method
</InlineFormLabel>
<Select
aria-label="Select HTTP method"
options={httpOptions}
value={httpOptions.find((o) => o.value === options.jsonData.httpMethod)}
onChange={onChangeHandler('httpMethod', options, onOptionsChange)}
className="width-6"
disabled={options.readOnly}
/>
</div>
</div>
<h3 className="page-heading">Type and version</h3>
<h6 className="page-heading">Query editor</h6>
<div className="gf-form-group">
<div className="gf-form">
<InlineField
label="Default editor"
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={<>Set default editor option for all users of this data source. {docsTip()}</>}
interactive={true}
disabled={options.readOnly}
>
<Select
aria-label={`Default Editor (Code or Builder)`}
options={editorOptions}
value={
editorOptions.find((o) => o.value === options.jsonData.defaultEditor) ??
editorOptions.find((o) => o.value === QueryEditorMode.Builder)
}
onChange={onChangeHandler('defaultEditor', options, onOptionsChange)}
width={40}
/>
</InlineField>
</div>
<div className="gf-form">
<InlineField
labelWidth={PROM_CONFIG_LABEL_WIDTH}
label="Disable metrics lookup"
tooltip={
<>
Checking this option will disable the metrics chooser and metric/label support in the query field&apos;s
autocomplete. This helps if you have performance issues with bigger Prometheus instances. {docsTip()}
</>
}
interactive={true}
disabled={options.readOnly}
className={styles.switchField}
>
<Switch
value={options.jsonData.disableMetricsLookup ?? false}
onChange={onUpdateDatasourceJsonDataOptionChecked(props, 'disableMetricsLookup')}
/>
</InlineField>
</div>
</div>
<h6 className="page-heading">Performance</h6>
{!options.jsonData.prometheusType && !options.jsonData.prometheusVersion && options.readOnly && (
<div style={{ marginBottom: '12px' }}>
<div className={styles.versionMargin}>
For more information on configuring prometheus type and version in data sources, see the{' '}
<a
style={{ textDecoration: 'underline' }}
className={styles.textUnderline}
href="https://grafana.com/docs/grafana/latest/administration/provisioning/"
>
provisioning documentation
@ -236,182 +289,212 @@ export const PromSettings = (props: Props) => {
</div>
)}
<div className="gf-form-group">
<div className="gf-form">
<div className="gf-form-inline">
<div className="gf-form">
<FormField
<InlineField
label="Prometheus type"
labelWidth={13}
inputEl={
<Select
aria-label="Prometheus type"
options={prometheusFlavorSelectItems}
value={prometheusFlavorSelectItems.find((o) => o.value === options.jsonData.prometheusType)}
onChange={onChangeHandler(
'prometheusType',
{
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
Set this to the type of your prometheus database, e.g. Prometheus, Cortex, Mimir or Thanos. Changing
this field will save your current settings, and attempt to detect the version. Certain types of
Prometheus support or do not support various APIs. For example, some types support regex matching for
label queries to improve performance. Some types have an API for metadata. If you set this incorrectly
you may experience odd behavior when querying metrics and labels. Please check your Prometheus
documentation to ensure you enter the correct type. {docsTip()}
</>
}
interactive={true}
disabled={options.readOnly}
>
<Select
aria-label="Prometheus type"
options={prometheusFlavorSelectItems}
value={prometheusFlavorSelectItems.find((o) => o.value === options.jsonData.prometheusType)}
onChange={onChangeHandler(
'prometheusType',
{
...options,
jsonData: { ...options.jsonData, prometheusVersion: undefined },
},
(options) => {
// Check buildinfo api and set default version if we can
setPrometheusVersion(options, onOptionsChange, onUpdate);
return onOptionsChange({
...options,
jsonData: { ...options.jsonData, prometheusVersion: undefined },
},
(options) => {
// Check buildinfo api and set default version if we can
setPrometheusVersion(options, onOptionsChange, onUpdate);
return onOptionsChange({
...options,
jsonData: { ...options.jsonData, prometheusVersion: undefined },
});
}
)}
width={20}
disabled={options.readOnly}
/>
}
tooltip="Set this to the type of your prometheus database, e.g. Prometheus, Cortex, Mimir or Thanos. Changing this field will save your current settings, and attempt to detect the version."
/>
});
}
)}
width={40}
/>
</InlineField>
</div>
</div>
<div className="gf-form">
{options.jsonData.prometheusType && (
<div className="gf-form">
<FormField
label={`${options.jsonData.prometheusType} version`}
labelWidth={13}
inputEl={
<Select
aria-label={`${options.jsonData.prometheusType} type`}
options={PromFlavorVersions[options.jsonData.prometheusType]}
value={PromFlavorVersions[options.jsonData.prometheusType]?.find(
(o) => o.value === options.jsonData.prometheusVersion
)}
onChange={onChangeHandler('prometheusVersion', options, onOptionsChange)}
width={20}
disabled={options.readOnly}
/>
}
tooltip={`Use this to set the version of your ${options.jsonData.prometheusType} instance if it is not automatically configured.`}
/>
</div>
)}
</div>
</div>
<h3 className="page-heading">Misc</h3>
<div className="gf-form-group">
<div className="gf-form">
<InlineField
labelWidth={28}
label="Disable metrics lookup"
tooltip="Checking this option will disable the metrics chooser and metric/label support in the query field's autocomplete. This helps if you have performance issues with bigger Prometheus instances."
disabled={options.readOnly}
>
<InlineSwitch
value={options.jsonData.disableMetricsLookup ?? false}
onChange={onUpdateDatasourceJsonDataOptionChecked(props, 'disableMetricsLookup')}
/>
</InlineField>
</div>
<div className="gf-form">
<FormField
label="Default editor"
labelWidth={14}
inputEl={
<Select
aria-label={`Default Editor (Code or Builder)`}
options={editorOptions}
value={editorOptions.find((o) => o.value === options.jsonData.defaultEditor)}
onChange={onChangeHandler('defaultEditor', options, onOptionsChange)}
width={20}
disabled={options.readOnly}
/>
}
tooltip={`Set default editor option (builder/code) for all users of this datasource. If no option was selected, the default editor will be the "builder". If they switch to other option rather than the specified with this setting on the panel we always show the selected editor for that user.`}
/>
</div>
<div className="gf-form-inline">
<div className="gf-form max-width-30">
<FormField
label="Custom query parameters"
labelWidth={14}
tooltip="Add custom parameters to all Prometheus or Thanos queries."
inputEl={
<Input
className="width-25"
value={options.jsonData.customQueryParameters}
onChange={onChangeHandler('customQueryParameters', options, onOptionsChange)}
spellCheck={false}
placeholder="Example: max_source_resolution=5m&timeout=10"
disabled={options.readOnly}
{options.jsonData.prometheusType && (
<div className="gf-form">
<InlineField
label={`${options.jsonData.prometheusType} version`}
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
Use this to set the version of your {options.jsonData.prometheusType} instance if it is not
automatically configured. {docsTip()}
</>
}
interactive={true}
disabled={options.readOnly}
>
<Select
aria-label={`${options.jsonData.prometheusType} type`}
options={PromFlavorVersions[options.jsonData.prometheusType]}
value={PromFlavorVersions[options.jsonData.prometheusType]?.find(
(o) => o.value === options.jsonData.prometheusVersion
)}
onChange={onChangeHandler('prometheusVersion', options, onOptionsChange)}
width={40}
/>
}
/>
</div>
</InlineField>
</div>
)}
</div>
{config.featureToggles.prometheusResourceBrowserCache && (
<div className="gf-form-inline">
<div className="gf-form max-width-30">
<FormField
<InlineField
label="Cache level"
labelWidth={14}
tooltip="Sets the browser caching level for editor queries. Higher cache settings are recommended for high cardinality data sources."
inputEl={
<Select
className={`width-25`}
onChange={onChangeHandler('cacheLevel', options, onOptionsChange)}
options={cacheValueOptions}
disabled={options.readOnly}
value={
cacheValueOptions.find((o) => o.value === options.jsonData.cacheLevel) ?? PrometheusCacheLevel.Low
}
/>
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
Sets the browser caching level for editor queries. Higher cache settings are recommended for high
cardinality data sources.
</>
}
/>
interactive={true}
disabled={options.readOnly}
>
<Select
width={40}
onChange={onChangeHandler('cacheLevel', options, onOptionsChange)}
options={cacheValueOptions}
value={
cacheValueOptions.find((o) => o.value === options.jsonData.cacheLevel) ?? PrometheusCacheLevel.Low
}
/>
</InlineField>
</div>
</div>
)}
<div className="gf-form-inline">
<div className="gf-form max-width-30">
<FormField
<InlineField
label="Incremental querying (beta)"
labelWidth={14}
tooltip="This feature will change the default behavior of relative queries to always request fresh data from the prometheus instance, instead query results will be cached, and only new records are requested. Turn this on to decrease database and network load."
inputEl={
<InlineSwitch
value={options.jsonData.incrementalQuerying ?? false}
onChange={onUpdateDatasourceJsonDataOptionChecked(props, 'incrementalQuerying')}
disabled={options.readOnly}
/>
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
This feature will change the default behavior of relative queries to always request fresh data from
the prometheus instance, instead query results will be cached, and only new records are requested.
Turn this on to decrease database and network load.
</>
}
/>
interactive={true}
className={styles.switchField}
disabled={options.readOnly}
>
<Switch
value={options.jsonData.incrementalQuerying ?? false}
onChange={onUpdateDatasourceJsonDataOptionChecked(props, 'incrementalQuerying')}
/>
</InlineField>
</div>
</div>
<div className="gf-form-inline">
{options.jsonData.incrementalQuerying && (
<FormField
<InlineField
label="Query overlap window"
labelWidth={14}
tooltip="Set a duration like 10m or 120s or 0s. Default of 10 minutes. This duration will be added to the duration of each incremental request."
inputEl={
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
Set a duration like 10m or 120s or 0s. Default of 10 minutes. This duration will be added to the
duration of each incremental request.
</>
}
interactive={true}
disabled={options.readOnly}
>
<>
<Input
validationEvents={{
onBlur: [
{
rule: (value) => isValidDuration(value),
errorMessage: 'Invalid duration. Example values: 100s, 10m',
},
],
}}
onBlur={(e) =>
updateValidDuration({ ...validDuration, incrementalQueryOverlapWindow: e.currentTarget.value })
}
className="width-25"
value={options.jsonData.incrementalQueryOverlapWindow ?? defaultPrometheusQueryOverlapWindow}
onChange={onChangeHandler('incrementalQueryOverlapWindow', options, onOptionsChange)}
spellCheck={false}
disabled={options.readOnly}
/>
}
/>
{validateInput(validDuration.incrementalQueryOverlapWindow, MULTIPLE_DURATION_REGEX, durationError)}
</>
</InlineField>
)}
</div>
</div>
<h6 className="page-heading">Other</h6>
<div className="gf-form-group">
<div className="gf-form-inline">
<div className="gf-form max-width-30">
<InlineField
label="Custom query parameters"
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
Add custom parameters to the Prometheus query URL. For example timeout, partial_response, dedup, or
max_source_resolution. Multiple parameters should be concatenated together with an &. {docsTip()}
</>
}
interactive={true}
disabled={options.readOnly}
>
<Input
className="width-20"
value={options.jsonData.customQueryParameters}
onChange={onChangeHandler('customQueryParameters', options, onOptionsChange)}
spellCheck={false}
placeholder="Example: max_source_resolution=5m&timeout=10"
/>
</InlineField>
</div>
</div>
<div className="gf-form-inline">
{/* HTTP Method */}
<div className="gf-form">
<InlineField
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
You can use either POST or GET HTTP method to query your Prometheus data source. POST is the
recommended method as it allows bigger queries. Change this to GET if you have a Prometheus version
older than 2.1 or if POST requests are restricted in your network. {docsTip()}
</>
}
interactive={true}
label="HTTP method"
disabled={options.readOnly}
>
<Select
width={40}
aria-label="Select HTTP method"
options={httpOptions}
value={httpOptions.find((o) => o.value === options.jsonData.httpMethod)}
onChange={onChangeHandler('httpMethod', options, onOptionsChange)}
/>
</InlineField>
</div>
</div>
</div>
<ExemplarsSettings
options={options.jsonData.exemplarTraceIdDestinations}
onChange={(exemplarOptions) =>
@ -427,15 +510,6 @@ export const PromSettings = (props: Props) => {
);
};
export const promSettingsValidationEvents = {
[EventsWithValidation.onBlur]: [
regexValidation(
/^$|^\d+(ms|[Mwdhmsy])$/,
'Value is not valid, you can use number with time unit specifier: y, M, w, d, h, m, s'
),
],
};
export const getValueFromEventItem = (eventItem: SyntheticEvent<HTMLInputElement> | SelectableValue<string>) => {
if (!eventItem) {
return '';

View File

@ -156,11 +156,6 @@ describe('PrometheusDatasource', () => {
// tested. Checked manually that this ends up with throwing
// await expect(directDs.metricFindQuery('label_names(foo)')).rejects.toBeDefined();
jest.spyOn(console, 'error').mockImplementation(() => {});
await expect(directDs.testDatasource()).resolves.toMatchObject({
message: expect.stringMatching('Browser access'),
status: 'error',
});
await expect(
directDs.annotationQuery({
range: { ...range, raw: range },

View File

@ -1051,44 +1051,6 @@ export class PrometheusDatasource
);
}
async testDatasource() {
const now = new Date().getTime();
const request: DataQueryRequest<PromQuery> = {
targets: [{ refId: 'test', expr: '1+1', instant: true }],
requestId: `${this.id}-health`,
scopedVars: {},
panelId: 0,
interval: '1m',
intervalMs: 60000,
maxDataPoints: 1,
range: {
from: dateTime(now - 1000),
to: dateTime(now),
},
} as DataQueryRequest<PromQuery>;
const buildInfo = await this.getBuildInfo();
return lastValueFrom(this.query(request))
.then((res: DataQueryResponse) => {
if (!res || !res.data || res.state !== LoadingState.Done) {
return { status: 'error', message: `Error reading Prometheus: ${res?.error?.message}` };
} else {
return {
status: 'success',
message: 'Data source is working',
details: buildInfo && {
verboseMessage: this.getBuildInfoMessage(buildInfo),
},
};
}
})
.catch((err: any) => {
console.error('Prometheus Error', err);
return { status: 'error', message: err.message };
});
}
interpolateVariablesInQueries(queries: PromQuery[], scopedVars: ScopedVars): PromQuery[] {
let expandedQueries = queries;
if (queries && queries.length) {