Prometheus: Config overhaul part two, auth and DataSourceHttpSettings overhaul (#71250)

* build httpsettings overhaul with new auth component

* remove test code

* add connection and advanced http settings components

* use tooltip with link

* add correct styling and spacing

* save option select for sigV4

* fix styles in Azure auth to fit new auth component

* add types in overhaul folder that are not available yet in grafana

* update e2e tests for new connection component

* update e2e tests for new connection component

* update width of azure inputs

* fix non custom auth selects

* add feature toggle

* wrap azure style changes behind the feature flag

* fix feature toggle rebase fix error

* move advanced http setting and wrap everything in the config subsection component to fix font

* fix input width

* use cx for conditional classes

* use cx for conditional class
This commit is contained in:
Brendan O'Handley
2023-07-26 12:09:53 -04:00
committed by GitHub
parent d39ec2428e
commit 98cb3ce3b6
16 changed files with 837 additions and 358 deletions

View File

@@ -128,6 +128,7 @@ Experimental features might be changed or removed without prior notice.
| `grafanaAPIServer` | Enable Kubernetes API Server for Grafana resources |
| `featureToggleAdminPage` | Enable admin page for managing feature toggles from the Grafana front-end |
| `awsAsyncQueryCaching` | Enable caching for async queries for Redshift and Athena. Requires that the `useCachingService` feature toggle is enabled and the datasource has caching and async query support enabled |
| `prometheusConfigOverhaulAuth` | Update the Prometheus configuration page with the new auth component |
## Development feature toggles

View File

@@ -116,4 +116,5 @@ export interface FeatureToggles {
awsAsyncQueryCaching?: boolean;
splitScopes?: boolean;
azureMonitorDataplane?: boolean;
prometheusConfigOverhaulAuth?: boolean;
}

View File

@@ -675,5 +675,11 @@ var (
Owner: grafanaPartnerPluginsSquad,
Expression: "true", // on by default
},
{
Name: "prometheusConfigOverhaulAuth",
Description: "Update the Prometheus configuration page with the new auth component",
Stage: FeatureStageExperimental,
Owner: grafanaObservabilityMetricsSquad,
},
}
)

View File

@@ -97,3 +97,4 @@ featureToggleAdminPage,experimental,@grafana/grafana-operator-experience-squad,f
awsAsyncQueryCaching,experimental,@grafana/aws-datasources,false,false,false,false
splitScopes,preview,@grafana/grafana-authnz-team,false,false,true,false
azureMonitorDataplane,GA,@grafana/partner-datasources,false,false,false,false
prometheusConfigOverhaulAuth,experimental,@grafana/observability-metrics,false,false,false,false
1 Name Stage Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
97 awsAsyncQueryCaching experimental @grafana/aws-datasources false false false false
98 splitScopes preview @grafana/grafana-authnz-team false false true false
99 azureMonitorDataplane GA @grafana/partner-datasources false false false false
100 prometheusConfigOverhaulAuth experimental @grafana/observability-metrics false false false false

View File

@@ -398,4 +398,8 @@ const (
// FlagAzureMonitorDataplane
// Adds dataplane compliant frame metadata in the Azure Monitor datasource
FlagAzureMonitorDataplane = "azureMonitorDataplane"
// FlagPrometheusConfigOverhaulAuth
// Update the Prometheus configuration page with the new auth component
FlagPrometheusConfigOverhaulAuth = "prometheusConfigOverhaulAuth"
)

View File

