diff --git a/CHANGELOG.md b/CHANGELOG.md
index 55656a16ab6ac..3e150d476b76e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ under the License.
## Change Log
+- [3.0.1](#301-tue-oct-13-103221-2023--0700)
- [3.0.0](#300-thu-aug-24-133627-2023--0600)
- [2.1.1](#211-sun-apr-23-154421-2023-0100)
- [2.1.0](#210-thu-mar-16-211305-2023--0700)
@@ -31,6 +32,51 @@ under the License.
- [1.4.2](#142-sat-mar-19-000806-2022-0200)
- [1.4.1](#141)
+### 3.0.1 (Tue Oct 13 10:32:21 2023 -0700)
+
+**Database Migrations**
+
+- [#25320](https://github.com/apache/superset/pull/25320) fix: Add explicit ON DELETE CASCADE for dashboard_roles (@john-bodley)
+
+**Fixes**
+
+- [#25541](https://github.com/apache/superset/pull/25541) fix: revert fix(sqllab): Force trino client async execution (#24859) (@villebro)
+- [#25618](https://github.com/apache/superset/pull/25618) fix: finestTemporalGrainFormatter (@betodealmeida)
+- [#25599](https://github.com/apache/superset/pull/25599) fix(window): unavailable localStorage and sessionStorage (@frassinier)
+- [#25579](https://github.com/apache/superset/pull/25579) fix(Charts): Set max row limit + removed the option to use an empty row limit value (@CorbinBullard)
+- [#25559](https://github.com/apache/superset/pull/25559) fix(Presto): catch DatabaseError when testing Presto views (@zhaorui2022)
+- [#25486](https://github.com/apache/superset/pull/25486) fix: thubmnails loading - Talisman default config (@Khrol)
+- [#25400](https://github.com/apache/superset/pull/25400) fix(RLS): Fix Info Tooltip + Button Alignment on RLS Modal (@CorbinBullard)
+- [#25590](https://github.com/apache/superset/pull/25590) fix: REST API CSRF exempt list (@dpgaspar)
+- [#25147](https://github.com/apache/superset/pull/25147) fix: Apply normalization to all dttm columns (@kgabryje)
+- [#25516](https://github.com/apache/superset/pull/25516) fix: tags permissions error message (@Khrol)
+- [#25519](https://github.com/apache/superset/pull/25519) fix: Expand error detail on screencapture (@justinpark)
+- [#25490](https://github.com/apache/superset/pull/25490) fix(sqllab): Broken query containing 'children' (@justinpark)
+- [#25390](https://github.com/apache/superset/pull/25390) fix: Unable to sync columns when database or dataset name contains `+` (@mapledan)
+- [#25494](https://github.com/apache/superset/pull/25494) fix: Address Mypy issue which is causing CI to fail (@john-bodley)
+- [#25469](https://github.com/apache/superset/pull/25469) fix(sqllab): error with lazy_gettext for tab titles (@nytai)
+- [#25468](https://github.com/apache/superset/pull/25468) fix: Styles not loading because of faulty CSP setting (@kgabryje)
+- [#24241](https://github.com/apache/superset/pull/24241) fix(mysql): handle string typed decimal results (@villebro)
+- [#25373](https://github.com/apache/superset/pull/25373) fix(helm chart): set chart appVersion to 3.0.0 (@celalettin1286)
+- [#25445](https://github.com/apache/superset/pull/25445) fix: update the SQLAlchemy model definition at json column for Log table (@cnabro)
+- [#25447](https://github.com/apache/superset/pull/25447) fix: Duplicate items when pasting into Select (@michael-s-molina)
+- [#25372](https://github.com/apache/superset/pull/25372) fix(SqlLab): make icon placement even (@CorbinBullard)
+- [#25282](https://github.com/apache/superset/pull/25282) fix(nativeFilters): Speed up native filters by removing unnecessary rerenders (@Always-prog)
+- [#25437](https://github.com/apache/superset/pull/25437) fix(sqllab): invalid start date (@justinpark)
+- [#25404](https://github.com/apache/superset/pull/25404) fix: smarter date formatter (@betodealmeida)
+- [#25368](https://github.com/apache/superset/pull/25368) fix: swagger UI CSP error (@dpgaspar)
+- [#25425](https://github.com/apache/superset/pull/25425) fix: chart import (@betodealmeida)
+- [#25106](https://github.com/apache/superset/pull/25106) fix: preventing save button from flickering in SQL Lab (@fisjac)
+- [#25424](https://github.com/apache/superset/pull/25424) fix: Rename on_delete parameter to ondelete (@john-bodley)
+- [#25398](https://github.com/apache/superset/pull/25398) fix(sqllab): invalid persisted tab state (#25308) (@justinpark)
+- [#25399](https://github.com/apache/superset/pull/25399) fix: Workaround for Cypress ECONNRESET error (@michael-s-molina)
+- [#25318](https://github.com/apache/superset/pull/25318) fix: datetime with timezone excel export (@betodealmeida)
+- [#25349](https://github.com/apache/superset/pull/25349) fix: DashboardRoles cascade operation (@michael-s-molina)
+- [#25239](https://github.com/apache/superset/pull/25239) fix: Improve the reliability of alerts & reports (@jfrag1)
+- [#25229](https://github.com/apache/superset/pull/25229) fix: Use RLS clause instead of ID for cache key (@jfrag1)
+- [#25126](https://github.com/apache/superset/pull/25126) fix(chart): Supporting custom SQL as temporal x-axis column with filter (@zephyring)
+- [#25290](https://github.com/apache/superset/pull/25290) fix: is_select with UNION (@betodealmeida)
+
### 3.0.0 (Thu Aug 24 13:36:27 2023 -0600)
**Database Migrations**
diff --git a/helm/superset/Chart.yaml b/helm/superset/Chart.yaml
index 8af3ae4acf579..2aa2bc49a3178 100644
--- a/helm/superset/Chart.yaml
+++ b/helm/superset/Chart.yaml
@@ -15,7 +15,7 @@
# limitations under the License.
#
apiVersion: v2
-appVersion: "2.1.0"
+appVersion: "3.0.0"
description: Apache Superset is a modern, enterprise-ready business intelligence web application
name: superset
icon: https://artifacthub.io/image/68c1d717-0e97-491f-b046-754e46f46922@2x
@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
-version: 0.10.6
+version: 0.10.9
dependencies:
- name: postgresql
version: 12.1.6
diff --git a/helm/superset/README.md b/helm/superset/README.md
index c3a46fbec4d6e..38c69c38b524d 100644
--- a/helm/superset/README.md
+++ b/helm/superset/README.md
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
-![Version: 0.10.6](https://img.shields.io/badge/Version-0.10.6-informational?style=flat-square)
+![Version: 0.10.9](https://img.shields.io/badge/Version-0.10.9-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application
diff --git a/requirements/base.txt b/requirements/base.txt
index 1a971fdab4910..d6ee2e6a6b9ef 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -88,7 +88,7 @@ flask==2.2.5
# flask-migrate
# flask-sqlalchemy
# flask-wtf
-flask-appbuilder==4.3.6
+flask-appbuilder==4.3.7
# via apache-superset
flask-babel==1.0.0
# via flask-appbuilder
diff --git a/setup.py b/setup.py
index 060ea19732b9b..3cb0c144b2f58 100644
--- a/setup.py
+++ b/setup.py
@@ -80,7 +80,7 @@ def get_git_sha() -> str:
"cryptography>=39.0.1, <40",
"deprecation>=2.1.0, <2.2.0",
"flask>=2.2.5, <3.0.0",
- "flask-appbuilder>=4.3.6, <5.0.0",
+ "flask-appbuilder>=4.3.7, <5.0.0",
"flask-caching>=1.11.1, <2.0",
"flask-compress>=1.13, <2.0",
"flask-talisman>=1.0.0, <2.0",
diff --git a/superset-frontend/cypress-base/cypress.config.ts b/superset-frontend/cypress-base/cypress.config.ts
index 1d2c3baf49600..7340830bf0016 100644
--- a/superset-frontend/cypress-base/cypress.config.ts
+++ b/superset-frontend/cypress-base/cypress.config.ts
@@ -38,6 +38,23 @@ export default defineConfig({
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
setupNodeEvents(on, config) {
+ // ECONNRESET on Chrome/Chromium 117.0.5851.0 when using Cypress <12.15.0
+ // Check https://github.com/cypress-io/cypress/issues/27804 for context
+ // TODO: This workaround should be removed when upgrading Cypress
+ on('before:browser:launch', (browser, launchOptions) => {
+ if (browser.name === 'chrome' && browser.isHeadless) {
+ // eslint-disable-next-line no-param-reassign
+ launchOptions.args = launchOptions.args.map(arg => {
+ if (arg === '--headless') {
+ return '--headless=new';
+ }
+
+ return arg;
+ });
+ }
+ return launchOptions;
+ });
+
// eslint-disable-next-line global-require,import/extensions
return require('./cypress/plugins/index.js')(on, config);
},
diff --git a/superset-frontend/jest.config.js b/superset-frontend/jest.config.js
index 24e4886ecda43..316102c5c20ff 100644
--- a/superset-frontend/jest.config.js
+++ b/superset-frontend/jest.config.js
@@ -17,6 +17,9 @@
* under the License.
*/
+// timezone for unit tests
+process.env.TZ = 'America/New_York';
+
module.exports = {
testRegex:
'\\/superset-frontend\\/(spec|src|plugins|packages|tools)\\/.*(_spec|\\.test)\\.[jt]sx?$',
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index ca68f91dee904..0bd3e5509444a 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "superset",
- "version": "3.0.0",
+ "version": "3.0.1",
"description": "Superset is a data exploration platform designed to be visual, intuitive, and interactive.",
"keywords": [
"big",
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
index abf5153bb0d51..69fa8a6864909 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
@@ -47,6 +47,8 @@ import {
isDefined,
hasGenericChartAxes,
NO_TIME_RANGE,
+ validateNonEmpty,
+ validateMaxValue,
} from '@superset-ui/core';
import {
@@ -245,7 +247,12 @@ const row_limit: SharedControlConfig<'SelectControl'> = {
type: 'SelectControl',
freeForm: true,
label: t('Row limit'),
- validators: [legacyValidateInteger],
+ clearable: false,
+ validators: [
+ validateNonEmpty,
+ legacyValidateInteger,
+ v => validateMaxValue(v, 100000),
+ ],
default: 10000,
choices: formatSelectOptions(ROW_LIMIT_OPTIONS),
description: t('Limits the number of rows that get displayed.'),
diff --git a/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts b/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts
index b3884a8488013..1c4d278f6cc46 100644
--- a/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts
+++ b/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts
@@ -58,6 +58,7 @@ export enum AppSection {
export type FilterState = { value?: any; [key: string]: any };
export type DataMask = {
+ __cache?: FilterState;
extraFormData?: ExtraFormData;
filterState?: FilterState;
ownState?: JsonObject;
diff --git a/superset-frontend/packages/superset-ui-core/src/time-format/formatters/finestTemporalGrain.test.ts b/superset-frontend/packages/superset-ui-core/src/time-format/formatters/finestTemporalGrain.test.ts
new file mode 100644
index 0000000000000..6e4f07df4b8bf
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/src/time-format/formatters/finestTemporalGrain.test.ts
@@ -0,0 +1,63 @@
+/*
+ * 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 finestTemporalGrain from './finestTemporalGrain';
+
+test('finestTemporalGrain', () => {
+ const monthFormatter = finestTemporalGrain([
+ new Date('2003-01-01 00:00:00Z').getTime(),
+ new Date('2003-02-01 00:00:00Z').getTime(),
+ ]);
+ expect(monthFormatter(new Date('2003-01-01 00:00:00Z').getTime())).toBe(
+ '2003-01-01',
+ );
+ expect(monthFormatter(new Date('2003-02-01 00:00:00Z').getTime())).toBe(
+ '2003-02-01',
+ );
+
+ const yearFormatter = finestTemporalGrain([
+ new Date('2003-01-01 00:00:00Z').getTime(),
+ new Date('2004-01-01 00:00:00Z').getTime(),
+ ]);
+ expect(yearFormatter(new Date('2003-01-01 00:00:00Z').getTime())).toBe(
+ '2003',
+ );
+ expect(yearFormatter(new Date('2004-01-01 00:00:00Z').getTime())).toBe(
+ '2004',
+ );
+
+ const milliSecondFormatter = finestTemporalGrain([
+ new Date('2003-01-01 00:00:00Z').getTime(),
+ new Date('2003-04-05 06:07:08.123Z').getTime(),
+ ]);
+ expect(milliSecondFormatter(new Date('2003-01-01 00:00:00Z').getTime())).toBe(
+ '2003-01-01 00:00:00.000',
+ );
+
+ const localTimeFormatter = finestTemporalGrain(
+ [
+ new Date('2003-01-01 00:00:00Z').getTime(),
+ new Date('2003-02-01 00:00:00Z').getTime(),
+ ],
+ true,
+ );
+ expect(localTimeFormatter(new Date('2003-01-01 00:00:00Z').getTime())).toBe(
+ '2002-12-31 19:00',
+ );
+});
diff --git a/superset-frontend/packages/superset-ui-core/src/time-format/formatters/finestTemporalGrain.ts b/superset-frontend/packages/superset-ui-core/src/time-format/formatters/finestTemporalGrain.ts
new file mode 100644
index 0000000000000..c03b7ec1593cf
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/src/time-format/formatters/finestTemporalGrain.ts
@@ -0,0 +1,80 @@
+/*
+ * 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 { utcFormat, timeFormat } from 'd3-time-format';
+import { utcUtils, localTimeUtils } from '../utils/d3Time';
+import TimeFormatter from '../TimeFormatter';
+
+/*
+ * A formatter that examines all the values, and uses the finest temporal grain.
+ */
+export default function finestTemporalGrain(
+ values: any[],
+ useLocalTime = false,
+) {
+ const format = useLocalTime ? timeFormat : utcFormat;
+
+ const formatMillisecond = format('%Y-%m-%d %H:%M:%S.%L');
+ const formatSecond = format('%Y-%m-%d %H:%M:%S');
+ const formatMinute = format('%Y-%m-%d %H:%M');
+ const formatHour = format('%Y-%m-%d %H:%M');
+ const formatDay = format('%Y-%m-%d');
+ const formatMonth = format('%Y-%m-%d');
+ const formatYear = format('%Y');
+
+ const {
+ hasMillisecond,
+ hasSecond,
+ hasMinute,
+ hasHour,
+ isNotFirstDayOfMonth,
+ isNotFirstMonth,
+ } = useLocalTime ? localTimeUtils : utcUtils;
+
+ let formatFunc = formatYear;
+ values.forEach((value: any) => {
+ if (formatFunc === formatYear && isNotFirstMonth(value)) {
+ formatFunc = formatMonth;
+ }
+ if (formatFunc === formatMonth && isNotFirstDayOfMonth(value)) {
+ formatFunc = formatDay;
+ }
+ if (formatFunc === formatDay && hasHour(value)) {
+ formatFunc = formatHour;
+ }
+ if (formatFunc === formatHour && hasMinute(value)) {
+ formatFunc = formatMinute;
+ }
+ if (formatFunc === formatMinute && hasSecond(value)) {
+ formatFunc = formatSecond;
+ }
+ if (formatFunc === formatSecond && hasMillisecond(value)) {
+ formatFunc = formatMillisecond;
+ }
+ });
+
+ return new TimeFormatter({
+ description:
+ 'Use the finest grain in an array of dates to format all dates in the array',
+ formatFunc,
+ id: 'finest_temporal_grain',
+ label: 'Format temporal columns with the finest grain',
+ useLocalTime,
+ });
+}
diff --git a/superset-frontend/packages/superset-ui-core/src/time-format/index.ts b/superset-frontend/packages/superset-ui-core/src/time-format/index.ts
index 53f23f36431cf..b0d95c1433940 100644
--- a/superset-frontend/packages/superset-ui-core/src/time-format/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/time-format/index.ts
@@ -35,6 +35,7 @@ export { default as createMultiFormatter } from './factories/createMultiFormatte
export { default as smartDateFormatter } from './formatters/smartDate';
export { default as smartDateDetailedFormatter } from './formatters/smartDateDetailed';
export { default as smartDateVerboseFormatter } from './formatters/smartDateVerbose';
+export { default as finestTemporalGrainFormatter } from './formatters/finestTemporalGrain';
export { default as normalizeTimestamp } from './utils/normalizeTimestamp';
export { default as denormalizeTimestamp } from './utils/denormalizeTimestamp';
diff --git a/superset-frontend/packages/superset-ui-core/src/validator/index.ts b/superset-frontend/packages/superset-ui-core/src/validator/index.ts
index 532efcc959116..fb37328c02290 100644
--- a/superset-frontend/packages/superset-ui-core/src/validator/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/validator/index.ts
@@ -22,3 +22,4 @@ export { default as legacyValidateNumber } from './legacyValidateNumber';
export { default as validateInteger } from './validateInteger';
export { default as validateNumber } from './validateNumber';
export { default as validateNonEmpty } from './validateNonEmpty';
+export { default as validateMaxValue } from './validateMaxValue';
diff --git a/superset-frontend/packages/superset-ui-core/src/validator/validateMaxValue.ts b/superset-frontend/packages/superset-ui-core/src/validator/validateMaxValue.ts
new file mode 100644
index 0000000000000..24c1da1c79dde
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/src/validator/validateMaxValue.ts
@@ -0,0 +1,8 @@
+import { t } from '../translation';
+
+export default function validateMaxValue(v: unknown, max: Number) {
+ if (Number(v) > +max) {
+ return t('Value cannot exceed %s', max);
+ }
+ return false;
+}
diff --git a/superset-frontend/packages/superset-ui-core/test/validator/validateMaxValue.test.ts b/superset-frontend/packages/superset-ui-core/test/validator/validateMaxValue.test.ts
new file mode 100644
index 0000000000000..70f3d332c52e3
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/test/validator/validateMaxValue.test.ts
@@ -0,0 +1,38 @@
+/*
+ * 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 { validateMaxValue } from '@superset-ui/core';
+import './setup';
+
+describe('validateInteger()', () => {
+ it('returns the warning message if invalid', () => {
+ expect(validateMaxValue(10.1, 10)).toBeTruthy();
+ expect(validateMaxValue(1, 0)).toBeTruthy();
+ expect(validateMaxValue('2', 1)).toBeTruthy();
+ });
+ it('returns false if the input is valid', () => {
+ expect(validateMaxValue(0, 1)).toBeFalsy();
+ expect(validateMaxValue(10, 10)).toBeFalsy();
+ expect(validateMaxValue(undefined, 1)).toBeFalsy();
+ expect(validateMaxValue(NaN, NaN)).toBeFalsy();
+ expect(validateMaxValue(null, 1)).toBeFalsy();
+ expect(validateMaxValue('1', 1)).toBeFalsy();
+ expect(validateMaxValue('a', 1)).toBeFalsy();
+ });
+});
diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ReactParallelCoordinates.jsx b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ReactParallelCoordinates.jsx
index 4a7675d555cd6..7f30716057604 100644
--- a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ReactParallelCoordinates.jsx
+++ b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ReactParallelCoordinates.jsx
@@ -106,7 +106,7 @@ export default styled(ParallelCoordinates)`
height: 18px;
margin: 0px;
}
- .parcoords .row:nth-child(odd) {
+ .parcoords .row:nth-of-type(odd) {
background: ${addAlpha(theme.colors.grayscale.dark2, 0.05)};
}
.parcoords .header {
diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/ReactNVD3.jsx b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/ReactNVD3.jsx
index 9a9962e8aaca4..f7f219a05d219 100644
--- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/ReactNVD3.jsx
+++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/ReactNVD3.jsx
@@ -152,7 +152,7 @@ export default styled(NVD3)`
white-space: nowrap;
font-weight: ${({ theme }) => theme.typography.weights.bold};
}
- tbody tr:not(.tooltip-header) td:nth-child(2) {
+ tbody tr:not(.tooltip-header) td:nth-of-type(2) {
word-break: break-word;
}
}
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.js
index 5d9ecdacdf6c3..fbfba6783e8e4 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.js
@@ -596,7 +596,12 @@ export function addNewQueryEditor() {
'-- Note: Unless you save your query, these tabs will NOT persist if you clear your cookies or change browsers.\n\n',
);
- const name = newQueryTabName(queryEditors || []);
+ const name = newQueryTabName(
+ queryEditors?.map(qe => ({
+ ...qe,
+ ...(qe.id === unsavedQueryEditor.id && unsavedQueryEditor),
+ })) || [],
+ );
return dispatch(
addQueryEditor({
@@ -614,10 +619,12 @@ export function addNewQueryEditor() {
export function cloneQueryToNewTab(query, autorun) {
return function (dispatch, getState) {
const state = getState();
- const { queryEditors, tabHistory } = state.sqlLab;
- const sourceQueryEditor = queryEditors.find(
- qe => qe.id === tabHistory[tabHistory.length - 1],
- );
+ const { queryEditors, unsavedQueryEditor, tabHistory } = state.sqlLab;
+ const sourceQueryEditor = {
+ ...queryEditors.find(qe => qe.id === tabHistory[tabHistory.length - 1]),
+ ...(tabHistory[tabHistory.length - 1] === unsavedQueryEditor.id &&
+ unsavedQueryEditor),
+ };
const queryEditor = {
name: t('Copy of %s', sourceQueryEditor.name),
dbId: query.dbId ? query.dbId : null,
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.js b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
index fc94a44645c7e..25f80aa1c386a 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.test.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
@@ -389,8 +389,11 @@ describe('async actions', () => {
const state = {
sqlLab: {
tabHistory: [id],
- queryEditors: [{ id, name: 'Dummy query editor' }],
- unsavedQueryEditor: {},
+ queryEditors: [{ id, name: 'out of updated title' }],
+ unsavedQueryEditor: {
+ id,
+ name: 'Dummy query editor',
+ },
},
};
const store = mockStore(state);
@@ -444,16 +447,23 @@ describe('async actions', () => {
describe('addNewQueryEditor', () => {
it('creates new query editor with new tab name', () => {
- const store = mockStore(initialState);
+ const store = mockStore({
+ ...initialState,
+ sqlLab: {
+ ...initialState.sqlLab,
+ unsavedQueryEditor: {
+ id: defaultQueryEditor.id,
+ name: 'Untitled Query 6',
+ },
+ },
+ });
const expectedActions = [
{
type: actions.ADD_QUERY_EDITOR,
queryEditor: {
id: 'abcd',
sql: expect.stringContaining('SELECT ...'),
- name: `Untitled Query ${
- store.getState().sqlLab.queryEditors.length + 1
- }`,
+ name: `Untitled Query 7`,
dbId: defaultQueryEditor.dbId,
schema: defaultQueryEditor.schema,
autorun: false,
diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx b/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx
index dbb25b138a58e..79a3bf0b8ee9c 100644
--- a/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx
@@ -44,7 +44,7 @@ const SaveDatasetActionButton = ({
font-weight: ${theme.gridUnit * 150};
background-color: ${theme.colors.primary.light4};
color: ${theme.colors.primary.dark1};
- &:nth-child(2) {
+ &:nth-of-type(2) {
&:before,
&:hover:before {
border-left: 2px solid ${theme.colors.primary.dark2};
diff --git a/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.tsx b/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.tsx
index f321a54ec4dbe..54b81df96013d 100644
--- a/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.tsx
@@ -27,7 +27,7 @@ import { initialState, databases } from 'src/SqlLab/fixtures';
const mockedProps = {
queryEditorId: '123',
animation: false,
- database: databases.result[0],
+ database: { ...databases.result[0], allows_virtual_table_explore: false },
onUpdate: () => {},
onSave: () => {},
saveQueryWarning: null,
@@ -61,6 +61,25 @@ const middlewares = [thunk];
const mockStore = configureStore(middlewares);
describe('SavedQuery', () => {
+ it('doesnt render save button when allows_virtual_table_explore is undefined', async () => {
+ const noRenderProps = {
+ ...mockedProps,
+ database: {
+ ...mockedProps.database,
+ allows_virtual_table_explore: undefined,
+ },
+ };
+ render(, {
+ useRedux: true,
+ store: mockStore(mockState),
+ });
+ expect(() => {
+ screen.getByRole('button', { name: /save/i });
+ }).toThrow(
+ 'Unable to find an accessible element with the role "button" and name `/save/i`',
+ );
+ });
+
it('renders a non-split save button when allows_virtual_table_explore is not enabled', () => {
render(, {
useRedux: true,
diff --git a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
index 4071b9e2d71d4..6ed0f4c668e7e 100644
--- a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
@@ -98,6 +98,8 @@ const SaveQuery = ({
const [showSaveDatasetModal, setShowSaveDatasetModal] = useState(false);
const isSaved = !!query.remoteId;
const canExploreDatabase = !!database?.allows_virtual_table_explore;
+ const shouldShowSaveButton =
+ database?.allows_virtual_table_explore !== undefined;
const overlayMenu = (