Compare commits

..

15 Commits

Author SHA1 Message Date
Michael S. Molina
961eba6d97 chore: Updates CHANGELOG.md for 3.0.2 (rc2) 2023-11-20 16:40:41 -03:00
Daniel Vaz Gaspar
08604cc686 fix: update FAB to 4.3.10, Azure user info fix (#26037)
(cherry picked from commit 628cd345f2)
2023-11-20 16:27:34 -03:00
JUST.in DO IT
49661bcc59 fix(native filters): rendering performance improvement by reduce overrendering (#25901)
(cherry picked from commit e1d73d5420)
2023-11-20 16:19:24 -03:00
John Bodley
da06206ea6 chore: Optimize fetching samples logic (#25995)
(cherry picked from commit 326ac4a6c4)
2023-11-20 16:19:24 -03:00
JUST.in DO IT
e3fbb01bb8 fix(explore): redandant force param (#25985)
(cherry picked from commit e7a1876807)
2023-11-20 16:19:24 -03:00
Jack Fragassi
ee1ba7e172 fix: Make Select component fire onChange listener when a selection is pasted in (#25993)
(cherry picked from commit 5fccf67cdc)
2023-11-16 18:30:35 -03:00
yousoph
8d873e6da6 fix(rls): Update text from tables to datasets in RLS modal (#25997)
(cherry picked from commit 210f1f8f95)
2023-11-16 17:28:25 -03:00
josedev-union
1d2a564d4e fix(helm): Restart all related deployments when bootstrap script changed (#25703) 2023-11-16 17:28:08 -03:00
Hugh A. Miles II
1c287dfc74 fix: naming denomalized to denormalized in helpers.py (#25973)
(cherry picked from commit 5def416f63)
2023-11-16 17:26:51 -03:00
John Bodley
fb1919a483 chore(colors): Updating Airbnb brand colors (#23619)
(cherry picked from commit 6d8424c104)
2023-11-16 17:26:51 -03:00
Hugh A. Miles II
e07eed10a2 fix: always denorm column value before querying values (#25919) 2023-11-16 17:26:15 -03:00
Giacomo Barone
a7fbdd607a fix: update flask-caching to avoid breaking redis cache, solves #25339 (#25947)
Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com>
2023-11-16 17:22:25 -03:00
JUST.in DO IT
6da18f8451 fix(sqllab): invalid sanitization on comparison symbol (#25903)
(cherry picked from commit 581d3c7108)
2023-11-16 17:21:04 -03:00
John Bodley
eea6a8ed4f fix(table): Double percenting ad-hoc percentage metrics (#25857)
(cherry picked from commit 784a478268)
2023-11-16 17:21:04 -03:00
FGrobelny
078b78f30b fix(trino): allow impersonate_user flag to be imported (#25872)
Co-authored-by: John Bodley <4567245+john-bodley@users.noreply.github.com>
(cherry picked from commit 458be8c848)
2023-11-16 17:21:04 -03:00
41 changed files with 335 additions and 278 deletions

View File

@@ -19,7 +19,7 @@ under the License.
## Change Log
- [3.0.2](#302-wed-nov-8-073838-2023--0500)
- [3.0.2](#302-mon-nov-20-073838-2023--0500)
- [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)
@@ -33,10 +33,22 @@ under the License.
- [1.4.2](#142-sat-mar-19-000806-2022-0200)
- [1.4.1](#141)
### 3.0.2 (Wed Nov 8 07:38:38 2023 -0500)
### 3.0.2 (Mon Nov 20 07:38:38 2023 -0500)
**Fixes**
- [#26037](https://github.com/apache/superset/pull/26037) fix: update FAB to 4.3.10, Azure user info fix (@dpgaspar)
- [#25901](https://github.com/apache/superset/pull/25901) fix(native filters): rendering performance improvement by reduce overrendering (@justinpark)
- [#25985](https://github.com/apache/superset/pull/25985) fix(explore): redandant force param (@justinpark)
- [#25993](https://github.com/apache/superset/pull/25993) fix: Make Select component fire onChange listener when a selection is pasted in (@jfrag1)
- [#25997](https://github.com/apache/superset/pull/25997) fix(rls): Update text from tables to datasets in RLS modal (@yousoph)
- [#25703](https://github.com/apache/superset/pull/25703) fix(helm): Restart all related deployments when bootstrap script changed (@josedev-union)
- [#25973](https://github.com/apache/superset/pull/25973) fix: naming denomalized to denormalized in helpers.py (@hughhhh)
- [#25919](https://github.com/apache/superset/pull/25919) fix: always denorm column value before querying values (@hughhhh)
- [#25947](https://github.com/apache/superset/pull/25947) fix: update flask-caching to avoid breaking redis cache, solves #25339 (@ggbaro)
- [#25903](https://github.com/apache/superset/pull/25903) fix(sqllab): invalid sanitization on comparison symbol (@justinpark)
- [#25857](https://github.com/apache/superset/pull/25857) fix(table): Double percenting ad-hoc percentage metrics (@john-bodley)
- [#25872](https://github.com/apache/superset/pull/25872) fix(trino): allow impersonate_user flag to be imported (@FGrobelny)
- [#25897](https://github.com/apache/superset/pull/25897) fix: trino cursor (@betodealmeida)
- [#25898](https://github.com/apache/superset/pull/25898) fix: database version field (@betodealmeida)
- [#25877](https://github.com/apache/superset/pull/25877) fix: Saving Mixed Chart with dashboard filter applied breaks adhoc_filter_b (@kgabryje)
@@ -69,6 +81,11 @@ under the License.
- [#25626](https://github.com/apache/superset/pull/25626) fix(sqllab): template validation error within comments (@justinpark)
- [#25523](https://github.com/apache/superset/pull/25523) fix(sqllab): Mistitled for new tab after rename (@justinpark)
**Others**
- [#25995](https://github.com/apache/superset/pull/25995) chore: Optimize fetching samples logic (@john-bodley)
- [#23619](https://github.com/apache/superset/pull/23619) chore(colors): Updating Airbnb brand colors (@john-bodley)
### 3.0.1 (Tue Oct 13 10:32:21 2023 -0700)
**Database Migrations**

View File

@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
version: 0.10.10
version: 0.10.15
dependencies:
- name: postgresql
version: 12.1.6

View File

@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
![Version: 0.10.10](https://img.shields.io/badge/Version-0.10.10-informational?style=flat-square)
![Version: 0.10.15](https://img.shields.io/badge/Version-0.10.15-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application

View File

@@ -42,6 +42,7 @@ spec:
metadata:
annotations:
checksum/superset_config.py: {{ include "superset-config" . | sha256sum }}
checksum/superset_bootstrap.sh: {{ tpl .Values.bootstrapScript . | sha256sum }}
checksum/connections: {{ .Values.supersetNode.connections | toYaml | sha256sum }}
checksum/extraConfigs: {{ .Values.extraConfigs | toYaml | sha256sum }}
checksum/extraSecrets: {{ .Values.extraSecrets | toYaml | sha256sum }}

View File

@@ -46,6 +46,7 @@ spec:
metadata:
annotations:
checksum/superset_config.py: {{ include "superset-config" . | sha256sum }}
checksum/superset_bootstrap.sh: {{ tpl .Values.bootstrapScript . | sha256sum }}
checksum/connections: {{ .Values.supersetNode.connections | toYaml | sha256sum }}
checksum/extraConfigs: {{ .Values.extraConfigs | toYaml | sha256sum }}
checksum/extraSecrets: {{ .Values.extraSecrets | toYaml | sha256sum }}

View File

@@ -27,8 +27,9 @@ billiard==3.6.4.0
# via celery
brotli==1.0.9
# via flask-compress
cachelib==0.6.0
# via flask-caching
cachelib==0.9.0
# via
# flask-caching
celery==5.2.2
# via apache-superset
cffi==1.15.1
@@ -88,11 +89,11 @@ flask==2.2.5
# flask-migrate
# flask-sqlalchemy
# flask-wtf
flask-appbuilder==4.3.9
flask-appbuilder==4.3.10
# via apache-superset
flask-babel==1.0.0
# via flask-appbuilder
flask-caching==1.11.1
flask-caching==2.1.0
# via apache-superset
flask-compress==1.13
# via apache-superset

View File

@@ -80,8 +80,8 @@ setup(
"cryptography>=39.0.1, <40",
"deprecation>=2.1.0, <2.2.0",
"flask>=2.2.5, <3.0.0",
"flask-appbuilder>=4.3.9, <5.0.0",
"flask-caching>=1.11.1, <2.0",
"flask-appbuilder>=4.3.10, <5.0.0",
"flask-caching>=2.1.0, <3",
"flask-compress>=1.13, <2.0",
"flask-talisman>=1.0.0, <2.0",
"flask-login>=0.6.0, < 1.0",

View File

@@ -515,7 +515,7 @@ describe('Dashboard edit', () => {
// label Anthony
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(2)
.should('have.css', 'fill', 'rgb(0, 122, 135)');
.should('have.css', 'fill', 'rgb(244, 176, 42)');
// open main tab and nested tab
openTab(0, 0);
@@ -526,7 +526,7 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(0, 122, 135)');
.should('have.css', 'fill', 'rgb(244, 176, 42)');
});
it('should apply the color scheme across main tabs', () => {
@@ -557,7 +557,7 @@ describe('Dashboard edit', () => {
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(204, 0, 134)');
.should('have.css', 'fill', 'rgb(156, 52, 152)');
// change scheme now that charts are rendered across the main tabs
editDashboard();

View File

@@ -89,6 +89,6 @@ describe('Visualization > Distribution bar chart', () => {
).should('exist');
cy.get('.dist_bar .nv-legend .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(255, 90, 95)');
.should('have.css', 'fill', 'rgb(41, 105, 107)');
});
});

View File

@@ -85,7 +85,7 @@ describe('Visualization > Line', () => {
).should('exist');
cy.get('.line .nv-legend .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(255, 90, 95)');
.should('have.css', 'fill', 'rgb(41, 105, 107)');
});
it('should work with adhoc metric', () => {

View File

@@ -58,7 +58,6 @@ export enum AppSection {
export type FilterState = { value?: any; [key: string]: any };
export type DataMask = {
__cache?: FilterState;
extraFormData?: ExtraFormData;
filterState?: FilterState;
ownState?: JsonObject;

View File

@@ -24,27 +24,19 @@ const schemes = [
id: 'bnbColors',
label: 'Airbnb Colors',
colors: [
'#ff5a5f', // rausch
'#7b0051', // hackb
'#007A87', // kazan
'#00d1c1', // babu
'#8ce071', // lima
'#ffb400', // beach
'#b4a76c', // barol
'#ff8083',
'#cc0086',
'#00a1b3',
'#00ffeb',
'#bbedab',
'#ffd266',
'#cbc29a',
'#ff3339',
'#ff1ab1',
'#005c66',
'#00b3a5',
'#55d12e',
'#b37e00',
'#988b4e',
'#29696B',
'#5BCACE',
'#F4B02A',
'#F1826A',
'#792EB2',
'#C96EC6',
'#921E50',
'#B27700',
'#9C3498',
'#9C3498',
'#E4679D',
'#C32F0E',
'#9D63CA',
],
},
].map(s => new CategoricalScheme(s));

View File

@@ -44,6 +44,9 @@ describe('isProbablyHTML', () => {
const plainText = 'Just a plain text';
const isHTML = isProbablyHTML(plainText);
expect(isHTML).toBe(false);
const trickyText = 'a <= 10 and b > 10';
expect(isProbablyHTML(trickyText)).toBe(false);
});
});

View File

@@ -28,7 +28,9 @@ export function sanitizeHtml(htmlString: string) {
}
export function isProbablyHTML(text: string) {
return /<[^>]+>/.test(text);
return Array.from(
new DOMParser().parseFromString(text, 'text/html').body.childNodes,
).some(({ nodeType }) => nodeType === 1);
}
export function sanitizeHtmlIfNeeded(htmlString: string) {

View File

@@ -42,7 +42,7 @@ export const Basic = () => {
allColumnsY: 'LAT',
clusteringRadius: '60',
globalOpacity: 1,
mapboxColor: 'rgb(0, 122, 135)',
mapboxColor: 'rgb(244, 176, 42)',
mapboxLabel: [],
mapboxStyle: 'mapbox://styles/mapbox/light-v9',
pandasAggfunc: 'sum',

View File

@@ -118,9 +118,10 @@ const processColumns = memoizeOne(function processColumns(
// because users can also add things like `MAX(str_col)` as a metric.
const isMetric = metricsSet.has(key) && isNumeric(key, records);
const isPercentMetric = percentMetricsSet.has(key);
const label = isPercentMetric
? `%${verboseMap?.[key.replace('%', '')] || key}`
: verboseMap?.[key] || key;
const label =
isPercentMetric && verboseMap?.hasOwnProperty(key.replace('%', ''))
? `%${verboseMap[key.replace('%', '')]}`
: verboseMap?.[key] || key;
const isTime = dataType === GenericDataType.TEMPORAL;
const isNumber = dataType === GenericDataType.NUMERIC;
const savedFormat = columnFormats?.[key];

View File

@@ -169,7 +169,7 @@ class Chart extends React.PureComponent {
// Create chart with POST request
this.props.actions.postChartFormData(
this.props.formData,
this.props.force || getUrlParam(URL_PARAMS.force), // allow override via url params force=true
Boolean(this.props.force || getUrlParam(URL_PARAMS.force)), // allow override via url params force=true
this.props.timeout,
this.props.chartId,
this.props.dashboardId,

View File

@@ -185,7 +185,7 @@ const v1ChartDataRequest = async (
const qs = {};
if (sliceId !== undefined) qs.form_data = `{"slice_id":${sliceId}}`;
if (dashboardId !== undefined) qs.dashboard_id = dashboardId;
if (force !== false) qs.force = force;
if (force) qs.force = force;
const allowDomainSharding =
// eslint-disable-next-line camelcase

View File

@@ -51,7 +51,7 @@ describe('chart actions', () => {
.callsFake(() => MOCK_URL);
getChartDataUriStub = sinon
.stub(exploreUtils, 'getChartDataUri')
.callsFake(() => URI(MOCK_URL));
.callsFake(({ qs }) => URI(MOCK_URL).query(qs));
fakeMetadata = { useLegacyApi: true };
metadataRegistryStub = sinon
.stub(chartlib, 'getChartMetadataRegistry')
@@ -81,7 +81,7 @@ describe('chart actions', () => {
});
it('should query with the built query', async () => {
const actionThunk = actions.postChartFormData({});
const actionThunk = actions.postChartFormData({}, null);
await actionThunk(dispatch);
expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);

View File

@@ -868,6 +868,20 @@ test('fires onChange when clearing the selection in multiple mode', async () =>
expect(onChange).toHaveBeenCalledTimes(1);
});
test('fires onChange when pasting a selection', async () => {
const onChange = jest.fn();
render(<AsyncSelect {...defaultProps} onChange={onChange} />);
await open();
const input = getElementByClassName('.ant-select-selection-search-input');
const paste = createEvent.paste(input, {
clipboardData: {
getData: () => OPTIONS[0].label,
},
});
fireEvent(input, paste);
expect(onChange).toHaveBeenCalledTimes(1);
});
test('does not duplicate options when using numeric values', async () => {
render(
<AsyncSelect

View File

@@ -554,6 +554,7 @@ const AsyncSelect = forwardRef(
...values,
]);
}
fireOnChange();
};
const shouldRenderChildrenOptions = useMemo(

View File

@@ -985,6 +985,20 @@ test('fires onChange when clearing the selection in multiple mode', async () =>
expect(onChange).toHaveBeenCalledTimes(1);
});
test('fires onChange when pasting a selection', async () => {
const onChange = jest.fn();
render(<Select {...defaultProps} onChange={onChange} />);
await open();
const input = getElementByClassName('.ant-select-selection-search-input');
const paste = createEvent.paste(input, {
clipboardData: {
getData: () => OPTIONS[0].label,
},
});
fireEvent(input, paste);
expect(onChange).toHaveBeenCalledTimes(1);
});
test('does not duplicate options when using numeric values', async () => {
render(
<Select

View File

@@ -571,6 +571,7 @@ const Select = forwardRef(
]);
}
}
fireOnChange();
};
return (

View File

@@ -25,9 +25,8 @@ import Loading from 'src/components/Loading';
import getBootstrapData from 'src/utils/getBootstrapData';
import getChartIdsFromLayout from '../util/getChartIdsFromLayout';
import getLayoutComponentFromChartId from '../util/getLayoutComponentFromChartId';
import DashboardBuilder from './DashboardBuilder/DashboardBuilder';
import {
chartPropShape,
slicePropShape,
dashboardInfoPropShape,
dashboardStatePropShape,
@@ -53,7 +52,6 @@ const propTypes = {
}).isRequired,
dashboardInfo: dashboardInfoPropShape.isRequired,
dashboardState: dashboardStatePropShape.isRequired,
charts: PropTypes.objectOf(chartPropShape).isRequired,
slices: PropTypes.objectOf(slicePropShape).isRequired,
activeFilters: PropTypes.object.isRequired,
chartConfiguration: PropTypes.object,
@@ -213,11 +211,6 @@ class Dashboard extends React.PureComponent {
}
}
// return charts in array
getAllCharts() {
return Object.values(this.props.charts);
}
applyFilters() {
const { appliedFilters } = this;
const { activeFilters, ownDataCharts } = this.props;
@@ -288,11 +281,7 @@ class Dashboard extends React.PureComponent {
if (this.context.loading) {
return <Loading />;
}
return (
<>
<DashboardBuilder />
</>
);
return this.props.children;
}
}

View File

@@ -21,7 +21,6 @@ import { shallow } from 'enzyme';
import sinon from 'sinon';
import Dashboard from 'src/dashboard/components/Dashboard';
import DashboardBuilder from 'src/dashboard/components/DashboardBuilder/DashboardBuilder';
import { CHART_TYPE } from 'src/dashboard/util/componentTypes';
import newComponentFactory from 'src/dashboard/util/newComponentFactory';
@@ -63,8 +62,14 @@ describe('Dashboard', () => {
loadStats: {},
};
const ChildrenComponent = () => <div>Test</div>;
function setup(overrideProps) {
const wrapper = shallow(<Dashboard {...props} {...overrideProps} />);
const wrapper = shallow(
<Dashboard {...props} {...overrideProps}>
<ChildrenComponent />
</Dashboard>,
);
return wrapper;
}
@@ -76,9 +81,9 @@ describe('Dashboard', () => {
'3_country_name': { values: ['USA'], scope: [] },
};
it('should render a DashboardBuilder', () => {
it('should render the children component', () => {
const wrapper = setup();
expect(wrapper.find(DashboardBuilder)).toExist();
expect(wrapper.find(ChildrenComponent)).toExist();
});
describe('UNSAFE_componentWillReceiveProps', () => {

View File

@@ -0,0 +1,34 @@
/**
* 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 React from 'react';
import { render } from 'spec/helpers/testing-library';
import { getItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
import SyncDashboardState from '.';
test('stores the dashboard info with local storages', () => {
const testDashboardPageId = 'dashboardPageId';
render(<SyncDashboardState dashboardPageId={testDashboardPageId} />, {
useRedux: true,
});
expect(getItem(LocalStorageKeys.dashboard__explore_context, {})).toEqual({
[testDashboardPageId]: expect.objectContaining({
dashboardPageId: testDashboardPageId,
}),
});
});

View File

@@ -0,0 +1,103 @@
/**
* 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 React, { useEffect } from 'react';
import pick from 'lodash/pick';
import { shallowEqual, useSelector } from 'react-redux';
import { DashboardContextForExplore } from 'src/types/DashboardContextForExplore';
import {
getItem,
LocalStorageKeys,
setItem,
} from 'src/utils/localStorageHelpers';
import { RootState } from 'src/dashboard/types';
import { getActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
type Props = { dashboardPageId: string };
const EMPTY_OBJECT = {};
export const getDashboardContextLocalStorage = () => {
const dashboardsContexts = getItem(
LocalStorageKeys.dashboard__explore_context,
{},
);
// A new dashboard tab id is generated on each dashboard page opening.
// We mark ids as redundant when user leaves the dashboard, because they won't be reused.
// Then we remove redundant dashboard contexts from local storage in order not to clutter it
return Object.fromEntries(
Object.entries(dashboardsContexts).filter(
([, value]) => !value.isRedundant,
),
);
};
const updateDashboardTabLocalStorage = (
dashboardPageId: string,
dashboardContext: DashboardContextForExplore,
) => {
const dashboardsContexts = getDashboardContextLocalStorage();
setItem(LocalStorageKeys.dashboard__explore_context, {
...dashboardsContexts,
[dashboardPageId]: dashboardContext,
});
};
const SyncDashboardState: React.FC<Props> = ({ dashboardPageId }) => {
const dashboardContextForExplore = useSelector<
RootState,
DashboardContextForExplore
>(
({ dashboardInfo, dashboardState, nativeFilters, dataMask }) => ({
labelColors: dashboardInfo.metadata?.label_colors || EMPTY_OBJECT,
sharedLabelColors:
dashboardInfo.metadata?.shared_label_colors || EMPTY_OBJECT,
colorScheme: dashboardState?.colorScheme,
chartConfiguration:
dashboardInfo.metadata?.chart_configuration || EMPTY_OBJECT,
nativeFilters: Object.entries(nativeFilters.filters).reduce(
(acc, [key, filterValue]) => ({
...acc,
[key]: pick(filterValue, ['chartsInScope']),
}),
{},
),
dataMask,
dashboardId: dashboardInfo.id,
filterBoxFilters: getActiveFilters(),
dashboardPageId,
}),
shallowEqual,
);
useEffect(() => {
updateDashboardTabLocalStorage(dashboardPageId, dashboardContextForExplore);
return () => {
// mark tab id as redundant when dashboard unmounts - case when user opens
// Explore in the same tab
updateDashboardTabLocalStorage(dashboardPageId, {
...dashboardContextForExplore,
isRedundant: true,
});
};
}, [dashboardContextForExplore, dashboardPageId]);
return null;
};
export default SyncDashboardState;

View File

@@ -52,6 +52,7 @@ import {
onFiltersRefreshSuccess,
setDirectPathToChild,
} from 'src/dashboard/actions/dashboardState';
import { RESPONSIVE_WIDTH } from 'src/filters/components/common';
import { FAST_DEBOUNCE } from 'src/constants';
import { dispatchHoverAction, dispatchFocusAction } from './utils';
import { FilterControlProps } from './types';
@@ -322,7 +323,7 @@ const FilterValue: React.FC<FilterControlProps> = ({
) : (
<SuperChart
height={HEIGHT}
width="100%"
width={RESPONSIVE_WIDTH}
showOverflow={showOverflow}
formData={formData}
displaySettings={displaySettings}

View File

@@ -39,7 +39,6 @@ function mapStateToProps(state: RootState) {
const {
datasources,
sliceEntities,
charts,
dataMask,
dashboardInfo,
dashboardState,
@@ -54,7 +53,6 @@ function mapStateToProps(state: RootState) {
userId: dashboardInfo.userId,
dashboardInfo,
dashboardState,
charts,
datasources,
// filters prop: a map structure for all the active filter_box's values and scope in this dashboard,
// for each filter field. map key is [chartId_column]

View File

@@ -28,7 +28,6 @@ import {
t,
useTheme,
} from '@superset-ui/core';
import pick from 'lodash/pick';
import { useDispatch, useSelector } from 'react-redux';
import { useToasts } from 'src/components/MessageToasts/withToasts';
import Loading from 'src/components/Loading';
@@ -42,11 +41,7 @@ import { setDatasources } from 'src/dashboard/actions/datasources';
import injectCustomCss from 'src/dashboard/util/injectCustomCss';
import setupPlugins from 'src/setup/setupPlugins';
import {
getItem,
LocalStorageKeys,
setItem,
} from 'src/utils/localStorageHelpers';
import { LocalStorageKeys, setItem } from 'src/utils/localStorageHelpers';
import { URL_PARAMS } from 'src/constants';
import { getUrlParam } from 'src/utils/urlUtils';
import { getFilterSets } from 'src/dashboard/actions/nativeFilters';
@@ -55,25 +50,28 @@ import {
getFilterValue,
getPermalinkValue,
} from 'src/dashboard/components/nativeFilters/FilterBar/keyValue';
import { DashboardContextForExplore } from 'src/types/DashboardContextForExplore';
import DashboardContainer from 'src/dashboard/containers/Dashboard';
import shortid from 'shortid';
import { RootState } from '../types';
import { getActiveFilters } from '../util/activeDashboardFilters';
import {
chartContextMenuStyles,
filterCardPopoverStyle,
headerStyles,
} from '../styles';
import SyncDashboardState, {
getDashboardContextLocalStorage,
} from '../components/SyncDashboardState';
export const DashboardPageIdContext = React.createContext('');
setupPlugins();
const DashboardContainer = React.lazy(
const DashboardBuilder = React.lazy(
() =>
import(
/* webpackChunkName: "DashboardContainer" */
/* webpackPreload: true */
'src/dashboard/containers/Dashboard'
'src/dashboard/components/DashboardBuilder/DashboardBuilder'
),
);
@@ -83,74 +81,15 @@ type PageProps = {
idOrSlug: string;
};
const getDashboardContextLocalStorage = () => {
const dashboardsContexts = getItem(
LocalStorageKeys.dashboard__explore_context,
{},
);
// A new dashboard tab id is generated on each dashboard page opening.
// We mark ids as redundant when user leaves the dashboard, because they won't be reused.
// Then we remove redundant dashboard contexts from local storage in order not to clutter it
return Object.fromEntries(
Object.entries(dashboardsContexts).filter(
([, value]) => !value.isRedundant,
),
);
};
const updateDashboardTabLocalStorage = (
dashboardPageId: string,
dashboardContext: DashboardContextForExplore,
) => {
const dashboardsContexts = getDashboardContextLocalStorage();
setItem(LocalStorageKeys.dashboard__explore_context, {
...dashboardsContexts,
[dashboardPageId]: dashboardContext,
});
};
const useSyncDashboardStateWithLocalStorage = () => {
const dashboardPageId = useMemo(() => shortid.generate(), []);
const dashboardContextForExplore = useSelector<
RootState,
DashboardContextForExplore
>(({ dashboardInfo, dashboardState, nativeFilters, dataMask }) => ({
labelColors: dashboardInfo.metadata?.label_colors || {},
sharedLabelColors: dashboardInfo.metadata?.shared_label_colors || {},
colorScheme: dashboardState?.colorScheme,
chartConfiguration: dashboardInfo.metadata?.chart_configuration || {},
nativeFilters: Object.entries(nativeFilters.filters).reduce(
(acc, [key, filterValue]) => ({
...acc,
[key]: pick(filterValue, ['chartsInScope']),
}),
{},
),
dataMask,
dashboardId: dashboardInfo.id,
filterBoxFilters: getActiveFilters(),
dashboardPageId,
}));
useEffect(() => {
updateDashboardTabLocalStorage(dashboardPageId, dashboardContextForExplore);
return () => {
// mark tab id as redundant when dashboard unmounts - case when user opens
// Explore in the same tab
updateDashboardTabLocalStorage(dashboardPageId, {
...dashboardContextForExplore,
isRedundant: true,
});
};
}, [dashboardContextForExplore, dashboardPageId]);
return dashboardPageId;
};
export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
const theme = useTheme();
const dispatch = useDispatch();
const history = useHistory();
const dashboardPageId = useSyncDashboardStateWithLocalStorage();
const dashboardPageId = useMemo(() => shortid.generate(), []);
const hasDashboardInfoInitiated = useSelector<RootState, Boolean>(
({ dashboardInfo }) =>
dashboardInfo && Object.keys(dashboardInfo).length > 0,
);
const { addDangerToast } = useToasts();
const { result: dashboard, error: dashboardApiError } =
useDashboard(idOrSlug);
@@ -284,7 +223,7 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
}, [addDangerToast, datasets, datasetsApiError, dispatch]);
if (error) throw error; // caught in error boundary
if (!readyToRender || !isDashboardHydrated.current) return <Loading />;
if (!readyToRender || !hasDashboardInfoInitiated) return <Loading />;
return (
<>
@@ -295,8 +234,11 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
chartContextMenuStyles(theme),
]}
/>
<SyncDashboardState dashboardPageId={dashboardPageId} />
<DashboardPageIdContext.Provider value={dashboardPageId}>
<DashboardContainer />
<DashboardContainer>
<DashboardBuilder />
</DashboardContainer>
</DashboardPageIdContext.Provider>
</>
);

View File

@@ -56,7 +56,6 @@ export function getInitialDataMask(
}
return {
...otherProps,
__cache: {},
extraFormData: {},
filterState: {},
ownState: {},

View File

@@ -385,10 +385,10 @@ function RowLevelSecurityModal(props: RowLevelSecurityModalProps) {
<StyledInputContainer>
<div className="control-label">
{t('Tables')} <span className="required">*</span>
{t('Datasets')} <span className="required">*</span>
<InfoTooltip
tooltip={t(
'These are the tables this filter will be applied to.',
'These are the datasets this filter will be applied to.',
)}
/>
</div>

View File

@@ -91,15 +91,6 @@ describe('SelectFilterPlugin', () => {
test('Add multiple values with first render', async () => {
getWrapper();
expect(setDataMask).toHaveBeenCalledWith({
extraFormData: {},
filterState: {
value: ['boy'],
},
});
expect(setDataMask).toHaveBeenCalledWith({
__cache: {
value: ['boy'],
},
extraFormData: {
filters: [
{
@@ -118,9 +109,6 @@ describe('SelectFilterPlugin', () => {
userEvent.click(screen.getByTitle('girl'));
expect(await screen.findByTitle(/girl/i)).toBeInTheDocument();
expect(setDataMask).toHaveBeenCalledWith({
__cache: {
value: ['boy'],
},
extraFormData: {
filters: [
{
@@ -146,9 +134,6 @@ describe('SelectFilterPlugin', () => {
}),
);
expect(setDataMask).toHaveBeenCalledWith({
__cache: {
value: ['boy'],
},
extraFormData: {
adhoc_filters: [
{
@@ -174,9 +159,6 @@ describe('SelectFilterPlugin', () => {
}),
);
expect(setDataMask).toHaveBeenCalledWith({
__cache: {
value: ['boy'],
},
extraFormData: {},
filterState: {
label: undefined,
@@ -191,9 +173,6 @@ describe('SelectFilterPlugin', () => {
expect(await screen.findByTitle('girl')).toBeInTheDocument();
userEvent.click(screen.getByTitle('girl'));
expect(setDataMask).toHaveBeenCalledWith({
__cache: {
value: ['boy'],
},
extraFormData: {
filters: [
{
@@ -216,9 +195,6 @@ describe('SelectFilterPlugin', () => {
expect(await screen.findByRole('combobox')).toBeInTheDocument();
userEvent.click(screen.getByTitle(NULL_STRING));
expect(setDataMask).toHaveBeenLastCalledWith({
__cache: {
value: ['boy'],
},
extraFormData: {
filters: [
{

View File

@@ -37,7 +37,6 @@ import { Select } from 'src/components';
import { SLOW_DEBOUNCE } from 'src/constants';
import { hasOption, propertyComparator } from 'src/components/Select/utils';
import { FilterBarOrientation } from 'src/dashboard/types';
import { uniqWith, isEqual } from 'lodash';
import { PluginFilterSelectProps, SelectValue } from './types';
import { FilterPluginStyle, StatusMessage, StyledFormItem } from '../common';
import { getDataRecordFormatter, getSelectExtraFormData } from '../../utils';
@@ -46,15 +45,11 @@ type DataMaskAction =
| { type: 'ownState'; ownState: JsonObject }
| {
type: 'filterState';
__cache: JsonObject;
extraFormData: ExtraFormData;
filterState: { value: SelectValue; label?: string };
};
function reducer(
draft: DataMask & { __cache?: JsonObject },
action: DataMaskAction,
) {
function reducer(draft: DataMask, action: DataMaskAction) {
switch (action.type) {
case 'ownState':
draft.ownState = {
@@ -63,10 +58,18 @@ function reducer(
};
return draft;
case 'filterState':
draft.extraFormData = action.extraFormData;
// eslint-disable-next-line no-underscore-dangle
draft.__cache = action.__cache;
draft.filterState = { ...draft.filterState, ...action.filterState };
if (
JSON.stringify(draft.extraFormData) !==
JSON.stringify(action.extraFormData)
) {
draft.extraFormData = action.extraFormData;
}
if (
JSON.stringify(draft.filterState) !== JSON.stringify(action.filterState)
) {
draft.filterState = { ...draft.filterState, ...action.filterState };
}
return draft;
default:
return draft;
@@ -130,7 +133,6 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
const suffix = inverseSelection && values?.length ? t(' (excluded)') : '';
dispatchDataMask({
type: 'filterState',
__cache: filterState,
extraFormData: getSelectExtraFormData(
col,
values,
@@ -219,16 +221,13 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
}, [filterState.validateMessage, filterState.validateStatus]);
const uniqueOptions = useMemo(() => {
const allOptions = [...data];
return uniqWith(allOptions, isEqual).map(row => {
const [value] = groupby.map(col => row[col]);
return {
label: labelFormatter(value, datatype),
value,
isNewOption: false,
};
});
}, [data, datatype, groupby, labelFormatter]);
const allOptions = new Set([...data.map(el => el[col])]);
return [...allOptions].map((value: string) => ({
label: labelFormatter(value, datatype),
value,
isNewOption: false,
}));
}, [data, datatype, col, labelFormatter]);
const options = useMemo(() => {
if (search && !multiSelect && !hasOption(search, uniqueOptions, true)) {

View File

@@ -20,9 +20,11 @@ import { styled } from '@superset-ui/core';
import { PluginFilterStylesProps } from './types';
import FormItem from '../../components/Form/FormItem';
export const RESPONSIVE_WIDTH = 0;
export const FilterPluginStyle = styled.div<PluginFilterStylesProps>`
min-height: ${({ height }) => height}px;
width: ${({ width }) => width}px;
width: ${({ width }) => (width === RESPONSIVE_WIDTH ? '100%' : `${width}px`)};
`;
export const StyledFormItem = styled(FormItem)`

View File

@@ -496,13 +496,6 @@ class BaseDatasource(
"""
raise NotImplementedError()
def values_for_column(self, column_name: str, limit: int = 10000) -> list[Any]:
"""Given a column, returns an iterable of distinct values
This is used to populate the dropdown showing a list of
values in filters in the explore view"""
raise NotImplementedError()
@staticmethod
def default_query(qry: Query) -> Query:
return qry

View File

@@ -46,7 +46,6 @@ from sqlalchemy import (
inspect,
Integer,
or_,
select,
String,
Table,
Text,
@@ -789,34 +788,6 @@ class SqlaTable(
)
) from ex
def values_for_column(self, column_name: str, limit: int = 10000) -> list[Any]:
"""Runs query against sqla to retrieve some
sample values for the given column.
"""
cols = {col.column_name: col for col in self.columns}
target_col = cols[column_name]
tp = self.get_template_processor()
tbl, cte = self.get_from_clause(tp)
qry = (
select([target_col.get_sqla_col(template_processor=tp)])
.select_from(tbl)
.distinct()
)
if limit:
qry = qry.limit(limit)
if self.fetch_values_predicate:
qry = qry.where(self.get_fetch_values_predicate(template_processor=tp))
with self.database.get_sqla_engine_with_context() as engine:
sql = qry.compile(engine, compile_kwargs={"literal_binds": True})
sql = self._apply_cte(sql, cte)
sql = self.mutate_query_from_config(sql)
df = pd.read_sql_query(sql=sql, con=engine)
return df[column_name].to_list()
def mutate_query_from_config(self, sql: str) -> str:
"""Apply config's SQL_QUERY_MUTATOR

View File

@@ -120,6 +120,10 @@ class DatasourceRestApi(BaseSupersetApi):
column_name=column_name, limit=row_limit
)
return self.response(200, result=payload)
except KeyError:
return self.response(
400, message=f"Column name {column_name} does not exist"
)
except NotImplementedError:
return self.response(
400,

View File

@@ -185,6 +185,7 @@ class Database(
"is_managed_externally",
"external_url",
"encrypted_extra",
"impersonate_user",
]
export_children = ["tables"]

View File

@@ -700,10 +700,7 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
"MIN": sa.func.MIN,
"MAX": sa.func.MAX,
}
@property
def fetch_value_predicate(self) -> str:
return "fix this!"
fetch_values_predicate = None
@property
def type(self) -> str:
@@ -761,7 +758,7 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
raise NotImplementedError()
@property
def database(self) -> builtins.type["Database"]:
def database(self) -> "Database":
raise NotImplementedError()
@property
@@ -776,23 +773,26 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
def columns(self) -> list[Any]:
raise NotImplementedError()
def get_fetch_values_predicate(
self, template_processor: Optional[BaseTemplateProcessor] = None
) -> TextClause:
raise NotImplementedError()
def get_extra_cache_keys(self, query_obj: dict[str, Any]) -> list[Hashable]:
raise NotImplementedError()
def get_template_processor(self, **kwargs: Any) -> BaseTemplateProcessor:
raise NotImplementedError()
def get_fetch_values_predicate(
self,
template_processor: Optional[ # pylint: disable=unused-argument
BaseTemplateProcessor
] = None,
) -> TextClause:
return self.fetch_values_predicate
def get_sqla_row_level_filters(
self,
template_processor: BaseTemplateProcessor,
) -> list[TextClause]:
"""
Return the appropriate row level security filters for this table and the
Returns the appropriate row level security filters for this table and the
current user. A custom username can be passed when the user is not present in the
Flask global namespace.
@@ -896,7 +896,7 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
self, query_obj: QueryObjectDict, mutate: bool = True
) -> QueryStringExtended:
sqlaq = self.get_sqla_query(**query_obj)
sql = self.database.compile_sqla_query(sqlaq.sqla_query) # type: ignore
sql = self.database.compile_sqla_query(sqlaq.sqla_query)
sql = self._apply_cte(sql, sqlaq.cte)
sql = sqlparse.format(sql, reindent=True)
if mutate:
@@ -935,7 +935,7 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
value = value.item()
column_ = columns_by_name[dimension]
db_extra: dict[str, Any] = self.database.get_extra() # type: ignore
db_extra: dict[str, Any] = self.database.get_extra()
if isinstance(column_, dict):
if (
@@ -1020,9 +1020,7 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
return df
try:
df = self.database.get_df(
sql, self.schema, mutator=assign_column_label # type: ignore
)
df = self.database.get_df(sql, self.schema, mutator=assign_column_label)
except Exception as ex: # pylint: disable=broad-except
df = pd.DataFrame()
status = QueryStatus.FAILED
@@ -1334,36 +1332,34 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
return and_(*l)
def values_for_column(self, column_name: str, limit: int = 10000) -> list[Any]:
"""Runs query against sqla to retrieve some
sample values for the given column.
"""
cols = {}
for col in self.columns:
if isinstance(col, dict):
cols[col.get("column_name")] = col
else:
cols[col.column_name] = col
target_col = cols[column_name]
tp = None # todo(hughhhh): add back self.get_template_processor()
# always denormalize column name before querying for values
db_dialect = self.database.get_dialect()
denormalized_col_name = self.database.db_engine_spec.denormalize_name(
db_dialect, column_name
)
cols = {col.column_name: col for col in self.columns}
target_col = cols[denormalized_col_name]
tp = self.get_template_processor()
tbl, cte = self.get_from_clause(tp)
if isinstance(target_col, dict):
sql_column = sa.column(target_col.get("name"))
else:
sql_column = target_col
qry = sa.select([sql_column]).select_from(tbl).distinct()
qry = (
sa.select([target_col.get_sqla_col(template_processor=tp)])
.select_from(tbl)
.distinct()
)
if limit:
qry = qry.limit(limit)
with self.database.get_sqla_engine_with_context() as engine: # type: ignore
if self.fetch_values_predicate:
qry = qry.where(self.get_fetch_values_predicate(template_processor=tp))
with self.database.get_sqla_engine_with_context() as engine:
sql = qry.compile(engine, compile_kwargs={"literal_binds": True})
sql = self._apply_cte(sql, cte)
sql = self.mutate_query_from_config(sql)
df = pd.read_sql_query(sql=sql, con=engine)
return df[column_name].to_list()
return df[denormalized_col_name].to_list()
def get_timestamp_expression(
self,
@@ -1935,7 +1931,7 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
)
having_clause_and += [self.text(having)]
if apply_fetch_values_predicate and self.fetch_values_predicate: # type: ignore
if apply_fetch_values_predicate and self.fetch_values_predicate:
qry = qry.where(
self.get_fetch_values_predicate(template_processor=template_processor)
)

View File

@@ -43,7 +43,7 @@ def get_limit_clause(page: Optional[int], per_page: Optional[int]) -> dict[str,
return {"row_offset": offset, "row_limit": limit}
def get_samples( # pylint: disable=too-many-arguments,too-many-locals
def get_samples( # pylint: disable=too-many-arguments
datasource_type: str,
datasource_id: int,
force: bool = False,
@@ -104,21 +104,18 @@ def get_samples( # pylint: disable=too-many-arguments,too-many-locals
result_type=ChartDataResultType.FULL,
force=force,
)
samples_results = samples_instance.get_payload()
count_star_results = count_star_instance.get_payload()
try:
sample_data = samples_results["queries"][0]
count_star_data = count_star_results["queries"][0]
failed_status = (
sample_data.get("status") == QueryStatus.FAILED
or count_star_data.get("status") == QueryStatus.FAILED
)
error_msg = sample_data.get("error") or count_star_data.get("error")
if failed_status and error_msg:
cache_key = sample_data.get("cache_key")
QueryCacheManager.delete(cache_key, region=CacheRegion.DATA)
raise DatasetSamplesFailedError(error_msg)
count_star_data = count_star_instance.get_payload()["queries"][0]
if count_star_data.get("status") == QueryStatus.FAILED:
raise DatasetSamplesFailedError(count_star_data.get("error"))
sample_data = samples_instance.get_payload()["queries"][0]
if sample_data.get("status") == QueryStatus.FAILED:
QueryCacheManager.delete(sample_data.get("cache_key"), CacheRegion.DATA)
raise DatasetSamplesFailedError(sample_data.get("error"))
sample_data["page"] = page
sample_data["per_page"] = per_page