@@ -1,6 +1,9 @@
import { cx } from '@emotion/css';
import React from 'react';
import { DataSourceJsonData, DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { ConfigSubSection } from '@grafana/experimental';
import { config } from '@grafana/runtime';
import { InlineField, Switch, useTheme2 } from '@grafana/ui';
import { docsTip, overhaulStyles } from './ConfigEditor';
@@ -19,9 +22,13 @@ export function AlertingSettingsOverhaul<T extends AlertingConfig>({
const theme = useTheme2();
const styles = overhaulStyles(theme);
const prometheusConfigOverhaulAuth = config.featureToggles.prometheusConfigOverhaulAuth;
return (
<>
<h3 className="page-heading">Alerting</h3>
<ConfigSubSection
title="Alerting"
className={cx(styles.container, { [styles.alertingTop]: prometheusConfigOverhaulAuth })}
>
<div className="gf-form-group">
<div className="gf-form-inline">
<div className="gf-form">
@@ -51,6 +58,6 @@ export function AlertingSettingsOverhaul<T extends AlertingConfig>({
</div>
</div>
</div>
</>
</ConfigSubSection>
);
}

View File

@@ -1,3 +1,4 @@
import { cx } from '@emotion/css';
import React, { FormEvent, useMemo, useState } from 'react';
import { config } from '@grafana/runtime';
@@ -40,6 +41,10 @@ export const AzureAuthSettings = (props: HttpSettingsBaseProps) => {
}
};
const prometheusConfigOverhaulAuth = config.featureToggles.prometheusConfigOverhaulAuth;
const labelWidth = prometheusConfigOverhaulAuth ? 24 : 26;
return (
<>
<h6>Azure authentication</h6>
@@ -53,15 +58,15 @@ export const AzureAuthSettings = (props: HttpSettingsBaseProps) => {
<h6>Azure configuration</h6>
<div className="gf-form-group">
<InlineFieldRow>
<InlineField labelWidth={26} label="Override AAD audience" disabled={dataSourceConfig.readOnly}>
<InlineField labelWidth={labelWidth} label="Override AAD audience" disabled={dataSourceConfig.readOnly}>
<InlineSwitch value={overrideAudienceChecked} onChange={onOverrideAudienceChange} />
</InlineField>
</InlineFieldRow>
{overrideAudienceChecked && (
<InlineFieldRow>
<InlineField labelWidth={26} label="Resource ID" disabled={dataSourceConfig.readOnly}>
<InlineField labelWidth={labelWidth} label="Resource ID" disabled={dataSourceConfig.readOnly}>
<Input
className="width-30"
className={cx(prometheusConfigOverhaulAuth ? 'width-20' : 'width-30')}
value={dataSourceConfig.jsonData.azureEndpointResourceId || ''}
onChange={onResourceIdChange}
/>

View File

@@ -1,6 +1,8 @@
import { cx } from '@emotion/css';
import React, { ChangeEvent, useEffect, useReducer, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { config } from '@grafana/runtime';
import { InlineFormLabel, Button } from '@grafana/ui/src/components';
import { Input } from '@grafana/ui/src/components/Forms/Legacy/Input/Input';
import { Select } from '@grafana/ui/src/components/Forms/Legacy/Select/Select';
@@ -148,6 +150,7 @@ export const AzureCredentialsForm = (props: Props) => {
onCredentialsChange(updated);
}
};
const prometheusConfigOverhaulAuth = config.featureToggles.prometheusConfigOverhaulAuth;
return (
<div className="gf-form-group">
@@ -190,7 +193,7 @@ export const AzureCredentialsForm = (props: Props) => {
<InlineFormLabel className="width-12">Directory (tenant) ID</InlineFormLabel>
<div className="width-15">
<Input
className="width-30"
className={cx(prometheusConfigOverhaulAuth ? 'width-20' : 'width-30')}
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.tenantId || ''}
onChange={onTenantIdChange}
@@ -204,7 +207,7 @@ export const AzureCredentialsForm = (props: Props) => {
<InlineFormLabel className="width-12">Application (client) ID</InlineFormLabel>
<div className="width-15">
<Input
className="width-30"
className={cx(prometheusConfigOverhaulAuth ? 'width-20' : 'width-30')}
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.clientId || ''}
onChange={onClientIdChange}
@@ -219,11 +222,20 @@ export const AzureCredentialsForm = (props: Props) => {
<InlineFormLabel htmlFor="azure-client-secret" className="width-12">
Client Secret
</InlineFormLabel>
<Input id="azure-client-secret" className="width-25" placeholder="configured" disabled />
<Input
id="azure-client-secret"
className={cx(prometheusConfigOverhaulAuth ? 'width-20' : 'width-25')}
placeholder="configured"
disabled
/>
</div>
{!disabled && (
<div className="gf-form">
<div className="max-width-30 gf-form-inline">
<div
className={cx(
prometheusConfigOverhaulAuth ? 'max-width-20 gf-form-inline' : 'max-width-30 gf-form-inline'
)}
>
<Button variant="secondary" type="button" onClick={onClientSecretReset}>
reset
</Button>
@@ -237,7 +249,7 @@ export const AzureCredentialsForm = (props: Props) => {
<InlineFormLabel className="width-12">Client Secret</InlineFormLabel>
<div className="width-15">
<Input
className="width-30"
className={cx(prometheusConfigOverhaulAuth ? 'width-20' : 'width-30')}
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.clientSecret || ''}
onChange={onClientSecretChange}
@@ -254,7 +266,7 @@ export const AzureCredentialsForm = (props: Props) => {
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-12">Default Subscription</InlineFormLabel>
<div className="width-25">
<div className={cx(prometheusConfigOverhaulAuth ? 'width-20' : 'width-25')}>
<Select
value={
credentials.defaultSubscriptionId

View File

@@ -3,6 +3,7 @@ import React, { useRef } from 'react';
import { SIGV4ConnectionConfig } from '@grafana/aws-sdk';
import { DataSourcePluginOptionsEditorProps, DataSourceSettings, GrafanaTheme2 } from '@grafana/data';
import { ConfigSection, DataSourceDescription } from '@grafana/experimental';
import { Alert, DataSourceHttpSettings, FieldValidationMessage, useTheme2 } from '@grafana/ui';
import { config } from 'app/core/config';
@@ -11,14 +12,19 @@ import { PromOptions } from '../types';
import { AlertingSettingsOverhaul } from './AlertingSettingsOverhaul';
import { AzureAuthSettings } from './AzureAuthSettings';
import { hasCredentials, setDefaultCredentials, resetCredentials } from './AzureCredentialsConfig';
import { DataSourcehttpSettingsOverhaul } from './DataSourceHttpSettingsOverhaul';
import { PromSettings } from './PromSettings';
import { AdvancedHttpSettings } from './overhaul/AdvancedHttpSettings';
export const PROM_CONFIG_LABEL_WIDTH = 30;
export type Props = DataSourcePluginOptionsEditorProps<PromOptions>;
export const ConfigEditor = (props: Props) => {
const { options, onOptionsChange } = props;
const prometheusConfigOverhaulAuth = config.featureToggles.prometheusConfigOverhaulAuth;
// 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');
@@ -40,29 +46,67 @@ export const ConfigEditor = (props: Props) => {
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}
showAccessOptions={showAccessOptions.current}
onChange={onOptionsChange}
sigV4AuthToggleEnabled={config.sigV4AuthEnabled}
azureAuthSettings={azureAuthSettings}
renderSigV4Editor={<SIGV4ConnectionConfig {...props}></SIGV4ConnectionConfig>}
secureSocksDSProxyEnabled={config.secureSocksDSProxyEnabled}
urlLabel="Prometheus server URL"
urlDocs={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>
{/* WRAP IN FEATURE TOGGLE */}
{prometheusConfigOverhaulAuth ? (
<>
<DataSourceDescription
dataSourceName="Prometheus"
docsLink="https://grafana.com/docs/grafana/latest/datasources/prometheus/configure-prometheus-data-source/"
/>
<hr className={`${styles.hrTopSpace} ${styles.hrBottomSpace}`} />
<DataSourcehttpSettingsOverhaul
options={options}
onOptionsChange={onOptionsChange}
azureAuthSettings={azureAuthSettings}
sigV4AuthToggleEnabled={config.sigV4AuthEnabled}
renderSigV4Editor={<SIGV4ConnectionConfig {...props}></SIGV4ConnectionConfig>}
secureSocksDSProxyEnabled={config.secureSocksDSProxyEnabled}
/>
</>
) : (
<DataSourceHttpSettings
defaultUrl="http://localhost:9090"
dataSourceConfig={options}
showAccessOptions={showAccessOptions.current}
onChange={onOptionsChange}
sigV4AuthToggleEnabled={config.sigV4AuthEnabled}
azureAuthSettings={azureAuthSettings}
renderSigV4Editor={<SIGV4ConnectionConfig {...props}></SIGV4ConnectionConfig>}
secureSocksDSProxyEnabled={config.secureSocksDSProxyEnabled}
urlLabel="Prometheus server URL"
urlDocs={docsTip()}
/>
)}
{prometheusConfigOverhaulAuth ? (
<>
<hr />
<ConfigSection
className={styles.advancedSettings}
title="Advanced settings"
description="Additional settings are optional settings that can be configured for more control over your data source."
>
<AdvancedHttpSettings
className={styles.advancedHTTPSettingsMargin}
config={options}
onChange={onOptionsChange}
/>
<AlertingSettingsOverhaul<PromOptions> options={options} onOptionsChange={onOptionsChange} />
<PromSettings options={options} onOptionsChange={onOptionsChange} />
</ConfigSection>
</>
) : (
<>
<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>
<AlertingSettingsOverhaul<PromOptions> options={options} onOptionsChange={onOptionsChange} />
<AlertingSettingsOverhaul<PromOptions> options={options} onOptionsChange={onOptionsChange} />
<PromSettings options={options} onOptionsChange={onOptionsChange} />
</>
<PromSettings options={options} onOptionsChange={onOptionsChange} />
</>
)}
</>
);
};
@@ -130,5 +174,20 @@ export function overhaulStyles(theme: GrafanaTheme2) {
versionMargin: css`
margin-bottom: 12px;
`,
advancedHTTPSettingsMargin: css`
margin: 24px 0 8px 0;
`,
advancedSettings: css`
padding-top: 32px;
`,
alertingTop: css`
margin-top: 40px !important;
`,
overhaulPageHeading: css`
font-weight: 400;
`,
container: css`
maxwidth: 578;
`,
};
}

View File

@@ -0,0 +1,179 @@
import React, { useCallback, useState } from 'react';
import { DataSourceSettings } from '@grafana/data';
import { Auth, ConnectionSettings, convertLegacyAuthProps } from '@grafana/experimental';
import { SecureSocksProxySettings, useTheme2 } from '@grafana/ui';
import { AzureAuthSettings } from '@grafana/ui/src/components/DataSourceSettings/types';
import { PromOptions } from '../types';
import { docsTip, overhaulStyles } from './ConfigEditor';
import { CustomMethod } from './overhaul/types';
type Props = {
options: DataSourceSettings<PromOptions, {}>;
onOptionsChange: (options: DataSourceSettings<PromOptions, {}>) => void;
azureAuthSettings: AzureAuthSettings;
sigV4AuthToggleEnabled: boolean | undefined;
renderSigV4Editor: React.ReactNode;
secureSocksDSProxyEnabled: boolean;
};
export const DataSourcehttpSettingsOverhaul = (props: Props) => {
const {
options,
onOptionsChange,
azureAuthSettings,
sigV4AuthToggleEnabled,
renderSigV4Editor,
secureSocksDSProxyEnabled,
} = props;
const newAuthProps = convertLegacyAuthProps({
config: options,
onChange: onOptionsChange,
});
const theme = useTheme2();
const styles = overhaulStyles(theme);
// for custom auth methods sigV4 and azure auth
let customMethods: CustomMethod[] = [];
const [sigV4Selected, setSigV4Selected] = useState<boolean>(options.jsonData.sigV4Auth || false);
const sigV4Id = 'custom-sigV4Id';
const sigV4Option: CustomMethod = {
id: sigV4Id,
label: 'SigV4 auth',
description: 'This is SigV4 auth description',
component: <>{renderSigV4Editor}</>,
};
if (sigV4AuthToggleEnabled) {
customMethods.push(sigV4Option);
}
const onSettingsChange = useCallback(
(change: Partial<DataSourceSettings<PromOptions, {}>>) => {
onOptionsChange({
...options,
...change,
});
},
[options, onOptionsChange]
);
const azureAuthEnabled: boolean =
(azureAuthSettings?.azureAuthSupported && azureAuthSettings.getAzureAuthEnabled(options)) || false;
const [azureAuthSelected, setAzureAuthSelected] = useState<boolean>(azureAuthEnabled);
const azureAuthId = 'custom-azureAuthId';
const azureAuthOption: CustomMethod = {
id: azureAuthId,
label: 'Azure auth',
description: 'This is Azure auth description',
component: (
<>
{azureAuthSettings.azureSettingsUI && (
<azureAuthSettings.azureSettingsUI dataSourceConfig={options} onChange={onOptionsChange} />
)}
</>
),
};
// allow the option to show in the dropdown
if (azureAuthSettings?.azureAuthSupported) {
customMethods.push(azureAuthOption);
}
function returnSelectedMethod() {
if (sigV4Selected) {
return sigV4Id;
}
if (azureAuthSelected) {
return azureAuthId;
}
return newAuthProps.selectedMethod;
}
// Do we need this switch anymore? Update the language.
let urlTooltip;
switch (options.access) {
case 'direct':
urlTooltip = (
<>
Your access method is <em>Browser</em>, this means the URL needs to be accessible from the browser.
{docsTip()}
</>
);
break;
case 'proxy':
urlTooltip = (
<>
Your access method is <em>Server</em>, this means the URL needs to be accessible from the grafana
backend/server.
{docsTip()}
</>
);
break;
default:
urlTooltip = <>Specify a complete HTTP URL (for example http://your_server:8080) {docsTip()}</>;
}
return (
<>
<ConnectionSettings
urlPlaceholder="http://localhost:9090"
config={options}
onChange={onOptionsChange}
urlLabel="Prometheus server URL"
urlTooltip={urlTooltip}
/>
<hr className={`${styles.hrTopSpace} ${styles.hrBottomSpace}`} />
<Auth
// Reshaped legacy props
{...newAuthProps}
// Your custom auth methods
customMethods={customMethods}
// Still need to call `onAuthMethodSelect` function from
// `newAuthProps` to store the legacy data correctly.
// Also make sure to store the data about your component
// being selected/unselected.
onAuthMethodSelect={(method) => {
// handle selecting of custom methods
// sigV4Id
if (sigV4AuthToggleEnabled) {
setSigV4Selected(method === sigV4Id);
onSettingsChange({
jsonData: { ...options.jsonData, sigV4Auth: method === sigV4Id },
});
}
// Azure
if (azureAuthSettings?.azureAuthSupported) {
setAzureAuthSelected(method === azureAuthId);
azureAuthSettings.setAzureAuthEnabled(options, method === azureAuthId);
}
newAuthProps.onAuthMethodSelect(method);
}}
// If your method is selected pass its id to `selectedMethod`,
// otherwise pass the id from converted legacy data
selectedMethod={returnSelectedMethod()}
/>
<div className={styles.sectionBottomPadding} />
{secureSocksDSProxyEnabled && (
<>
<SecureSocksProxySettings options={options} onOptionsChange={onOptionsChange} />
<div className={styles.sectionBottomPadding} />
</>
)}
</>
);
};

View File

@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import React from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { ConfigSubSection } from '@grafana/experimental';
import { Button, useTheme2 } from '@grafana/ui';
import { ExemplarTraceIdDestination } from '../types';
@@ -20,47 +21,47 @@ export function ExemplarsSettings({ options, onChange, disabled }: Props) {
const styles = overhaulStyles(theme);
return (
<div className={styles.sectionBottomPadding}>
<h3 className="page-heading">Exemplars</h3>
<ConfigSubSection title="Exemplars" className={styles.container}>
{options &&
options.map((option, index) => {
return (
<ExemplarSetting
key={index}
value={option}
onChange={(newField) => {
const newOptions = [...options];
newOptions.splice(index, 1, newField);
onChange(newOptions);
}}
onDelete={() => {
const newOptions = [...options];
newOptions.splice(index, 1);
onChange(newOptions);
}}
disabled={disabled}
/>
);
})}
{options &&
options.map((option, index) => {
return (
<ExemplarSetting
key={index}
value={option}
onChange={(newField) => {
const newOptions = [...options];
newOptions.splice(index, 1, newField);
onChange(newOptions);
}}
onDelete={() => {
const newOptions = [...options];
newOptions.splice(index, 1);
onChange(newOptions);
}}
disabled={disabled}
/>
);
})}
{!disabled && (
<Button
variant="secondary"
aria-label={selectors.components.DataSource.Prometheus.configPage.exemplarsAddButton}
className={css`
margin-bottom: 10px;
`}
icon="plus"
onClick={(event) => {
event.preventDefault();
const newOptions = [...(options || []), { name: 'traceID' }];
onChange(newOptions);
}}
>
Add
</Button>
)}
{disabled && !options && <i>No exemplars configurations</i>}
{!disabled && (
<Button
variant="secondary"
aria-label={selectors.components.DataSource.Prometheus.configPage.exemplarsAddButton}
className={css`
margin-bottom: 10px;
`}
icon="plus"
onClick={(event) => {
event.preventDefault();
const newOptions = [...(options || []), { name: 'traceID' }];
onChange(newOptions);
}}
>
Add
</Button>
)}
{disabled && !options && <i>No exemplars configurations</i>}
</ConfigSubSection>
</div>
);
}

View File

@@ -8,6 +8,7 @@ import {
SelectableValue,
updateDatasourcePluginJsonDataOption,
} from '@grafana/data';
import { ConfigSubSection } from '@grafana/experimental';
import { getBackendSrv } from '@grafana/runtime/src';
import { InlineField, Input, Select, Switch, useTheme2 } from '@grafana/ui';
@@ -171,346 +172,353 @@ export const PromSettings = (props: Props) => {
return (
<>
<h3 className="page-heading">Interval behaviour</h3>
<div className="gf-form-group">
{/* Scrape interval */}
<div className="gf-form-inline">
<div className="gf-form">
<InlineField
label="Scrape interval"
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<ConfigSubSection title="Interval behaviour" className={styles.container}>
<div className="gf-form-group">
{/* Scrape interval */}
<div className="gf-form-inline">
<div className="gf-form">
<InlineField
label="Scrape interval"
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}
>
<>
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()}
<Input
className="width-20"
value={options.jsonData.timeInterval}
spellCheck={false}
placeholder="15s"
onChange={onChangeHandler('timeInterval', options, onOptionsChange)}
onBlur={(e) => updateValidDuration({ ...validDuration, timeInterval: e.currentTarget.value })}
/>
{validateInput(validDuration.timeInterval, DURATION_REGEX, durationError)}
</>
}
interactive={true}
disabled={options.readOnly}
>
<>
<Input
className="width-20"
value={options.jsonData.timeInterval}
spellCheck={false}
placeholder="15s"
onChange={onChangeHandler('timeInterval', options, onOptionsChange)}
onBlur={(e) => updateValidDuration({ ...validDuration, timeInterval: e.currentTarget.value })}
/>
{validateInput(validDuration.timeInterval, DURATION_REGEX, durationError)}
</>
</InlineField>
</InlineField>
</div>
</div>
{/* Query Timeout */}
<div className="gf-form-inline">
<div className="gf-form">
<InlineField
label="Query timeout"
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={<>Set the Prometheus query timeout. {docsTip()}</>}
interactive={true}
disabled={options.readOnly}
>
<>
<Input
className="width-20"
value={options.jsonData.queryTimeout}
onChange={onChangeHandler('queryTimeout', options, onOptionsChange)}
spellCheck={false}
placeholder="60s"
onBlur={(e) => updateValidDuration({ ...validDuration, queryTimeout: e.currentTarget.value })}
/>
{validateInput(validDuration.queryTimeout, DURATION_REGEX, durationError)}
</>
</InlineField>
</div>
</div>
</div>
{/* Query Timeout */}
<div className="gf-form-inline">
</ConfigSubSection>
<ConfigSubSection title="Query editor" className={styles.container}>
<div className="gf-form-group">
<div className="gf-form">
<InlineField
label="Query timeout"
label="Default editor"
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={<>Set the Prometheus query timeout. {docsTip()}</>}
interactive={true}
disabled={options.readOnly}
>
<>
<Input
className="width-20"
value={options.jsonData.queryTimeout}
onChange={onChangeHandler('queryTimeout', options, onOptionsChange)}
spellCheck={false}
placeholder="60s"
onBlur={(e) => updateValidDuration({ ...validDuration, queryTimeout: e.currentTarget.value })}
/>
{validateInput(validDuration.queryTimeout, DURATION_REGEX, durationError)}
</>
</InlineField>
</div>
</div>
</div>
<h3 className="page-heading">Query editor</h3>
<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>
<h3 className="page-heading">Performance</h3>
{!options.jsonData.prometheusType && !options.jsonData.prometheusVersion && options.readOnly && (
<div className={styles.versionMargin}>
For more information on configuring prometheus type and version in data sources, see the{' '}
<a
className={styles.textUnderline}
href="https://grafana.com/docs/grafana/latest/administration/provisioning/"
>
provisioning documentation
</a>
.
</div>
)}
<div className="gf-form-group">
<div className="gf-form-inline">
<div className="gf-form">
<InlineField
label="Prometheus type"
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()}
</>
}
tooltip={<>Set default editor option for all users of this data source. {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 },
});
}
)}
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>
<div className="gf-form-inline">
{options.jsonData.prometheusType && (
</ConfigSubSection>
<ConfigSubSection title="Performance" className={styles.container}>
{!options.jsonData.prometheusType && !options.jsonData.prometheusVersion && options.readOnly && (
<div className={styles.versionMargin}>
For more information on configuring prometheus type and version in data sources, see the{' '}
<a
className={styles.textUnderline}
href="https://grafana.com/docs/grafana/latest/administration/provisioning/"
>
provisioning documentation
</a>
.
</div>
)}
<div className="gf-form-group">
<div className="gf-form-inline">
<div className="gf-form">
<InlineField
label={`${options.jsonData.prometheusType} version`}
label="Prometheus type"
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()}
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={`${options.jsonData.prometheusType} type`}
options={PromFlavorVersions[options.jsonData.prometheusType]}
value={PromFlavorVersions[options.jsonData.prometheusType]?.find(
(o) => o.value === options.jsonData.prometheusVersion
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 },
});
}
)}
onChange={onChangeHandler('prometheusVersion', options, onOptionsChange)}
width={40}
/>
</InlineField>
</div>
</div>
<div className="gf-form-inline">
{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}
/>
</InlineField>
</div>
)}
</div>
{config.featureToggles.prometheusResourceBrowserCache && (
<div className="gf-form-inline">
<div className="gf-form max-width-30">
<InlineField
label="Cache level"
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>
{config.featureToggles.prometheusResourceBrowserCache && (
<div className="gf-form-inline">
<div className="gf-form max-width-30">
<InlineField
label="Cache level"
label="Incremental querying (beta)"
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
Sets the browser caching level for editor queries. Higher cache settings are recommended for high
cardinality data sources.
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 && (
<InlineField
label="Query overlap window"
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}
>
<Select
width={40}
onChange={onChangeHandler('cacheLevel', options, onOptionsChange)}
options={cacheValueOptions}
value={
cacheValueOptions.find((o) => o.value === options.jsonData.cacheLevel) ?? PrometheusCacheLevel.Low
}
<>
<Input
onBlur={(e) =>
updateValidDuration({ ...validDuration, incrementalQueryOverlapWindow: e.currentTarget.value })
}
className="width-20"
value={options.jsonData.incrementalQueryOverlapWindow ?? defaultPrometheusQueryOverlapWindow}
onChange={onChangeHandler('incrementalQueryOverlapWindow', options, onOptionsChange)}
spellCheck={false}
/>
{validateInput(validDuration.incrementalQueryOverlapWindow, MULTIPLE_DURATION_REGEX, durationError)}
</>
</InlineField>
)}
</div>
<div className="gf-form-inline">
<div className="gf-form max-width-30">
<InlineField
label="Disable recording rules (beta)"
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={<>This feature will disable recording rules Turn this on to improve dashboard performance</>}
interactive={true}
className={styles.switchField}
disabled={options.readOnly}
>
<Switch
value={options.jsonData.disableRecordingRules ?? false}
onChange={onUpdateDatasourceJsonDataOptionChecked(props, 'disableRecordingRules')}
/>
</InlineField>
</div>
</div>
)}
<div className="gf-form-inline">
<div className="gf-form max-width-30">
<InlineField
label="Incremental querying (beta)"
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>
</ConfigSubSection>
<div className="gf-form-inline">
{options.jsonData.incrementalQuerying && (
<InlineField
label="Query overlap window"
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}
>
<>
<ConfigSubSection title="Other" className={styles.container}>
<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
onBlur={(e) =>
updateValidDuration({ ...validDuration, incrementalQueryOverlapWindow: e.currentTarget.value })
}
className="width-25"
value={options.jsonData.incrementalQueryOverlapWindow ?? defaultPrometheusQueryOverlapWindow}
onChange={onChangeHandler('incrementalQueryOverlapWindow', options, onOptionsChange)}
className="width-20"
value={options.jsonData.customQueryParameters}
onChange={onChangeHandler('customQueryParameters', options, onOptionsChange)}
spellCheck={false}
placeholder="Example: max_source_resolution=5m&timeout=10"
/>
{validateInput(validDuration.incrementalQueryOverlapWindow, MULTIPLE_DURATION_REGEX, durationError)}
</>
</InlineField>
)}
</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>
</ConfigSubSection>
<div className="gf-form-inline">
<div className="gf-form max-width-30">
<InlineField
label="Disable recording rules (beta)"
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={<>This feature will disable recording rules Turn this on to improve dashboard performance</>}
interactive={true}
className={styles.switchField}
disabled={options.readOnly}
>
<Switch
value={options.jsonData.disableRecordingRules ?? false}
onChange={onUpdateDatasourceJsonDataOptionChecked(props, 'disableRecordingRules')}
/>
</InlineField>
</div>
</div>
</div>
<h3 className="page-heading">Other</h3>
<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) =>

