diff --git a/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.test.tsx b/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.test.tsx
new file mode 100644
index 00000000000..9ce9e66c563
--- /dev/null
+++ b/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.test.tsx
@@ -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 = Value is not valid;
+// replaces promSettingsValidationEvents to display a 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 = {customMessage};
+ expect(validateInput(invalidDuration, DURATION_REGEX, customMessage)).toStrictEqual(errorWithCustomMessage);
+ });
+});
diff --git a/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.tsx
index 6470645ed3d..7df57461f82 100644
--- a/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.tsx
+++ b/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.tsx
@@ -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;
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' && (
- 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.
)}
-
{
azureAuthSettings={azureAuthSettings}
renderSigV4Editor={}
secureSocksDSProxyEnabled={config.secureSocksDSProxyEnabled}
+ connectionElements={{
+ label: 'Prometheus server URL',
+ tooltip: docsTip(),
+ }}
/>
+ <>
+
+
Additional settings
+
+ Additional settings are optional settings that can be configured for more control over your data source.
+
- options={options} onOptionsChange={onOptionsChange} />
+ 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 (
+
+ Visit docs for more details here.
+
+ );
+}
+
+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 {errorMessage ? errorMessage : defaultErrorMessage};
+ } 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;
+ `,
+ };
+}
diff --git a/public/app/plugins/datasource/prometheus/configuration/ExemplarSetting.tsx b/public/app/plugins/datasource/prometheus/configuration/ExemplarSetting.tsx
index acc75e8e29b..ba13f98e692 100644
--- a/public/app/plugins/datasource/prometheus/configuration/ExemplarSetting.tsx
+++ b/public/app/plugins/datasource/prometheus/configuration/ExemplarSetting.tsx
@@ -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 (
-
+
+ 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}
+ >
<>
- setIsInternalLink(ev.currentTarget.checked)}
/>
- {!disabled && (
-
{isInternalLink ? (
The data source the exemplar is going to navigate to. {docsTip()}>}
disabled={disabled}
+ interactive={true}
>
The URL of the trace backend the user would go to see its trace. {docsTip()}>}
disabled={disabled}
+ interactive={true}
>
Use to override the button label on the exemplar traceID field. {docsTip()}>}
disabled={disabled}
+ interactive={true}
>
The name of the field in the labels object that should be used to get the traceID. {docsTip()}>}
disabled={disabled}
+ interactive={true}
>
+ {!disabled && (
+
+
+ )}
);
}
diff --git a/public/app/plugins/datasource/prometheus/configuration/ExemplarsSettings.tsx b/public/app/plugins/datasource/prometheus/configuration/ExemplarsSettings.tsx
index 7c8abc3ae80..b13f7105c18 100644
--- a/public/app/plugins/datasource/prometheus/configuration/ExemplarsSettings.tsx
+++ b/public/app/plugins/datasource/prometheus/configuration/ExemplarsSettings.tsx
@@ -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 (
- <>
-
);
}
diff --git a/public/app/plugins/datasource/prometheus/configuration/PromSettings.test.tsx b/public/app/plugins/datasource/prometheus/configuration/PromSettings.test.tsx
index bf91f3d5be8..aea4755989e 100644
--- a/public/app/plugins/datasource/prometheus/configuration/PromSettings.test.tsx
+++ b/public/app/plugins/datasource/prometheus/configuration/PromSettings.test.tsx
@@ -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();
diff --git a/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx b/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx
index eeabf4a8591..07fb205f58f 100644
--- a/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx
+++ b/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx
@@ -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, '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({
+ timeInterval: '',
+ queryTimeout: '',
+ incrementalQueryOverlapWindow: '',
+ });
+
return (
<>
+
Interval behaviour
{/* Scrape interval */}
-
+ 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}
+ >
+ <>
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)}
+ >
+
+ Set default editor option for all users of this data source. {docsTip()}>}
+ interactive={true}
+ disabled={options.readOnly}
+ >
+
+
+
+
+ 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. {docsTip()}
+ >
+ }
+ interactive={true}
+ disabled={options.readOnly}
+ className={styles.switchField}
+ >
+
+
+
- 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}
+ >
+
-
- {options.jsonData.prometheusType && (
-
- 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.`}
- />
-
- )}
-
-
-
-
Misc
-
-
-
-
-
-
-
- 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.`}
- />
-
-
-
+
+ Use this to set the version of your {options.jsonData.prometheusType} instance if it is not
+ automatically configured. {docsTip()}
+ >
+ }
+ interactive={true}
+ disabled={options.readOnly}
+ >
+
- 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}
+ >
+
)}
-
+ 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}
+ >
+
+
{options.jsonData.incrementalQuerying && (
-
+ 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}
+ >
+ <>
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)}
+ >
+
)}
+
+
Other
+
+
+
+
+ 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}
+ >
+
+
+
+
+
+ {/* HTTP Method */}
+
+
+ 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}
+ >
+
+