InfluxDB: Config page refresh (#103060)
This commit is contained in:
@@ -1037,4 +1037,9 @@ export interface FeatureToggles {
|
||||
* @default true
|
||||
*/
|
||||
tabularNumbers?: boolean;
|
||||
/**
|
||||
* Enables new design for the InfluxDB data source configuration page
|
||||
* @default false
|
||||
*/
|
||||
newInfluxDSConfigPageDesign?: boolean;
|
||||
}
|
||||
|
||||
@@ -1780,6 +1780,14 @@ var (
|
||||
Owner: grafanaFrontendPlatformSquad,
|
||||
Expression: "true",
|
||||
},
|
||||
{
|
||||
Name: "newInfluxDSConfigPageDesign",
|
||||
Description: "Enables new design for the InfluxDB data source configuration page",
|
||||
Stage: FeatureStagePrivatePreview,
|
||||
FrontendOnly: false,
|
||||
Owner: grafanaPartnerPluginsSquad,
|
||||
Expression: "false",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -232,3 +232,4 @@ alertEnrichment,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertingImportAlertmanagerAPI,experimental,@grafana/alerting-squad,false,false,false
|
||||
preferLibraryPanelTitle,privatePreview,@grafana/dashboards-squad,false,false,false
|
||||
tabularNumbers,GA,@grafana/grafana-frontend-platform,false,false,false
|
||||
newInfluxDSConfigPageDesign,privatePreview,@grafana/partner-datasources,false,false,false
|
||||
|
||||
|
@@ -938,4 +938,8 @@ const (
|
||||
// FlagTabularNumbers
|
||||
// Use fixed-width numbers globally in the UI
|
||||
FlagTabularNumbers = "tabularNumbers"
|
||||
|
||||
// FlagNewInfluxDSConfigPageDesign
|
||||
// Enables new design for the InfluxDB data source configuration page
|
||||
FlagNewInfluxDSConfigPageDesign = "newInfluxDSConfigPageDesign"
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,7 @@ export function ButtonRow({ canSave, canDelete, onDelete, onSubmit, onTest }: Pr
|
||||
disabled={!canSave}
|
||||
onClick={onSubmit}
|
||||
data-testid={selectors.pages.DataSource.saveAndTest}
|
||||
id={selectors.pages.DataSource.saveAndTest}
|
||||
>
|
||||
<Trans i18nKey="datasources.button-row.save-and-test">Save & test</Trans>
|
||||
</Button>
|
||||
|
||||
@@ -123,6 +123,16 @@ export function EditDataSourceView({
|
||||
let currentJsonData = dataSource.jsonData;
|
||||
let currentSecureJsonData = dataSource.secureJsonData;
|
||||
|
||||
const isPDCInjected = components.some((component) => component.meta.pluginId === 'grafana-pdc-app');
|
||||
|
||||
const dataSourceWithIsPDCInjected = {
|
||||
...dataSource,
|
||||
jsonData: {
|
||||
...dataSource.jsonData,
|
||||
pdcInjected: isPDCInjected,
|
||||
},
|
||||
};
|
||||
|
||||
const dsi = getDataSourceSrv()?.getInstanceSettings(dataSource.uid);
|
||||
|
||||
const onSubmit = async (e: React.MouseEvent<HTMLButtonElement> | React.FormEvent<HTMLFormElement>) => {
|
||||
@@ -190,7 +200,7 @@ export function EditDataSourceView({
|
||||
<DataSourcePluginContextProvider instanceSettings={dsi}>
|
||||
<DataSourcePluginSettings
|
||||
plugin={plugin}
|
||||
dataSource={dataSource}
|
||||
dataSource={dataSourceWithIsPDCInjected}
|
||||
dataSourceMeta={dataSourceMeta}
|
||||
onModelChange={onOptionsChange}
|
||||
/>
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
|
||||
import { InfluxVersion } from '../../../types';
|
||||
|
||||
import { AdvancedDbConnectionSettings } from './AdvancedDBConnectionSettings';
|
||||
import { createTestProps } from './helpers';
|
||||
|
||||
describe('AdvancedDbConnectionSettings', () => {
|
||||
const onOptionsChangeMock = jest.fn();
|
||||
|
||||
const defaultProps = createTestProps({
|
||||
options: {
|
||||
jsonData: {},
|
||||
secureJsonData: {},
|
||||
secureJsonFields: {},
|
||||
},
|
||||
mocks: {
|
||||
onOptionsChange: onOptionsChangeMock,
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('toggles visibility of advanced settings', () => {
|
||||
render(<AdvancedDbConnectionSettings {...defaultProps} />);
|
||||
const toggle = screen.getByTestId('influxdb-v2-config-toggle-switch');
|
||||
fireEvent.click(toggle);
|
||||
});
|
||||
|
||||
it('renders HTTP Method field for InfluxQL version', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
jsonData: { version: InfluxVersion.InfluxQL },
|
||||
},
|
||||
};
|
||||
|
||||
render(<AdvancedDbConnectionSettings {...props} />);
|
||||
const toggle = screen.getByTestId('influxdb-v2-config-toggle-switch');
|
||||
fireEvent.click(toggle);
|
||||
expect(screen.getByTestId('influxdb-v2-config-http-method-select')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders insecure connection switch for SQL version and triggers change', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
jsonData: { version: InfluxVersion.SQL, insecureGrpc: false },
|
||||
},
|
||||
};
|
||||
|
||||
render(<AdvancedDbConnectionSettings {...props} />);
|
||||
const toggle = screen.getByTestId('influxdb-v2-config-toggle-switch');
|
||||
fireEvent.click(toggle);
|
||||
|
||||
const switchEl = screen.getByTestId('influxdb-v2-config-insecure-switch');
|
||||
expect(switchEl).toBeInTheDocument();
|
||||
fireEvent.click(switchEl);
|
||||
expect(onOptionsChangeMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders Min time interval input for InfluxQL', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
jsonData: { version: InfluxVersion.InfluxQL, timeInterval: '' },
|
||||
},
|
||||
};
|
||||
|
||||
render(<AdvancedDbConnectionSettings {...props} />);
|
||||
const toggle = screen.getByTestId('influxdb-v2-config-toggle-switch');
|
||||
fireEvent.click(toggle);
|
||||
|
||||
const input = screen.getByTestId('influxdb-v2-config-time-interval');
|
||||
expect(input).toBeInTheDocument();
|
||||
fireEvent.change(input, { target: { value: '15' } });
|
||||
expect(onOptionsChangeMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders Min time interval input for Flux', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
jsonData: { version: InfluxVersion.Flux, timeInterval: '' },
|
||||
},
|
||||
};
|
||||
|
||||
render(<AdvancedDbConnectionSettings {...props} />);
|
||||
const toggle = screen.getByTestId('influxdb-v2-config-toggle-switch');
|
||||
fireEvent.click(toggle);
|
||||
|
||||
const input = screen.getByTestId('influxdb-v2-config-time-interval');
|
||||
expect(input).toBeInTheDocument();
|
||||
fireEvent.change(input, { target: { value: '15' } });
|
||||
expect(onOptionsChangeMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import { cx } from '@emotion/css';
|
||||
import { useState } from 'react';
|
||||
|
||||
import {
|
||||
onUpdateDatasourceJsonDataOption,
|
||||
onUpdateDatasourceJsonDataOptionChecked,
|
||||
onUpdateDatasourceJsonDataOptionSelect,
|
||||
} from '@grafana/data';
|
||||
import { InlineFieldRow, InlineField, Combobox, InlineSwitch, Input, Space, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { InfluxVersion } from '../../../types';
|
||||
|
||||
import { getInlineLabelStyles, HTTP_MODES } from './constants';
|
||||
import {
|
||||
trackInfluxDBConfigV2AdvancedDbConnectionSettingsHTTPMethodClicked,
|
||||
trackInfluxDBConfigV2AdvancedDbConnectionSettingsInsecureConnectClicked,
|
||||
trackInfluxDBConfigV2AdvancedDbConnectionSettingsMinTimeClicked,
|
||||
trackInfluxDBConfigV2AdvancedDbConnectionSettingsToggleClicked,
|
||||
} from './tracking';
|
||||
import { Props } from './types';
|
||||
|
||||
export const AdvancedDbConnectionSettings = (props: Props) => {
|
||||
const { options } = props;
|
||||
const styles = useStyles2(getInlineLabelStyles);
|
||||
|
||||
const [advancedDbConnectionSettingsIsOpen, setAdvancedDbConnectionSettingsIsOpen] = useState(
|
||||
() => !!options.jsonData.timeInterval || !!options.jsonData.insecureGrpc
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Space v={2} />
|
||||
<InlineField label={<div className={cx(styles.label)}>Advanced Database Settings</div>} labelWidth={40}>
|
||||
<InlineSwitch
|
||||
data-testid="influxdb-v2-config-toggle-switch"
|
||||
value={advancedDbConnectionSettingsIsOpen}
|
||||
onChange={() => setAdvancedDbConnectionSettingsIsOpen(!advancedDbConnectionSettingsIsOpen)}
|
||||
onBlur={trackInfluxDBConfigV2AdvancedDbConnectionSettingsToggleClicked}
|
||||
/>
|
||||
</InlineField>
|
||||
|
||||
{advancedDbConnectionSettingsIsOpen && (
|
||||
<>
|
||||
{options.jsonData.version === InfluxVersion.InfluxQL && (
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label="HTTP Method"
|
||||
labelWidth={30}
|
||||
tooltip="You can use either GET or POST HTTP method to query your InfluxDB database. The POST
|
||||
method allows you to perform heavy requests (with a lots of WHERE clause) while the GET method
|
||||
will restrict you and return an error if the query is too large."
|
||||
>
|
||||
<Combobox
|
||||
width={30}
|
||||
value={HTTP_MODES.find((httpMode) => httpMode.value === options.jsonData.httpMode)}
|
||||
options={HTTP_MODES}
|
||||
onChange={onUpdateDatasourceJsonDataOptionSelect(props, 'httpMode')}
|
||||
onBlur={trackInfluxDBConfigV2AdvancedDbConnectionSettingsHTTPMethodClicked}
|
||||
data-testid="influxdb-v2-config-http-method-select"
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
)}
|
||||
|
||||
{options.jsonData.version === InfluxVersion.SQL && (
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Insecure Connection" labelWidth={30}>
|
||||
<InlineSwitch
|
||||
data-testid="influxdb-v2-config-insecure-switch"
|
||||
value={options.jsonData.insecureGrpc ?? false}
|
||||
onChange={onUpdateDatasourceJsonDataOptionChecked(props, 'insecureGrpc')}
|
||||
onBlur={trackInfluxDBConfigV2AdvancedDbConnectionSettingsInsecureConnectClicked}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
)}
|
||||
|
||||
{(options.jsonData.version === InfluxVersion.InfluxQL || options.jsonData.version === InfluxVersion.Flux) && (
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label="Min time interval"
|
||||
labelWidth={30}
|
||||
tooltip="A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example 1m if your data is written every minute."
|
||||
>
|
||||
<Input
|
||||
className="width-15"
|
||||
data-testid="influxdb-v2-config-time-interval"
|
||||
onBlur={trackInfluxDBConfigV2AdvancedDbConnectionSettingsMinTimeClicked}
|
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'timeInterval')}
|
||||
placeholder="10s"
|
||||
value={options.jsonData.timeInterval || ''}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
|
||||
import { AdvancedHttpSettings } from './AdvancedHttpSettings';
|
||||
import { createTestProps } from './helpers';
|
||||
|
||||
describe('AdvancedHttpSettings', () => {
|
||||
const onOptionsChangeMock = jest.fn();
|
||||
|
||||
const defaultProps = createTestProps({
|
||||
options: {},
|
||||
mocks: {
|
||||
onOptionsChange: onOptionsChangeMock,
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders toggle and toggles section visibility', () => {
|
||||
render(<AdvancedHttpSettings {...defaultProps} />);
|
||||
|
||||
const toggle = screen.getByTestId('influxdb-v2-config-advanced-http-settings-toggle');
|
||||
expect(toggle).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(toggle);
|
||||
expect(screen.getByLabelText('Timeout in seconds')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onOptionsChange when timeout is changed', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
jsonData: {
|
||||
...defaultProps.options.jsonData,
|
||||
timeout: 10,
|
||||
keepCookies: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
render(<AdvancedHttpSettings {...props} />);
|
||||
|
||||
const input = screen.getByLabelText('Timeout in seconds');
|
||||
fireEvent.change(input, { target: { value: '20' } });
|
||||
expect(props.onOptionsChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onOptionsChange when allowed cookies are changed', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
jsonData: {
|
||||
...defaultProps.options.jsonData,
|
||||
timeout: 10,
|
||||
keepCookies: ['session'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
render(<AdvancedHttpSettings {...props} />);
|
||||
|
||||
const cookieInput = screen.getByPlaceholderText('New cookie (hit enter to add)');
|
||||
fireEvent.change(cookieInput, { target: { value: 'auth' } });
|
||||
fireEvent.keyDown(cookieInput, { key: 'Enter', code: 'Enter' });
|
||||
|
||||
expect(props.onOptionsChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import { cx } from '@emotion/css';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
|
||||
import {
|
||||
Box,
|
||||
InlineField,
|
||||
InlineSwitch,
|
||||
Field,
|
||||
TagsInput,
|
||||
Input,
|
||||
CustomHeadersSettings,
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { InfluxOptions } from '../../../types';
|
||||
|
||||
import { getInlineLabelStyles } from './constants';
|
||||
import {
|
||||
trackInfluxDBConfigV2AdvancedHTTPSettingsTimeoutField,
|
||||
trackInfluxDBConfigV2AdvancedHTTPSettingsToggleClicked,
|
||||
} from './tracking';
|
||||
|
||||
export type Props = DataSourcePluginOptionsEditorProps<InfluxOptions>;
|
||||
|
||||
export const AdvancedHttpSettings = ({ options, onOptionsChange }: Props) => {
|
||||
const styles = useStyles2(getInlineLabelStyles);
|
||||
|
||||
const [advancedHttpSettingsIsOpen, setAdvancedHttpSettingsIsOpen] = useState(
|
||||
() => 'keepCookies' in options.jsonData || 'timeout' in options.jsonData
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box display="flex" alignItems="center">
|
||||
<InlineField label={<div className={cx(styles.label)}>Advanced HTTP Settings</div>} labelWidth={40}>
|
||||
<InlineSwitch
|
||||
data-testid="influxdb-v2-config-advanced-http-settings-toggle"
|
||||
value={advancedHttpSettingsIsOpen}
|
||||
onChange={() => setAdvancedHttpSettingsIsOpen(!advancedHttpSettingsIsOpen)}
|
||||
onBlur={trackInfluxDBConfigV2AdvancedHTTPSettingsToggleClicked}
|
||||
/>
|
||||
</InlineField>
|
||||
</Box>
|
||||
{advancedHttpSettingsIsOpen && options.access === 'proxy' && (
|
||||
<>
|
||||
<Box paddingLeft={1} marginY={1}>
|
||||
<Box width="50%" marginBottom={2}>
|
||||
<Field
|
||||
label="Allowed cookies"
|
||||
description="Grafana proxy deletes forwarded cookies by default. Specify cookies by name that should
|
||||
be forwarded to the data source."
|
||||
disabled={options.readOnly}
|
||||
noMargin
|
||||
>
|
||||
<TagsInput
|
||||
id="advanced-http-cookies"
|
||||
placeholder="New cookie (hit enter to add)"
|
||||
tags={
|
||||
'keepCookies' in options.jsonData && Array.isArray(options.jsonData.keepCookies)
|
||||
? options.jsonData.keepCookies
|
||||
: []
|
||||
}
|
||||
onChange={(e) => {
|
||||
onOptionsChange({
|
||||
...options,
|
||||
jsonData: {
|
||||
...options.jsonData,
|
||||
...{ keepCookies: e },
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
</Box>
|
||||
|
||||
<Box width="50%" marginBottom={2}>
|
||||
<Field
|
||||
htmlFor="advanced-http-timeout"
|
||||
label="Timeout"
|
||||
description="HTTP request timeout in seconds."
|
||||
disabled={options.readOnly}
|
||||
noMargin
|
||||
>
|
||||
<Input
|
||||
id="advanced-http-timeout"
|
||||
type="number"
|
||||
min={0}
|
||||
placeholder="Timeout in seconds"
|
||||
aria-label="Timeout in seconds"
|
||||
value={
|
||||
'timeout' in options.jsonData && typeof options.jsonData.timeout === 'number'
|
||||
? options.jsonData.timeout.toString()
|
||||
: ''
|
||||
}
|
||||
onChange={(e) => {
|
||||
const parsed = parseInt(e.currentTarget.value, 10);
|
||||
onOptionsChange({
|
||||
...options,
|
||||
jsonData: {
|
||||
...options.jsonData,
|
||||
...{ timeout: parsed },
|
||||
},
|
||||
});
|
||||
}}
|
||||
onBlur={trackInfluxDBConfigV2AdvancedHTTPSettingsTimeoutField}
|
||||
/>
|
||||
</Field>
|
||||
</Box>
|
||||
|
||||
{advancedHttpSettingsIsOpen && (
|
||||
<CustomHeadersSettings dataSourceConfig={options} onChange={onOptionsChange} />
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen, fireEvent, within } from '@testing-library/react';
|
||||
|
||||
import { AuthSettings } from './AuthSettings';
|
||||
import { createTestProps } from './helpers';
|
||||
|
||||
describe('AuthSettings', () => {
|
||||
const onOptionsChangeMock = jest.fn();
|
||||
|
||||
const defaultProps = createTestProps({
|
||||
options: {
|
||||
jsonData: {},
|
||||
secureJsonData: {},
|
||||
secureJsonFields: {},
|
||||
},
|
||||
mocks: {
|
||||
onOptionsChange: onOptionsChangeMock,
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('collapsible behaviour', () => {
|
||||
it('starts collapsed when no auth option is active', () => {
|
||||
render(<AuthSettings {...defaultProps} />);
|
||||
|
||||
// Heading inside the collapsible body should be absent
|
||||
expect(screen.queryByText(/Authentication Method/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('expands when the top‑level switch is toggled', () => {
|
||||
render(<AuthSettings {...defaultProps} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('influxdb-v2-config-auth-settings-toggle'));
|
||||
|
||||
expect(screen.getByText(/Authentication Method/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Basic Auth', () => {
|
||||
beforeEach(() => {
|
||||
render(<AuthSettings {...defaultProps} />);
|
||||
|
||||
// open section first
|
||||
fireEvent.click(screen.getByTestId('influxdb-v2-config-auth-settings-toggle'));
|
||||
});
|
||||
|
||||
it('reveals Basic Auth inputs when Basic Auth is selected', () => {
|
||||
fireEvent.click(screen.getByRole('radio', { name: /Basic Auth/i }));
|
||||
|
||||
expect(screen.getByPlaceholderText('User')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Password')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('propagates user input via onOptionsChange', async () => {
|
||||
fireEvent.click(screen.getByRole('radio', { name: /Basic Auth/i }));
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('User'), {
|
||||
target: { value: 'john_doe' },
|
||||
});
|
||||
|
||||
expect(onOptionsChangeMock).toHaveBeenCalledWith(expect.objectContaining({ basicAuthUser: 'john_doe' }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('TLS Settings', () => {
|
||||
beforeEach(() => {
|
||||
render(<AuthSettings {...defaultProps} />);
|
||||
|
||||
// Expand the settings panel once for all tests
|
||||
fireEvent.click(screen.getByTestId('influxdb-v2-config-auth-settings-toggle'));
|
||||
});
|
||||
|
||||
describe('TLS Client Auth', () => {
|
||||
it('shows and hides Server Name input when toggled Enabled/Disabled', () => {
|
||||
const tlsRow = screen.getByTestId('influxdb-v2-config-auth-settings-tls-client-auth-toggle');
|
||||
|
||||
// Initially hidden
|
||||
expect(screen.queryByPlaceholderText('domain.example.com')).not.toBeInTheDocument();
|
||||
|
||||
// Enable TLS Client Auth
|
||||
fireEvent.click(within(tlsRow).getByText('Enabled'));
|
||||
expect(screen.getByPlaceholderText('domain.example.com')).toBeInTheDocument();
|
||||
|
||||
// Disable again
|
||||
fireEvent.click(within(tlsRow).getByText('Disabled'));
|
||||
expect(screen.queryByPlaceholderText('domain.example.com')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CA Cert', () => {
|
||||
it('shows and hides certificate textarea when toggled Enabled/Disabled', () => {
|
||||
const placeholderText = 'Begins with -----BEGIN CERTIFICATE-----';
|
||||
|
||||
const caRow = screen.getByTestId('influxdb-v2-config-auth-settings-ca-cert-toggle');
|
||||
const enabledRadio = within(caRow).getByText('Enabled');
|
||||
const disabledRadio = within(caRow).getByText('Disabled');
|
||||
|
||||
// Initially hidden
|
||||
expect(screen.queryByPlaceholderText(placeholderText)).not.toBeInTheDocument();
|
||||
|
||||
// Enable
|
||||
fireEvent.click(enabledRadio);
|
||||
expect(screen.getByPlaceholderText(placeholderText)).toBeInTheDocument();
|
||||
|
||||
// Disable
|
||||
fireEvent.click(disabledRadio);
|
||||
expect(screen.queryByPlaceholderText(placeholderText)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Skip TLS Verify', () => {
|
||||
it('toggles checked state of the switch', () => {
|
||||
const skipSwitch = screen.getByTestId('influxdb-v2-config-auth-settings-skip-tls-verify');
|
||||
|
||||
// Default unchecked
|
||||
expect(skipSwitch).not.toBeChecked();
|
||||
|
||||
// Enable
|
||||
fireEvent.click(skipSwitch);
|
||||
expect(skipSwitch).toBeChecked();
|
||||
|
||||
// Disable
|
||||
fireEvent.click(skipSwitch);
|
||||
expect(skipSwitch).not.toBeChecked();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,267 @@
|
||||
import { cx } from '@emotion/css';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
onUpdateDatasourceOption,
|
||||
onUpdateDatasourceSecureJsonDataOption,
|
||||
updateDatasourcePluginResetOption,
|
||||
} from '@grafana/data';
|
||||
import { AuthMethod, convertLegacyAuthProps } from '@grafana/plugin-ui';
|
||||
import {
|
||||
Box,
|
||||
CertificationKey,
|
||||
Field,
|
||||
InlineField,
|
||||
InlineSwitch,
|
||||
Input,
|
||||
Label,
|
||||
RadioButtonGroup,
|
||||
SecretInput,
|
||||
useStyles2,
|
||||
Text,
|
||||
Stack,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { AUTH_RADIO_BUTTON_OPTIONS, getInlineLabelStyles, RADIO_BUTTON_OPTIONS } from './constants';
|
||||
import {
|
||||
trackInfluxDBConfigV2AuthSettingsAuthMethodSelected,
|
||||
trackInfluxDBConfigV2AuthSettingsToggleClicked,
|
||||
} from './tracking';
|
||||
import { Props } from './types';
|
||||
|
||||
type AuthOptionState = {
|
||||
basicAuth: boolean;
|
||||
tlsClientAuth: boolean;
|
||||
caCert: boolean;
|
||||
skipTLS: boolean;
|
||||
oAuthForward: boolean;
|
||||
withCredentials: boolean;
|
||||
};
|
||||
|
||||
export const AuthSettings = (props: Props) => {
|
||||
const { options, onOptionsChange } = props;
|
||||
const styles = useStyles2(getInlineLabelStyles);
|
||||
|
||||
/**
|
||||
* Derived props from legacy helpers
|
||||
*/
|
||||
const authProps = useMemo(
|
||||
() =>
|
||||
convertLegacyAuthProps({
|
||||
config: options,
|
||||
onChange: onOptionsChange,
|
||||
}),
|
||||
[options, onOptionsChange]
|
||||
);
|
||||
|
||||
/**
|
||||
* Selected authentication method. Fallback to the most common if the selected one is missing.
|
||||
*/
|
||||
const isAuthMethod = (v: unknown): v is AuthMethod =>
|
||||
v === AuthMethod.NoAuth || v === AuthMethod.BasicAuth || v === AuthMethod.OAuthForward;
|
||||
|
||||
const selectedMethod = useMemo<AuthMethod | undefined>(() => {
|
||||
if (isAuthMethod(authProps.selectedMethod)) {
|
||||
return authProps.selectedMethod;
|
||||
}
|
||||
return isAuthMethod(authProps.mostCommonMethod) ? authProps.mostCommonMethod : undefined;
|
||||
}, [authProps.selectedMethod, authProps.mostCommonMethod]);
|
||||
|
||||
/**
|
||||
* Local UI state
|
||||
*/
|
||||
const [authOptions, setAuthOptions] = useState<AuthOptionState>({
|
||||
basicAuth: selectedMethod === AuthMethod.BasicAuth,
|
||||
tlsClientAuth: authProps.TLS?.TLSClientAuth.enabled ?? false,
|
||||
caCert: authProps.TLS?.selfSignedCertificate.enabled ?? false,
|
||||
skipTLS: authProps.TLS?.skipTLSVerification.enabled ?? false,
|
||||
oAuthForward: selectedMethod === AuthMethod.OAuthForward,
|
||||
withCredentials: options.withCredentials ?? false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Expand/collapse top–level section
|
||||
*/
|
||||
const [authenticationSettingsIsOpen, setAuthenticationSettingsIsOpen] = useState(
|
||||
Object.values(authOptions).some(Boolean)
|
||||
);
|
||||
|
||||
const toggleOpen = useCallback(() => {
|
||||
setAuthenticationSettingsIsOpen((prev) => {
|
||||
trackInfluxDBConfigV2AuthSettingsToggleClicked();
|
||||
return !prev;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleAuthMethodChange = useCallback(
|
||||
(option: AuthMethod) => {
|
||||
authProps.onAuthMethodSelect(option);
|
||||
setAuthOptions((prev) => ({
|
||||
...prev,
|
||||
basicAuth: option === AuthMethod.BasicAuth,
|
||||
oAuthForward: option === AuthMethod.OAuthForward,
|
||||
}));
|
||||
trackInfluxDBConfigV2AuthSettingsAuthMethodSelected({ authMethod: option });
|
||||
},
|
||||
[authProps]
|
||||
);
|
||||
|
||||
/**
|
||||
* Wraps the toggle of an auth option, updates the local state and calls the onToggle callback
|
||||
* provided by the legacy `authProps` provided by `@grafana/plugin-ui`.
|
||||
*/
|
||||
const toggleOption = useCallback((key: keyof AuthOptionState, onToggle: (value: boolean) => void) => {
|
||||
setAuthOptions((prev) => {
|
||||
const nextValue = !prev[key];
|
||||
const next = { ...prev, [key]: nextValue };
|
||||
onToggle(nextValue);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack direction="column">
|
||||
{/* Header toggle */}
|
||||
<Box alignItems="center">
|
||||
<InlineField label={<div className={cx(styles.label)}>Auth and TLS/SSL Settings</div>} labelWidth={35}>
|
||||
<InlineSwitch
|
||||
data-testid="influxdb-v2-config-auth-settings-toggle"
|
||||
value={authenticationSettingsIsOpen}
|
||||
onChange={toggleOpen}
|
||||
/>
|
||||
</InlineField>
|
||||
</Box>
|
||||
|
||||
{/* Collapsible settings body */}
|
||||
{authenticationSettingsIsOpen && (
|
||||
<Box paddingLeft={1}>
|
||||
{/* Authentication Method */}
|
||||
<Box marginBottom={2}>
|
||||
<Field label={<Text element="h5">Authentication Method</Text>} noMargin>
|
||||
<Box width="50%" marginY={2}>
|
||||
<RadioButtonGroup
|
||||
options={AUTH_RADIO_BUTTON_OPTIONS}
|
||||
value={selectedMethod}
|
||||
onChange={handleAuthMethodChange}
|
||||
/>
|
||||
</Box>
|
||||
</Field>
|
||||
</Box>
|
||||
|
||||
<Box marginBottom={2}>
|
||||
{/* Basic Auth settings */}
|
||||
{authOptions.basicAuth && (
|
||||
<>
|
||||
<Box display="flex" direction="column" width="60%" marginBottom={2}>
|
||||
<InlineField label="User" labelWidth={14} grow>
|
||||
<Input
|
||||
placeholder="User"
|
||||
onChange={onUpdateDatasourceOption(props, 'basicAuthUser')}
|
||||
value={options.basicAuthUser || ''}
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField label="Password" labelWidth={14} grow>
|
||||
<SecretInput
|
||||
placeholder="Password"
|
||||
isConfigured={options.secureJsonFields.basicAuthPassword || false}
|
||||
onChange={onUpdateDatasourceSecureJsonDataOption(props, 'basicAuthPassword')}
|
||||
onReset={() => updateDatasourcePluginResetOption(props, 'basicAuthPassword')}
|
||||
value={options.secureJsonData?.basicAuthPassword || ''}
|
||||
/>
|
||||
</InlineField>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* TLS Client Auth */}
|
||||
<Box marginBottom={2}>
|
||||
<Field noMargin>
|
||||
<>
|
||||
<Text element="h5">TLS Settings</Text>
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
data-testid="influxdb-v2-config-auth-settings-tls-client-auth-toggle"
|
||||
marginTop={2}
|
||||
>
|
||||
<Label style={{ width: '125px' }}>TLS Client Auth</Label>
|
||||
<RadioButtonGroup
|
||||
options={RADIO_BUTTON_OPTIONS}
|
||||
value={authOptions.tlsClientAuth}
|
||||
onChange={() => toggleOption('tlsClientAuth', authProps.TLS!.TLSClientAuth.onToggle)}
|
||||
size="sm"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{authOptions.tlsClientAuth && (
|
||||
<Box marginTop={2}>
|
||||
<InlineField label="Server Name" labelWidth={14} grow>
|
||||
<Input
|
||||
placeholder="domain.example.com"
|
||||
onChange={(e) => authProps.TLS?.TLSClientAuth.onServerNameChange(e.currentTarget.value)}
|
||||
value={authProps.TLS?.TLSClientAuth.serverName || ''}
|
||||
/>
|
||||
</InlineField>
|
||||
<CertificationKey
|
||||
label="Client Cert"
|
||||
placeholder="Begins with -----BEGIN CERTIFICATE-----"
|
||||
onChange={(e) => authProps.TLS?.TLSClientAuth.onClientCertificateChange(e.currentTarget.value)}
|
||||
hasCert={!!authProps.TLS?.TLSClientAuth.clientCertificateConfigured}
|
||||
onClick={() => authProps.TLS?.TLSClientAuth.onClientCertificateReset()}
|
||||
/>
|
||||
<CertificationKey
|
||||
label="Client Key"
|
||||
placeholder="Begins with -----BEGIN RSA PRIVATE KEY-----"
|
||||
onChange={(e) => authProps.TLS?.TLSClientAuth.onClientKeyChange(e.currentTarget.value)}
|
||||
hasCert={!!authProps.TLS?.TLSClientAuth.clientKeyConfigured}
|
||||
onClick={() => authProps.TLS?.TLSClientAuth.onClientKeyReset()}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
</Field>
|
||||
</Box>
|
||||
|
||||
{/* CA Cert */}
|
||||
<Box marginBottom={2}>
|
||||
<Field noMargin>
|
||||
<>
|
||||
<Box display="flex" alignItems="center" data-testid="influxdb-v2-config-auth-settings-ca-cert-toggle">
|
||||
<Label style={{ width: '125px' }}>CA Cert</Label>
|
||||
<RadioButtonGroup
|
||||
options={RADIO_BUTTON_OPTIONS}
|
||||
value={authOptions.caCert}
|
||||
onChange={() => toggleOption('caCert', authProps.TLS!.selfSignedCertificate.onToggle)}
|
||||
size="sm"
|
||||
/>
|
||||
</Box>
|
||||
{authOptions.caCert && (
|
||||
<Box marginTop={3}>
|
||||
<CertificationKey
|
||||
label="CA Cert"
|
||||
placeholder="Begins with -----BEGIN CERTIFICATE-----"
|
||||
onChange={(e) => authProps.TLS?.selfSignedCertificate.onCertificateChange(e.currentTarget.value)}
|
||||
hasCert={!!authProps.TLS?.selfSignedCertificate.certificateConfigured}
|
||||
onClick={() => authProps.TLS?.selfSignedCertificate.onCertificateReset()}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
</Field>
|
||||
</Box>
|
||||
|
||||
{/* Skip TLS verify */}
|
||||
<Box display="flex" direction="row" alignItems="center">
|
||||
<Label style={{ width: '125px' }}>Skip TLS Verify</Label>
|
||||
<InlineSwitch
|
||||
data-testid="influxdb-v2-config-auth-settings-skip-tls-verify"
|
||||
value={authOptions.skipTLS}
|
||||
onChange={() => toggleOption('skipTLS', authProps.TLS!.skipTLSVerification.onToggle)}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { ConfigEditor } from './ConfigEditor';
|
||||
import { createTestProps } from './helpers';
|
||||
|
||||
jest.mock('./LeftSideBar', () => ({
|
||||
LeftSideBar: () => <div data-testid="left-sidebar" />,
|
||||
}));
|
||||
|
||||
jest.mock('./UrlAndAuthenticationSection', () => ({
|
||||
UrlAndAuthenticationSection: () => <div data-testid="url-auth-section" />,
|
||||
}));
|
||||
|
||||
jest.mock('./DatabaseConnectionSection', () => ({
|
||||
DatabaseConnectionSection: () => <div data-testid="db-connection-section" />,
|
||||
}));
|
||||
|
||||
describe('ConfigEditor', () => {
|
||||
const defaultProps = createTestProps({
|
||||
options: {
|
||||
jsonData: {},
|
||||
secureJsonData: {},
|
||||
secureJsonFields: {},
|
||||
},
|
||||
mocks: {
|
||||
onOptionsChange: jest.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
it('renders the LeftSideBar, UrlAndAuthenticationSection, and DatabaseConnectionSection', () => {
|
||||
render(<ConfigEditor {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId('left-sidebar')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('url-auth-section')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('db-connection-section')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the informational alert', () => {
|
||||
render(<ConfigEditor {...defaultProps} />);
|
||||
expect(screen.getByText(/You are viewing a new design/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Alert, Box, Stack, TextLink } from '@grafana/ui';
|
||||
|
||||
import { DatabaseConnectionSection } from './DatabaseConnectionSection';
|
||||
import { LeftSideBar } from './LeftSideBar';
|
||||
import { UrlAndAuthenticationSection } from './UrlAndAuthenticationSection';
|
||||
import { trackInfluxDBConfigV2FeedbackButtonClicked } from './tracking';
|
||||
import { Props } from './types';
|
||||
|
||||
export const ConfigEditor: React.FC<Props> = ({ onOptionsChange, options }: Props) => {
|
||||
return (
|
||||
<Stack justifyContent="space-between">
|
||||
<Box width="250px" flex="0 0 250px">
|
||||
<LeftSideBar pdcInjected={options?.jsonData?.pdcInjected!!} />
|
||||
</Box>
|
||||
<Box width="60%" flex="1 1 auto">
|
||||
<Stack direction="column">
|
||||
<Alert severity="info" title="You are viewing a new design for the InfluxDB configuration settings.">
|
||||
<>
|
||||
If something isn't working correctly, you can revert to the original configuration page design by
|
||||
disabling the <code>newInfluxDSConfigPageDesign</code> feature flag.{' '}
|
||||
<TextLink
|
||||
href="https://docs.google.com/forms/d/1wiJJ5WFu33maVNaWDmJuW0VbhoeR6-VSuu6y2mHMb_4/viewform"
|
||||
external
|
||||
onClick={trackInfluxDBConfigV2FeedbackButtonClicked}
|
||||
>
|
||||
Submit feedback.
|
||||
</TextLink>
|
||||
</>
|
||||
</Alert>
|
||||
<UrlAndAuthenticationSection options={options} onOptionsChange={onOptionsChange} />
|
||||
<DatabaseConnectionSection options={options} onOptionsChange={onOptionsChange} />
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box width="20%" flex="0 0 20%">
|
||||
{/* TODO: Right sidebar */}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { InfluxVersion } from '../../../types';
|
||||
|
||||
import { DatabaseConnectionSection } from './DatabaseConnectionSection';
|
||||
import { createTestProps } from './helpers';
|
||||
|
||||
jest.mock('./AdvancedDBConnectionSettings', () => ({
|
||||
AdvancedDbConnectionSettings: () => <div data-testid="advanced-db-settings" />,
|
||||
}));
|
||||
|
||||
jest.mock('./InfluxFluxDBConnection', () => ({
|
||||
InfluxFluxDBConnection: () => <div data-testid="flux-connection" />,
|
||||
}));
|
||||
|
||||
jest.mock('./InfluxSQLDBConnection', () => ({
|
||||
InfluxSQLDBConnection: () => <div data-testid="sql-connection" />,
|
||||
}));
|
||||
|
||||
jest.mock('./InfluxInfluxQLDBConnection', () => ({
|
||||
InfluxInfluxQLDBConnection: () => <div data-testid="influxql-connection" />,
|
||||
}));
|
||||
|
||||
describe('DatabaseConnectionSection', () => {
|
||||
const onOptionsChangeMock = jest.fn();
|
||||
|
||||
const defaultProps = createTestProps({
|
||||
options: {
|
||||
jsonData: {},
|
||||
secureJsonData: {},
|
||||
secureJsonFields: {},
|
||||
},
|
||||
mocks: {
|
||||
onOptionsChange: onOptionsChangeMock,
|
||||
},
|
||||
});
|
||||
|
||||
it('shows alert when version is missing', () => {
|
||||
render(<DatabaseConnectionSection {...defaultProps} />);
|
||||
expect(screen.getByText(/To view connection settings/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Flux connection component when version is Flux', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
jsonData: { version: InfluxVersion.Flux },
|
||||
},
|
||||
};
|
||||
|
||||
render(<DatabaseConnectionSection {...props} />);
|
||||
expect(screen.getByTestId('flux-connection')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders InfluxQL connection component when version is InfluxQL', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
jsonData: { version: InfluxVersion.InfluxQL },
|
||||
},
|
||||
};
|
||||
|
||||
render(<DatabaseConnectionSection {...props} />);
|
||||
expect(screen.getByTestId('influxql-connection')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders SQL connection component when version is SQL', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
jsonData: { version: InfluxVersion.SQL },
|
||||
},
|
||||
};
|
||||
|
||||
render(<DatabaseConnectionSection {...props} />);
|
||||
expect(screen.getByTestId('sql-connection')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('always renders AdvancedDbConnectionSettings', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
jsonData: { version: InfluxVersion.SQL },
|
||||
},
|
||||
};
|
||||
|
||||
render(<DatabaseConnectionSection {...props} />);
|
||||
expect(screen.getByTestId('advanced-db-settings')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { Box, CollapsableSection, Alert, Space, Text } from '@grafana/ui';
|
||||
|
||||
import { InfluxVersion } from '../../../types';
|
||||
|
||||
import { AdvancedDbConnectionSettings } from './AdvancedDBConnectionSettings';
|
||||
import { InfluxFluxDBConnection } from './InfluxFluxDBConnection';
|
||||
import { InfluxInfluxQLDBConnection } from './InfluxInfluxQLDBConnection';
|
||||
import { InfluxSQLDBConnection } from './InfluxSQLDBConnection';
|
||||
import { CONFIG_SECTION_HEADERS } from './constants';
|
||||
import { Props } from './types';
|
||||
|
||||
export const DatabaseConnectionSection = ({ options, onOptionsChange }: Props) => (
|
||||
<>
|
||||
<Box borderStyle="solid" borderColor="weak" padding={2} marginBottom={4} id={`${CONFIG_SECTION_HEADERS[1].id}`}>
|
||||
<CollapsableSection
|
||||
label={<Text element="h3">2. {CONFIG_SECTION_HEADERS[1].label}</Text>}
|
||||
isOpen={CONFIG_SECTION_HEADERS[1].isOpen}
|
||||
>
|
||||
{!options.jsonData.version && (
|
||||
<Alert severity="info" title="Query language required">
|
||||
<p>To view connection settings, first choose a query language in the URL and Connection section.</p>
|
||||
</Alert>
|
||||
)}
|
||||
{options.jsonData.version === InfluxVersion.InfluxQL && (
|
||||
<>
|
||||
<Alert severity="info" title="Database Access">
|
||||
<p>
|
||||
Setting the database for this datasource does not deny access to other databases. The InfluxDB query
|
||||
syntax allows switching the database in the query. For example:
|
||||
<code>SHOW MEASUREMENTS ON _internal</code> or
|
||||
<code>SELECT * FROM "_internal".."database" LIMIT 10</code>
|
||||
<br />
|
||||
<br />
|
||||
To support data isolation and security, make sure appropriate permissions are configured in InfluxDB.
|
||||
</p>
|
||||
</Alert>
|
||||
</>
|
||||
)}
|
||||
{options.jsonData.version && (
|
||||
<>
|
||||
<Text color="secondary">
|
||||
Provide the necessary database connection details based on your selected InfluxDB product and query
|
||||
language.
|
||||
</Text>
|
||||
<Space v={2} />
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
{options.jsonData.version === InfluxVersion.InfluxQL && (
|
||||
<InfluxInfluxQLDBConnection options={options} onOptionsChange={onOptionsChange} />
|
||||
)}
|
||||
{options.jsonData.version === InfluxVersion.Flux && (
|
||||
<InfluxFluxDBConnection options={options} onOptionsChange={onOptionsChange} />
|
||||
)}
|
||||
{options.jsonData.version === InfluxVersion.SQL && (
|
||||
<InfluxSQLDBConnection options={options} onOptionsChange={onOptionsChange} />
|
||||
)}
|
||||
{options.jsonData.version && (
|
||||
<AdvancedDbConnectionSettings options={options} onOptionsChange={onOptionsChange} />
|
||||
)}
|
||||
</>
|
||||
</CollapsableSection>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
|
||||
import { InfluxFluxDBConnection } from './InfluxFluxDBConnection';
|
||||
import { createTestProps } from './helpers';
|
||||
|
||||
describe('InfluxFluxDBConnection', () => {
|
||||
const onOptionsChangeMock = jest.fn();
|
||||
|
||||
const defaultProps = createTestProps({
|
||||
options: {
|
||||
jsonData: {
|
||||
organization: 'MyOrg',
|
||||
defaultBucket: 'MyBucket',
|
||||
},
|
||||
secureJsonData: {
|
||||
token: 'my-token',
|
||||
},
|
||||
secureJsonFields: {
|
||||
token: true,
|
||||
},
|
||||
},
|
||||
mocks: {
|
||||
onOptionsChange: onOptionsChangeMock,
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders organization, bucket and token inputs', () => {
|
||||
render(<InfluxFluxDBConnection {...defaultProps} />);
|
||||
expect(screen.getByLabelText(/Organization/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/Default Bucket/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/Token/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onOptionsChange on input change', () => {
|
||||
render(<InfluxFluxDBConnection {...defaultProps} />);
|
||||
const orgInput = screen.getByLabelText(/Organization/i);
|
||||
fireEvent.change(orgInput, { target: { value: 'NewOrg' } });
|
||||
expect(onOptionsChangeMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
onUpdateDatasourceJsonDataOption,
|
||||
onUpdateDatasourceSecureJsonDataOption,
|
||||
updateDatasourcePluginResetOption,
|
||||
} from '@grafana/data';
|
||||
import { InlineFieldRow, InlineField, Input, SecretInput } from '@grafana/ui';
|
||||
|
||||
import {
|
||||
trackInfluxDBConfigV2FluxDBDetailsDefaultBucketInputField,
|
||||
trackInfluxDBConfigV2FluxDBDetailsOrgInputField,
|
||||
trackInfluxDBConfigV2FluxDBDetailsTokenInputField,
|
||||
} from './tracking';
|
||||
import { Props } from './types';
|
||||
|
||||
export const InfluxFluxDBConnection = (props: Props) => {
|
||||
const {
|
||||
options: { jsonData, secureJsonData, secureJsonFields },
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Organization" labelWidth={30} grow>
|
||||
<Input
|
||||
id="organization"
|
||||
placeholder="myorg"
|
||||
onBlur={trackInfluxDBConfigV2FluxDBDetailsOrgInputField}
|
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'organization')}
|
||||
value={jsonData.organization || ''}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField labelWidth={30} label="Default Bucket" grow>
|
||||
<Input
|
||||
id="default-bucket"
|
||||
onBlur={trackInfluxDBConfigV2FluxDBDetailsDefaultBucketInputField}
|
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'defaultBucket')}
|
||||
placeholder="mybucket"
|
||||
value={jsonData.defaultBucket || ''}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField labelWidth={30} label="Token" grow>
|
||||
<SecretInput
|
||||
id="token"
|
||||
isConfigured={Boolean(secureJsonFields && secureJsonFields.token)}
|
||||
onBlur={trackInfluxDBConfigV2FluxDBDetailsTokenInputField}
|
||||
onChange={onUpdateDatasourceSecureJsonDataOption(props, 'token')}
|
||||
onReset={() => updateDatasourcePluginResetOption(props, 'token')}
|
||||
value={secureJsonData?.token || ''}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
|
||||
import { InfluxInfluxQLDBConnection } from './InfluxInfluxQLDBConnection';
|
||||
import { createTestProps } from './helpers';
|
||||
|
||||
describe('InfluxInfluxQLDBConnection', () => {
|
||||
const onOptionsChangeMock = jest.fn();
|
||||
|
||||
const defaultProps = createTestProps({
|
||||
options: {
|
||||
user: 'admin',
|
||||
jsonData: {
|
||||
dbName: 'influxdb',
|
||||
},
|
||||
secureJsonData: {
|
||||
password: 'secret',
|
||||
},
|
||||
secureJsonFields: {
|
||||
password: true,
|
||||
},
|
||||
},
|
||||
mocks: {
|
||||
onOptionsChange: onOptionsChangeMock,
|
||||
},
|
||||
});
|
||||
|
||||
it('renders dbName, user and password fields', () => {
|
||||
render(<InfluxInfluxQLDBConnection {...defaultProps} />);
|
||||
expect(screen.getByLabelText(/Database/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/User/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/Password/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onOptionsChange on input changes', () => {
|
||||
render(<InfluxInfluxQLDBConnection {...defaultProps} />);
|
||||
fireEvent.change(screen.getByLabelText(/User/i), { target: { value: 'newuser' } });
|
||||
expect(onOptionsChangeMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
onUpdateDatasourceJsonDataOption,
|
||||
onUpdateDatasourceOption,
|
||||
onUpdateDatasourceSecureJsonDataOption,
|
||||
updateDatasourcePluginResetOption,
|
||||
} from '@grafana/data';
|
||||
import { InlineFieldRow, InlineField, Input, SecretInput } from '@grafana/ui';
|
||||
|
||||
import {
|
||||
trackInfluxDBConfigV2InfluxQLDBDetailsDatabaseInputField,
|
||||
trackInfluxDBConfigV2InfluxQLDBDetailsPasswordInputField,
|
||||
trackInfluxDBConfigV2InfluxQLDBDetailsUserInputField,
|
||||
} from './tracking';
|
||||
import { Props } from './types';
|
||||
|
||||
export const InfluxInfluxQLDBConnection = (props: Props) => {
|
||||
const { options } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Database" labelWidth={30} grow>
|
||||
<Input
|
||||
id="database"
|
||||
placeholder="mydb"
|
||||
value={options.jsonData.dbName}
|
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'dbName')}
|
||||
onBlur={trackInfluxDBConfigV2InfluxQLDBDetailsDatabaseInputField}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="User" labelWidth={30} grow>
|
||||
<Input
|
||||
id="user"
|
||||
placeholder="myuser"
|
||||
value={options.user || ''}
|
||||
onChange={onUpdateDatasourceOption(props, 'user')}
|
||||
onBlur={trackInfluxDBConfigV2InfluxQLDBDetailsUserInputField}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Password" labelWidth={30} grow>
|
||||
<SecretInput
|
||||
id="password"
|
||||
isConfigured={Boolean(options.secureJsonFields && options.secureJsonFields.password)}
|
||||
value={options.secureJsonData?.password || ''}
|
||||
onReset={() => updateDatasourcePluginResetOption(props, 'password')}
|
||||
onChange={onUpdateDatasourceSecureJsonDataOption(props, 'password')}
|
||||
onBlur={trackInfluxDBConfigV2InfluxQLDBDetailsPasswordInputField}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
|
||||
import { InfluxSQLDBConnection } from './InfluxSQLDBConnection';
|
||||
import { createTestProps } from './helpers';
|
||||
|
||||
describe('InfluxSQLDBConnection', () => {
|
||||
const onOptionsChangeMock = jest.fn();
|
||||
|
||||
const defaultProps = createTestProps({
|
||||
options: {
|
||||
jsonData: {
|
||||
dbName: 'testdb',
|
||||
},
|
||||
secureJsonData: {
|
||||
token: 'abc123',
|
||||
},
|
||||
secureJsonFields: {
|
||||
token: true,
|
||||
},
|
||||
},
|
||||
mocks: {
|
||||
onOptionsChange: onOptionsChangeMock,
|
||||
},
|
||||
});
|
||||
|
||||
it('renders database and token fields', () => {
|
||||
render(<InfluxSQLDBConnection {...defaultProps} />);
|
||||
expect(screen.getByLabelText(/Database/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/Token/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onOptionsChange on dbName change', () => {
|
||||
render(<InfluxSQLDBConnection {...defaultProps} />);
|
||||
fireEvent.change(screen.getByLabelText(/Database/i), { target: { value: 'newdb' } });
|
||||
expect(onOptionsChangeMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
onUpdateDatasourceJsonDataOption,
|
||||
onUpdateDatasourceSecureJsonDataOption,
|
||||
updateDatasourcePluginResetOption,
|
||||
} from '@grafana/data';
|
||||
import { InlineFieldRow, InlineField, Input, SecretInput } from '@grafana/ui';
|
||||
|
||||
import {
|
||||
trackInfluxDBConfigV2SQLDBDetailsDatabaseInputField,
|
||||
trackInfluxDBConfigV2SQLDBDetailsTokenInputField,
|
||||
} from './tracking';
|
||||
import { Props } from './types';
|
||||
|
||||
export const InfluxSQLDBConnection = (props: Props) => {
|
||||
const { options } = props;
|
||||
const { secureJsonData, secureJsonFields } = options;
|
||||
|
||||
return (
|
||||
<>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Database" labelWidth={30} grow>
|
||||
<Input
|
||||
id="database"
|
||||
placeholder="mydb"
|
||||
value={options.jsonData.dbName}
|
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'dbName')}
|
||||
onBlur={trackInfluxDBConfigV2SQLDBDetailsDatabaseInputField}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField labelWidth={30} label="Token" grow>
|
||||
<SecretInput
|
||||
id="token"
|
||||
isConfigured={Boolean(secureJsonFields && secureJsonFields.token)}
|
||||
onBlur={trackInfluxDBConfigV2SQLDBDetailsTokenInputField}
|
||||
onChange={onUpdateDatasourceSecureJsonDataOption(props, 'token')}
|
||||
onReset={() => updateDatasourcePluginResetOption(props, 'token')}
|
||||
value={secureJsonData?.token || ''}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { LeftSideBar } from './LeftSideBar';
|
||||
|
||||
describe('LeftSideBar', () => {
|
||||
it('renders sidebar title when pdcInjected is true', () => {
|
||||
render(<LeftSideBar pdcInjected={true} />);
|
||||
expect(screen.getByTestId('Private data source connect-sidebar')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render sidebar title when pdcInjected is false', () => {
|
||||
render(<LeftSideBar pdcInjected={false} />);
|
||||
expect(screen.queryByTestId('Private data source connect-sidebar')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Box, InlineField, LinkButton, Space, Stack, Text } from '@grafana/ui';
|
||||
|
||||
import { CONFIG_SECTION_HEADERS, CONFIG_SECTION_HEADERS_WITH_PDC } from './constants';
|
||||
|
||||
interface LeftSideBarProps {
|
||||
pdcInjected: boolean;
|
||||
}
|
||||
|
||||
export const LeftSideBar = ({ pdcInjected }: LeftSideBarProps) => {
|
||||
const headers = pdcInjected ? CONFIG_SECTION_HEADERS_WITH_PDC : CONFIG_SECTION_HEADERS;
|
||||
return (
|
||||
<Stack>
|
||||
<Box flex={1} marginY={5}>
|
||||
<Text element="h4">InfluxDB</Text>
|
||||
<Box paddingTop={2}>
|
||||
{headers.map((header, index) => (
|
||||
<div key={index} data-testid={`${header.label}-sidebar`}>
|
||||
<InlineField label={`${index + 1}`} style={{ display: 'flex', alignItems: 'center' }} grow>
|
||||
<LinkButton
|
||||
variant="secondary"
|
||||
fill="text"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
const target = document.getElementById(header.id);
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{header.label}
|
||||
</LinkButton>
|
||||
</InlineField>
|
||||
<Space v={1} />
|
||||
</div>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
|
||||
import { InfluxVersion } from '../../../types';
|
||||
|
||||
import { UrlAndAuthenticationSection } from './UrlAndAuthenticationSection';
|
||||
import { createTestProps } from './helpers';
|
||||
|
||||
describe('UrlAndAuthenticationSection', () => {
|
||||
const onOptionsChangeMock = jest.fn();
|
||||
|
||||
const defaultProps = createTestProps({
|
||||
options: {
|
||||
jsonData: {
|
||||
url: 'http://localhost:8086',
|
||||
product: '',
|
||||
version: '',
|
||||
},
|
||||
secureJsonData: {},
|
||||
secureJsonFields: {},
|
||||
},
|
||||
mocks: {
|
||||
onOptionsChange: onOptionsChangeMock,
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('calls onOptionsChange when URL is changed', () => {
|
||||
render(<UrlAndAuthenticationSection {...defaultProps} />);
|
||||
|
||||
const input = screen.getByTestId('influxdb-v2-config-url-input');
|
||||
fireEvent.change(input, { target: { value: 'http://example.com' } });
|
||||
|
||||
expect(onOptionsChangeMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders DRBP warning for InfluxDB OSS 1.x and InfluxQL', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
jsonData: { product: 'InfluxDB OSS 1.x', version: InfluxVersion.InfluxQL },
|
||||
},
|
||||
};
|
||||
|
||||
render(<UrlAndAuthenticationSection {...props} />);
|
||||
expect(screen.getByText(/requires DRBP mapping/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders DRBP warning for InfluxDB OSS 2.x and InfluxQL', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
jsonData: { product: 'InfluxDB OSS 2.x', version: InfluxVersion.InfluxQL },
|
||||
},
|
||||
};
|
||||
|
||||
render(<UrlAndAuthenticationSection {...props} />);
|
||||
expect(screen.getByText(/requires DRBP mapping/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render DRBP warning for InfluxDB OSS 1.x and Flux', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
jsonData: { product: 'InfluxDB OSS 1.x', version: InfluxVersion.Flux },
|
||||
},
|
||||
};
|
||||
|
||||
render(<UrlAndAuthenticationSection {...props} />);
|
||||
expect(screen.queryByText(/requires DRBP mapping/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render DRBP warning for InfluxDB OSS 2.x and Flux', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
jsonData: { product: 'InfluxDB OSS 2.x', version: InfluxVersion.Flux },
|
||||
},
|
||||
};
|
||||
|
||||
render(<UrlAndAuthenticationSection {...props} />);
|
||||
expect(screen.queryByText(/requires DRBP mapping/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
import { onUpdateDatasourceJsonDataOptionSelect, onUpdateDatasourceOption } from '@grafana/data';
|
||||
import {
|
||||
Box,
|
||||
CollapsableSection,
|
||||
TextLink,
|
||||
Field,
|
||||
Input,
|
||||
Combobox,
|
||||
Space,
|
||||
Stack,
|
||||
Text,
|
||||
ComboboxOption,
|
||||
Alert,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { InfluxVersion } from '../../../types';
|
||||
|
||||
import { AdvancedHttpSettings } from './AdvancedHttpSettings';
|
||||
import { AuthSettings } from './AuthSettings';
|
||||
import { CONFIG_SECTION_HEADERS } from './constants';
|
||||
import {
|
||||
trackInfluxDBConfigV2ProductSelected,
|
||||
trackInfluxDBConfigV2QueryLanguageSelected,
|
||||
trackInfluxDBConfigV2URLInputField,
|
||||
} from './tracking';
|
||||
import { Props } from './types';
|
||||
import { INFLUXDB_VERSION_MAP } from './versions';
|
||||
|
||||
const getQueryLanguageOptions = (productName: string): Array<{ value: string }> => {
|
||||
const product = INFLUXDB_VERSION_MAP.find(({ name }) => name === productName);
|
||||
return product?.queryLanguages?.map(({ name }) => ({ value: name })) ?? [];
|
||||
};
|
||||
|
||||
export const UrlAndAuthenticationSection = (props: Props) => {
|
||||
const { options, onOptionsChange } = props;
|
||||
|
||||
const isInfluxVersion = (v: string): v is InfluxVersion =>
|
||||
typeof v === 'string' && (v === InfluxVersion.Flux || v === InfluxVersion.InfluxQL || v === InfluxVersion.SQL);
|
||||
|
||||
// Database + Retention Policy (DBRP) mapping is required for InfluxDB OSS 1.x and 2.x when using InfluxQL
|
||||
const requiresDrbpMapping =
|
||||
options.jsonData.product &&
|
||||
options.jsonData.version === InfluxVersion.InfluxQL &&
|
||||
['InfluxDB OSS 1.x', 'InfluxDB OSS 2.x'].includes(options.jsonData.product);
|
||||
|
||||
const onProductChange = ({ value }: ComboboxOption) =>
|
||||
onOptionsChange({ ...options, jsonData: { ...options.jsonData, product: value, version: undefined } });
|
||||
|
||||
const onQueryLanguageChange = (option: ComboboxOption) => {
|
||||
const { value } = option;
|
||||
|
||||
if (isInfluxVersion(value)) {
|
||||
onUpdateDatasourceJsonDataOptionSelect(props, 'version')(option);
|
||||
}
|
||||
};
|
||||
|
||||
const onUrlChange = (event: React.ChangeEvent<HTMLInputElement>) => onUpdateDatasourceOption(props, 'url')(event);
|
||||
|
||||
return (
|
||||
<Box borderStyle="solid" borderColor="weak" padding={2} marginBottom={4} id={`${CONFIG_SECTION_HEADERS[0].id}`}>
|
||||
<CollapsableSection
|
||||
label={<Text element="h3">1. {CONFIG_SECTION_HEADERS[0].label}</Text>}
|
||||
isOpen={CONFIG_SECTION_HEADERS[0].isOpen}
|
||||
>
|
||||
<Text color="secondary">
|
||||
Enter the URL of your InfluxDB instance, then select your product and query language. This will determine the
|
||||
available settings and authentication methods in the next steps. If you are unsure what product you are using,
|
||||
view the{' '}
|
||||
<TextLink href="https://docs.influxdata.com/" external>
|
||||
InfluxDB Docs.
|
||||
</TextLink>
|
||||
.
|
||||
</Text>
|
||||
|
||||
<Box direction="column" gap={2} marginTop={3}>
|
||||
<Field label={<div style={{ marginBottom: '5px' }}>URL</div>} noMargin>
|
||||
<Input
|
||||
data-testid="influxdb-v2-config-url-input"
|
||||
placeholder="http://localhost:3000/"
|
||||
onChange={onUrlChange}
|
||||
value={options.url || ''}
|
||||
onBlur={trackInfluxDBConfigV2URLInputField}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Box marginTop={2}>
|
||||
<Stack direction="row" gap={2}>
|
||||
<Box flex={1}>
|
||||
<Field label={<div style={{ marginBottom: '5px' }}>Product</div>} noMargin>
|
||||
<Combobox
|
||||
data-testid="influxdb-v2-config-product-select"
|
||||
value={options.jsonData.product}
|
||||
options={INFLUXDB_VERSION_MAP.map(({ name }) => ({ value: name }))}
|
||||
onChange={onProductChange}
|
||||
onBlur={() => trackInfluxDBConfigV2ProductSelected({ product: options.jsonData.product! })}
|
||||
/>
|
||||
</Field>
|
||||
</Box>
|
||||
<Box flex={1}>
|
||||
<Field label={<div style={{ marginBottom: '5px' }}>Query language</div>} noMargin>
|
||||
<Combobox
|
||||
data-testid="influxdb-v2-config-query-language-select"
|
||||
value={options.jsonData.product !== '' ? options.jsonData.version : ''}
|
||||
options={getQueryLanguageOptions(options.jsonData.product || '')}
|
||||
onChange={onQueryLanguageChange}
|
||||
onBlur={() => trackInfluxDBConfigV2QueryLanguageSelected({ version: options.url })}
|
||||
/>
|
||||
</Field>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Space v={2} />
|
||||
|
||||
{requiresDrbpMapping && (
|
||||
<Alert severity="warning" title="InfluxQL requires DRBP mapping">
|
||||
InfluxDB OSS 1.x and 2.x users must configure a Database + Retention Policy (DBRP) mapping via the CLI or
|
||||
API before data can be queried.{' '}
|
||||
<TextLink href="https://docs.influxdata.com/influxdb/cloud/query-data/influxql/dbrp/" external>
|
||||
Learn how to set this up
|
||||
</TextLink>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<AdvancedHttpSettings options={options} onOptionsChange={onOptionsChange} />
|
||||
<AuthSettings options={options} onOptionsChange={onOptionsChange} />
|
||||
</Box>
|
||||
</CollapsableSection>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { AuthMethod } from '@grafana/plugin-ui';
|
||||
import { ComboboxOption } from '@grafana/ui';
|
||||
|
||||
export const RADIO_BUTTON_OPTIONS = [
|
||||
{ label: 'Enabled', value: true },
|
||||
{ label: 'Disabled', value: false },
|
||||
];
|
||||
|
||||
export const AUTH_RADIO_BUTTON_OPTIONS = [
|
||||
{ label: 'No Authentication', value: AuthMethod.NoAuth },
|
||||
{ label: 'Basic Authentication', value: AuthMethod.BasicAuth },
|
||||
{ label: 'Forward OAuth Identity', value: AuthMethod.OAuthForward },
|
||||
];
|
||||
|
||||
export const CONFIG_SECTION_HEADERS = [
|
||||
{ label: 'URL and authentication', id: 'url', isOpen: true },
|
||||
{ label: 'Database settings', id: 'tls', isOpen: true },
|
||||
{ label: 'Save & test', id: `${selectors.pages.DataSource.saveAndTest}`, isOpen: true },
|
||||
];
|
||||
|
||||
export const CONFIG_SECTION_HEADERS_WITH_PDC = [
|
||||
{ label: 'URL and authentication', id: 'url', isOpen: true },
|
||||
{ label: 'Database settings', id: 'tls', isOpen: true },
|
||||
{ label: 'Private data source connect', id: 'pdc', isOpen: true },
|
||||
{ label: 'Save & test', id: `${selectors.pages.DataSource.saveAndTest}`, isOpen: true },
|
||||
];
|
||||
|
||||
export const HTTP_MODES: ComboboxOption[] = [
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'POST', value: 'POST' },
|
||||
];
|
||||
|
||||
export const getInlineLabelStyles = (theme: GrafanaTheme2, transparent = false, width?: number | 'auto') => {
|
||||
return {
|
||||
label: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flexShrink: 0,
|
||||
padding: theme.spacing(0, 1),
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
fontSize: theme.typography.size.md,
|
||||
backgroundColor: transparent ? 'transparent' : theme.colors.background.secondary,
|
||||
height: theme.spacing(theme.components.height.md),
|
||||
lineHeight: theme.spacing(theme.components.height.md),
|
||||
marginRight: theme.spacing(0.5),
|
||||
borderRadius: theme.shape.radius.default,
|
||||
border: 'none',
|
||||
width: '240px',
|
||||
color: theme.colors.text.primary,
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Creates a set of test props for the InfluxDB V2 config page for use in tests.
|
||||
* This function allows you to override default properties for specific test cases.
|
||||
*/
|
||||
export const createTestProps = (overrides: { options?: object; mocks?: object }) => ({
|
||||
options: {
|
||||
access: 'proxy',
|
||||
basicAuth: false,
|
||||
basicAuthUser: '',
|
||||
database: '',
|
||||
id: 1,
|
||||
isDefault: false,
|
||||
jsonData: {
|
||||
httpMode: 'POST',
|
||||
timeInterval: '5',
|
||||
},
|
||||
name: 'InfluxDB',
|
||||
orgId: 1,
|
||||
readOnly: false,
|
||||
secureJsonFields: {},
|
||||
type: 'influxdb',
|
||||
typeLogoUrl: '',
|
||||
typeName: 'Influx',
|
||||
uid: 'z',
|
||||
url: '',
|
||||
user: '',
|
||||
version: 1,
|
||||
withCredentials: false,
|
||||
...overrides.options,
|
||||
},
|
||||
onOptionsChange: jest.fn(),
|
||||
...overrides.mocks,
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
|
||||
// Config Section
|
||||
export const trackInfluxDBConfigV2FeedbackButtonClicked = () => {
|
||||
reportInteraction('influxdb-config-v2-feedback-button-clicked');
|
||||
};
|
||||
|
||||
// URL and Auth Section
|
||||
export const trackInfluxDBConfigV2URLInputField = () => {
|
||||
reportInteraction('influxdb-config-v2-url-input-field');
|
||||
};
|
||||
|
||||
export const trackInfluxDBConfigV2QueryLanguageSelected = (props: { version: string }) => {
|
||||
reportInteraction('influxdb-config-v2-query-language-dropdown', props);
|
||||
};
|
||||
|
||||
export const trackInfluxDBConfigV2ProductSelected = (props: { product: string }) => {
|
||||
reportInteraction('influxdb-config-v2-product-selection-dropdown', props);
|
||||
};
|
||||
|
||||
// Flux Database Details Fields
|
||||
export const trackInfluxDBConfigV2FluxDBDetailsOrgInputField = () => {
|
||||
reportInteraction('influxdb-config-v2-flux-dbdetails-org-input-field');
|
||||
};
|
||||
|
||||
export const trackInfluxDBConfigV2FluxDBDetailsDefaultBucketInputField = () => {
|
||||
reportInteraction('influxdb-config-v2-flux-dbdetails-default-bucket-input-field');
|
||||
};
|
||||
|
||||
export const trackInfluxDBConfigV2FluxDBDetailsTokenInputField = () => {
|
||||
reportInteraction('influxdb-config-v2-flux-dbdetails-token-input-field');
|
||||
};
|
||||
|
||||
// InfluxQL Database Details Fields
|
||||
export const trackInfluxDBConfigV2InfluxQLDBDetailsDatabaseInputField = () => {
|
||||
reportInteraction('influxdb-config-v2-influxql-dbdetails-database-input-field');
|
||||
};
|
||||
|
||||
export const trackInfluxDBConfigV2InfluxQLDBDetailsUserInputField = () => {
|
||||
reportInteraction('influxdb-config-v2-influxql-dbdetails-user-input-field');
|
||||
};
|
||||
|
||||
export const trackInfluxDBConfigV2InfluxQLDBDetailsPasswordInputField = () => {
|
||||
reportInteraction('influxdb-config-v2-influxql-dbdetails-password-input-field');
|
||||
};
|
||||
|
||||
// SQL Database Details Fields
|
||||
export const trackInfluxDBConfigV2SQLDBDetailsDatabaseInputField = () => {
|
||||
reportInteraction('influxdb-config-v2-sql-dbdetails-database-input-field');
|
||||
};
|
||||
|
||||
export const trackInfluxDBConfigV2SQLDBDetailsTokenInputField = () => {
|
||||
reportInteraction('influxdb-config-v2-sql-dbdetails-token-input-field');
|
||||
};
|
||||
|
||||
// Advanced DB Connection Settings
|
||||
export const trackInfluxDBConfigV2AdvancedDbConnectionSettingsToggleClicked = () => {
|
||||
reportInteraction('influxdb-config-v2-advanceddb-settings-toggle-clicked');
|
||||
};
|
||||
|
||||
export const trackInfluxDBConfigV2AdvancedDbConnectionSettingsHTTPMethodClicked = () => {
|
||||
reportInteraction('influxdb-config-v2-advanceddb-settings-http-method-clicked');
|
||||
};
|
||||
|
||||
export const trackInfluxDBConfigV2AdvancedDbConnectionSettingsInsecureConnectClicked = () => {
|
||||
reportInteraction('influxdb-config-v2-advanceddb-settings-insecure-connection-clicked');
|
||||
};
|
||||
|
||||
export const trackInfluxDBConfigV2AdvancedDbConnectionSettingsMinTimeClicked = () => {
|
||||
reportInteraction('influxdb-config-v2-advanceddb-settings-min-time-clicked');
|
||||
};
|
||||
|
||||
// Advanced HTTP Settings
|
||||
export const trackInfluxDBConfigV2AdvancedHTTPSettingsToggleClicked = () => {
|
||||
reportInteraction('influxdb-config-v2-advanced-http-settings-toggle-clicked');
|
||||
};
|
||||
|
||||
export const trackInfluxDBConfigV2AdvancedHTTPSettingsTimeoutField = () => {
|
||||
reportInteraction('influxdb-config-v2-advanced-http-settings-timeout-field');
|
||||
};
|
||||
|
||||
// Auth Settings
|
||||
export const trackInfluxDBConfigV2AuthSettingsToggleClicked = () => {
|
||||
reportInteraction('influxdb-config-v2-auth-settings-toggle-clicked');
|
||||
};
|
||||
|
||||
export const trackInfluxDBConfigV2AuthSettingsAuthMethodSelected = (props: { authMethod: string }) => {
|
||||
reportInteraction('influxdb-config-v2-advanced-http-settings-timeout-field', props);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
|
||||
|
||||
import { InfluxOptions, InfluxSecureJsonData } from '../../../types';
|
||||
|
||||
// As we're not using the auth component in `@grafana/plugin-ui`, we're defining the missing properties here
|
||||
// to ensure the types are compatible with the existing code.
|
||||
//
|
||||
// They should be removed at a later point.
|
||||
|
||||
type InfluxBasicAuthData = {
|
||||
basicAuth?: boolean;
|
||||
basicAuthUser?: string;
|
||||
};
|
||||
|
||||
type InfluxSecureBasicAuthData = {
|
||||
basicAuthPassword?: string;
|
||||
};
|
||||
|
||||
export type Props = DataSourcePluginOptionsEditorProps<
|
||||
InfluxOptions & InfluxBasicAuthData,
|
||||
InfluxSecureJsonData & InfluxSecureBasicAuthData
|
||||
>;
|
||||
@@ -0,0 +1,141 @@
|
||||
import { InfluxVersion } from '../../../types';
|
||||
|
||||
interface AuthMethod {
|
||||
type: 'Basic' | 'Token';
|
||||
fields: string[];
|
||||
}
|
||||
|
||||
interface QueryLanguageConfig {
|
||||
name: InfluxVersion;
|
||||
fields: Array<string | AuthMethod>;
|
||||
}
|
||||
|
||||
interface DetectionMethod {
|
||||
urlContains?: string[];
|
||||
pingHeaderResponse?: Record<string, string>;
|
||||
}
|
||||
interface InfluxDBProduct {
|
||||
name: string;
|
||||
queryLanguages?: QueryLanguageConfig[];
|
||||
detectionMethod?: DetectionMethod;
|
||||
}
|
||||
|
||||
// Complete Data Structure:
|
||||
export const INFLUXDB_VERSION_MAP: InfluxDBProduct[] = [
|
||||
{
|
||||
name: 'InfluxDB Cloud Dedicated',
|
||||
queryLanguages: [
|
||||
{ name: InfluxVersion.SQL, fields: ['Host', 'Database', 'Token'] },
|
||||
{ name: InfluxVersion.InfluxQL, fields: ['Host', 'Database', 'Token'] },
|
||||
],
|
||||
detectionMethod: {
|
||||
urlContains: ['influxdb.io'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'InfluxDB Cloud Serverless',
|
||||
queryLanguages: [
|
||||
{ name: InfluxVersion.SQL, fields: ['Host', 'Bucket', 'Token'] },
|
||||
{ name: InfluxVersion.InfluxQL, fields: ['Host', 'Bucket', 'Token'] },
|
||||
{ name: InfluxVersion.Flux, fields: ['Host', 'Organization', 'Token', 'Default bucket'] },
|
||||
],
|
||||
detectionMethod: {
|
||||
urlContains: ['us-east-1-1.aws.cloud2.influxdata.com', 'eu-central-1-1.aws.cloud2.influxdata.com'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'InfluxDB Clustered',
|
||||
queryLanguages: [
|
||||
{ name: InfluxVersion.SQL, fields: ['Host', 'Database', 'Token'] },
|
||||
{ name: InfluxVersion.InfluxQL, fields: ['URL', 'Database', 'Token'] },
|
||||
],
|
||||
detectionMethod: {
|
||||
pingHeaderResponse: {
|
||||
'x-influxdb-version': '\\s*influxqlbridged-development',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'InfluxDB Enterprise 1.x',
|
||||
queryLanguages: [
|
||||
{ name: InfluxVersion.InfluxQL, fields: ['URL', 'Database', 'User', 'Password'] },
|
||||
{ name: InfluxVersion.Flux, fields: ['URL', 'User', 'Password', 'Default database'] },
|
||||
],
|
||||
detectionMethod: {
|
||||
pingHeaderResponse: {
|
||||
'x-influxdb-build': 'Enterprise (needs confirmation)',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'InfluxDB Enterprise 3.x',
|
||||
queryLanguages: [
|
||||
{ name: InfluxVersion.SQL, fields: ['URL', 'Token'] },
|
||||
{ name: InfluxVersion.InfluxQL, fields: ['URL', 'Token'] },
|
||||
],
|
||||
detectionMethod: {
|
||||
pingHeaderResponse: {
|
||||
'x-influxdb-build': 'TBD',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'InfluxDB Cloud (TSM)',
|
||||
queryLanguages: [
|
||||
{ name: InfluxVersion.InfluxQL, fields: ['URL', 'Database', 'Token'] },
|
||||
{ name: InfluxVersion.Flux, fields: ['URL', 'Organization', 'Token', 'Default bucket'] },
|
||||
],
|
||||
detectionMethod: {
|
||||
urlContains: [
|
||||
'us-west-2-1.aws.cloud2.influxdata.com',
|
||||
'us-west-2-2.aws.cloud2.influxdata.com',
|
||||
'us-east-1-1.aws.cloud2.influxdata.com',
|
||||
'eu-central-1-1.aws.cloud2.influxdata.com',
|
||||
'us-central1-1.gcp.cloud2.influxdata.com',
|
||||
'westeurope-1.azure.cloud2.influxdata.com',
|
||||
'eastus-1.azure.cloud2.influxdata.com',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'InfluxDB Cloud 1',
|
||||
queryLanguages: [{ name: InfluxVersion.InfluxQL, fields: ['URL', 'Database', 'Username', 'Password'] }],
|
||||
detectionMethod: {
|
||||
urlContains: ['influxcloud.net'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'InfluxDB OSS 1.x',
|
||||
queryLanguages: [
|
||||
{ name: InfluxVersion.InfluxQL, fields: ['URL', 'Database', 'Username', 'Password'] },
|
||||
{ name: InfluxVersion.Flux, fields: ['URL', 'Username', 'Password', 'Default database'] },
|
||||
],
|
||||
detectionMethod: {
|
||||
pingHeaderResponse: {
|
||||
'x-influxdb-build': 'OSS',
|
||||
'x-influxdb-version': '^1\\.',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'InfluxDB OSS 2.x',
|
||||
queryLanguages: [
|
||||
{
|
||||
name: InfluxVersion.InfluxQL,
|
||||
fields: [
|
||||
'URL',
|
||||
'Database',
|
||||
{ type: 'Basic', fields: ['Username', 'Password'] },
|
||||
{ type: 'Token', fields: ['Token'] },
|
||||
],
|
||||
},
|
||||
{ name: InfluxVersion.Flux, fields: ['URL', 'Token', 'Default bucket'] },
|
||||
],
|
||||
detectionMethod: {
|
||||
pingHeaderResponse: {
|
||||
'x-influxdb-build': 'OSS',
|
||||
'x-influxdb-version': '^2\\.',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,11 +1,16 @@
|
||||
import { DataSourcePlugin } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import ConfigEditor from './components/editor/config/ConfigEditor';
|
||||
import { ConfigEditor as ConfigEditorV1 } from './components/editor/config/ConfigEditor';
|
||||
import { ConfigEditor as ConfigEditorV2 } from './components/editor/config-v2/ConfigEditor';
|
||||
import { QueryEditor } from './components/editor/query/QueryEditor';
|
||||
import { InfluxStartPage } from './components/editor/query/influxql/InfluxStartPage';
|
||||
import InfluxDatasource from './datasource';
|
||||
|
||||
// ConfigEditorV2 is the new design for the InfluxDB configuration page
|
||||
const configEditor = config.featureToggles.newInfluxDSConfigPageDesign ? ConfigEditorV2 : ConfigEditorV1;
|
||||
|
||||
export const plugin = new DataSourcePlugin(InfluxDatasource)
|
||||
.setConfigEditor(ConfigEditor)
|
||||
.setConfigEditor(configEditor)
|
||||
.setQueryEditor(QueryEditor)
|
||||
.setQueryEditorHelp(InfluxStartPage);
|
||||
|
||||
@@ -15,6 +15,8 @@ export interface InfluxOptions extends DataSourceJsonData {
|
||||
httpMode?: string;
|
||||
|
||||
dbName?: string;
|
||||
product?: string;
|
||||
pdcInjected?: boolean;
|
||||
|
||||
// With Flux
|
||||
organization?: string;
|
||||
|
||||
Reference in New Issue
Block a user