View File

@@ -0,0 +1,93 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { ConfigSubSection } from '@grafana/experimental';
import { InlineField, Input, TagsInput } from '@grafana/ui';
import { PROM_CONFIG_LABEL_WIDTH } from '../ConfigEditor';
import { Config, OnChangeHandler } from './ConnectionSettings';
// THIS FILE IS COPIED FROM GRAFANA/EXPERIMENTAL
// BECAUSE THE STYLES DO NOT MATCH THE PROM CONFIG
// THE TYPES ARE WRITTEN THERE WHERE THEY ARE NOT AS STRICT
// @ts-ignore
export type Props<C extends Config = Config> = {
config: C;
onChange: OnChangeHandler<C>;
className?: string;
};
// @ts-ignore
export const AdvancedHttpSettings: <C extends Config = Config>(props: Props<C>) => JSX.Element = ({
config,
onChange,
className,
}) => {
const onCookiesChange = (cookies: string[]) => {
onChange({
...config,
jsonData: {
...config.jsonData,
keepCookies: cookies,
},
});
};
const onTimeoutChange = (event: React.FormEvent<HTMLInputElement>) => {
onChange({
...config,
jsonData: {
...config.jsonData,
timeout: parseInt(event.currentTarget.value, 10),
},
});
};
const styles = {
container: css({
maxWidth: 578,
}),
};
return (
<>
<ConfigSubSection title="Advanced HTTP settings" className={cx(styles.container, className)}>
<InlineField
htmlFor="advanced-http-cookies"
label="Allowed cookies"
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip="Grafana proxy deletes forwarded cookies by default. Specify cookies by name that should be forwarded to the data source."
disabled={config.readOnly}
grow
>
<TagsInput
className="width-20"
id="advanced-http-cookies"
placeholder="New cookie (hit enter to add)"
tags={config.jsonData.keepCookies}
onChange={onCookiesChange}
/>
</InlineField>
<InlineField
htmlFor="advanced-http-timeout"
label="Timeout"
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip="HTTP request timeout in seconds"
disabled={config.readOnly}
grow
>
<Input
className="width-20"
id="advanced-http-timeout"
type="number"
min={0}
placeholder="Timeout in seconds"
aria-label="Timeout in seconds"
value={config.jsonData.timeout}
onChange={onTimeoutChange}
/>
</InlineField>
</ConfigSubSection>
</>
);
};

