From 08655a7559718e10049fa16f83f40146402405fa Mon Sep 17 00:00:00 2001 From: Luiz Otavio <45200344+luizotavio32@users.noreply.github.com> Date: Mon, 9 Jun 2025 08:32:12 -0300 Subject: [PATCH 001/148] fix: Migrate charts with empty query_context (#33710) --- .../migrations/shared/migrate_viz/base.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/superset/migrations/shared/migrate_viz/base.py b/superset/migrations/shared/migrate_viz/base.py index 929039c0f67..e2a09731a12 100644 --- a/superset/migrations/shared/migrate_viz/base.py +++ b/superset/migrations/shared/migrate_viz/base.py @@ -4,7 +4,7 @@ # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at +# with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # @@ -157,9 +157,10 @@ class MigrateViz: # because a source viz can be mapped to different target viz types slc.viz_type = clz.target_viz_type - backup = {FORM_DATA_BAK_FIELD_NAME: form_data_bak} + backup: Any | dict[str, Any] = {FORM_DATA_BAK_FIELD_NAME: form_data_bak} query_context = try_load_json(slc.query_context) + queries_bak = None if query_context: if "form_data" in query_context: @@ -169,10 +170,11 @@ class MigrateViz: queries = clz._build_query()["queries"] query_context["queries"] = queries + else: + query_context = clz._build_query() - slc.query_context = json.dumps(query_context) - backup[QUERIES_BAK_FIELD_NAME] = queries_bak - + slc.query_context = json.dumps(query_context) + backup[QUERIES_BAK_FIELD_NAME] = queries_bak slc.params = json.dumps({**clz.data, **backup}) except Exception as e: @@ -189,11 +191,14 @@ class MigrateViz: slc.viz_type = form_data_bak.get("viz_type") query_context = try_load_json(slc.query_context) queries_bak = form_data.get(QUERIES_BAK_FIELD_NAME, {}) - query_context["queries"] = queries_bak - if "form_data" in query_context: - query_context["form_data"] = form_data_bak + if queries_bak: + query_context["queries"] = queries_bak + if "form_data" in query_context: + query_context["form_data"] = form_data_bak + slc.query_context = json.dumps(query_context) + else: + slc.query_context = None - slc.query_context = json.dumps(query_context) except Exception as e: logger.warning(f"Failed to downgrade slice {slc.id}: {e}") From 2fba789e8d9f3f9009233606fa4223d88ca7aff6 Mon Sep 17 00:00:00 2001 From: jqqin <63757369+jqqin@users.noreply.github.com> Date: Mon, 9 Jun 2025 12:42:55 -0400 Subject: [PATCH 002/148] fix(dataset): prevent metric duplication error when editing SQL and adding metric (#33523) Co-authored-by: QinQin --- .../Datasource/DatasourceModal.test.jsx | 4 +- .../components/Datasource/DatasourceModal.tsx | 42 ++----------------- 2 files changed, 6 insertions(+), 40 deletions(-) diff --git a/superset-frontend/src/components/Datasource/DatasourceModal.test.jsx b/superset-frontend/src/components/Datasource/DatasourceModal.test.jsx index d84411f48ef..7fe567657d3 100644 --- a/superset-frontend/src/components/Datasource/DatasourceModal.test.jsx +++ b/superset-frontend/src/components/Datasource/DatasourceModal.test.jsx @@ -34,8 +34,8 @@ import mockDatasource from 'spec/fixtures/mockDatasource'; // Define your constants here const SAVE_ENDPOINT = 'glob:*/api/v1/dataset/7'; const SAVE_PAYLOAD = { new: 'data' }; -const SAVE_DATASOURCE_ENDPOINT = 'glob:*/api/v1/dataset/7'; -const GET_DATASOURCE_ENDPOINT = SAVE_DATASOURCE_ENDPOINT; +const SAVE_DATASOURCE_ENDPOINT = 'glob:*/api/v1/dataset/7?override_columns=*'; +const GET_DATASOURCE_ENDPOINT = 'glob:*/api/v1/dataset/7'; const GET_DATABASE_ENDPOINT = 'glob:*/api/v1/database/?q=*'; const mockedProps = { diff --git a/superset-frontend/src/components/Datasource/DatasourceModal.tsx b/superset-frontend/src/components/Datasource/DatasourceModal.tsx index 51a60e11bef..c89f588a0cb 100644 --- a/superset-frontend/src/components/Datasource/DatasourceModal.tsx +++ b/superset-frontend/src/components/Datasource/DatasourceModal.tsx @@ -17,7 +17,7 @@ * under the License. */ import { FunctionComponent, useState, useRef } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import Alert from 'src/components/Alert'; import Button from 'src/components/Button'; import { @@ -36,15 +36,6 @@ import Modal from 'src/components/Modal'; import AsyncEsmComponent from 'src/components/AsyncEsmComponent'; import ErrorMessageWithStackTrace from 'src/components/ErrorMessage/ErrorMessageWithStackTrace'; import withToasts from 'src/components/MessageToasts/withToasts'; -import { - startMetaDataLoading, - stopMetaDataLoading, - syncDatasourceMetadata, -} from 'src/explore/actions/exploreActions'; -import { - fetchSyncedColumns, - updateColumns, -} from 'src/components/Datasource/utils'; import { DatasetObject } from '../../features/datasets/types'; const DatasourceEditor = AsyncEsmComponent(() => import('./DatasourceEditor')); @@ -98,14 +89,12 @@ function buildExtraJsonObject( const DatasourceModal: FunctionComponent = ({ addSuccessToast, - addDangerToast, datasource, onDatasourceSave, onHide, show, }) => { const theme = useTheme(); - const dispatch = useDispatch(); const [currentDatasource, setCurrentDatasource] = useState(datasource); const currencies = useSelector< { @@ -200,36 +189,13 @@ const DatasourceModal: FunctionComponent = ({ const onConfirmSave = async () => { // Pull out extra fields into the extra object setIsSaving(true); + const overrideColumns = datasource.sql !== currentDatasource.sql; try { await SupersetClient.put({ - endpoint: `/api/v1/dataset/${currentDatasource.id}`, + endpoint: `/api/v1/dataset/${currentDatasource.id}?override_columns=${overrideColumns}`, jsonPayload: buildPayload(currentDatasource), }); - if (datasource.sql !== currentDatasource.sql) { - // if sql has changed, save a second time with synced columns - dispatch(startMetaDataLoading()); - try { - const columnJson = await fetchSyncedColumns(currentDatasource); - const columnChanges = updateColumns( - currentDatasource.columns, - columnJson, - addSuccessToast, - ); - currentDatasource.columns = columnChanges.finalColumns; - dispatch(syncDatasourceMetadata(currentDatasource)); - dispatch(stopMetaDataLoading()); - addSuccessToast(t('Metadata has been synced')); - } catch (error) { - dispatch(stopMetaDataLoading()); - addDangerToast( - t('An error has occurred while syncing virtual dataset columns'), - ); - } - await SupersetClient.put({ - endpoint: `/api/v1/dataset/${currentDatasource.id}`, - jsonPayload: buildPayload(currentDatasource), - }); - } + const { json } = await SupersetClient.get({ endpoint: `/api/v1/dataset/${currentDatasource?.id}`, }); From 57bb425fb08fec0e8d8f4e415d08f0d518e26ae3 Mon Sep 17 00:00:00 2001 From: Pat Buxton <45275736+rad-pat@users.noreply.github.com> Date: Mon, 9 Jun 2025 18:33:54 +0100 Subject: [PATCH 003/148] fix(dashboard): show dashboard thumbnail images when retrieved (#33726) --- superset-frontend/src/features/dashboards/DashboardCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/src/features/dashboards/DashboardCard.tsx b/superset-frontend/src/features/dashboards/DashboardCard.tsx index d8d50f5a7f7..2289104f872 100644 --- a/superset-frontend/src/features/dashboards/DashboardCard.tsx +++ b/superset-frontend/src/features/dashboards/DashboardCard.tsx @@ -159,7 +159,7 @@ function DashboardCard({ } url={bulkSelectEnabled ? undefined : dashboard.url} linkComponent={Link} - imgURL={dashboard.thumbnail_url} + imgURL={thumbnailUrl} imgFallbackURL={assetUrl( '/static/assets/images/dashboard-card-fallback.svg', )} From 3ef92e56104c123bec22a47a0bbf08afef76dfb2 Mon Sep 17 00:00:00 2001 From: Vladislav Korenkov <73882772+Quatters@users.noreply.github.com> Date: Tue, 10 Jun 2025 03:52:47 +1000 Subject: [PATCH 004/148] fix(Alerts & reports): invalid "Last updated" time formatting (#33719) --- superset-frontend/src/utils/dates.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/superset-frontend/src/utils/dates.ts b/superset-frontend/src/utils/dates.ts index a1155dc1af9..8124490fdbe 100644 --- a/superset-frontend/src/utils/dates.ts +++ b/superset-frontend/src/utils/dates.ts @@ -25,6 +25,7 @@ import relativeTime from 'dayjs/plugin/relativeTime'; import customParseFormat from 'dayjs/plugin/customParseFormat'; import duration from 'dayjs/plugin/duration'; import updateLocale from 'dayjs/plugin/updateLocale'; +import localizedFormat from 'dayjs/plugin/localizedFormat'; import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'; dayjs.extend(utc); @@ -34,6 +35,7 @@ dayjs.extend(relativeTime); dayjs.extend(customParseFormat); dayjs.extend(duration); dayjs.extend(updateLocale); +dayjs.extend(localizedFormat); dayjs.extend(isSameOrBefore); dayjs.updateLocale('en', { From 6513445000d0bbf437766d47a7485a8e4456e018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lourdu=20Radjou=F0=9F=8E=B6?= Date: Mon, 9 Jun 2025 23:45:14 +0530 Subject: [PATCH 005/148] docs: fix typo and improve alt text in README (#33721) Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1cc4c44a6d0..2361ecbf739 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ Here are some of the major database solutions that are supported:

redshift - google-biquery + google-bigquery snowflake trino presto @@ -136,7 +136,7 @@ Here are some of the major database solutions that are supported: starrocks doris oceanbase - oceanbase + sap-hana denodo ydb TDengine From 2f007bf7a5a110631ce48a3fdd595289642aa18d Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Mon, 9 Jun 2025 17:03:31 -0400 Subject: [PATCH 006/148] fix: typo in SQL dialect map (#33727) --- superset/sql/parse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/sql/parse.py b/superset/sql/parse.py index ce26e8d13f8..07b9ae5461f 100644 --- a/superset/sql/parse.py +++ b/superset/sql/parse.py @@ -75,7 +75,7 @@ SQLGLOT_DIALECTS = { # "impala": ??? # "kustosql": ??? # "kylin": ??? - "mariadb: ": Dialects.MYSQL, + "mariadb": Dialects.MYSQL, "motherduck": Dialects.DUCKDB, "mssql": Dialects.TSQL, "mysql": Dialects.MYSQL, From d11b6d557e06579e735f58628d6cbdc106b4009e Mon Sep 17 00:00:00 2001 From: nmdo Date: Mon, 9 Jun 2025 17:59:54 -0400 Subject: [PATCH 007/148] feat(MixedTimeSeries): Add onlyTotal and Sort Series to Mixed TimeSeries (#33634) --- .../src/MixedTimeseries/controlPanel.tsx | 49 ++++++++++++++++ .../src/MixedTimeseries/transformProps.ts | 58 ++++++++++++------- 2 files changed, 87 insertions(+), 20 deletions(-) diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx index 29ce20e9d66..3d67f51e830 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx @@ -19,6 +19,7 @@ import { ensureIsArray, t } from '@superset-ui/core'; import { cloneDeep } from 'lodash'; import { + ControlPanelsContainerProps, ControlPanelConfig, ControlPanelSectionConfig, ControlSetRow, @@ -27,6 +28,8 @@ import { getStandardizedControls, sections, sharedControls, + DEFAULT_SORT_SERIES_DATA, + SORT_SERIES_CHOICES, } from '@superset-ui/chart-controls'; import { DEFAULT_FORM_DATA } from './types'; @@ -196,6 +199,23 @@ function createCustomizeSection( }, }, ], + [ + { + name: `only_total${controlSuffix}`, + config: { + type: 'CheckboxControl', + label: t('Only Total'), + default: true, + renderTrigger: true, + description: t( + 'Only show the total value on the stacked chart, and not show on the selected category', + ), + visibility: ({ controls }: ControlPanelsContainerProps) => + Boolean(controls?.show_value?.value) && + Boolean(controls?.stack?.value), + }, + }, + ], [ { name: `opacity${controlSuffix}`, @@ -258,6 +278,35 @@ function createCustomizeSection( }, }, ], + [{t('Series Order')}], + [ + { + name: `sort_series_type${controlSuffix}`, + config: { + type: 'SelectControl', + freeForm: false, + label: t('Sort Series By'), + choices: SORT_SERIES_CHOICES, + default: DEFAULT_SORT_SERIES_DATA.sort_series_type, + renderTrigger: true, + description: t( + 'Based on what should series be ordered on the chart and legend', + ), + }, + }, + ], + [ + { + name: `sort_series_ascending${controlSuffix}`, + config: { + type: 'CheckboxControl', + label: t('Sort Series Ascending'), + default: DEFAULT_SORT_SERIES_DATA.sort_series_ascending, + renderTrigger: true, + description: t('Sort series in ascending order'), + }, + }, + ], ]; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts index 478915cfce1..18e46368774 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts @@ -173,6 +173,8 @@ export default function transformProps( showLegend, showValue, showValueB, + onlyTotal, + onlyTotalB, stack, stackB, truncateXAxis, @@ -203,6 +205,10 @@ export default function transformProps( yAxisTitleMargin, yAxisTitlePosition, sliceId, + sortSeriesType, + sortSeriesTypeB, + sortSeriesAscending, + sortSeriesAscendingB, timeGrainSqla, percentageThreshold, metrics = [], @@ -223,18 +229,42 @@ export default function transformProps( } const rebasedDataA = rebaseForecastDatum(data1, verboseMap); + const { totalStackedValues, thresholdValues } = extractDataTotalValues( + rebasedDataA, + { + stack, + percentageThreshold, + xAxisCol: xAxisLabel, + }, + ); const MetricDisplayNameA = getMetricDisplayName(metrics[0], verboseMap); const MetricDisplayNameB = getMetricDisplayName(metricsB[0], verboseMap); - const [rawSeriesA] = extractSeries(rebasedDataA, { + const [rawSeriesA, sortedTotalValuesA] = extractSeries(rebasedDataA, { fillNeighborValue: stack ? 0 : undefined, xAxis: xAxisLabel, + sortSeriesType, + sortSeriesAscending, + stack, + totalStackedValues, }); const rebasedDataB = rebaseForecastDatum(data2, verboseMap); - const [rawSeriesB] = extractSeries(rebasedDataB, { + const { + totalStackedValues: totalStackedValuesB, + thresholdValues: thresholdValuesB, + } = extractDataTotalValues(rebasedDataB, { + stack: Boolean(stackB), + percentageThreshold, + xAxisCol: xAxisLabel, + }); + const [rawSeriesB, sortedTotalValuesB] = extractSeries(rebasedDataB, { fillNeighborValue: stackB ? 0 : undefined, xAxis: xAxisLabel, + sortSeriesType: sortSeriesTypeB, + sortSeriesAscending: sortSeriesAscendingB, + stack: Boolean(stackB), + totalStackedValues: totalStackedValuesB, }); const dataTypes = getColtypesMapping(queriesData[0]); @@ -292,25 +322,11 @@ export default function transformProps( ); const showValueIndexesA = extractShowValueIndexes(rawSeriesA, { stack, + onlyTotal, }); const showValueIndexesB = extractShowValueIndexes(rawSeriesB, { stack, - }); - const { totalStackedValues, thresholdValues } = extractDataTotalValues( - rebasedDataA, - { - stack, - percentageThreshold, - xAxisCol: xAxisLabel, - }, - ); - const { - totalStackedValues: totalStackedValuesB, - thresholdValues: thresholdValuesB, - } = extractDataTotalValues(rebasedDataB, { - stack: Boolean(stackB), - percentageThreshold, - xAxisCol: xAxisLabel, + onlyTotal, }); annotationLayers @@ -406,6 +422,7 @@ export default function transformProps( areaOpacity: opacity, seriesType, showValue, + onlyTotal, stack: Boolean(stack), stackIdSuffix: '\na', yAxisIndex, @@ -420,8 +437,8 @@ export default function transformProps( formatter: seriesFormatter, }) : seriesFormatter, + totalStackedValues: sortedTotalValuesA, showValueIndexes: showValueIndexesA, - totalStackedValues, thresholdValues, timeShiftColor, }, @@ -464,6 +481,7 @@ export default function transformProps( areaOpacity: opacityB, seriesType: seriesTypeB, showValue: showValueB, + onlyTotal: onlyTotalB, stack: Boolean(stackB), stackIdSuffix: '\nb', yAxisIndex: yAxisIndexB, @@ -478,8 +496,8 @@ export default function transformProps( formatter: seriesFormatter, }) : seriesFormatter, + totalStackedValues: sortedTotalValuesB, showValueIndexes: showValueIndexesB, - totalStackedValues: totalStackedValuesB, thresholdValues: thresholdValuesB, timeShiftColor, }, From 3a3984006cfd506449323125f0a3138fe7650964 Mon Sep 17 00:00:00 2001 From: "JUST.in DO IT" Date: Mon, 9 Jun 2025 15:11:54 -0700 Subject: [PATCH 008/148] chore(explore): Add format sql and view in SQL Lab option in View Query (#33341) --- .../components/controls/ViewQuery.test.tsx | 158 ++++++++++++++++++ .../explore/components/controls/ViewQuery.tsx | 144 ++++++++++++++-- .../components/controls/ViewQueryModal.tsx | 15 +- 3 files changed, 294 insertions(+), 23 deletions(-) create mode 100644 superset-frontend/src/explore/components/controls/ViewQuery.test.tsx diff --git a/superset-frontend/src/explore/components/controls/ViewQuery.test.tsx b/superset-frontend/src/explore/components/controls/ViewQuery.test.tsx new file mode 100644 index 00000000000..9a18020c342 --- /dev/null +++ b/superset-frontend/src/explore/components/controls/ViewQuery.test.tsx @@ -0,0 +1,158 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + screen, + render, + fireEvent, + waitFor, +} from 'spec/helpers/testing-library'; +import fetchMock from 'fetch-mock'; +import copyTextToClipboard from 'src/utils/copy'; +import ViewQuery, { ViewQueryProps } from './ViewQuery'; + +const mockHistoryPush = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + push: mockHistoryPush, + }), +})); + +jest.mock('src/utils/copy'); + +function setup(props: ViewQueryProps) { + return render(, { useRouter: true, useRedux: true }); +} + +const mockProps = { + sql: 'select * from table', + datasource: '1__table', +}; + +const datasetApiEndpoint = 'glob:*/api/v1/dataset/1?**'; +const formatSqlEndpoint = 'glob:*/api/v1/sqllab/format_sql/'; +const formattedSQL = 'SELECT * FROM table;'; + +beforeEach(() => { + fetchMock.get(datasetApiEndpoint, { + result: { + database: { + backend: 'sqlite', + }, + }, + }); + fetchMock.post(formatSqlEndpoint, { + result: formattedSQL, + }); +}); + +afterEach(() => { + jest.resetAllMocks(); + fetchMock.restore(); +}); + +const getFormatSwitch = () => + screen.getByRole('switch', { name: 'Show original SQL' }); + +test('renders the component with Formatted SQL and buttons', async () => { + const { container } = setup(mockProps); + expect(screen.getByText('Copy')).toBeInTheDocument(); + expect(getFormatSwitch()).toBeInTheDocument(); + expect(screen.getByText('View in SQL Lab')).toBeInTheDocument(); + + await waitFor(() => + expect(fetchMock.calls(formatSqlEndpoint)).toHaveLength(1), + ); + + expect(container).toHaveTextContent(formattedSQL); +}); + +test('copies the SQL to the clipboard when Copy button is clicked', async () => { + setup(mockProps); + + (copyTextToClipboard as jest.Mock).mockResolvedValue(''); + const copyButton = screen.getByText('Copy'); + expect(copyTextToClipboard as jest.Mock).not.toHaveBeenCalled(); + fireEvent.click(copyButton); + expect(copyTextToClipboard as jest.Mock).toHaveBeenCalled(); +}); + +test('shows the original SQL when Format switch is unchecked', async () => { + const { container } = setup(mockProps); + const formatButton = getFormatSwitch(); + + await waitFor(() => + expect(fetchMock.calls(formatSqlEndpoint)).toHaveLength(1), + ); + + fireEvent.click(formatButton); + + expect(container).toHaveTextContent(mockProps.sql); +}); + +test('toggles back to formatted SQL when Format switch is clicked', async () => { + const { container } = setup(mockProps); + const formatButton = getFormatSwitch(); + + await waitFor(() => + expect(fetchMock.calls(formatSqlEndpoint)).toHaveLength(1), + ); + + // Click to format SQL + fireEvent.click(formatButton); + + await waitFor(() => expect(container).toHaveTextContent(mockProps.sql)); + + // Toggle format switch + fireEvent.click(formatButton); + + await waitFor(() => expect(container).toHaveTextContent(formattedSQL)); +}); + +test('navigates to SQL Lab when View in SQL Lab button is clicked', () => { + setup(mockProps); + + const viewInSQLLabButton = screen.getByText('View in SQL Lab'); + fireEvent.click(viewInSQLLabButton); + + expect(mockHistoryPush).toHaveBeenCalledWith('/sqllab', { + state: { + requestedQuery: { + datasourceKey: mockProps.datasource, + sql: mockProps.sql, + }, + }, + }); +}); + +test('opens SQL Lab in a new tab when View in SQL Lab button is clicked with meta key', () => { + window.open = jest.fn(); + + setup(mockProps); + const viewInSQLLabButton = screen.getByText('View in SQL Lab'); + + fireEvent.click(viewInSQLLabButton, { metaKey: true }); + + const { datasource, sql } = mockProps; + expect(window.open).toHaveBeenCalledWith( + `/sqllab?datasourceKey=${datasource}&sql=${sql}`, + '_blank', + ); +}); diff --git a/superset-frontend/src/explore/components/controls/ViewQuery.tsx b/superset-frontend/src/explore/components/controls/ViewQuery.tsx index 1b3e359ce10..804ca879975 100644 --- a/superset-frontend/src/explore/components/controls/ViewQuery.tsx +++ b/superset-frontend/src/explore/components/controls/ViewQuery.tsx @@ -16,10 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -// TODO: Remove fa-icon -/* eslint-disable icons/no-fa-icons-usage */ -import { FC } from 'react'; -import { styled } from '@superset-ui/core'; +import { + FC, + KeyboardEvent, + MouseEvent, + useCallback, + useEffect, + useState, +} from 'react'; +import rison from 'rison'; +import { styled, SupersetClient, t } from '@superset-ui/core'; import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/light'; import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github'; import CopyToClipboard from 'src/components/CopyToClipboard'; @@ -28,6 +34,9 @@ import markdownSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/mar import htmlSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/htmlbars'; import sqlSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'; import jsonSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/json'; +import { useHistory } from 'react-router-dom'; +import { Switch } from 'src/components/Switch'; +import { Button, Skeleton } from 'src/components'; const CopyButtonViewQuery = styled(CopyButton)` && { @@ -40,8 +49,9 @@ SyntaxHighlighter.registerLanguage('html', htmlSyntax); SyntaxHighlighter.registerLanguage('sql', sqlSyntax); SyntaxHighlighter.registerLanguage('json', jsonSyntax); -interface ViewQueryProps { +export interface ViewQueryProps { sql: string; + datasource: string; language?: string; } @@ -51,26 +61,124 @@ const StyledSyntaxContainer = styled.div` flex-direction: column; `; +const StyledHeaderMenuContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + margin-top: ${({ theme }) => -theme.gridUnit * 4}px; + align-items: flex-end; +`; + +const StyledHeaderActionContainer = styled.div` + display: flex; + flex-direction: row; + column-gap: ${({ theme }) => theme.gridUnit * 2}px; +`; + const StyledSyntaxHighlighter = styled(SyntaxHighlighter)` flex: 1; `; +const StyledLabel = styled.label` + font-size: ${({ theme }) => theme.typography.sizes.m}px; +`; + +const DATASET_BACKEND_QUERY = { + keys: ['none'], + columns: ['database.backend'], +}; + const ViewQuery: FC = props => { - const { sql, language = 'sql' } = props; + const { sql, language = 'sql', datasource } = props; + const datasetId = datasource.split('__')[0]; + const [formattedSQL, setFormattedSQL] = useState(); + const [showFormatSQL, setShowFormatSQL] = useState(true); + const history = useHistory(); + const currentSQL = (showFormatSQL ? formattedSQL : sql) ?? sql; + + const formatCurrentQuery = useCallback(() => { + if (formattedSQL) { + setShowFormatSQL(val => !val); + } else { + const queryParams = rison.encode(DATASET_BACKEND_QUERY); + SupersetClient.get({ + endpoint: `/api/v1/dataset/${datasetId}?q=${queryParams}`, + }) + .then(({ json }) => + SupersetClient.post({ + endpoint: `/api/v1/sqllab/format_sql/`, + body: JSON.stringify({ + sql, + engine: json.result.database.backend, + }), + headers: { 'Content-Type': 'application/json' }, + }), + ) + .then(({ json }) => { + setFormattedSQL(json.result); + setShowFormatSQL(true); + }) + .catch(() => { + setShowFormatSQL(true); + }); + } + }, [sql, datasetId, formattedSQL]); + + const navToSQLLab = useCallback( + (domEvent: KeyboardEvent | MouseEvent) => { + const requestedQuery = { + datasourceKey: datasource, + sql: currentSQL, + }; + if (domEvent.metaKey || domEvent.ctrlKey) { + domEvent.preventDefault(); + window.open( + `/sqllab?datasourceKey=${datasource}&sql=${currentSQL}`, + '_blank', + ); + } else { + history.push('/sqllab', { state: { requestedQuery } }); + } + }, + [history, datasource, currentSQL], + ); + + useEffect(() => { + formatCurrentQuery(); + }, [sql]); + return ( - - - - } - /> - - {sql} - + + + + {t('Copy')} + + } + /> + + + + + + {t('Show original SQL')} + + + + {!formattedSQL && } + {formattedSQL && ( + + {currentSQL} + + )} ); }; diff --git a/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx b/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx index ab4093d4bba..4d25b22b2a9 100644 --- a/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx +++ b/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx @@ -23,13 +23,14 @@ import { ensureIsArray, t, getClientErrorObject, + QueryFormData, } from '@superset-ui/core'; import Loading from 'src/components/Loading'; import { getChartDataRequest } from 'src/components/Chart/chartAction'; import ViewQuery from 'src/explore/components/controls/ViewQuery'; interface Props { - latestQueryFormData: object; + latestQueryFormData: QueryFormData; } type Result = { @@ -43,7 +44,7 @@ const ViewQueryModalContainer = styled.div` flex-direction: column; `; -const ViewQueryModal: FC = props => { +const ViewQueryModal: FC = ({ latestQueryFormData }) => { const [result, setResult] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -51,7 +52,7 @@ const ViewQueryModal: FC = props => { const loadChartData = (resultType: string) => { setIsLoading(true); getChartDataRequest({ - formData: props.latestQueryFormData, + formData: latestQueryFormData, resultFormat: 'json', resultType, }) @@ -74,7 +75,7 @@ const ViewQueryModal: FC = props => { }; useEffect(() => { loadChartData('query'); - }, [JSON.stringify(props.latestQueryFormData)]); + }, [JSON.stringify(latestQueryFormData)]); if (isLoading) { return ; @@ -87,7 +88,11 @@ const ViewQueryModal: FC = props => { {result.map(item => item.query ? ( - + ) : null, )} From fc7ba060c1f01ba3b4c07006bfa35f2bca573c24 Mon Sep 17 00:00:00 2001 From: Le Xich Long Date: Tue, 10 Jun 2025 06:18:05 +0800 Subject: [PATCH 009/148] feat(clickhouse): allow dynamic schema (#32610) --- superset/db_engine_specs/clickhouse.py | 15 ++++++++++++ .../db_engine_specs/test_clickhouse.py | 24 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/superset/db_engine_specs/clickhouse.py b/superset/db_engine_specs/clickhouse.py index 8fb8e1d0992..91e7ccf4f51 100644 --- a/superset/db_engine_specs/clickhouse.py +++ b/superset/db_engine_specs/clickhouse.py @@ -20,6 +20,7 @@ import logging import re from datetime import datetime from typing import Any, cast, TYPE_CHECKING +from urllib import parse from flask import current_app from flask_babel import gettext as __ @@ -267,6 +268,8 @@ class ClickHouseConnectEngineSpec(BasicParametersMixin, ClickHouseEngineSpec): parameters_schema = ClickHouseParametersSchema() encryption_parameters = {"secure": "true"} + supports_dynamic_schema = True + @classmethod def get_dbapi_exception_mapping(cls) -> dict[type[Exception], type[Exception]]: return {} @@ -414,3 +417,15 @@ class ClickHouseConnectEngineSpec(BasicParametersMixin, ClickHouseEngineSpec): :return: Conditionally mutated label """ return f"{label}_{md5_sha_from_str(label)[:6]}" + + @classmethod + def adjust_engine_params( + cls, + uri: URL, + connect_args: dict[str, Any], + catalog: str | None = None, + schema: str | None = None, + ) -> tuple[URL, dict[str, Any]]: + if schema: + uri = uri.set(database=parse.quote(schema, safe="")) + return uri, connect_args diff --git a/tests/unit_tests/db_engine_specs/test_clickhouse.py b/tests/unit_tests/db_engine_specs/test_clickhouse.py index c4277ce4ed6..5e532e6ffdc 100644 --- a/tests/unit_tests/db_engine_specs/test_clickhouse.py +++ b/tests/unit_tests/db_engine_specs/test_clickhouse.py @@ -20,6 +20,7 @@ from typing import Any, Optional from unittest.mock import Mock import pytest +from sqlalchemy.engine.url import make_url from sqlalchemy.types import ( Boolean, Date, @@ -225,3 +226,26 @@ def test_connect_make_label_compatible(column_name: str, expected_result: str) - label = spec.make_label_compatible(column_name) assert label == expected_result + + +@pytest.mark.parametrize( + "schema, expected_result", + [ + (None, "clickhousedb+connect://localhost:443/__default__"), + ( + "new_schema", + "clickhousedb+connect://localhost:443/new_schema", + ), + ], +) +def test_adjust_engine_params_fully_qualified( + schema: str, expected_result: str +) -> None: + from superset.db_engine_specs.clickhouse import ( + ClickHouseConnectEngineSpec as spec, # noqa: N813 + ) + + url = make_url("clickhousedb+connect://localhost:443/__default__") + + uri = spec.adjust_engine_params(url, {}, None, schema)[0] + assert str(uri) == expected_result From 5f2f12d347fb2e886c3af61adc8919580ea8f27d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 16:19:11 -0600 Subject: [PATCH 010/148] chore(deps-dev): bump @typescript-eslint/parser from 8.29.0 to 8.33.0 in /superset-websocket (#33650) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- superset-websocket/package-lock.json | 182 +++++++++++++++++++-------- superset-websocket/package.json | 2 +- 2 files changed, 132 insertions(+), 52 deletions(-) diff --git a/superset-websocket/package-lock.json b/superset-websocket/package-lock.json index 04cdc67d67d..2d4650cee0d 100644 --- a/superset-websocket/package-lock.json +++ b/superset-websocket/package-lock.json @@ -30,7 +30,7 @@ "@types/uuid": "^10.0.0", "@types/ws": "^8.5.12", "@typescript-eslint/eslint-plugin": "^8.26.0", - "@typescript-eslint/parser": "^8.29.0", + "@typescript-eslint/parser": "^8.33.0", "eslint": "^9.27.0", "eslint-config-prettier": "^10.1.5", "eslint-plugin-lodash": "^8.0.0", @@ -2071,16 +2071,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.0.tgz", - "integrity": "sha512-8C0+jlNJOwQso2GapCVWWfW/rzaq7Lbme+vGUFKE31djwNncIpgXD7Cd4weEsDdkoZDjH0lwwr3QDQFuyrMg9g==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz", + "integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.29.0", - "@typescript-eslint/types": "8.29.0", - "@typescript-eslint/typescript-estree": "8.29.0", - "@typescript-eslint/visitor-keys": "8.29.0", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4" }, "engines": { @@ -2096,14 +2096,14 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.0.tgz", - "integrity": "sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz", + "integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.29.0", - "@typescript-eslint/visitor-keys": "8.29.0" + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2114,9 +2114,9 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.0.tgz", - "integrity": "sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz", + "integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==", "dev": true, "license": "MIT", "engines": { @@ -2128,20 +2128,22 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.0.tgz", - "integrity": "sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz", + "integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.29.0", - "@typescript-eslint/visitor-keys": "8.29.0", + "@typescript-eslint/project-service": "8.33.0", + "@typescript-eslint/tsconfig-utils": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2155,13 +2157,13 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.0.tgz", - "integrity": "sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz", + "integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/types": "8.33.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2224,6 +2226,39 @@ "typescript": ">=4.8.4" } }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz", + "integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.33.0", + "@typescript-eslint/types": "^8.33.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/project-service/node_modules/@typescript-eslint/types": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz", + "integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.0.tgz", @@ -2241,6 +2276,23 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz", + "integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/type-utils": { "version": "8.26.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.0.tgz", @@ -8635,57 +8687,59 @@ } }, "@typescript-eslint/parser": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.0.tgz", - "integrity": "sha512-8C0+jlNJOwQso2GapCVWWfW/rzaq7Lbme+vGUFKE31djwNncIpgXD7Cd4weEsDdkoZDjH0lwwr3QDQFuyrMg9g==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz", + "integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "8.29.0", - "@typescript-eslint/types": "8.29.0", - "@typescript-eslint/typescript-estree": "8.29.0", - "@typescript-eslint/visitor-keys": "8.29.0", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.0.tgz", - "integrity": "sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz", + "integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==", "dev": true, "requires": { - "@typescript-eslint/types": "8.29.0", - "@typescript-eslint/visitor-keys": "8.29.0" + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0" } }, "@typescript-eslint/types": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.0.tgz", - "integrity": "sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz", + "integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.0.tgz", - "integrity": "sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz", + "integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==", "dev": true, "requires": { - "@typescript-eslint/types": "8.29.0", - "@typescript-eslint/visitor-keys": "8.29.0", + "@typescript-eslint/project-service": "8.33.0", + "@typescript-eslint/tsconfig-utils": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" } }, "@typescript-eslint/visitor-keys": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.0.tgz", - "integrity": "sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz", + "integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==", "dev": true, "requires": { - "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/types": "8.33.0", "eslint-visitor-keys": "^4.2.0" } }, @@ -8722,6 +8776,25 @@ } } }, + "@typescript-eslint/project-service": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz", + "integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==", + "dev": true, + "requires": { + "@typescript-eslint/tsconfig-utils": "^8.33.0", + "@typescript-eslint/types": "^8.33.0", + "debug": "^4.3.4" + }, + "dependencies": { + "@typescript-eslint/types": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz", + "integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==", + "dev": true + } + } + }, "@typescript-eslint/scope-manager": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.0.tgz", @@ -8732,6 +8805,13 @@ "@typescript-eslint/visitor-keys": "8.19.0" } }, + "@typescript-eslint/tsconfig-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz", + "integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==", + "dev": true, + "requires": {} + }, "@typescript-eslint/type-utils": { "version": "8.26.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.0.tgz", diff --git a/superset-websocket/package.json b/superset-websocket/package.json index 61ffc639598..edde363a1ed 100644 --- a/superset-websocket/package.json +++ b/superset-websocket/package.json @@ -38,7 +38,7 @@ "@types/uuid": "^10.0.0", "@types/ws": "^8.5.12", "@typescript-eslint/eslint-plugin": "^8.26.0", - "@typescript-eslint/parser": "^8.29.0", + "@typescript-eslint/parser": "^8.33.0", "eslint": "^9.27.0", "eslint-config-prettier": "^10.1.5", "eslint-plugin-lodash": "^8.0.0", From ae6c072661e597093e7eb9b8b70a122fc55a500b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 16:19:25 -0600 Subject: [PATCH 011/148] chore(deps-dev): bump @docusaurus/tsconfig from 3.7.0 to 3.8.0 in /docs (#33645) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/package.json | 2 +- docs/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/package.json b/docs/package.json index 1fa0b5feb5a..ddc710c6e24 100644 --- a/docs/package.json +++ b/docs/package.json @@ -39,7 +39,7 @@ }, "devDependencies": { "@docusaurus/module-type-aliases": "^3.7.0", - "@docusaurus/tsconfig": "^3.7.0", + "@docusaurus/tsconfig": "^3.8.0", "@types/react": "^18.3.12", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", diff --git a/docs/yarn.lock b/docs/yarn.lock index 095df59e0dc..7bfe365ec03 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -1935,10 +1935,10 @@ fs-extra "^11.1.1" tslib "^2.6.0" -"@docusaurus/tsconfig@^3.7.0": - version "3.7.0" - resolved "https://registry.yarnpkg.com/@docusaurus/tsconfig/-/tsconfig-3.7.0.tgz#654dcc524e25b8809af0f1b0b42485c18c047ab5" - integrity sha512-vRsyj3yUZCjscgfgcFYjIsTcAru/4h4YH2/XAE8Rs7wWdnng98PgWKvP5ovVc4rmRpRg2WChVW0uOy2xHDvDBQ== +"@docusaurus/tsconfig@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/tsconfig/-/tsconfig-3.8.0.tgz#ea7ee0917e1562cf0a6e95e049c42f1f61351f32" + integrity sha512-utLl48nNjSYBoq47RKukZ9fPLEX3nJWThzrujb0ndQQ1jc/gh4RhTRaAqItH9nImnsgGKmLMnyoMBpfGmoop+w== "@docusaurus/types@3.7.0": version "3.7.0" From ca74ae75a648d323f78c0ff72643b406c57f9fae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 16:19:49 -0600 Subject: [PATCH 012/148] chore(deps-dev): bump webpack from 5.99.8 to 5.99.9 in /docs (#33643) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/package.json | 2 +- docs/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/package.json b/docs/package.json index ddc710c6e24..99bd59f59cc 100644 --- a/docs/package.json +++ b/docs/package.json @@ -49,7 +49,7 @@ "eslint-plugin-react": "^7.37.5", "prettier": "^2.0.0", "typescript": "~5.8.3", - "webpack": "^5.99.8" + "webpack": "^5.99.9" }, "browserslist": { "production": [ diff --git a/docs/yarn.lock b/docs/yarn.lock index 7bfe365ec03..a79cd662638 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -13033,10 +13033,10 @@ webpack-sources@^3.2.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@^5.88.1, webpack@^5.95.0, webpack@^5.99.8: - version "5.99.8" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.8.tgz#dd31a020b7c092d30c4c6d9a4edb95809e7f5946" - integrity sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ== +webpack@^5.88.1, webpack@^5.95.0, webpack@^5.99.9: + version "5.99.9" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.9.tgz#d7de799ec17d0cce3c83b70744b4aedb537d8247" + integrity sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg== dependencies: "@types/eslint-scope" "^3.7.7" "@types/estree" "^1.0.6" From bb6bd85c1d66a3a4c2fb8827ad95477e7c5de9f5 Mon Sep 17 00:00:00 2001 From: Phuc Hung Nguyen <42292807+anthonyhungnguyen@users.noreply.github.com> Date: Mon, 9 Jun 2025 19:07:44 -0400 Subject: [PATCH 013/148] fix(chart): set tab name as chart name (#33694) Co-authored-by: Phuc Hung Nguyen --- .../src/dashboard/containers/DashboardPage.tsx | 4 +--- .../explore/components/ExploreViewContainer/index.jsx | 9 +++++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/superset-frontend/src/dashboard/containers/DashboardPage.tsx b/superset-frontend/src/dashboard/containers/DashboardPage.tsx index fb8bb6c05d8..e56c400c320 100644 --- a/superset-frontend/src/dashboard/containers/DashboardPage.tsx +++ b/superset-frontend/src/dashboard/containers/DashboardPage.tsx @@ -71,8 +71,6 @@ const DashboardBuilder = lazy( ), ); -const originalDocumentTitle = document.title; - type PageProps = { idOrSlug: string; }; @@ -204,7 +202,7 @@ export const DashboardPage: FC = ({ idOrSlug }: PageProps) => { document.title = dashboard_title; } return () => { - document.title = originalDocumentTitle; + document.title = 'Superset'; }; }, [dashboard_title]); diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx index 822a54ecafe..af71b790ec1 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx @@ -269,6 +269,15 @@ function ExploreViewContainer(props) { const theme = useTheme(); + useEffect(() => { + if (props.sliceName) { + document.title = props.sliceName; + } + return () => { + document.title = 'Superset'; + }; + }, [props.sliceName]); + const addHistory = useCallback( async ({ isReplace = false, title } = {}) => { const formData = props.dashboardId From 86e7139245e7444e749b5e8ad1d5167b4df53ce9 Mon Sep 17 00:00:00 2001 From: Vitor Avila <96086495+Vitor-Avila@users.noreply.github.com> Date: Mon, 9 Jun 2025 22:47:14 -0300 Subject: [PATCH 014/148] fix: Dataset currency (#33682) --- .../superset-ui-chart-controls/src/types.ts | 1 + .../components/Datasource/DatasourceModal.tsx | 5 +- .../explore/actions/hydrateExplore.test.ts | 48 ++++++++++ .../src/explore/actions/hydrateExplore.ts | 9 ++ .../src/hooks/apiResources/dashboards.test.ts | 89 +++++++++++++++++++ .../src/hooks/apiResources/dashboards.ts | 17 +++- superset/datasets/schemas.py | 7 +- ...vert_metric_currencies_from_str_to_json.py | 84 +++++++++++++++++ tests/integration_tests/datasets/api_tests.py | 23 +++++ 9 files changed, 277 insertions(+), 6 deletions(-) create mode 100644 superset-frontend/src/hooks/apiResources/dashboards.test.ts create mode 100644 superset/migrations/versions/2025-06-06_00-39_363a9b1e8992_convert_metric_currencies_from_str_to_json.py diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts index 71517884daa..418efe55fcd 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts @@ -69,6 +69,7 @@ export interface Dataset { columns: ColumnMeta[]; metrics: Metric[]; column_formats: Record; + currency_formats?: Record; verbose_map: Record; main_dttm_col: string; // eg. ['["ds", true]', 'ds [asc]'] diff --git a/superset-frontend/src/components/Datasource/DatasourceModal.tsx b/superset-frontend/src/components/Datasource/DatasourceModal.tsx index c89f588a0cb..e5c3e5c6cd9 100644 --- a/superset-frontend/src/components/Datasource/DatasourceModal.tsx +++ b/superset-frontend/src/components/Datasource/DatasourceModal.tsx @@ -21,7 +21,6 @@ import { useSelector } from 'react-redux'; import Alert from 'src/components/Alert'; import Button from 'src/components/Button'; import { - isDefined, styled, SupersetClient, getClientErrorObject, @@ -141,9 +140,7 @@ const DatasourceModal: FunctionComponent = ({ metric_name: metric.metric_name, metric_type: metric.metric_type, d3format: metric.d3format || null, - currency: !isDefined(metric.currency) - ? null - : JSON.stringify(metric.currency), + currency: metric.currency, verbose_name: metric.verbose_name, warning_text: metric.warning_text, uuid: metric.uuid, diff --git a/superset-frontend/src/explore/actions/hydrateExplore.test.ts b/superset-frontend/src/explore/actions/hydrateExplore.test.ts index cdbca5b94df..f0703699147 100644 --- a/superset-frontend/src/explore/actions/hydrateExplore.test.ts +++ b/superset-frontend/src/explore/actions/hydrateExplore.test.ts @@ -213,3 +213,51 @@ test('uses configured default time range if not set', () => { }), ); }); + +test('extracts currency formats from metrics in dataset', () => { + const dispatch = jest.fn(); + const getState = jest.fn(() => ({ + user: {}, + charts: {}, + datasources: {}, + common: {}, + explore: {}, + })); + + const datasetWithMetrics = { + ...exploreInitialData.dataset, + metrics: [ + { + metric_name: 'count', + currency: { symbol: 'GBP', symbolPosition: 'prefix' }, + }, + { + metric_name: 'revenue', + currency: { symbol: 'USD', symbolPosition: 'suffix' }, + }, + { metric_name: 'no_currency' }, + ], + }; + + // @ts-ignore + hydrateExplore({ ...exploreInitialData, dataset: datasetWithMetrics })( + dispatch, + // @ts-ignore + getState, + ); + + expect(dispatch).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + datasources: expect.objectContaining({ + '8__table': expect.objectContaining({ + currency_formats: { + count: { symbol: 'GBP', symbolPosition: 'prefix' }, + revenue: { symbol: 'USD', symbolPosition: 'suffix' }, + }, + }), + }), + }), + }), + ); +}); diff --git a/superset-frontend/src/explore/actions/hydrateExplore.ts b/superset-frontend/src/explore/actions/hydrateExplore.ts index 2ee46c1e93e..b8e0375a648 100644 --- a/superset-frontend/src/explore/actions/hydrateExplore.ts +++ b/superset-frontend/src/explore/actions/hydrateExplore.ts @@ -27,6 +27,7 @@ import { getChartKey } from 'src/explore/exploreUtils'; import { getControlsState } from 'src/explore/store'; import { Dispatch } from 'redux'; import { + Currency, ensureIsArray, getCategoricalSchemeRegistry, getColumnLabel, @@ -97,6 +98,14 @@ export const hydrateExplore = } const initialDatasource = dataset; + initialDatasource.currency_formats = Object.fromEntries( + (initialDatasource.metrics ?? []) + .filter(metric => !!metric.currency) + .map((metric): [string, Currency] => [ + metric.metric_name, + metric.currency!, + ]), + ); const initialExploreState = { form_data: initialFormData, diff --git a/superset-frontend/src/hooks/apiResources/dashboards.test.ts b/superset-frontend/src/hooks/apiResources/dashboards.test.ts new file mode 100644 index 00000000000..3a6265163e0 --- /dev/null +++ b/superset-frontend/src/hooks/apiResources/dashboards.test.ts @@ -0,0 +1,89 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { renderHook } from '@testing-library/react-hooks'; +import fetchMock from 'fetch-mock'; +import { useDashboardDatasets } from './dashboards'; + +describe('useDashboardDatasets', () => { + const mockDatasets = [ + { + id: 1, + metrics: [ + { + metric_name: 'count', + currency: { symbol: 'GBP', symbolPosition: 'prefix' }, + }, + { + metric_name: 'revenue', + currency: { symbol: 'USD', symbolPosition: 'suffix' }, + }, + { metric_name: 'no_currency' }, + ], + }, + { + id: 2, + metrics: [{ metric_name: 'no_currency' }], + }, + { + id: 3, + metrics: [ + { + metric_name: 'other_currency', + currency: { symbol: 'CNY', symbolPosition: 'suffix' }, + }, + ], + }, + ]; + + beforeEach(() => { + fetchMock.reset(); + }); + + it('adds currencyFormats to datasets', async () => { + fetchMock.get('glob:*/api/v1/dashboard/*/datasets', { + result: mockDatasets, + }); + + const { result, waitForNextUpdate } = renderHook(() => + useDashboardDatasets(1), + ); + await waitForNextUpdate(); + + const expectedContent = [ + { + ...mockDatasets[0], + currencyFormats: { + count: { symbol: 'GBP', symbolPosition: 'prefix' }, + revenue: { symbol: 'USD', symbolPosition: 'suffix' }, + }, + }, + { + ...mockDatasets[1], + currencyFormats: {}, + }, + { + ...mockDatasets[2], + currencyFormats: { + other_currency: { symbol: 'CNY', symbolPosition: 'suffix' }, + }, + }, + ]; + expect(result.current.result).toEqual(expectedContent); + }); +}); diff --git a/superset-frontend/src/hooks/apiResources/dashboards.ts b/superset-frontend/src/hooks/apiResources/dashboards.ts index 61896ba1309..65e4b1c0bdd 100644 --- a/superset-frontend/src/hooks/apiResources/dashboards.ts +++ b/superset-frontend/src/hooks/apiResources/dashboards.ts @@ -19,6 +19,7 @@ import { Dashboard, Datasource, EmbeddedDashboard } from 'src/dashboard/types'; import { Chart } from 'src/types/Chart'; +import { Currency } from '@superset-ui/core'; import { useApiV1Resource, useTransformedResource } from './apiResources'; export const useDashboard = (idOrSlug: string | number) => @@ -43,7 +44,21 @@ export const useDashboardCharts = (idOrSlug: string | number) => // important: this endpoint only returns the fields in the dataset // that are necessary for rendering the given dashboard export const useDashboardDatasets = (idOrSlug: string | number) => - useApiV1Resource(`/api/v1/dashboard/${idOrSlug}/datasets`); + useTransformedResource( + useApiV1Resource(`/api/v1/dashboard/${idOrSlug}/datasets`), + datasets => + datasets.map(dataset => ({ + ...dataset, + currencyFormats: Object.fromEntries( + (dataset.metrics ?? []) + .filter(metric => !!metric.currency) + .map((metric): [string, Currency] => [ + metric.metric_name, + metric.currency!, + ]), + ), + })), + ); export const useEmbeddedDashboard = (idOrSlug: string | number) => useApiV1Resource(`/api/v1/dashboard/${idOrSlug}/embedded`); diff --git a/superset/datasets/schemas.py b/superset/datasets/schemas.py index 54ce4481737..405d0bab3dd 100644 --- a/superset/datasets/schemas.py +++ b/superset/datasets/schemas.py @@ -74,6 +74,11 @@ class DatasetColumnsPutSchema(Schema): uuid = fields.UUID(allow_none=True) +class DatasetMetricCurrencyPutSchema(Schema): + symbol = fields.String(validate=Length(1, 128)) + symbolPosition = fields.String(validate=Length(1, 128)) # noqa: N815 + + class DatasetMetricsPutSchema(Schema): id = fields.Integer() expression = fields.String(required=True) @@ -82,7 +87,7 @@ class DatasetMetricsPutSchema(Schema): metric_name = fields.String(required=True, validate=Length(1, 255)) metric_type = fields.String(allow_none=True, validate=Length(1, 32)) d3format = fields.String(allow_none=True, validate=Length(1, 128)) - currency = fields.String(allow_none=True, required=False, validate=Length(1, 128)) + currency = fields.Nested(DatasetMetricCurrencyPutSchema, allow_none=True) verbose_name = fields.String(allow_none=True, metadata={Length: (1, 1024)}) warning_text = fields.String(allow_none=True) uuid = fields.UUID(allow_none=True) diff --git a/superset/migrations/versions/2025-06-06_00-39_363a9b1e8992_convert_metric_currencies_from_str_to_json.py b/superset/migrations/versions/2025-06-06_00-39_363a9b1e8992_convert_metric_currencies_from_str_to_json.py new file mode 100644 index 00000000000..708e7c52dae --- /dev/null +++ b/superset/migrations/versions/2025-06-06_00-39_363a9b1e8992_convert_metric_currencies_from_str_to_json.py @@ -0,0 +1,84 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""convert_metric_currencies_from_str_to_json + +Revision ID: 363a9b1e8992 +Revises: f1edd4a4d4f2 +Create Date: 2025-06-06 00:39:00.107746 + +""" + +import json +import logging + +from alembic import op +from sqlalchemy import Column, Integer, JSON, String +from sqlalchemy.ext.declarative import declarative_base + +from superset import db +from superset.migrations.shared.utils import paginated_update + +logger = logging.getLogger("alembic") +logger.setLevel(logging.INFO) + +# revision identifiers, used by Alembic. +revision = "363a9b1e8992" +down_revision = "f1edd4a4d4f2" + +Base = declarative_base() + + +class SqlMetric(Base): + __tablename__ = "sql_metrics" + + id = Column(Integer, primary_key=True) + metric_name = Column(String(512)) + currency = Column(JSON) + + +def upgrade(): + bind = op.get_bind() + session = db.Session(bind=bind) + currency_configs = session.query(SqlMetric).filter(SqlMetric.currency.isnot(None)) + for metric in paginated_update( + currency_configs, + lambda current, total: logger.info((f"Upgrading {current}/{total} metrics")), + ): + while True: + if isinstance(metric.currency, str): + try: + metric.currency = json.loads(metric.currency) + except Exception as e: + logger.error( + f"Error loading metric {metric.metric_name} as json: {e}" + ) + metric.currency = {} + break + else: + break + + +def downgrade(): + """ + No op downgrade. + + The downgrade could just do `metric.currency = json.dumps(metric.currency)`. However + this is happening after `f1edd4a4d4f2` which already converted the currency column + to JSON at the DB level, so we shouldn't have currencies in str anymore. It was the + case because the client was still stringifying it. + """ + pass diff --git a/tests/integration_tests/datasets/api_tests.py b/tests/integration_tests/datasets/api_tests.py index ab645d300e1..9d226f793dc 100644 --- a/tests/integration_tests/datasets/api_tests.py +++ b/tests/integration_tests/datasets/api_tests.py @@ -1462,6 +1462,29 @@ class TestDatasetApi(SupersetTestCase): assert data == expected_result self.items_to_delete = [dataset] + def test_update_dataset_update_metric_invalid_currency(self): + """ + Dataset API: Test update dataset metric with an invalid currency config + """ + + dataset = self.insert_default_dataset() + + self.login(ADMIN_USERNAME) + uri = f"api/v1/dataset/{dataset.id}" + data = { + "metrics": [ + { + "metric_name": "test", + "expression": "COUNT(*)", + "currency": '{"symbol": "USD", "symbolPosition": "suffix"}', + }, + ] + } + rv = self.put_assert_metric(uri, data, "put") + assert rv.status_code == 422 + + self.items_to_delete = [dataset] + def test_update_dataset_item_gamma(self): """ Dataset API: Test update dataset item gamma From e05ccb3824c5f463ff82d354aa7b762fdf71ce22 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Tue, 10 Jun 2025 21:40:56 -0600 Subject: [PATCH 015/148] feat: x axis interval control to show ALL ticks on timeseries charts (#33729) --- .../src/Bubble/constants.ts | 1 + .../src/Bubble/controlPanel.tsx | 2 ++ .../src/Bubble/transformProps.ts | 2 ++ .../src/MixedTimeseries/controlPanel.tsx | 2 ++ .../src/MixedTimeseries/transformProps.ts | 2 ++ .../src/MixedTimeseries/types.ts | 2 ++ .../src/Timeseries/Area/controlPanel.tsx | 2 ++ .../src/Timeseries/Regular/Bar/controlPanel.tsx | 13 +++++++++++++ .../Timeseries/Regular/Line/controlPanel.tsx | 2 ++ .../Timeseries/Regular/Scatter/controlPanel.tsx | 2 ++ .../Regular/SmoothLine/controlPanel.tsx | 2 ++ .../src/Timeseries/Step/controlPanel.tsx | 2 ++ .../src/Timeseries/constants.ts | 1 + .../src/Timeseries/transformProps.ts | 2 ++ .../src/Timeseries/types.ts | 1 + .../plugin-chart-echarts/src/controls.tsx | 17 +++++++++++++++++ .../plugin-chart-echarts/src/defaults.ts | 1 + 17 files changed, 56 insertions(+) diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/constants.ts index 1c70e872e6f..12e928b1396 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/constants.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/constants.ts @@ -32,6 +32,7 @@ export const DEFAULT_FORM_DATA: Partial = { xAxisBounds: [null, null], yAxisBounds: [null, null], xAxisLabelRotation: defaultXAxis.xAxisLabelRotation, + xAxisLabelInterval: defaultXAxis.xAxisLabelInterval, opacity: 0.6, }; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/controlPanel.tsx index 521ae98130d..3c58e0aecff 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/controlPanel.tsx @@ -31,6 +31,7 @@ import { truncateXAxis, xAxisBounds, xAxisLabelRotation, + xAxisLabelInterval, } from '../controls'; import { defaultYAxis } from '../defaults'; @@ -133,6 +134,7 @@ const config: ControlPanelConfig = { }, ], [xAxisLabelRotation], + [xAxisLabelInterval], [ { name: 'x_axis_title_margin', diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts index 49e51f511b3..68c152d07d4 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts @@ -120,6 +120,7 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) { truncateXAxis, truncateYAxis, xAxisLabelRotation, + xAxisLabelInterval, yAxisLabelRotation, tooltipSizeFormat, opacity, @@ -197,6 +198,7 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) { }, }, nameRotate: xAxisLabelRotation, + interval: xAxisLabelInterval, scale: true, name: bubbleXAxisTitle, nameLocation: 'middle', diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx index 3d67f51e830..c45d85d92c6 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx @@ -41,6 +41,7 @@ import { truncateXAxis, xAxisBounds, xAxisLabelRotation, + xAxisLabelInterval, } from '../controls'; const { @@ -368,6 +369,7 @@ const config: ControlPanelConfig = { [{t('X Axis')}], ['x_axis_time_format'], [xAxisLabelRotation], + [xAxisLabelInterval], ...richTooltipSection, // eslint-disable-next-line react/jsx-key [{t('Y Axis')}], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts index 18e46368774..fc2a6fe4383 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts @@ -195,6 +195,7 @@ export default function transformProps( tooltipSortByMetric, xAxisBounds, xAxisLabelRotation, + xAxisLabelInterval, groupby, groupbyB, xAxis: xAxisOrig, @@ -554,6 +555,7 @@ export default function transformProps( axisLabel: { formatter: xAxisFormatter, rotate: xAxisLabelRotation, + interval: xAxisLabelInterval, }, minorTick: { show: minorTicks }, minInterval: diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts index e79523d176d..a8a2bac65d6 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts @@ -61,6 +61,7 @@ export type EchartsMixedTimeseriesFormData = QueryFormData & { zoomable: boolean; richTooltip: boolean; xAxisLabelRotation: number; + xAxisLabelInterval?: number | string; colorScheme?: string; // types specific to Query A and Query B area: boolean; @@ -133,6 +134,7 @@ export const DEFAULT_FORM_DATA: EchartsMixedTimeseriesFormData = { zoomable: TIMESERIES_DEFAULTS.zoomable, richTooltip: TIMESERIES_DEFAULTS.richTooltip, xAxisLabelRotation: TIMESERIES_DEFAULTS.xAxisLabelRotation, + xAxisLabelInterval: TIMESERIES_DEFAULTS.xAxisLabelInterval, ...DEFAULT_TITLE_FORM_DATA, }; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx index daf42f6228a..aafdc860371 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx @@ -37,6 +37,7 @@ import { seriesOrderSection, percentageThresholdControl, xAxisLabelRotation, + xAxisLabelInterval, truncateXAxis, xAxisBounds, minorTicks, @@ -195,6 +196,7 @@ const config: ControlPanelConfig = { }, ], [xAxisLabelRotation], + [xAxisLabelInterval], ...richTooltipSection, // eslint-disable-next-line react/jsx-key [{t('Y Axis')}], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx index 3c61cab8093..5d480c3e096 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx @@ -38,6 +38,7 @@ import { truncateXAxis, xAxisBounds, xAxisLabelRotation, + xAxisLabelInterval, } from '../../../controls'; import { OrientationType } from '../../types'; @@ -188,6 +189,18 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] { }, }, ], + [ + { + name: xAxisLabelInterval.name, + config: { + ...xAxisLabelInterval.config, + visibility: ({ controls }: ControlPanelsContainerProps) => + isXAxis ? isVertical(controls) : isHorizontal(controls), + disableStash: true, + resetOnHide: false, + }, + }, + ], [ { name: 'y_axis_format', diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx index 8bf40c06c82..847b1645a3f 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx @@ -41,6 +41,7 @@ import { truncateXAxis, xAxisBounds, xAxisLabelRotation, + xAxisLabelInterval, } from '../../../controls'; const { @@ -183,6 +184,7 @@ const config: ControlPanelConfig = { }, ], [xAxisLabelRotation], + [xAxisLabelInterval], ...richTooltipSection, // eslint-disable-next-line react/jsx-key [{t('Y Axis')}], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx index c5bbe03ffb9..6991196cdf0 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx @@ -40,6 +40,7 @@ import { truncateXAxis, xAxisBounds, xAxisLabelRotation, + xAxisLabelInterval, } from '../../../controls'; const { @@ -126,6 +127,7 @@ const config: ControlPanelConfig = { }, ], [xAxisLabelRotation], + [xAxisLabelInterval], // eslint-disable-next-line react/jsx-key ...richTooltipSection, // eslint-disable-next-line react/jsx-key diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx index 3275fad1587..676b7278873 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx @@ -40,6 +40,7 @@ import { truncateXAxis, xAxisBounds, xAxisLabelRotation, + xAxisLabelInterval, } from '../../../controls'; const { @@ -125,6 +126,7 @@ const config: ControlPanelConfig = { }, ], [xAxisLabelRotation], + [xAxisLabelInterval], // eslint-disable-next-line react/jsx-key ...richTooltipSection, // eslint-disable-next-line react/jsx-key diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx index 5956d2efe19..6146db2572b 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx @@ -38,6 +38,7 @@ import { truncateXAxis, xAxisBounds, xAxisLabelRotation, + xAxisLabelInterval, } from '../../controls'; const { @@ -177,6 +178,7 @@ const config: ControlPanelConfig = { }, ], [xAxisLabelRotation], + [xAxisLabelInterval], ...richTooltipSection, // eslint-disable-next-line react/jsx-key [{t('Y Axis')}], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts index 32e8d876aa5..e7e3034a5f1 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts @@ -78,6 +78,7 @@ export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = { richTooltip: true, xAxisForceCategorical: false, xAxisLabelRotation: defaultXAxis.xAxisLabelRotation, + xAxisLabelInterval: defaultXAxis.xAxisLabelInterval, groupby: [], showValue: false, onlyTotal: false, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts index ac39507964d..6a7119a33c6 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -179,6 +179,7 @@ export default function transformProps( xAxisBounds, xAxisForceCategorical, xAxisLabelRotation, + xAxisLabelInterval, xAxisSort, xAxisSortAsc, xAxisTimeFormat, @@ -501,6 +502,7 @@ export default function transformProps( hideOverlap: true, formatter: xAxisFormatter, rotate: xAxisLabelRotation, + interval: xAxisLabelInterval, }, minorTick: { show: minorTicks }, minInterval: diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts index bdcb736956c..76f8b6387f9 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts @@ -90,6 +90,7 @@ export type EchartsTimeseriesFormData = QueryFormData & { zoomable: boolean; richTooltip: boolean; xAxisLabelRotation: number; + xAxisLabelInterval: number | string; showValue: boolean; onlyTotal: boolean; showExtraControls: boolean; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx index 5c52bb1762c..b6055d8688e 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx @@ -292,6 +292,23 @@ export const xAxisLabelRotation = { }, }; +export const xAxisLabelInterval = { + name: 'xAxisLabelInterval', + config: { + type: 'SelectControl', + freeForm: false, + clearable: false, + label: t('X Axis Label Interval'), + choices: [ + ['auto', t('Auto')], + ['0', t('All')], + ], + default: defaultXAxis.xAxisLabelInterval, + renderTrigger: true, + description: t('Choose how many X-Axis labels to show'), + }, +}; + export const seriesOrderSection: ControlSetRow[] = [ [{t('Series Order')}], [sortSeriesType], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/defaults.ts b/superset-frontend/plugins/plugin-chart-echarts/src/defaults.ts index be37d6fcbf7..798e785f059 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/defaults.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/defaults.ts @@ -29,6 +29,7 @@ export const defaultYAxis = { export const defaultXAxis = { xAxisLabelRotation: 0, + xAxisLabelInterval: 'auto', }; export const defaultLegendPadding = { From 59e3645c17655e6c5225710b9aaf27b6c7171a03 Mon Sep 17 00:00:00 2001 From: Zack Date: Tue, 10 Jun 2025 20:41:54 -0700 Subject: [PATCH 016/148] fix: clarify GUEST_TOKEN_JWT_AUDIENCE usage in the SDK (#33673) --- superset-embedded-sdk/README.md | 14 ++++++++++++-- superset/config.py | 5 ++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/superset-embedded-sdk/README.md b/superset-embedded-sdk/README.md index 63d8e706183..f9250fd1920 100644 --- a/superset-embedded-sdk/README.md +++ b/superset-embedded-sdk/README.md @@ -116,8 +116,11 @@ Example `POST /security/guest_token` payload: } ``` -Alternatively, a guest token can be created directly in your app with a json like the following, and then signed -with the secret set in configuration variable `GUEST_TOKEN_JWT_SECRET` (see configuration file config.py) +Alternatively, a guest token can be created directly in your app without interacting with the Superset API. +To do this, you should update the `GUEST_TOKEN_JWT_SECRET` +in the Superset [config.py](https://github.com/apache/superset/blob/master/superset/config.py). Also set the +`GUEST_TOKEN_JWT_AUDIENCE` variable that matches what is set for the `aud` in the JSON payload: + ``` { "user": { @@ -139,6 +142,13 @@ with the secret set in configuration variable `GUEST_TOKEN_JWT_SECRET` (see conf } ``` +In this example, the configuration file includes the following setting: + +```python +GUEST_TOKEN_JWT_AUDIENCE="superset" +``` + + ### Sandbox iframe The Embedded SDK creates an iframe with [sandbox](https://developer.mozilla.org/es/docs/Web/HTML/Element/iframe#sandbox) mode by default diff --git a/superset/config.py b/superset/config.py index 0711717ab17..b5dcee20065 100644 --- a/superset/config.py +++ b/superset/config.py @@ -1806,7 +1806,10 @@ GUEST_TOKEN_JWT_SECRET = "test-guest-secret-change-me" # noqa: S105 GUEST_TOKEN_JWT_ALGO = "HS256" # noqa: S105 GUEST_TOKEN_HEADER_NAME = "X-GuestToken" # noqa: S105 GUEST_TOKEN_JWT_EXP_SECONDS = 300 # 5 minutes -# Guest token audience for the embedded superset, either string or callable +# Audience for the Superset guest token used in embedded mode. +# Can be a string or a callable. Defaults to WEBDRIVER_BASEURL. +# When generating the guest token, ensure the +# payload's `aud` matches GUEST_TOKEN_JWT_AUDIENCE. GUEST_TOKEN_JWT_AUDIENCE: Callable[[], str] | str | None = None # A callable that can be supplied to do extra validation of guest token configuration From bce3d4f19ecef87ccfe16ce2fa0c5a65719fece3 Mon Sep 17 00:00:00 2001 From: Vladislav Korenkov <73882772+Quatters@users.noreply.github.com> Date: Wed, 11 Jun 2025 22:03:53 +1000 Subject: [PATCH 017/148] fix(explore): add gap to the "Cached" button (#33717) --- .../src/components/CachedLabel/index.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/superset-frontend/src/components/CachedLabel/index.tsx b/superset-frontend/src/components/CachedLabel/index.tsx index 9fd35b41543..3ac26c2f5e0 100644 --- a/superset-frontend/src/components/CachedLabel/index.tsx +++ b/superset-frontend/src/components/CachedLabel/index.tsx @@ -18,10 +18,11 @@ */ import { useState, MouseEventHandler, FC } from 'react'; -import { t } from '@superset-ui/core'; +import { css, t } from '@superset-ui/core'; import Label from 'src/components/Label'; import { Tooltip } from 'src/components/Tooltip'; import { TooltipContent } from './TooltipContent'; +import { Icons } from '../Icons'; export interface CacheLabelProps { onClick?: MouseEventHandler; @@ -44,14 +45,16 @@ const CacheLabel: FC = ({ > ); From a64b9ac84f48074340d14e5cf92505627451ec52 Mon Sep 17 00:00:00 2001 From: Pat Buxton <45275736+rad-pat@users.noreply.github.com> Date: Wed, 11 Jun 2025 13:26:30 +0100 Subject: [PATCH 018/148] fix(dataset): Fix plural toast messages (#33743) --- superset-frontend/src/components/Datasource/utils.js | 3 +++ superset-frontend/src/components/Datasource/utils.test.tsx | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/superset-frontend/src/components/Datasource/utils.js b/superset-frontend/src/components/Datasource/utils.js index dd319adbe2e..471a7a0065b 100644 --- a/superset-frontend/src/components/Datasource/utils.js +++ b/superset-frontend/src/components/Datasource/utils.js @@ -105,6 +105,7 @@ export function updateColumns(prevCols, newCols, addSuccessToast) { 'Modified 1 column in the virtual dataset', 'Modified %s columns in the virtual dataset', columnChanges.modified.length, + columnChanges.modified.length, ), ); } @@ -114,6 +115,7 @@ export function updateColumns(prevCols, newCols, addSuccessToast) { 'Removed 1 column from the virtual dataset', 'Removed %s columns from the virtual dataset', columnChanges.removed.length, + columnChanges.removed.length, ), ); } @@ -123,6 +125,7 @@ export function updateColumns(prevCols, newCols, addSuccessToast) { 'Added 1 new column to the virtual dataset', 'Added %s new columns to the virtual dataset', columnChanges.added.length, + columnChanges.added.length, ), ); } diff --git a/superset-frontend/src/components/Datasource/utils.test.tsx b/superset-frontend/src/components/Datasource/utils.test.tsx index c247f5d4a79..83b6ca99870 100644 --- a/superset-frontend/src/components/Datasource/utils.test.tsx +++ b/superset-frontend/src/components/Datasource/utils.test.tsx @@ -51,6 +51,7 @@ describe('updateColumns', () => { 'Added 1 new column to the virtual dataset', 'Added %s new columns to the virtual dataset', 2, + 2, ), ); }); @@ -88,6 +89,7 @@ describe('updateColumns', () => { 'Modified 1 column in the virtual dataset', 'Modified %s columns in the virtual dataset', 1, + 1, ), ); }); @@ -114,6 +116,7 @@ describe('updateColumns', () => { 'Removed 1 column from the virtual dataset', 'Removed %s columns from the virtual dataset', 1, + 1, ), ); }); @@ -146,6 +149,7 @@ describe('updateColumns', () => { 'Modified 1 column in the virtual dataset', 'Modified %s columns in the virtual dataset', 1, + 1, ), ], [ @@ -153,6 +157,7 @@ describe('updateColumns', () => { 'Removed 1 column from the virtual dataset', 'Removed %s columns from the virtual dataset', 1, + 1, ), ], [ @@ -160,6 +165,7 @@ describe('updateColumns', () => { 'Added 1 new column to the virtual dataset', 'Added %s new columns to the virtual dataset', 1, + 1, ), ], ]); @@ -195,6 +201,7 @@ describe('updateColumns', () => { 'Modified 1 column in the virtual dataset', 'Modified %s columns in the virtual dataset', 1, + 1, ), ], ]); From e6af4ea126058052506e5437cbe90f9c5f2192d0 Mon Sep 17 00:00:00 2001 From: Mehmet Salih Yavuz Date: Thu, 12 Jun 2025 15:38:17 +0300 Subject: [PATCH 019/148] feat(DatasourceEditor): Format sql shortcut and bigger table (#33709) --- .../Datasource/DatasourceEditor.jsx | 95 ++++++++++++++++++- .../src/components/Icons/AntdEnhanced.tsx | 2 + superset-frontend/src/database/actions.ts | 22 ++++- superset-frontend/src/database/reducers.ts | 7 ++ .../components/controls/TextAreaControl.jsx | 12 +++ 5 files changed, 132 insertions(+), 6 deletions(-) diff --git a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx index 9723e825875..4556b178d7c 100644 --- a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx +++ b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx @@ -54,8 +54,13 @@ import SpatialControl from 'src/explore/components/controls/SpatialControl'; import withToasts from 'src/components/MessageToasts/withToasts'; import { Icons } from 'src/components/Icons'; import CurrencyControl from 'src/explore/components/controls/CurrencyControl'; -import { executeQuery, resetDatabaseState } from 'src/database/actions'; +import { + executeQuery, + formatQuery, + resetDatabaseState, +} from 'src/database/actions'; import { connect } from 'react-redux'; +import Mousetrap from 'mousetrap'; import CollectionTable from './CollectionTable'; import Fieldset from './Fieldset'; import Field from './Field'; @@ -640,6 +645,7 @@ class DatasourceEditor extends PureComponent { this.setColumns = this.setColumns.bind(this); this.validateAndChange = this.validateAndChange.bind(this); this.handleTabSelect = this.handleTabSelect.bind(this); + this.formatSql = this.formatSql.bind(this); this.currencies = ensureIsArray(props.currencies).map(currencyCode => ({ value: currencyCode, label: `${getCurrencySymbol({ @@ -715,6 +721,27 @@ class DatasourceEditor extends PureComponent { }); } + async onQueryFormat() { + const { datasource } = this.state; + if (!datasource.sql || !this.state.isEditMode) { + return; + } + + try { + const response = await this.props.formatQuery(datasource.sql); + this.onDatasourcePropChange('sql', response.json.result); + this.props.addSuccessToast(t('SQL was formatted')); + } catch (error) { + const { error: clientError, statusText } = + await getClientErrorObject(error); + this.props.addDangerToast( + clientError || + statusText || + t('An error occurred while formatting SQL'), + ); + } + } + getSQLLabUrl() { const queryParams = new URLSearchParams({ dbid: this.state.datasource.database.id, @@ -738,6 +765,31 @@ class DatasourceEditor extends PureComponent { }); } + async formatSql() { + const { datasource } = this.state; + if (!datasource.sql) { + return; + } + + try { + const response = await SupersetClient.post({ + endpoint: '/api/v1/sql/format', + body: JSON.stringify({ sql: datasource.sql }), + headers: { 'Content-Type': 'application/json' }, + }); + this.onDatasourcePropChange('sql', response.json.result); + this.props.addSuccessToast(t('SQL was formatted')); + } catch (error) { + const { error: clientError, statusText } = + await getClientErrorObject(error); + this.props.addDangerToast( + clientError || + statusText || + t('An error occurred while formatting SQL'), + ); + } + } + async syncMetadata() { const { datasource } = this.state; this.setState({ metadataLoading: true }); @@ -1187,6 +1239,16 @@ class DatasourceEditor extends PureComponent { <> {this.renderSqlEditorOverlay()} { + this.onQueryFormat(); + }, + }, + ]} language="sql" offerEditInModal={false} minLines={10} @@ -1197,6 +1259,16 @@ class DatasourceEditor extends PureComponent { ) : ( { + this.onQueryFormat(); + }, + }, + ]} language="sql" offerEditInModal={false} minLines={10} @@ -1213,6 +1285,7 @@ class DatasourceEditor extends PureComponent { right: 0; top: 0; z-index: 2; + display: flex; `} > ))} + {/* accessibility begin */} +

+ {t( + '%s tab selected', + options.find(([val]) => val === currentValue)?.[1], + )} +
+ {/* accessibility end */} ); } diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.tsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.tsx index d372c8fb07d..a9b4443e344 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.tsx @@ -129,7 +129,7 @@ test('table should be visible when expanded is true', async () => { name: 'Select database or type to search databases', }); const schemaSelect = getByRole('combobox', { - name: 'Select schema or type to search schemas', + name: 'Select schema or type to search schemas: main', }); const tableSelect = getAllByLabelText( /Select table or type to search tables/i, diff --git a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx index 1adb20ad807..1b3c5655093 100644 --- a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx +++ b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx @@ -211,7 +211,7 @@ test('Refresh should work', async () => { expect(fetchMock.calls(schemaApiRoute).length).toBe(0); const select = screen.getByRole('combobox', { - name: 'Select schema or type to search schemas', + name: 'Select schema or type to search schemas: public', }); userEvent.click(select); @@ -324,7 +324,7 @@ test('Should schema select display options', async () => { const props = createProps(); render(, { useRedux: true, store }); const select = screen.getByRole('combobox', { - name: 'Select schema or type to search schemas', + name: 'Select schema or type to search schemas: public', }); expect(select).toBeInTheDocument(); userEvent.click(select); @@ -370,7 +370,7 @@ test('Sends the correct schema when changing the schema', async () => { rerender(); expect(props.onSchemaChange).toHaveBeenCalledTimes(0); const select = screen.getByRole('combobox', { - name: 'Select schema or type to search schemas', + name: 'Select schema or type to search schemas: public', }); expect(select).toBeInTheDocument(); userEvent.click(select); diff --git a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx index 4556b178d7c..2f1d021ac6f 100644 --- a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx +++ b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx @@ -429,6 +429,10 @@ function ColumnCollectionTable({ ).is_dttm; return ( ; } interface StyledModalProps { @@ -276,22 +277,34 @@ const CustomModal = ({ resizableConfig = defaultResizableConfig(hideFooter), draggableConfig, destroyOnClose, + openerRef, ...rest }: ModalProps) => { const draggableRef = useRef(null); const [bounds, setBounds] = useState(); const [dragDisabled, setDragDisabled] = useState(true); + + const handleOnHide = () => { + openerRef?.current?.focus(); + onHide(); + }; + let FooterComponent; if (isValidElement(footer)) { // If a footer component is provided inject a closeModal function // so the footer can provide a "close" button if desired FooterComponent = cloneElement(footer, { - closeModal: onHide, + closeModal: handleOnHide, } as Partial); } const modalFooter = isNil(FooterComponent) ? [ - ,