From 1cb66d86b0d977678bb2a353fc887534cb5518a5 Mon Sep 17 00:00:00 2001 From: Alyssa Joyner <58453566+alyssajoyner@users.noreply.github.com> Date: Tue, 28 Oct 2025 08:09:43 -0600 Subject: [PATCH] [InfluxDB]: Update product selection and UI (#112074) --- .../editor/config-v2/ConfigEditor.tsx | 11 +- .../config-v2/DatabaseConnectionSection.tsx | 2 +- .../InfluxInfluxQLDBConnection.test.tsx | 9 +- .../editor/config-v2/LeftSideBar.tsx | 75 +++++++--- .../UrlAndAuthenticationSection.test.tsx | 62 +++++---- .../config-v2/UrlAndAuthenticationSection.tsx | 130 +++++++++++++----- .../components/editor/config-v2/constants.ts | 18 +-- .../components/editor/config-v2/versions.ts | 23 +++- 8 files changed, 235 insertions(+), 95 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/components/editor/config-v2/ConfigEditor.tsx b/public/app/plugins/datasource/influxdb/components/editor/config-v2/ConfigEditor.tsx index 508a9b5617d..a6cc7eb3747 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/config-v2/ConfigEditor.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/config-v2/ConfigEditor.tsx @@ -15,7 +15,7 @@ export const ConfigEditor: React.FC = ({ onOptionsChange, options }: Prop const styles = useStyles2(getStyles); return ( -
+
@@ -38,7 +38,7 @@ export const ConfigEditor: React.FC = ({ onOptionsChange, options }: Prop to help us make it even better. - + Fields marked with * are required @@ -61,6 +61,13 @@ const getStyles = (theme: GrafanaTheme2) => { display: 'none', }, }), + leftSticky: css({ + position: 'sticky', + top: '100px', + alignSelf: 'flex-start', + maxHeight: 'calc(100vh - 100px)', + overflow: 'hidden', + }), alertHeight: css({ height: '100px', }), diff --git a/public/app/plugins/datasource/influxdb/components/editor/config-v2/DatabaseConnectionSection.tsx b/public/app/plugins/datasource/influxdb/components/editor/config-v2/DatabaseConnectionSection.tsx index e06fa0baf32..46cf67b96ff 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/config-v2/DatabaseConnectionSection.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/config-v2/DatabaseConnectionSection.tsx @@ -20,7 +20,7 @@ export const DatabaseConnectionSection = ({ options, onOptionsChange }: Props) = minWidth={CONTAINER_MIN_WIDTH} > 2. {CONFIG_SECTION_HEADERS[1].label}} + label={{CONFIG_SECTION_HEADERS[1].label}} isOpen={CONFIG_SECTION_HEADERS[1].isOpen} > {!options.jsonData.version && ( diff --git a/public/app/plugins/datasource/influxdb/components/editor/config-v2/InfluxInfluxQLDBConnection.test.tsx b/public/app/plugins/datasource/influxdb/components/editor/config-v2/InfluxInfluxQLDBConnection.test.tsx index 9351cbda5de..cb4de921fcc 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/config-v2/InfluxInfluxQLDBConnection.test.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/config-v2/InfluxInfluxQLDBConnection.test.tsx @@ -28,14 +28,17 @@ describe('InfluxInfluxQLDBConnection', () => { it('renders dbName, user and password fields', () => { render(); - expect(screen.getByLabelText(/Database/i)).toBeInTheDocument(); - expect(screen.getByLabelText(/User/i)).toBeInTheDocument(); - expect(screen.getByLabelText(/Password/i)).toBeInTheDocument(); + + expect(screen.getByLabelText(/^Database\b/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/^User\b/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/^Password\b/i)).toBeInTheDocument(); }); it('calls onOptionsChange on input changes', () => { render(); + fireEvent.change(screen.getByLabelText(/User/i), { target: { value: 'newuser' } }); + expect(onOptionsChangeMock).toHaveBeenCalled(); }); }); diff --git a/public/app/plugins/datasource/influxdb/components/editor/config-v2/LeftSideBar.tsx b/public/app/plugins/datasource/influxdb/components/editor/config-v2/LeftSideBar.tsx index cc970f35575..26604ac1f9b 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/config-v2/LeftSideBar.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/config-v2/LeftSideBar.tsx @@ -1,4 +1,6 @@ -import { Box, InlineField, LinkButton, Space, Stack, Text } from '@grafana/ui'; +import { css } from '@emotion/css'; + +import { Box, Icon, LinkButton, Space, Stack, Text, useStyles2 } from '@grafana/ui'; import { CONFIG_SECTION_HEADERS, CONFIG_SECTION_HEADERS_WITH_PDC } from './constants'; @@ -8,29 +10,40 @@ interface LeftSideBarProps { export const LeftSideBar = ({ pdcInjected }: LeftSideBarProps) => { const headers = pdcInjected ? CONFIG_SECTION_HEADERS_WITH_PDC : CONFIG_SECTION_HEADERS; + const styles = useStyles2(getStyles); + return ( - - - InfluxDB + + Connect data source {headers.map((header, index) => (
- - { - e.preventDefault(); - const target = document.getElementById(header.id); - if (target) { - target.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - }} - > - {header.label} - - + + { + e.preventDefault(); + const target = document.getElementById(header.id); + if (target) { + const y = target.getBoundingClientRect().top + window.scrollY - 60; + window.scrollTo({ top: y, behavior: 'smooth' }); + } + }} + > +
+
{header.label}
+ {header.isOptional && ( +
+ + optional + +
+ )} +
+
))} @@ -39,3 +52,27 @@ export const LeftSideBar = ({ pdcInjected }: LeftSideBarProps) => {
); }; + +const getStyles = () => ({ + inlineField: css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }), + sidebarText: css({ + display: 'flex', + flexDirection: 'column', + }), + sidebarLabel: css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + marginBottom: 0, + lineHeight: 1, + }), + sidebarOptional: css({ + marginTop: 0, + marginBottom: 0, + lineHeight: 1, + }), +}); diff --git a/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.test.tsx b/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.test.tsx index 2f6704ea66a..7bc084744df 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.test.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.test.tsx @@ -1,4 +1,16 @@ +const backendSrv = { + fetch: jest.fn(), +} as unknown as BackendSrv; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => backendSrv, +})); + import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { of } from 'rxjs'; + +import { BackendSrv } from '@grafana/runtime'; import { InfluxVersion } from '../../../types'; @@ -11,6 +23,7 @@ describe('UrlAndAuthenticationSection', () => { const defaultProps = createTestProps({ options: { + id: 1234, jsonData: { url: 'http://localhost:8086', product: '', @@ -24,6 +37,29 @@ describe('UrlAndAuthenticationSection', () => { }, }); + const mockFetchPing = ({ build, version, status = 204 }: { build?: string; version?: string; status?: number }) => { + backendSrv.fetch = jest.fn().mockReturnValue( + of({ + status, + ok: status >= 200 && status < 300, + data: status === 204 ? '' : {}, + headers: { + get: (k: string) => { + const key = k.toLowerCase(); + if (key === 'x-influxdb-build') { + return build ?? null; + } + if (key === 'x-influxdb-version') { + return version ?? null; + } + return null; + }, + }, + url: '/api/datasources/proxy/1234/ping', + }) + ); + }; + beforeEach(() => { // Mock console.error to suppress React act() warnings consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); @@ -239,7 +275,7 @@ describe('UrlAndAuthenticationSection', () => { }, }; - mockFetchPing({ ok: true, build: 'OSS', version: '1.8.10' }); + mockFetchPing({ build: 'OSS', version: '1.8.10' }); render(); const input = screen.getByTestId('influxdb-v2-config-url-input'); @@ -268,7 +304,7 @@ describe('UrlAndAuthenticationSection', () => { }, }; - mockFetchPing({ ok: true, build: 'OSS', version: '2.7.1' }); + mockFetchPing({ build: 'OSS', version: '2.7.1' }); render(); const input = screen.getByTestId('influxdb-v2-config-url-input'); @@ -297,7 +333,7 @@ describe('UrlAndAuthenticationSection', () => { }, }; - mockFetchPing({ ok: true, build: undefined, version: undefined }); + mockFetchPing({ build: undefined, version: undefined }); render(); const input = screen.getByTestId('influxdb-v2-config-url-input'); @@ -358,23 +394,3 @@ describe('UrlAndAuthenticationSection', () => { }); }); }); - -export function mockFetchPing(resp: { ok?: boolean; build?: string; version?: string } = {}) { - const { ok = true, build, version } = resp; - - global.fetch = jest.fn().mockResolvedValue({ - ok, - headers: { - get: (key: string) => { - const normalized = key.toLowerCase(); - if (normalized === 'x-influxdb-build') { - return build ?? null; - } - if (normalized === 'x-influxdb-version') { - return version ?? null; - } - return null; - }, - }, - }); -} diff --git a/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.tsx b/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.tsx index 5926ea6703a..df866807b64 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.tsx @@ -1,4 +1,8 @@ +import { css } from '@emotion/css'; +import { firstValueFrom } from 'rxjs'; + import { onUpdateDatasourceJsonDataOptionSelect, onUpdateDatasourceOption } from '@grafana/data'; +import { getBackendSrv } from '@grafana/runtime'; import { Box, CollapsableSection, @@ -11,6 +15,7 @@ import { Text, ComboboxOption, Alert, + useStyles2, } from '@grafana/ui'; import { InfluxVersion } from '../../../types'; @@ -33,6 +38,7 @@ const getQueryLanguageOptions = (productName: string): Array<{ value: string }> export const UrlAndAuthenticationSection = (props: Props) => { const { options, onOptionsChange } = props; + const styles = useStyles2(getStyles); const isInfluxVersion = (v: string): v is InfluxVersion => typeof v === 'string' && (v === InfluxVersion.Flux || v === InfluxVersion.InfluxQL || v === InfluxVersion.SQL); @@ -68,13 +74,30 @@ export const UrlAndAuthenticationSection = (props: Props) => { }; const pingInfluxForProductDetection = async (urlValue: string) => { - const base = urlValue.replace(/\/$/, ''); + const dsId = options.id; + if (!dsId) { + return; + } try { - const res = await fetch(`${base}/ping`); + const res = await firstValueFrom( + getBackendSrv().fetch({ + method: 'GET', + url: `/api/datasources/proxy/${dsId}/ping`, + headers: { Accept: 'application/json' }, + responseType: 'text', + showErrorAlert: false, + showSuccessAlert: false, + }) + ); if (res.ok) { - const product = res.headers.get('x-influxdb-build') ?? undefined; - const version = res.headers.get('x-influxdb-version') ?? undefined; + let product: string | undefined; + let version: string | undefined; + + if (res.headers && typeof res.headers.get === 'function') { + product = res.headers.get('x-influxdb-build') ?? undefined; + version = res.headers.get('x-influxdb-version') ?? undefined; + } if (product || version) { return { product, version }; @@ -136,21 +159,19 @@ export const UrlAndAuthenticationSection = (props: Props) => { borderStyle="solid" borderColor="weak" padding={2} - marginBottom={4} id={`${CONFIG_SECTION_HEADERS[0].id}`} minWidth={CONTAINER_MIN_WIDTH} > 1. {CONFIG_SECTION_HEADERS[0].label}} + label={{CONFIG_SECTION_HEADERS[0].label}} isOpen={CONFIG_SECTION_HEADERS[0].isOpen} > 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. - - - URL *
} noMargin required> + + { }} /> - - - - Product *
} noMargin required> - ({ value: name }))} - onChange={onProductChange} - /> - - - - Query language *} noMargin> - - - + +
+ + + + Use{' '} + + InfluxDB detection + {' '} + to identify the product + +
+ } + noMargin + required + > + ({ value: name }))} + onChange={onProductChange} + /> + + + +
+ + The query language depends on product selection
} + noMargin + required + > + + + +
- - {requiresDbrpMapping && ( {`${options.jsonData.product} requires a Database + Retention Policy (DBRP) mapping via the CLI or @@ -199,7 +245,6 @@ export const UrlAndAuthenticationSection = (props: Props) => { )} - @@ -207,3 +252,20 @@ export const UrlAndAuthenticationSection = (props: Props) => { ); }; + +const getStyles = () => { + return { + dropdown: css({ + display: 'flex', + alignItems: 'center', + height: '18px', + }), + col: css({ + flex: '1 1 48%', + minWidth: '320px', + }), + '@media (max-width: 768px)': { + flexBasis: '100%', + }, + }; +}; diff --git a/public/app/plugins/datasource/influxdb/components/editor/config-v2/constants.ts b/public/app/plugins/datasource/influxdb/components/editor/config-v2/constants.ts index a3c63953d4e..0a795e71dae 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/config-v2/constants.ts +++ b/public/app/plugins/datasource/influxdb/components/editor/config-v2/constants.ts @@ -17,16 +17,16 @@ export const AUTH_RADIO_BUTTON_OPTIONS = [ ]; 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 }, + { label: 'URL and authentication', id: 'url', isOpen: true, isOptional: false }, + { label: 'Database settings', id: 'db', isOpen: true, isOptional: false }, + { label: 'Save & test', id: `${selectors.pages.DataSource.saveAndTest}`, isOpen: true, isOptional: null }, ]; 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 }, + { label: 'URL and authentication', id: 'url', isOpen: true, isOptional: false }, + { label: 'Database settings', id: 'db', isOpen: true, isOptional: false }, + { label: 'Private data source connect', id: 'pdc', isOpen: false, isOptional: true }, + { label: 'Save & test', id: `${selectors.pages.DataSource.saveAndTest}`, isOpen: true, isOptional: null }, ]; export const HTTP_MODES: ComboboxOption[] = [ @@ -34,7 +34,7 @@ export const HTTP_MODES: ComboboxOption[] = [ { label: 'GET', value: 'GET' }, ]; -export const getInlineLabelStyles = (theme: GrafanaTheme2, transparent = false, width?: number | 'auto') => { +export const getInlineLabelStyles = (theme: GrafanaTheme2, transparent = false) => { return { label: css({ display: 'flex', @@ -56,5 +56,5 @@ export const getInlineLabelStyles = (theme: GrafanaTheme2, transparent = false, }; }; -export const DB_SETTINGS_LABEL_WIDTH = 18; export const CONTAINER_MIN_WIDTH = '450px'; +export const DB_SETTINGS_LABEL_WIDTH = 22; diff --git a/public/app/plugins/datasource/influxdb/components/editor/config-v2/versions.ts b/public/app/plugins/datasource/influxdb/components/editor/config-v2/versions.ts index f5dc31a75d3..2b2a65111e1 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/config-v2/versions.ts +++ b/public/app/plugins/datasource/influxdb/components/editor/config-v2/versions.ts @@ -63,7 +63,8 @@ export const INFLUXDB_VERSION_MAP: InfluxDBProduct[] = [ ], detectionMethod: { pingHeaderResponse: { - 'x-influxdb-build': 'Enterprise (needs confirmation)', + 'x-influxdb-version': '^v?1\\.', + 'x-influxdb-build': 'Enterprise', }, }, }, @@ -75,7 +76,8 @@ export const INFLUXDB_VERSION_MAP: InfluxDBProduct[] = [ ], detectionMethod: { pingHeaderResponse: { - 'x-influxdb-build': 'TBD', + 'x-influxdb-version': '^v?3\\.', + 'x-influxdb-build': 'Enterprise', }, }, }, @@ -113,7 +115,7 @@ export const INFLUXDB_VERSION_MAP: InfluxDBProduct[] = [ detectionMethod: { pingHeaderResponse: { 'x-influxdb-build': 'OSS', - 'x-influxdb-version': '^1\\.', + 'x-influxdb-version': '^v?1\\.', }, }, }, @@ -134,7 +136,20 @@ export const INFLUXDB_VERSION_MAP: InfluxDBProduct[] = [ detectionMethod: { pingHeaderResponse: { 'x-influxdb-build': 'OSS', - 'x-influxdb-version': '^2\\.', + 'x-influxdb-version': '^v?2\\.', + }, + }, + }, + { + name: 'InfluxDB OSS 3.x', + queryLanguages: [ + { name: InfluxVersion.SQL, fields: ['URL', 'Token'] }, + { name: InfluxVersion.InfluxQL, fields: ['URL', 'Token'] }, + ], + detectionMethod: { + pingHeaderResponse: { + 'x-influxdb-build': 'OSS', + 'x-influxdb-version': '^v?3\\.', }, }, },