mirror of https://github.com/grafana/grafana
InfluxDB: Config page refresh (#103060)
parent
35f89a456c
commit
3503fc209e
|
File diff suppressed because it is too large
Load Diff
@ -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(); |
||||
}); |
||||
}); |
||||
@ -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> |
||||
)} |
||||
</> |
||||
)} |
||||
</> |
||||
); |
||||
}; |
||||
@ -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(); |
||||
}); |
||||
}); |
||||
@ -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> |
||||
</> |
||||
)} |
||||
</> |
||||
); |
||||
}; |
||||
@ -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> |
||||
); |
||||
}; |
||||
@ -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> |
||||
); |
||||
}; |
||||
@ -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(); |
||||
}); |
||||
}); |
||||
@ -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> |
||||
</> |
||||
); |
||||
@ -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(); |
||||
}); |
||||
}); |
||||
@ -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> |
||||
</> |
||||
); |
||||
}; |
||||
@ -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(); |
||||
}); |
||||
}); |
||||
@ -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> |
||||
</> |
||||
); |
||||
}; |
||||
@ -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(); |
||||
}); |
||||
}); |
||||
@ -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> |
||||
</> |
||||
); |
||||
}; |
||||
@ -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> |
||||
); |
||||
}; |
||||
@ -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(); |
||||
}); |
||||
}); |
||||
@ -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); |
||||
|
||||
Loading…
Reference in new issue