View File

@@ -0,0 +1,90 @@
import { css, cx } from '@emotion/css';
import React, { ReactNode } from 'react';
import { DataSourceJsonData, DataSourceSettings } from '@grafana/data';
import { ConfigSection } from '@grafana/experimental';
import { InlineField, Input, PopoverContent } from '@grafana/ui';
import { PromOptions } from '../../types';
// THIS FILE IS COPIED FROM GRAFANA/EXPERIMENTAL
// BECAUSE IT CONTAINS TYPES THAT ARE REQUIRED IN THE ADVANCEDHTTPSETTINGS COMPONENT
// THE TYPES ARE WRITTEN IN EXPERIMENTAL WHERE THEY ARE NOT AS STRICT
// @ts-ignore
export type Config<JSONData extends DataSourceJsonData, SecureJSONData> = DataSourceSettings<
// @ts-ignore
JSONData,
// @ts-ignore
SecureJSONData
>;
// @ts-ignore
export type OnChangeHandler<C extends Config = Config> = (options: DataSourceSettings<PromOptions, {}>) => void;
// @ts-ignore
export type Props<C extends Config = Config> = {
config: C;
onChange: OnChangeHandler<C>;
description?: ReactNode;
urlPlaceholder?: string;
urlTooltip?: PopoverContent;
urlLabel?: string;
className?: string;
};
// @ts-ignore
export const ConnectionSettings: <C extends Config = Config>(props: Props<C>) => JSX.Element = ({
config,
onChange,
description,
urlPlaceholder,
urlTooltip,
urlLabel,
className,
}) => {
const isValidUrl = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/.test(
config.url
);
const styles = {
container: css({
maxWidth: 578,
}),
};
return (
<>
<ConfigSection title="Connection" description={description} className={cx(styles.container, className)}>
<InlineField
htmlFor="connection-url"
label={urlLabel || 'URL'}
labelWidth={24}
tooltip={
urlTooltip || (
<>
Specify a complete HTTP URL
<br />
(for example https://example.com:8080)
</>
)
}
grow
disabled={config.readOnly}
required
invalid={!isValidUrl && !config.readOnly}
error={isValidUrl ? '' : 'Please enter a valid URL'}
interactive
>
<Input
id="connection-url"
aria-label="Datasource HTTP settings url"
onChange={(event) =>
onChange({
...config,
url: event.currentTarget.value,
})
}
value={config.url || ''}
placeholder={urlPlaceholder || 'URL'}
/>
</InlineField>
</ConfigSection>
</>
);
};

View File

@@ -0,0 +1,11 @@
import { ReactElement } from 'react';
// these are not available yet in grafana
export type CustomMethodId = `custom-${string}`;
export type CustomMethod = {
id: CustomMethodId;
label: string;
description: string;
component: ReactElement;
};

View File

@@ -45,6 +45,7 @@ export interface PromOptions extends DataSourceJsonData {
incrementalQuerying?: boolean;
incrementalQueryOverlapWindow?: string;
disableRecordingRules?: boolean;
sigV4Auth?: boolean;
}
export type ExemplarTraceIdDestination = {