diff --git a/docs/sources/datasources/mssql/query-editor/index.md b/docs/sources/datasources/mssql/query-editor/index.md
index a50c05944db..d2b3a15a063 100644
--- a/docs/sources/datasources/mssql/query-editor/index.md
+++ b/docs/sources/datasources/mssql/query-editor/index.md
@@ -87,12 +87,12 @@ Code mode supports autocompletion of tables, columns, SQL keywords, standard SQL
In **Builder mode**, you can build queries using a visual interface.
-### Select a dataset and table
+### Dataset and table selection
-In the **Dataset** dropdown, select the MS SQL database to query.
+In the **Dataset** dropdown, select the MSSQL database to query. Grafana populates the dropdown with all databases that the user can access.
+Once you select a database, Grafana populates the dropdown with all available tables.
-Grafana populates the dropdown with the databases that the configured user can access.
-When you select a dataset, Grafana populates the **Table** dropdown with available tables.
+**Note:** If a default database has been configured through the Data Source Configuration page (or through a provisioning configuration file), the user will only be able to use that single preconfigured database for querying.
### Select columns and aggregation functions (SELECT)
diff --git a/docs/sources/datasources/mysql/_index.md b/docs/sources/datasources/mysql/_index.md
index e6916e44b01..5c02e1c0983 100644
--- a/docs/sources/datasources/mysql/_index.md
+++ b/docs/sources/datasources/mysql/_index.md
@@ -186,6 +186,8 @@ If your table or database name contains a reserved word or a [not permitted char
In the dataset dropdown, choose the MySQL database to query. The dropdown is be populated with the databases that the user has access to.
When the dataset is selected, the table dropdown is populated with the tables that are available.
+**Note:** If a default database has been configured through the Data Source Configuration page (or through a provisioning configuration file), the user will only be able to use that single preconfigured database for querying.
+
### Columns and Aggregation functions (SELECT)
Using the dropdown, select a column to include in the data. You can also specify an optional aggregation function.
diff --git a/docs/sources/datasources/postgres/_index.md b/docs/sources/datasources/postgres/_index.md
index 3f004d1ec4e..925ca9e130a 100644
--- a/docs/sources/datasources/postgres/_index.md
+++ b/docs/sources/datasources/postgres/_index.md
@@ -88,7 +88,7 @@ Make sure the user does not get any unwanted privileges from the public role.
## Query builder
-{{< figure src="/static/img/docs/v92/postgresql_query_builder.png" class="docs-image--no-shadow" caption="PostgreSQL query builder" >}}
+{{< figure src="/static/img/docs/screenshot-postgres-query-editor.png" class="docs-image--no-shadow" caption="PostgreSQL query builder" >}}
The PostgreSQL query builder is available when editing a panel using a PostgreSQL data source. The built query can be run by pressing the `Run query` button in the top right corner of the editor.
@@ -96,10 +96,10 @@ The PostgreSQL query builder is available when editing a panel using a PostgreSQ
The response from PostgreSQL can be formatted as either a table or as a time series. To use the time series format one of the columns must be named `time`.
-### Dataset and Table selection
+### Dataset and table selection
-In the dataset dropdown, choose the PostgreSQL database to query. The dropdown is be populated with the databases that the user has access to.
-When the dataset is selected, the table dropdown is populated with the tables that are available.
+The dataset dropdown will be populated with the configured database to which the user has access.
+The table dropdown is populated with the tables that are available within that database.
### Columns and Aggregation functions (SELECT)
diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
index 60923087c26..dd24f98b9aa 100644
--- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
+++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
@@ -64,6 +64,7 @@ Some stable features are enabled by default. You can disable a stable feature by
| `faroDatasourceSelector` | Enable the data source selector within the Frontend Apps section of the Frontend Observability |
| `enableDatagridEditing` | Enables the edit functionality in the datagrid panel |
| `dataSourcePageHeader` | Apply new pageHeader UI in data source edit page |
+| `sqlDatasourceDatabaseSelection` | Enables previous SQL data source dataset dropdown behavior |
## Alpha feature toggles
diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts
index e4b4d58eddb..64f007ad613 100644
--- a/packages/grafana-data/src/types/featureToggles.gen.ts
+++ b/packages/grafana-data/src/types/featureToggles.gen.ts
@@ -100,4 +100,5 @@ export interface FeatureToggles {
extraThemes?: boolean;
lokiPredefinedOperations?: boolean;
pluginsFrontendSandbox?: boolean;
+ sqlDatasourceDatabaseSelection?: boolean;
}
diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go
index c70337522f0..0504937f8d9 100644
--- a/pkg/api/frontendsettings.go
+++ b/pkg/api/frontendsettings.go
@@ -375,6 +375,16 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug
}
}
+ // Update `jsonData.database` for outdated provisioned SQL datasources created WITHOUT the `jsonData` object in their configuration.
+ // In these cases, the `Database` value is defined (if at all) on the root level of the provisioning config object.
+ // This is done for easier warning/error checking on the front end.
+ if (ds.Type == datasources.DS_MSSQL) || (ds.Type == datasources.DS_MYSQL) || (ds.Type == datasources.DS_POSTGRES) {
+ // Only update if the value isn't already assigned.
+ if dsDTO.JSONData["database"] == nil || dsDTO.JSONData["database"] == "" {
+ dsDTO.JSONData["database"] = ds.Database
+ }
+ }
+
if (ds.Type == datasources.DS_INFLUXDB) || (ds.Type == datasources.DS_ES) {
dsDTO.Database = ds.Database
}
diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go
index 1e48109c6da..f7353b18e92 100644
--- a/pkg/services/featuremgmt/registry.go
+++ b/pkg/services/featuremgmt/registry.go
@@ -555,5 +555,12 @@ var (
FrontendOnly: true,
Owner: grafanaPluginsPlatformSquad,
},
+ {
+ Name: "sqlDatasourceDatabaseSelection",
+ Description: "Enables previous SQL data source dataset dropdown behavior",
+ FrontendOnly: true,
+ State: FeatureStateBeta,
+ Owner: grafanaBiSquad,
+ },
}
)
diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv
index 883b1369817..ea99bbb62b5 100644
--- a/pkg/services/featuremgmt/toggles_gen.csv
+++ b/pkg/services/featuremgmt/toggles_gen.csv
@@ -81,3 +81,4 @@ dataSourcePageHeader,beta,@grafana/enterprise-datasources,false,false,false,true
extraThemes,alpha,@grafana/grafana-frontend-platform,false,false,false,true
lokiPredefinedOperations,alpha,@grafana/observability-logs,false,false,false,true
pluginsFrontendSandbox,alpha,@grafana/plugins-platform-backend,false,false,false,true
+sqlDatasourceDatabaseSelection,beta,@grafana/grafana-bi-squad,false,false,false,true
diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go
index d29209eda2a..555a4c376f1 100644
--- a/pkg/services/featuremgmt/toggles_gen.go
+++ b/pkg/services/featuremgmt/toggles_gen.go
@@ -334,4 +334,8 @@ const (
// FlagPluginsFrontendSandbox
// Enables the plugins frontend sandbox
FlagPluginsFrontendSandbox = "pluginsFrontendSandbox"
+
+ // FlagSqlDatasourceDatabaseSelection
+ // Enables previous SQL data source dataset dropdown behavior
+ FlagSqlDatasourceDatabaseSelection = "sqlDatasourceDatabaseSelection"
)
diff --git a/public/app/features/plugins/sql/components/DatasetSelector.tsx b/public/app/features/plugins/sql/components/DatasetSelector.tsx
index ac968f04f9a..2df95568ccf 100644
--- a/public/app/features/plugins/sql/components/DatasetSelector.tsx
+++ b/public/app/features/plugins/sql/components/DatasetSelector.tsx
@@ -6,47 +6,78 @@ import { Select } from '@grafana/ui';
import { DB, ResourceSelectorProps, toOption } from '../types';
-interface DatasetSelectorProps extends ResourceSelectorProps {
+import { isSqlDatasourceDatabaseSelectionFeatureFlagEnabled } from './QueryEditorFeatureFlag.utils';
+
+export interface DatasetSelectorProps extends ResourceSelectorProps {
db: DB;
- value: string | null;
- applyDefault?: boolean;
- disabled?: boolean;
+ dataset: string | undefined;
+ preconfiguredDataset: string;
+ isPostgresInstance: boolean | undefined;
onChange: (v: SelectableValue) => void;
}
-export const DatasetSelector = ({ db, value, onChange, disabled, className, applyDefault }: DatasetSelectorProps) => {
+export const DatasetSelector = ({
+ dataset,
+ db,
+ isPostgresInstance,
+ onChange,
+ preconfiguredDataset,
+}: DatasetSelectorProps) => {
+ /*
+ The behavior of this component - for MSSQL and MySQL datasources - is based on whether the user chose to create a datasource
+ with or without a default database (preconfiguredDataset). If the user configured a default database, this selector
+ should only allow that single preconfigured database option to be selected. If the user chose to NOT assign/configure a default database,
+ then the user should be able to use this component to choose between multiple databases available to the datasource.
+ */
+ // `hasPreconfigCondition` is true if either 1) the sql datasource has a preconfigured default database,
+ // OR if 2) the datasource is Postgres. In either case the only option available to the user is the preconfigured database.
+ const hasPreconfigCondition = !!preconfiguredDataset || isPostgresInstance;
+
const state = useAsync(async () => {
+ if (isSqlDatasourceDatabaseSelectionFeatureFlagEnabled()) {
+ // If a default database is already configured for a MSSQL or MySQL data source, OR the data source is Postgres, no need to fetch other databases.
+ if (hasPreconfigCondition) {
+ // Set the current database to the preconfigured database.
+ onChange(toOption(preconfiguredDataset));
+ return [toOption(preconfiguredDataset)];
+ }
+ }
+
+ // If there is no preconfigured database, but there is a selected dataset, set the current database to the selected dataset.
+ if (dataset) {
+ onChange(toOption(dataset));
+ }
+
+ // Otherwise, fetch all databases available to the datasource.
const datasets = await db.datasets();
return datasets.map(toOption);
}, []);
useEffect(() => {
- if (!applyDefault) {
- return;
- }
- // Set default dataset when values are fetched
- if (!value) {
- if (state.value && state.value[0]) {
- onChange(state.value[0]);
- }
- } else {
- if (state.value && state.value.find((v) => v.value === value) === undefined) {
- // if value is set and newly fetched values does not contain selected value
- if (state.value.length > 0) {
+ if (!isSqlDatasourceDatabaseSelectionFeatureFlagEnabled()) {
+ // Set default dataset when values are fetched
+ if (!dataset) {
+ if (state.value && state.value[0]) {
onChange(state.value[0]);
}
+ } else {
+ if (state.value && state.value.find((v) => v.value === dataset) === undefined) {
+ // if value is set and newly fetched values does not contain selected value
+ if (state.value.length > 0) {
+ onChange(state.value[0]);
+ }
+ }
}
}
- }, [state.value, value, applyDefault, onChange]);
+ }, [state.value, onChange, dataset]);
return (
diff --git a/public/app/features/plugins/sql/components/QueryEditor.tsx b/public/app/features/plugins/sql/components/QueryEditor.tsx
index 89af73cf12c..00427b35276 100644
--- a/public/app/features/plugins/sql/components/QueryEditor.tsx
+++ b/public/app/features/plugins/sql/components/QueryEditor.tsx
@@ -13,13 +13,23 @@ import { QueryHeader, QueryHeaderProps } from './QueryHeader';
import { RawEditor } from './query-editor-raw/RawEditor';
import { VisualEditor } from './visual-query-builder/VisualEditor';
-interface Props extends QueryEditorProps {
- queryHeaderProps?: Pick;
+interface SqlQueryEditorProps extends QueryEditorProps {
+ queryHeaderProps?: Pick;
}
-export function SqlQueryEditor({ datasource, query, onChange, onRunQuery, range, queryHeaderProps }: Props) {
+export function SqlQueryEditor({
+ datasource,
+ query,
+ onChange,
+ onRunQuery,
+ range,
+ queryHeaderProps,
+}: SqlQueryEditorProps) {
const [isQueryRunnable, setIsQueryRunnable] = useState(true);
const db = datasource.getDB();
+
+ const { preconfiguredDatabase } = datasource;
+ const isPostgresInstance = !!queryHeaderProps?.isPostgresInstance;
const { loading, error } = useAsync(async () => {
return () => {
if (datasource.getDB(datasource.id).init !== undefined) {
@@ -80,13 +90,14 @@ export function SqlQueryEditor({ datasource, query, onChange, onRunQuery, range,
<>
diff --git a/public/app/features/plugins/sql/components/QueryEditorFeatureFlag.utils.ts b/public/app/features/plugins/sql/components/QueryEditorFeatureFlag.utils.ts
new file mode 100644
index 00000000000..59f9d51ca47
--- /dev/null
+++ b/public/app/features/plugins/sql/components/QueryEditorFeatureFlag.utils.ts
@@ -0,0 +1,5 @@
+import { config } from '@grafana/runtime';
+
+export const isSqlDatasourceDatabaseSelectionFeatureFlagEnabled = () => {
+ return !!config.featureToggles.sqlDatasourceDatabaseSelection;
+};
diff --git a/public/app/features/plugins/sql/components/QueryHeader.tsx b/public/app/features/plugins/sql/components/QueryHeader.tsx
index eb14bd251a3..87471504564 100644
--- a/public/app/features/plugins/sql/components/QueryHeader.tsx
+++ b/public/app/features/plugins/sql/components/QueryHeader.tsx
@@ -10,17 +10,19 @@ import { SQLQuery, QueryFormat, QueryRowFilter, QUERY_FORMAT_OPTIONS, DB } from
import { ConfirmModal } from './ConfirmModal';
import { DatasetSelector } from './DatasetSelector';
+import { isSqlDatasourceDatabaseSelectionFeatureFlagEnabled } from './QueryEditorFeatureFlag.utils';
import { TableSelector } from './TableSelector';
export interface QueryHeaderProps {
db: DB;
- query: QueryWithDefaults;
- onChange: (query: SQLQuery) => void;
- onRunQuery: () => void;
- onQueryRowChange: (queryRowFilter: QueryRowFilter) => void;
- queryRowFilter: QueryRowFilter;
+ isPostgresInstance?: boolean;
isQueryRunnable: boolean;
- isDatasetSelectorHidden?: boolean;
+ onChange: (query: SQLQuery) => void;
+ onQueryRowChange: (queryRowFilter: QueryRowFilter) => void;
+ onRunQuery: () => void;
+ preconfiguredDataset: string;
+ query: QueryWithDefaults;
+ queryRowFilter: QueryRowFilter;
}
const editorModes = [
@@ -30,13 +32,14 @@ const editorModes = [
export function QueryHeader({
db,
+ isPostgresInstance,
+ isQueryRunnable,
+ onChange,
+ onQueryRowChange,
+ onRunQuery,
+ preconfiguredDataset,
query,
queryRowFilter,
- onChange,
- onRunQuery,
- onQueryRowChange,
- isQueryRunnable,
- isDatasetSelectorHidden,
}: QueryHeaderProps) {
const { editorMode } = query;
const [_, copyToClipboard] = useCopyToClipboard();
@@ -86,9 +89,20 @@ export function QueryHeader({
sql: undefined,
rawSql: '',
};
+
onChange(next);
};
+ const datasetDropdownIsAvailable = () => {
+ // If the feature flag is DISABLED, && the datasource is Postgres (`isPostgresInstance`),
+ // we want to hide the dropdown - as per previous behavior.
+ if (!isSqlDatasourceDatabaseSelectionFeatureFlagEnabled() && isPostgresInstance) {
+ return false;
+ }
+
+ return true;
+ };
+
return (
<>
@@ -205,24 +219,23 @@ export function QueryHeader({
<>
- {isDatasetSelectorHidden ? null : (
+ {datasetDropdownIsAvailable() && (
)}
-
diff --git a/public/app/features/plugins/sql/components/SqlComponents.test.tsx b/public/app/features/plugins/sql/components/SqlComponents.test.tsx
new file mode 100644
index 00000000000..26eb3e66d67
--- /dev/null
+++ b/public/app/features/plugins/sql/components/SqlComponents.test.tsx
@@ -0,0 +1,65 @@
+import { render, waitFor } from '@testing-library/react';
+import React from 'react';
+
+import { config } from '@grafana/runtime';
+
+import { DatasetSelector } from './DatasetSelector';
+import { buildMockDatasetSelectorProps, buildMockTableSelectorProps } from './SqlComponents.testHelpers';
+import { TableSelector } from './TableSelector';
+
+beforeEach(() => {
+ config.featureToggles.sqlDatasourceDatabaseSelection = true;
+});
+
+afterEach(() => {
+ config.featureToggles.sqlDatasourceDatabaseSelection = false;
+});
+
+describe('DatasetSelector', () => {
+ it('should only query the database when needed', async () => {
+ const mockProps = buildMockDatasetSelectorProps();
+ render();
+
+ await waitFor(() => {
+ expect(mockProps.db.datasets).toHaveBeenCalled();
+ });
+ });
+
+ it('should not query the database if Postgres instance, and no preconfigured database', async () => {
+ const mockProps = buildMockDatasetSelectorProps({ isPostgresInstance: true });
+ render();
+
+ await waitFor(() => {
+ expect(mockProps.db.datasets).not.toHaveBeenCalled();
+ });
+ });
+
+ it('should not query the database if preconfigured', async () => {
+ const mockProps = buildMockDatasetSelectorProps({ preconfiguredDataset: 'database 1' });
+ render();
+
+ await waitFor(() => {
+ expect(mockProps.db.datasets).not.toHaveBeenCalled();
+ });
+ });
+});
+
+describe('TableSelector', () => {
+ it('should only query the database when needed', async () => {
+ const mockProps = buildMockTableSelectorProps({ dataset: 'database 1' });
+ render();
+
+ await waitFor(() => {
+ expect(mockProps.db.tables).toHaveBeenCalled();
+ });
+ });
+
+ it('should not query the database if no dataset is passed as a prop', async () => {
+ const mockProps = buildMockTableSelectorProps();
+ render();
+
+ await waitFor(() => {
+ expect(mockProps.db.tables).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/public/app/features/plugins/sql/components/SqlComponents.testHelpers.ts b/public/app/features/plugins/sql/components/SqlComponents.testHelpers.ts
new file mode 100644
index 00000000000..4f605bbc0fb
--- /dev/null
+++ b/public/app/features/plugins/sql/components/SqlComponents.testHelpers.ts
@@ -0,0 +1,94 @@
+import { TimeRange, PluginType } from '@grafana/data';
+
+import { DB, SQLQuery, SQLSelectableValue, ValidationResults } from '../types';
+
+import { DatasetSelectorProps } from './DatasetSelector';
+import { TableSelectorProps } from './TableSelector';
+
+const buildMockDB = (): DB => ({
+ datasets: jest.fn(() => Promise.resolve(['dataset1', 'dataset2'])),
+ tables: jest.fn((_ds: string | undefined) => Promise.resolve(['table1', 'table2'])),
+ fields: jest.fn((_query: SQLQuery, _order?: boolean) => Promise.resolve([])),
+ validateQuery: jest.fn((_query: SQLQuery, _range?: TimeRange) =>
+ Promise.resolve({ query: { refId: '123' }, error: '', isError: false, isValid: true })
+ ),
+ dsID: jest.fn(() => 1234),
+ getEditorLanguageDefinition: jest.fn(() => ({ id: '4567' })),
+ toRawSql: (_query: SQLQuery) => '',
+});
+
+// This data is of type `SqlDatasource`
+export const buildMockDatasource = (hasDefaultDatabaseConfigured?: boolean) => {
+ return {
+ id: Infinity,
+ type: '',
+ name: '',
+ uid: '',
+ responseParser: { transformMetricFindResponse: jest.fn() },
+ interval: '',
+ db: buildMockDB(),
+ preconfiguredDatabase: hasDefaultDatabaseConfigured ? 'default database' : '',
+ getDB: () => buildMockDB(),
+ getQueryModel: jest.fn(),
+ getResponseParser: jest.fn(),
+ interpolateVariable: jest.fn(),
+ interpolateVariablesInQueries: jest.fn(),
+ filterQuery: jest.fn(),
+ applyTemplateVariables: jest.fn(),
+ metricFindQuery: jest.fn(),
+ templateSrv: {
+ getVariables: jest.fn(),
+ replace: jest.fn(),
+ containsTemplate: jest.fn(),
+ updateTimeRange: jest.fn(),
+ },
+ runSql: jest.fn(),
+ runMetaQuery: jest.fn(),
+ targetContainsTemplate: jest.fn(),
+ query: jest.fn(),
+ getRequestHeaders: jest.fn(),
+ streamOptionsProvider: jest.fn(),
+ getResource: jest.fn(),
+ postResource: jest.fn(),
+ callHealthCheck: jest.fn(),
+ testDatasource: jest.fn(),
+ getRef: jest.fn(),
+ meta: {
+ id: '',
+ name: '',
+ type: PluginType.panel,
+ info: {
+ author: { name: '' },
+ description: '',
+ links: [],
+ logos: { large: '', small: '' },
+ screenshots: [],
+ updated: '',
+ version: '',
+ },
+ module: '',
+ baseUrl: '',
+ },
+ };
+};
+
+export function buildMockDatasetSelectorProps(overrides?: Partial): DatasetSelectorProps {
+ return {
+ db: buildMockDB(),
+ dataset: '',
+ isPostgresInstance: false,
+ onChange: jest.fn(),
+ preconfiguredDataset: '',
+ ...overrides,
+ };
+}
+
+export function buildMockTableSelectorProps(overrides?: Partial): TableSelectorProps {
+ return {
+ db: buildMockDB(),
+ dataset: '',
+ table: '',
+ onChange: jest.fn(),
+ ...overrides,
+ };
+}
diff --git a/public/app/features/plugins/sql/components/TableSelector.tsx b/public/app/features/plugins/sql/components/TableSelector.tsx
index 6d72a272ebd..bfd3f1f4828 100644
--- a/public/app/features/plugins/sql/components/TableSelector.tsx
+++ b/public/app/features/plugins/sql/components/TableSelector.tsx
@@ -4,32 +4,32 @@ import { useAsync } from 'react-use';
import { SelectableValue, toOption } from '@grafana/data';
import { Select } from '@grafana/ui';
-import { QueryWithDefaults } from '../defaults';
import { DB, ResourceSelectorProps } from '../types';
-interface TableSelectorProps extends ResourceSelectorProps {
+export interface TableSelectorProps extends ResourceSelectorProps {
db: DB;
- value: string | null;
- query: QueryWithDefaults;
+ table: string | undefined;
+ dataset: string | undefined;
onChange: (v: SelectableValue) => void;
- forceFetch?: boolean;
}
-export const TableSelector = ({ db, query, value, className, onChange, forceFetch }: TableSelectorProps) => {
+export const TableSelector = ({ db, dataset, table, className, onChange }: TableSelectorProps) => {
const state = useAsync(async () => {
- if (!query.dataset && !forceFetch) {
+ // No need to attempt to fetch tables for an unknown dataset.
+ if (!dataset) {
return [];
}
- const tables = await db.tables(query.dataset);
+
+ const tables = await db.tables(dataset);
return tables.map(toOption);
- }, [query.dataset]);
+ }, [dataset]);
return (