mirror of
https://github.com/apache/superset.git
synced 2026-05-18 22:35:14 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
961eba6d97 | ||
|
|
08604cc686 | ||
|
|
49661bcc59 | ||
|
|
da06206ea6 | ||
|
|
e3fbb01bb8 | ||
|
|
ee1ba7e172 | ||
|
|
8d873e6da6 | ||
|
|
1d2a564d4e | ||
|
|
1c287dfc74 | ||
|
|
fb1919a483 | ||
|
|
e07eed10a2 | ||
|
|
a7fbdd607a | ||
|
|
6da18f8451 | ||
|
|
eea6a8ed4f | ||
|
|
078b78f30b |
21
CHANGELOG.md
21
CHANGELOG.md
@@ -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**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
||||
|
||||
# superset
|
||||
|
||||

|
||||

|
||||
|
||||
Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
4
setup.py
4
setup.py
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -554,6 +554,7 @@ const AsyncSelect = forwardRef(
|
||||
...values,
|
||||
]);
|
||||
}
|
||||
fireOnChange();
|
||||
};
|
||||
|
||||
const shouldRenderChildrenOptions = useMemo(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -571,6 +571,7 @@ const Select = forwardRef(
|
||||
]);
|
||||
}
|
||||
}
|
||||
fireOnChange();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -56,7 +56,6 @@ export function getInitialDataMask(
|
||||
}
|
||||
return {
|
||||
...otherProps,
|
||||
__cache: {},
|
||||
extraFormData: {},
|
||||
filterState: {},
|
||||
ownState: {},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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)`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -185,6 +185,7 @@ class Database(
|
||||
"is_managed_externally",
|
||||
"external_url",
|
||||
"encrypted_extra",
|
||||
"impersonate_user",
|
||||
]
|
||||
export_children = ["tables"]
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user