Merging lyftga into master

This commit is contained in:
Maxime Beauchemin 2019-04-18 21:21:54 -07:00
commit 5f28027ce7
116 changed files with 2722 additions and 979 deletions

1
.gitignore vendored
View File

@ -40,6 +40,7 @@ dump.rdb
env
env_py3
envpy3
env36
local_config.py
superset_config.py
superset.egg-info/

View File

@ -517,6 +517,12 @@ Run Cypress tests:
cd /superset/superset/assets
npm run build
npm run cypress run
# run tests from a specific file
npm run cypress run -- --spec cypress/integration/explore/link.test.js
# run specific file with video capture
npm run cypress run -- --spec cypress/integration/dashboard/index.test.js --config video=true
```
## Translating

View File

@ -479,6 +479,26 @@ into your global default defined in ``CACHE_CONFIG``.
'CACHE_REDIS_URL': 'redis://localhost:6379/0',
}
Superset has a Celery task that will periodically warm up the cache based on
different strategies. To use it, add the following to the `CELERYBEAT_SCHEDULE`
section in `config.py`:
.. code-block:: python
CELERYBEAT_SCHEDULE = {
'cache-warmup-hourly': {
'task': 'cache-warmup',
'schedule': crontab(minute=0, hour='*'), # hourly
'kwargs': {
'strategy_name': 'top_n_dashboards',
'top_n': 5,
'since': '7 days ago',
},
},
}
This will cache all the charts in the top 5 most popular dashboards every hour.
For other strategies, check the `superset/tasks/cache.py` file.
Deeper SQLAlchemy integration

View File

@ -49,7 +49,7 @@ pathlib2==2.3.0
polyline==1.3.2
py==1.7.0 # via retry
pycparser==2.19 # via cffi
pydruid==0.5.0
pydruid==0.5.2
pyjwt==1.7.1 # via flask-appbuilder
python-dateutil==2.6.1
python-editor==1.0.3 # via alembic

View File

@ -1635,7 +1635,7 @@
"freeForm": true,
"label": "Number format",
"renderTrigger": true,
"default": ".3s",
"default": "SMART_NUMBER",
"choices": [
[
".1s",
@ -2085,7 +2085,7 @@
"freeForm": true,
"label": "X Axis Format",
"renderTrigger": true,
"default": ".3s",
"default": "SMART_NUMBER",
"choices": [
[
".1s",
@ -2165,7 +2165,7 @@
"freeForm": true,
"label": "Y Axis Format",
"renderTrigger": true,
"default": ".3s",
"default": "SMART_NUMBER",
"choices": [
[
".1s",
@ -2206,7 +2206,7 @@
"type": "SelectControl",
"freeForm": true,
"label": "Right Axis Format",
"default": ".3s",
"default": "SMART_NUMBER",
"choices": [
[
".1s",
@ -2497,6 +2497,64 @@
"default": "150",
"description": "Font size for the biggest value in the list"
},
"header_font_size": {
"type": "SelectControl",
"label": "Header Font Size",
"renderTrigger": true,
"clearable": false,
"default": 0.3,
"options": [
{
"label": "Tiny",
"value": 0.125
},
{
"label": "Small",
"value": 0.2
},
{
"label": "Normal",
"value": 0.3
},
{
"label": "Large",
"value": 0.4
},
{
"label": "Huge",
"value": 0.5
}
]
},
"subheader_font_size": {
"type": "SelectControl",
"label": "Subheader Font Size",
"renderTrigger": true,
"clearable": false,
"default": 0.125,
"options": [
{
"label": "Tiny",
"value": 0.125
},
{
"label": "Small",
"value": 0.2
},
{
"label": "Normal",
"value": 0.3
},
{
"label": "Large",
"value": 0.4
},
{
"label": "Huge",
"value": 0.5
}
]
},
"instant_filtering": {
"type": "CheckboxControl",
"label": "Instant Filtering",
@ -3220,6 +3278,11 @@
"renderTrigger": true,
"description": "The color scheme for rendering chart"
},
"label_colors": {
"type": "ColorMapControl",
"label": "Color Map",
"default": {}
},
"significance_level": {
"type": "TextControl",
"label": "Significance Level",

View File

@ -32,18 +32,18 @@ export default () => describe('top-level controls', () => {
cy.get('#app').then((data) => {
const bootstrapData = JSON.parse(data[0].dataset.bootstrap);
const dashboard = bootstrapData.dashboard_data;
const sliceIds = dashboard.slices.map(slice => (slice.slice_id));
mapId = dashboard.slices.find(slice => (slice.form_data.viz_type === 'world_map')).slice_id;
sliceIds
.forEach((id) => {
const sliceRequest = `getJson_${id}`;
dashboard.slices
.forEach((slice) => {
const sliceRequest = `getJson_${slice.slice_id}`;
sliceRequests.push(`@${sliceRequest}`);
cy.route('POST', `/superset/explore_json/?form_data={"slice_id":${id}}`).as(sliceRequest);
const formData = `{"slice_id":${slice.slice_id},"viz_type":"${slice.form_data.viz_type}"}`;
cy.route('GET', `/superset/explore_json/?form_data=${formData}`).as(sliceRequest);
const forceRefresh = `getJson_${id}_force`;
const forceRefresh = `postJson_${slice.slice_id}_force`;
forceRefreshRequests.push(`@${forceRefresh}`);
cy.route('POST', `/superset/explore_json/?form_data={"slice_id":${id}}&force=true`).as(forceRefresh);
cy.route('POST', `/superset/explore_json/?form_data={"slice_id":${slice.slice_id}}&force=true`).as(forceRefresh);
});
});
});
@ -69,7 +69,7 @@ export default () => describe('top-level controls', () => {
.parent()
.should('have.class', 'disabled');
cy.wait(`@getJson_${mapId}_force`);
cy.wait(`@postJson_${mapId}_force`);
cy.get('#save-dash-split-button').trigger('click');
cy.contains('Force refresh dashboard').parent().not('have.class', 'disabled');
});

View File

@ -28,8 +28,9 @@ export default () => describe('edit mode', () => {
const bootstrapData = JSON.parse(data[0].dataset.bootstrap);
const dashboard = bootstrapData.dashboard_data;
const boxplotChartId = dashboard.slices.find(slice => (slice.form_data.viz_type === 'box_plot')).slice_id;
const boxplotRequest = `/superset/explore_json/?form_data={"slice_id":${boxplotChartId}}`;
cy.route('POST', boxplotRequest).as('boxplotRequest');
const formData = `{"slice_id":${boxplotChartId},"viz_type":"box_plot"}`;
const boxplotRequest = `/superset/explore_json/?form_data=${formData}`;
cy.route('GET', boxplotRequest).as('boxplotRequest');
});
cy.get('.dashboard-header').contains('Edit dashboard').click();

View File

@ -39,8 +39,9 @@ export default () => describe('dashboard filter', () => {
it('should apply filter', () => {
const aliases = [];
const filterRoute = `/superset/explore_json/?form_data={"slice_id":${filterId}}`;
cy.route('POST', filterRoute).as('fetchFilter');
const formData = `{"slice_id":${filterId},"viz_type":"filter_box"}`;
const filterRoute = `/superset/explore_json/?form_data=${formData}`;
cy.route('GET', filterRoute).as('fetchFilter');
cy.wait('@fetchFilter');
sliceIds
.filter(id => (parseInt(id, 10) !== filterId))

View File

@ -34,7 +34,8 @@ export default () => describe('load', () => {
// then define routes and create alias for each requests
slices.forEach((slice) => {
const alias = `getJson_${slice.slice_id}`;
cy.route('POST', `/superset/explore_json/?form_data={"slice_id":${slice.slice_id}}`).as(alias);
const formData = `{"slice_id":${slice.slice_id},"viz_type":"${slice.form_data.viz_type}"}`;
cy.route('GET', `/superset/explore_json/?form_data=${formData}`).as(alias);
aliases.push(`@${alias}`);
});
});

View File

@ -56,8 +56,9 @@ export default () => describe('save', () => {
cy.wait('@copyRequest');
// should have box_plot chart
const boxplotRequest = `/superset/explore_json/?form_data={"slice_id":${boxplotChartId}}`;
cy.route('POST', boxplotRequest).as('boxplotRequest');
const formData = `{"slice_id":${boxplotChartId},"viz_type":"box_plot"}`;
const boxplotRequest = `/superset/explore_json/?form_data=${formData}`;
cy.route('GET', boxplotRequest).as('boxplotRequest');
cy.wait('@boxplotRequest');
cy.get('.grid-container .box_plot').should('be.exist');

View File

@ -26,9 +26,10 @@ describe('Groupby', () => {
cy.server();
cy.login();
cy.route('POST', '/superset/explore_json/**').as('getJson');
cy.route('GET', '/superset/explore_json/**').as('getJson');
cy.route('POST', '/superset/explore_json/**').as('postJson');
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('[data-test=groupby]').within(() => {
cy.get('.Select-control').click();
@ -36,7 +37,7 @@ describe('Groupby', () => {
cy.get('.VirtualizedSelectFocusedOption').click();
});
cy.get('button.query').click();
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
cy.verifySliceSuccess({ waitAlias: '@postJson', chartSelector: 'svg' });
});
});
@ -44,14 +45,15 @@ describe('AdhocMetrics', () => {
beforeEach(() => {
cy.login();
cy.server();
cy.route('POST', '/superset/explore_json/**').as('getJson');
cy.route('GET', '/superset/explore_json/**').as('getJson');
cy.route('POST', '/superset/explore_json/**').as('postJson');
});
it('Clear metric and set simple adhoc metric', () => {
const metricName = 'Girl Births';
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('[data-test=metrics]').within(() => {
cy.get('.select-clear').click();
@ -74,7 +76,7 @@ describe('AdhocMetrics', () => {
cy.get('button.query').click();
cy.verifySliceSuccess({
waitAlias: '@getJson',
waitAlias: '@postJson',
querySubstring: metricName,
chartSelector: 'svg',
});
@ -84,7 +86,7 @@ describe('AdhocMetrics', () => {
const metric = 'SUM(num)/COUNT(DISTINCT name)';
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('[data-test=metrics]').within(() => {
cy.get('.select-clear').click();
@ -105,7 +107,7 @@ describe('AdhocMetrics', () => {
cy.get('button.query').click();
cy.verifySliceSuccess({
waitAlias: '@getJson',
waitAlias: '@postJson',
querySubstring: metric,
chartSelector: 'svg',
});
@ -113,7 +115,7 @@ describe('AdhocMetrics', () => {
it('Switch between simple and custom sql tabs', () => {
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('[data-test=metrics]').within(() => {
cy.get('.select-clear').click();
@ -137,7 +139,7 @@ describe('AdhocMetrics', () => {
cy.get('button.query').click();
cy.verifySliceSuccess({
waitAlias: '@getJson',
waitAlias: '@postJson',
chartSelector: 'svg',
});
});
@ -147,12 +149,13 @@ describe('AdhocFilters', () => {
beforeEach(() => {
cy.login();
cy.server();
cy.route('POST', '/superset/explore_json/**').as('getJson');
cy.route('GET', '/superset/explore_json/**').as('getJson');
cy.route('POST', '/superset/explore_json/**').as('postJson');
});
it('Set simple adhoc filter', () => {
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('[data-test=adhoc_filters]').within(() => {
cy.get('.Select-control').click({ force: true });
@ -177,14 +180,14 @@ describe('AdhocFilters', () => {
cy.get('button.query').click();
cy.verifySliceSuccess({
waitAlias: '@getJson',
waitAlias: '@postJson',
chartSelector: 'svg',
});
});
it('Set custom adhoc filter', () => {
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('[data-test=adhoc_filters]').within(() => {
cy.get('.Select-control').click({ force: true });
@ -206,7 +209,7 @@ describe('AdhocFilters', () => {
cy.get('button.query').click();
cy.verifySliceSuccess({
waitAlias: '@getJson',
waitAlias: '@postJson',
chartSelector: 'svg',
});
});
@ -217,12 +220,13 @@ describe('Advanced analytics', () => {
beforeEach(() => {
cy.login();
cy.server();
cy.route('POST', '/superset/explore_json/**').as('getJson');
cy.route('GET', '/superset/explore_json/**').as('getJson');
cy.route('POST', '/superset/explore_json/**').as('postJson');
});
it('Create custom time compare', () => {
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('span')
.contains('Advanced Analytics')
@ -240,10 +244,10 @@ describe('Advanced analytics', () => {
});
cy.get('button.query').click();
cy.wait('@getJson');
cy.wait('@postJson');
cy.reload();
cy.verifySliceSuccess({
waitAlias: '@getJson',
waitAlias: '@postJson',
chartSelector: 'svg',
});
@ -257,12 +261,13 @@ describe('Annotations', () => {
beforeEach(() => {
cy.login();
cy.server();
cy.route('POST', '/superset/explore_json/**').as('getJson');
cy.route('GET', '/superset/explore_json/**').as('getJson');
cy.route('POST', '/superset/explore_json/**').as('postJson');
});
it('Create formula annotation y-axis goal line', () => {
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('[data-test=annotation_layers]').within(() => {
cy.get('button').click();
@ -280,7 +285,7 @@ describe('Annotations', () => {
cy.get('button.query').click();
cy.verifySliceSuccess({
waitAlias: '@getJson',
waitAlias: '@postJson',
chartSelector: 'svg',
});
@ -292,7 +297,8 @@ describe('Time range filter', () => {
beforeEach(() => {
cy.login();
cy.server();
cy.route('POST', '/superset/explore_json/**').as('getJson');
cy.route('GET', '/superset/explore_json/**').as('getJson');
cy.route('POST', '/superset/explore_json/**').as('postJson');
});
it('Defaults to the correct tab for time_range params', () => {
@ -304,7 +310,7 @@ describe('Time range filter', () => {
};
cy.visitChartByParams(JSON.stringify(formData));
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('[data-test=time_range]').within(() => {
cy.get('span.label').click();

View File

@ -26,16 +26,17 @@ describe('Test explore links', () => {
beforeEach(() => {
cy.login();
cy.server();
cy.route('POST', '/superset/explore_json/**').as('getJson');
cy.route('GET', '/superset/explore_json/**').as('getJson');
cy.route('POST', '/superset/explore_json/**').as('postJson');
});
it('Open and close view query modal', () => {
cy.visitChartByName('Growth Rate');
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('button#query').click();
cy.get('span').contains('View query').parent().click();
cy.wait('@getJson').then(() => {
cy.wait('@postJson').then(() => {
cy.get('code');
});
cy.get('.modal-header').within(() => {
@ -47,7 +48,7 @@ describe('Test explore links', () => {
cy.route('POST', 'r/shortner/').as('getShortUrl');
cy.visitChartByName('Growth Rate');
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('[data-test=short-link-button]').click();
@ -60,12 +61,12 @@ describe('Test explore links', () => {
.then((text) => {
cy.visit(text);
});
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
});
it('Test iframe link', () => {
cy.visitChartByName('Growth Rate');
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('[data-test=embed-code-button]').click();
cy.get('#embed-code-popover').within(() => {
@ -83,7 +84,7 @@ describe('Test explore links', () => {
const newChartName = 'Test chart';
cy.visitChartByParams(JSON.stringify(formData));
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.url().then((url) => {
cy.get('button[data-target="#save_modal"]').click();
cy.get('.modal-content').within(() => {
@ -93,14 +94,14 @@ describe('Test explore links', () => {
cy.url().should('eq', url);
cy.visitChartByName(newChartName);
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
});
});
xit('Test chart save', () => {
const chartName = 'Test chart';
cy.visitChartByName(chartName);
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('[data-test=groupby]').within(() => {
cy.get('span.select-clear-zone').click();
@ -109,7 +110,7 @@ describe('Test explore links', () => {
cy.get('.modal-content').within(() => {
cy.get('button#btn_modal_save').click();
});
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.request(`/chart/api/read?_flt_3_slice_name=${chartName}`).then((response) => {
cy.request('DELETE', `/chart/api/delete/${response.body.pks[0]}`);
});
@ -117,7 +118,7 @@ describe('Test explore links', () => {
it('Test chart save as and add to new dashboard', () => {
cy.visitChartByName('Growth Rate');
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
const dashboardTitle = 'Test dashboard';
cy.get('button[data-target="#save_modal"]').click();
@ -127,7 +128,7 @@ describe('Test explore links', () => {
cy.get('input[placeholder="[dashboard name]"]').type(dashboardTitle);
cy.get('button#btn_modal_save').click();
});
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.request(`/dashboard/api/read?_flt_3_dashboard_title=${dashboardTitle}`).then((response) => {
expect(response.body.pks[0]).not.equals(null);
});
@ -135,7 +136,7 @@ describe('Test explore links', () => {
it('Test chart save as and add to existing dashboard', () => {
cy.visitChartByName('Most Populated Countries');
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
const chartName = 'New Most Populated Countries';
const dashboardTitle = 'Test dashboard';
@ -150,7 +151,7 @@ describe('Test explore links', () => {
});
cy.get('button#btn_modal_save').click();
});
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.request(`/chart/api/read?_flt_3_slice_name=${chartName}`).then((response) => {
cy.request('DELETE', `/chart/api/delete/${response.body.pks[0]}`);
});

File diff suppressed because it is too large Load Diff

View File

@ -81,7 +81,6 @@
"@superset-ui/translation": "^0.11.0",
"@vx/responsive": "0.0.172",
"abortcontroller-polyfill": "^1.1.9",
"bignumber.js": "^8.1.1",
"bootstrap": "^3.3.6",
"bootstrap-slider": "^10.0.0",
"brace": "^0.11.1",

View File

@ -51,7 +51,7 @@ describe('chart actions', () => {
});
it('should dispatch CHART_UPDATE_STARTED action before the query', () => {
const actionThunk = actions.runQuery({});
const actionThunk = actions.postChartFormData({});
return actionThunk(dispatch).then(() => {
// chart update, trigger query, update form data, success
@ -64,7 +64,7 @@ describe('chart actions', () => {
});
it('should dispatch TRIGGER_QUERY action with the query', () => {
const actionThunk = actions.runQuery({});
const actionThunk = actions.postChartFormData({});
return actionThunk(dispatch).then(() => {
// chart update, trigger query, update form data, success
expect(dispatch.callCount).toBe(5);
@ -76,7 +76,7 @@ describe('chart actions', () => {
});
it('should dispatch UPDATE_QUERY_FORM_DATA action with the query', () => {
const actionThunk = actions.runQuery({});
const actionThunk = actions.postChartFormData({});
return actionThunk(dispatch).then(() => {
// chart update, trigger query, update form data, success
expect(dispatch.callCount).toBe(5);
@ -88,7 +88,7 @@ describe('chart actions', () => {
});
it('should dispatch logEvent async action', () => {
const actionThunk = actions.runQuery({});
const actionThunk = actions.postChartFormData({});
return actionThunk(dispatch).then(() => {
// chart update, trigger query, update form data, success
expect(dispatch.callCount).toBe(5);
@ -104,7 +104,7 @@ describe('chart actions', () => {
});
it('should dispatch CHART_UPDATE_SUCCEEDED action upon success', () => {
const actionThunk = actions.runQuery({});
const actionThunk = actions.postChartFormData({});
return actionThunk(dispatch).then(() => {
// chart update, trigger query, update form data, success
expect(dispatch.callCount).toBe(5);
@ -120,7 +120,7 @@ describe('chart actions', () => {
fetchMock.post(MOCK_URL, () => unresolvingPromise, { overwriteRoutes: true });
const timeoutInSec = 1 / 1000;
const actionThunk = actions.runQuery({}, false, timeoutInSec);
const actionThunk = actions.postChartFormData({}, false, timeoutInSec);
return actionThunk(dispatch).then(() => {
// chart update, trigger query, update form data, fail
@ -136,7 +136,7 @@ describe('chart actions', () => {
fetchMock.post(MOCK_URL, { throws: { statusText: 'misc error' } }, { overwriteRoutes: true });
const timeoutInSec = 1 / 1000;
const actionThunk = actions.runQuery({}, false, timeoutInSec);
const actionThunk = actions.postChartFormData({}, false, timeoutInSec);
return actionThunk(dispatch).then(() => {
// chart update, trigger query, update form data, fail

View File

@ -1,53 +0,0 @@
/**
* 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 BigNumber from 'bignumber.js';
import transform from 'src/chart/transformBigNumber';
describe('transformBigNumber', () => {
it('should transform BigNumber on its own', () => {
expect(transform(new BigNumber(123.456))).toBe(123.456);
});
it('should transform BigNumber in objects', () => {
expect(transform({
foo: new BigNumber(123),
bar: 456,
baz: null,
})).toEqual({ foo: 123, bar: 456, baz: null });
});
it('should transform BigNumber in arrays', () => {
expect(transform([
{ foo: new BigNumber(123) },
{ bar: 456 },
])).toEqual([{ foo: 123 }, { bar: 456 }]);
});
it('should transform BigNumber in nested structures', () => {
expect(transform([{
x: new BigNumber(123),
y: [{ foo: new BigNumber(456) }, { bar: 'str' }],
z: { some: [new BigNumber(789)] },
}])).toEqual([{
x: 123,
y: [{ foo: 456 }, { bar: 'str' }],
z: { some: [789] },
}]);
});
});

View File

@ -31,6 +31,7 @@ import DashboardComponent from '../../../../src/dashboard/containers/DashboardCo
import DashboardHeader from '../../../../src/dashboard/containers/DashboardHeader';
import DashboardGrid from '../../../../src/dashboard/containers/DashboardGrid';
import * as dashboardStateActions from '../../../../src/dashboard/actions/dashboardState';
import { BUILDER_PANE_TYPE } from '../../../../src/dashboard/util/constants';
import WithDragDropContext from '../helpers/WithDragDropContext';
import {
@ -61,7 +62,10 @@ describe('DashboardBuilder', () => {
dashboardLayout,
deleteTopLevelTabs() {},
editMode: false,
showBuilderPane: false,
showBuilderPane() {},
builderPaneType: BUILDER_PANE_TYPE.NONE,
setColorSchemeAndUnsavedChanges() {},
colorScheme: undefined,
handleComponentDrop() {},
toggleBuilderPane() {},
};
@ -143,11 +147,27 @@ describe('DashboardBuilder', () => {
expect(parentSize.find(DashboardGrid)).toHaveLength(expectedCount);
});
it('should render a BuilderComponentPane if editMode=showBuilderPane=true', () => {
it('should render a BuilderComponentPane if editMode=true and user selects "Insert Components" pane', () => {
const wrapper = setup();
expect(wrapper.find(BuilderComponentPane)).toHaveLength(0);
wrapper.setProps({ ...props, editMode: true, showBuilderPane: true });
wrapper.setProps({
...props,
editMode: true,
builderPaneType: BUILDER_PANE_TYPE.ADD_COMPONENTS,
});
expect(wrapper.find(BuilderComponentPane)).toHaveLength(1);
});
it('should render a BuilderComponentPane if editMode=true and user selects "Colors" pane', () => {
const wrapper = setup();
expect(wrapper.find(BuilderComponentPane)).toHaveLength(0);
wrapper.setProps({
...props,
editMode: true,
builderPaneType: BUILDER_PANE_TYPE.COLORS,
});
expect(wrapper.find(BuilderComponentPane)).toHaveLength(1);
});

View File

@ -39,7 +39,7 @@ describe('Dashboard', () => {
actions: {
addSliceToDashboard() {},
removeSliceFromDashboard() {},
runQuery() {},
postChartFormData() {},
logEvent() {},
},
initMessages: [],
@ -82,15 +82,15 @@ describe('Dashboard', () => {
},
};
it('should call runQuery for all non-exempt slices', () => {
it('should call postChartFormData for all non-exempt slices', () => {
const wrapper = setup({ charts: overrideCharts, slices: overrideSlices });
const spy = sinon.spy(props.actions, 'runQuery');
const spy = sinon.spy(props.actions, 'postChartFormData');
wrapper.instance().refreshExcept('1001');
spy.restore();
expect(spy.callCount).toBe(Object.keys(overrideCharts).length - 1);
});
it('should not call runQuery for filter_immune_slices', () => {
it('should not call postChartFormData for filter_immune_slices', () => {
const wrapper = setup({
charts: overrideCharts,
dashboardInfo: {
@ -103,7 +103,7 @@ describe('Dashboard', () => {
},
},
});
const spy = sinon.spy(props.actions, 'runQuery');
const spy = sinon.spy(props.actions, 'postChartFormData');
wrapper.instance().refreshExcept();
spy.restore();
expect(spy.callCount).toBe(0);

View File

@ -71,9 +71,9 @@ describe('HeaderActionsDropdown', () => {
expect(wrapper.find(MenuItem)).toHaveLength(1);
});
it('should render the RefreshIntervalModal', () => {
it('should not render the RefreshIntervalModal', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(RefreshIntervalModal)).toHaveLength(1);
expect(wrapper.find(RefreshIntervalModal)).toHaveLength(0);
});
it('should render the URLShortLinkModal', () => {
@ -105,9 +105,9 @@ describe('HeaderActionsDropdown', () => {
expect(wrapper.find(MenuItem)).toHaveLength(2);
});
it('should render the RefreshIntervalModal', () => {
it('should not render the RefreshIntervalModal', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(RefreshIntervalModal)).toHaveLength(1);
expect(wrapper.find(RefreshIntervalModal)).toHaveLength(0);
});
it('should render the URLShortLinkModal', () => {

View File

@ -24,6 +24,7 @@ import FaveStar from '../../../../src/components/FaveStar';
import HeaderActionsDropdown from '../../../../src/dashboard/components/HeaderActionsDropdown';
import Button from '../../../../src/components/Button';
import UndoRedoKeylisteners from '../../../../src/dashboard/components/UndoRedoKeylisteners';
import { BUILDER_PANE_TYPE } from '../../../../src/dashboard/util/constants';
describe('Header', () => {
const props = {
@ -46,7 +47,8 @@ describe('Header', () => {
updateDashboardTitle: () => {},
editMode: false,
setEditMode: () => {},
showBuilderPane: false,
showBuilderPane: () => {},
builderPaneType: BUILDER_PANE_TYPE.NONE,
toggleBuilderPane: () => {},
updateCss: () => {},
hasUnsavedChanges: false,
@ -150,9 +152,9 @@ describe('Header', () => {
expect(wrapper.find(HeaderActionsDropdown)).toHaveLength(1);
});
it('should render four Buttons', () => {
it('should render five Buttons', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(Button)).toHaveLength(4);
expect(wrapper.find(Button)).toHaveLength(5);
});
it('should set up undo/redo', () => {

View File

@ -17,6 +17,7 @@
* under the License.
*/
import { id as sliceId } from './mockChartQueries';
import { BUILDER_PANE_TYPE } from '../../../../src/dashboard/util/constants';
export default {
sliceIds: [sliceId],
@ -24,7 +25,7 @@ export default {
filters: {},
expandedSlices: {},
editMode: false,
showBuilderPane: false,
builderPaneType: BUILDER_PANE_TYPE.NONE,
hasUnsavedChanges: false,
maxUndoHistoryExceeded: false,
isStarred: true,

View File

@ -25,12 +25,12 @@ import {
SET_EDIT_MODE,
SET_MAX_UNDO_HISTORY_EXCEEDED,
SET_UNSAVED_CHANGES,
TOGGLE_BUILDER_PANE,
TOGGLE_EXPAND_SLICE,
TOGGLE_FAVE_STAR,
} from '../../../../src/dashboard/actions/dashboardState';
import dashboardStateReducer from '../../../../src/dashboard/reducers/dashboardState';
import { BUILDER_PANE_TYPE } from '../../../../src/dashboard/util/constants';
describe('dashboardState reducer', () => {
it('should return initial state', () => {
@ -79,23 +79,10 @@ describe('dashboardState reducer', () => {
{ editMode: false },
{ type: SET_EDIT_MODE, editMode: true },
),
).toEqual({ editMode: true, showBuilderPane: true });
});
it('should toggle builder pane', () => {
expect(
dashboardStateReducer(
{ showBuilderPane: false },
{ type: TOGGLE_BUILDER_PANE },
),
).toEqual({ showBuilderPane: true });
expect(
dashboardStateReducer(
{ showBuilderPane: true },
{ type: TOGGLE_BUILDER_PANE },
),
).toEqual({ showBuilderPane: false });
).toEqual({
editMode: true,
builderPaneType: BUILDER_PANE_TYPE.ADD_COMPONENTS,
});
});
it('should toggle expanded slices', () => {
@ -150,6 +137,8 @@ describe('dashboardState reducer', () => {
hasUnsavedChanges: false,
maxUndoHistoryExceeded: false,
editMode: false,
builderPaneType: BUILDER_PANE_TYPE.NONE,
updatedColorScheme: false,
});
});

View File

@ -17,7 +17,7 @@
* under the License.
*/
import { max } from 'd3-array';
import { getAggFunc } from '../../../../../src/visualizations/deckgl/layers/common';
import { getAggFunc, getBounds } from '../../../../../src/visualizations/deckgl/layers/common';
describe('deckgl layers common', () => {
it('getAggFunc', () => {
@ -46,4 +46,65 @@ describe('deckgl layers common', () => {
expect(getAggFunc('p95', accessor)(arr)).toEqual(2.9);
expect(getAggFunc('p99', accessor)(arr)).toEqual(2.98);
});
describe('getBounds', () => {
it('should return valid bounds for multiple points', () => {
const points = [
[0, 20],
[5, 25],
[10, 15],
];
expect(getBounds(points)).toEqual([
[0, 15],
[10, 25],
]);
});
it('should return valid bounds for single latitude point', () => {
const points = [
[0, 0],
[5, 0],
];
expect(getBounds(points)).toEqual([
[0, -0.25],
[5, 0.25],
]);
});
it('should return valid bounds for single longitude point', () => {
const points = [
[0, 0],
[0, 5],
];
expect(getBounds(points)).toEqual([
[-0.25, 0],
[0.25, 5],
]);
});
it('should return valid bounds for single point', () => {
const points = [
[0, 0],
];
expect(getBounds(points)).toEqual([
[-0.25, -0.25],
[0.25, 0.25],
]);
});
it('should return valid bounds for point 90, 180', () => {
const points = [
[180, 90],
];
expect(getBounds(points)).toEqual([
[179.75, 89.75],
[180, 90],
]);
});
it('should return valid bounds for point -90, -180', () => {
const points = [
[-180, -90],
];
expect(getBounds(points)).toEqual([
[-180, -90],
[-179.75, -89.75],
]);
});
});
});

View File

@ -30,7 +30,7 @@ describe('getBreakPoints', () => {
});
it('returns sorted break points', () => {
const fd = { breakPoints: ['0', '10', '100', '50', '1000'] };
const fd = { break_points: ['0', '10', '100', '50', '1000'] };
const result = getBreakPoints(fd, [], metricAccessor);
const expected = ['0', '10', '50', '100', '1000'];
expect(result).toEqual(expected);
@ -45,7 +45,7 @@ describe('getBreakPoints', () => {
});
it('formats number with proper precision', () => {
const fd = { metric: 'count', numBuckets: 2 };
const fd = { metric: 'count', num_buckets: 2 };
const features = [0, 1 / 3, 2 / 3, 1].map(count => ({ count }));
const result = getBreakPoints(fd, features, metricAccessor);
const expected = ['0.0', '0.5', '1.0'];
@ -53,7 +53,7 @@ describe('getBreakPoints', () => {
});
it('works with a zero range', () => {
const fd = { metric: 'count', numBuckets: 1 };
const fd = { metric: 'count', num_buckets: 1 };
const features = [1, 1, 1].map(count => ({ count }));
const result = getBreakPoints(fd, features, metricAccessor);
const expected = ['1', '1'];
@ -69,7 +69,7 @@ describe('getBreakPointColorScaler', () => {
it('returns linear color scaler if there are no break points', () => {
const fd = {
metric: 'count',
linearColorScheme: ['#000000', '#ffffff'],
linear_color_scheme: ['#000000', '#ffffff'],
opacity: 100,
};
const features = [10, 20, 30].map(count => ({ count }));
@ -82,8 +82,8 @@ describe('getBreakPointColorScaler', () => {
it('returns bucketing scaler if there are break points', () => {
const fd = {
metric: 'count',
linearColorScheme: ['#000000', '#ffffff'],
breakPoints: ['0', '1', '10'],
linear_color_scheme: ['#000000', '#ffffff'],
break_points: ['0', '1', '10'],
opacity: 100,
};
const features = [];
@ -97,8 +97,8 @@ describe('getBreakPointColorScaler', () => {
it('mask values outside the break points', () => {
const fd = {
metric: 'count',
linearColorScheme: ['#000000', '#ffffff'],
breakPoints: ['0', '1', '10'],
linear_color_scheme: ['#000000', '#ffffff'],
break_points: ['0', '1', '10'],
opacity: 100,
};
const features = [];
@ -116,8 +116,8 @@ describe('getBuckets', () => {
it('computes buckets for break points', () => {
const fd = {
metric: 'count',
linearColorScheme: ['#000000', '#ffffff'],
breakPoints: ['0', '1', '10'],
linear_color_scheme: ['#000000', '#ffffff'],
break_points: ['0', '1', '10'],
opacity: 100,
};
const features = [];

View File

@ -17,6 +17,7 @@
* under the License.
*/
import shortid from 'shortid';
import JSONbig from 'json-bigint';
import { t } from '@superset-ui/translation';
import { SupersetClient } from '@superset-ui/connection';
@ -128,9 +129,11 @@ export function fetchQueryResults(query) {
return SupersetClient.get({
endpoint: `/superset/results/${query.resultsKey}/`,
parseMethod: 'text',
})
.then(({ json = {} }) => {
dispatch(querySuccess(query, json));
.then(({ text = '{}' }) => {
const bigIntJson = JSONbig.parse(text);
dispatch(querySuccess(query, bigIntJson));
})
.catch(response =>
getClientErrorObject(response).then((error) => {
@ -164,10 +167,12 @@ export function runQuery(query) {
endpoint: `/superset/sql_json/${window.location.search}`,
postPayload,
stringify: false,
parseMethod: 'text',
})
.then(({ json }) => {
.then(({ text = '{}' }) => {
if (!query.runAsync) {
dispatch(querySuccess(query, json));
const bigIntJson = JSONbig.parse(text);
dispatch(querySuccess(query, bigIntJson));
}
})
.catch(response =>

View File

@ -18,6 +18,8 @@
*/
import PropTypes from 'prop-types';
import React from 'react';
import { Alert } from 'react-bootstrap';
import { Logger, LOG_ACTIONS_RENDER_CHART_CONTAINER } from '../logger/LogUtils';
import Loading from '../components/Loading';
import RefreshChartOverlay from '../components/RefreshChartOverlay';
@ -68,12 +70,23 @@ class Chart extends React.PureComponent {
}
componentDidMount() {
if (this.props.triggerQuery) {
this.props.actions.runQuery(
this.props.formData,
false,
this.props.timeout,
this.props.chartId,
);
if (this.props.chartId > 0) {
// Load saved chart with a GET request
this.props.actions.getSavedChart(
this.props.formData,
false,
this.props.timeout,
this.props.chartId,
);
} else {
// Create chart with POST request
this.props.actions.postChartFormData(
this.props.formData,
false,
this.props.timeout,
this.props.chartId,
);
}
}
}
@ -122,7 +135,9 @@ class Chart extends React.PureComponent {
if (chartStatus === 'failed') {
return this.renderStackTraceMessage();
}
if (errorMessage) {
return <Alert bsStyle="warning">{errorMessage}</Alert>;
}
return (
<ErrorBoundary onError={this.handleRenderContainerFailure} showMessage={false}>
<div

View File

@ -23,7 +23,6 @@ import React from 'react';
import { ChartProps, SuperChart } from '@superset-ui/chart';
import { Tooltip } from 'react-bootstrap';
import { Logger, LOG_ACTIONS_RENDER_CHART } from '../logger/LogUtils';
import transformBigNumber from './transformBigNumber';
const propTypes = {
annotationData: PropTypes.object,
@ -68,7 +67,6 @@ class ChartRenderer extends React.Component {
this.handleAddFilter = this.handleAddFilter.bind(this);
this.handleRenderSuccess = this.handleRenderSuccess.bind(this);
this.handleRenderFailure = this.handleRenderFailure.bind(this);
this.preTransformProps = this.preTransformProps.bind(this);
}
shouldComponentUpdate(nextProps, nextState) {
@ -87,7 +85,8 @@ class ChartRenderer extends React.Component {
nextProps.height !== this.props.height ||
nextProps.width !== this.props.width ||
nextState.tooltip !== this.state.tooltip ||
nextProps.triggerRender) {
nextProps.triggerRender ||
nextProps.formData.color_scheme !== this.props.formData.color_scheme) {
return true;
}
}
@ -166,18 +165,6 @@ class ChartRenderer extends React.Component {
}
}
preTransformProps(chartProps) {
const payload = chartProps.payload;
const data = transformBigNumber(payload.data);
return new ChartProps({
...chartProps,
payload: {
...payload,
data,
},
});
}
renderTooltip() {
const { tooltip } = this.state;
if (tooltip && tooltip.content) {
@ -212,9 +199,8 @@ class ChartRenderer extends React.Component {
const isLoading = chartStatus === 'loading';
const skipChartRendering = isLoading || !!chartAlert;
const skipChartRendering = isLoading || !!chartAlert || chartStatus === null;
this.renderStartTime = Logger.getTimestamp();
return (
<React.Fragment>
{this.renderTooltip()}
@ -223,7 +209,6 @@ class ChartRenderer extends React.Component {
className={`${snakeCase(vizType)}`}
chartType={vizType}
chartProps={skipChartRendering ? null : this.prepareChartProps()}
preTransformProps={this.preTransformProps}
onRenderSuccess={this.handleRenderSuccess}
onRenderFailure={this.handleRenderFailure}
/>

View File

@ -165,14 +165,14 @@ export function addChart(chart, key) {
return { type: ADD_CHART, chart, key };
}
export const RUN_QUERY = 'RUN_QUERY';
export function runQuery(formData, force = false, timeout = 60, key) {
export function exploreJSON(formData, force = false, timeout = 60, key, method) {
return (dispatch) => {
const { url, payload } = getExploreUrlAndPayload({
formData,
endpointType: 'json',
force,
allowDomainSharding: true,
method,
});
const logStart = Logger.getTimestamp();
const controller = new AbortController();
@ -193,7 +193,9 @@ export function runQuery(formData, force = false, timeout = 60, key) {
credentials: 'include',
};
}
const queryPromise = SupersetClient.post(querySettings)
const clientMethod = method === 'GET' ? SupersetClient.get : SupersetClient.post;
const queryPromise = clientMethod(querySettings)
.then(({ json }) => {
dispatch(logEvent(LOG_ACTIONS_LOAD_CHART, {
slice_id: key,
@ -246,6 +248,32 @@ export function runQuery(formData, force = false, timeout = 60, key) {
};
}
export const GET_SAVED_CHART = 'GET_SAVED_CHART';
export function getSavedChart(formData, force = false, timeout = 60, key) {
/*
* Perform a GET request to `/explore_json`.
*
* This will return the payload of a saved chart, optionally filtered by
* ad-hoc or extra filters from dashboards. Eg:
*
* GET /explore_json?{"chart_id":1}
* GET /explore_json?{"chart_id":1,"extra_filters":"..."}
*
*/
return exploreJSON(formData, force, timeout, key, 'GET');
}
export const POST_CHART_FORM_DATA = 'POST_CHART_FORM_DATA';
export function postChartFormData(formData, force = false, timeout = 60, key) {
/*
* Perform a POST request to `/explore_json`.
*
* This will post the form data to the endpoint, returning a new chart.
*
*/
return exploreJSON(formData, force, timeout, key, 'POST');
}
export function redirectSQLLab(formData) {
return (dispatch) => {
const { url } = getExploreUrlAndPayload({ formData, endpointType: 'query' });
@ -272,6 +300,6 @@ export function refreshChart(chart, force, timeout) {
if (!chart.latestQueryFormData || Object.keys(chart.latestQueryFormData).length === 0) {
return;
}
dispatch(runQuery(chart.latestQueryFormData, force, timeout, chart.id));
dispatch(postChartFormData(chart.latestQueryFormData, force, timeout, chart.id));
};
}

View File

@ -47,7 +47,6 @@ export default function chartReducer(charts = {}, action) {
return { ...state,
chartStatus: 'success',
queryResponse: action.queryResponse,
chartUpdateEndTime: now(),
chartAlert: null,
};
},
@ -72,6 +71,7 @@ export default function chartReducer(charts = {}, action) {
[actions.CHART_RENDERING_SUCCEEDED](state) {
return { ...state,
chartStatus: 'rendered',
chartUpdateEndTime: now(),
};
},
[actions.CHART_RENDERING_FAILED](state) {

View File

@ -1,45 +0,0 @@
/**
* 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.
*/
// This method transforms any BigNumber object in the given payload to its
// 64-bit float representation. It is a temporary fix so charts receive
// floats instead of BigNumber instances in their props to properly render.
import BigNumber from 'bignumber.js';
export default function transform(payload) {
if (!payload) {
return payload;
} else if (BigNumber.isBigNumber(payload)) {
return payload.toNumber();
} else if (payload.constructor === Object) {
for (const key in payload) {
if (payload.hasOwnProperty(key)) {
// Modify in place to prevent creating large payloads
// eslint-disable-next-line no-param-reassign
payload[key] = transform(payload[key]);
}
}
} else if (payload.constructor === Array) {
payload.forEach((elem, idx) => {
// Modify in place to prevent creating large payloads
// eslint-disable-next-line no-param-reassign
payload[idx] = transform(elem);
});
}
return payload;
}

View File

@ -209,7 +209,8 @@ export function undoLayoutAction() {
if (
dashboardLayout.past.length === 0 &&
!dashboardState.maxUndoHistoryExceeded
!dashboardState.maxUndoHistoryExceeded &&
!dashboardState.updatedColorScheme
) {
dispatch(setUnsavedChanges(false));
}

View File

@ -226,9 +226,9 @@ export function startPeriodicRender(interval) {
};
}
export const TOGGLE_BUILDER_PANE = 'TOGGLE_BUILDER_PANE';
export function toggleBuilderPane() {
return { type: TOGGLE_BUILDER_PANE };
export const SHOW_BUILDER_PANE = 'SHOW_BUILDER_PANE';
export function showBuilderPane(builderPaneType) {
return { type: SHOW_BUILDER_PANE, builderPaneType };
}
export function addSliceToDashboard(id) {
@ -267,6 +267,18 @@ export function removeSliceFromDashboard(id) {
};
}
export const SET_COLOR_SCHEME = 'SET_COLOR_SCHEME';
export function setColorScheme(colorScheme) {
return { type: SET_COLOR_SCHEME, colorScheme };
}
export function setColorSchemeAndUnsavedChanges(colorScheme) {
return dispatch => {
dispatch(setColorScheme(colorScheme));
dispatch(setUnsavedChanges(true));
};
}
// Undo history ---------------------------------------------------------------
export const SET_MAX_UNDO_HISTORY_EXCEEDED = 'SET_MAX_UNDO_HISTORY_EXCEEDED';
export function setMaxUndoHistoryExceeded(maxUndoHistoryExceeded = true) {

View File

@ -19,49 +19,37 @@
/* eslint-env browser */
import PropTypes from 'prop-types';
import React from 'react';
import cx from 'classnames';
import { StickyContainer, Sticky } from 'react-sticky';
import { ParentSize } from '@vx/responsive';
import { t } from '@superset-ui/translation';
import NewColumn from './gridComponents/new/NewColumn';
import NewDivider from './gridComponents/new/NewDivider';
import NewHeader from './gridComponents/new/NewHeader';
import NewRow from './gridComponents/new/NewRow';
import NewTabs from './gridComponents/new/NewTabs';
import NewMarkdown from './gridComponents/new/NewMarkdown';
import SliceAdder from '../containers/SliceAdder';
const SUPERSET_HEADER_HEIGHT = 59;
import InsertComponentPane, {
SUPERSET_HEADER_HEIGHT,
} from './InsertComponentPane';
import ColorComponentPane from './ColorComponentPane';
import { BUILDER_PANE_TYPE } from '../util/constants';
const propTypes = {
topOffset: PropTypes.number,
toggleBuilderPane: PropTypes.func.isRequired,
showBuilderPane: PropTypes.func.isRequired,
builderPaneType: PropTypes.string.isRequired,
setColorSchemeAndUnsavedChanges: PropTypes.func.isRequired,
colorScheme: PropTypes.string,
};
const defaultProps = {
topOffset: 0,
colorScheme: undefined,
};
class BuilderComponentPane extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
slideDirection: 'slide-out',
};
this.openSlicesPane = this.slide.bind(this, 'slide-in');
this.closeSlicesPane = this.slide.bind(this, 'slide-out');
}
slide(direction) {
this.setState({
slideDirection: direction,
});
}
render() {
const { topOffset } = this.props;
const {
topOffset,
builderPaneType,
showBuilderPane,
setColorSchemeAndUnsavedChanges,
colorScheme,
} = this.props;
return (
<div
className="dashboard-builder-sidepane"
@ -78,56 +66,22 @@ class BuilderComponentPane extends React.PureComponent {
className="viewport"
style={isSticky ? { ...style, top: topOffset } : null}
>
<div
className={cx(
'slider-container',
this.state.slideDirection,
)}
>
<div className="component-layer slide-content">
<div className="dashboard-builder-sidepane-header">
<span>{t('Insert components')}</span>
<i
className="fa fa-times trigger"
onClick={this.props.toggleBuilderPane}
role="none"
/>
</div>
<div
className="new-component static"
role="none"
onClick={this.openSlicesPane}
>
<div className="new-component-placeholder fa fa-area-chart" />
<div className="new-component-label">
{t('Your charts & filters')}
</div>
<i className="fa fa-arrow-right trigger" />
</div>
<NewTabs />
<NewRow />
<NewColumn />
<NewHeader />
<NewMarkdown />
<NewDivider />
</div>
<div className="slices-layer slide-content">
<div
className="dashboard-builder-sidepane-header"
onClick={this.closeSlicesPane}
role="none"
>
<i className="fa fa-arrow-left trigger" />
<span>{t('Your charts and filters')}</span>
</div>
<SliceAdder
height={
height + (isSticky ? SUPERSET_HEADER_HEIGHT : 0)
}
/>
</div>
</div>
{builderPaneType === BUILDER_PANE_TYPE.ADD_COMPONENTS && (
<InsertComponentPane
height={height}
isSticky={isSticky}
showBuilderPane={showBuilderPane}
/>
)}
{builderPaneType === BUILDER_PANE_TYPE.COLORS && (
<ColorComponentPane
showBuilderPane={showBuilderPane}
setColorSchemeAndUnsavedChanges={
setColorSchemeAndUnsavedChanges
}
colorScheme={colorScheme}
/>
)}
</div>
)}
</Sticky>

View File

@ -0,0 +1,107 @@
/**
* 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.
*/
/* eslint-env browser */
import PropTypes from 'prop-types';
import React from 'react';
import { getCategoricalSchemeRegistry } from '@superset-ui/color';
import { t } from '@superset-ui/translation';
import ColorSchemeControl from '../../explore/components/controls/ColorSchemeControl';
import { BUILDER_PANE_TYPE } from '../util/constants';
const propTypes = {
showBuilderPane: PropTypes.func.isRequired,
setColorSchemeAndUnsavedChanges: PropTypes.func.isRequired,
colorScheme: PropTypes.string,
};
const defaultProps = {
colorScheme: undefined,
};
class ColorComponentPane extends React.PureComponent {
constructor(props) {
super(props);
this.state = { hovered: false };
this.categoricalSchemeRegistry = getCategoricalSchemeRegistry();
this.getChoices = this.getChoices.bind(this);
this.getSchemes = this.getSchemes.bind(this);
this.onCloseButtonClick = this.onCloseButtonClick.bind(this);
this.onMouseEnter = this.setHover.bind(this, true);
this.onMouseLeave = this.setHover.bind(this, false);
}
onCloseButtonClick() {
this.props.showBuilderPane(BUILDER_PANE_TYPE.NONE);
}
getChoices() {
return this.categoricalSchemeRegistry.keys().map(s => [s, s]);
}
getSchemes() {
return this.categoricalSchemeRegistry.getMap();
}
setHover(hovered) {
this.setState({ hovered });
}
render() {
const { setColorSchemeAndUnsavedChanges, colorScheme } = this.props;
return (
<div className="slider-container">
<div className="component-layer slide-content">
<div className="dashboard-builder-sidepane-header">
<span>{'Color Settings'}</span>
<i
className="fa fa-times trigger"
onClick={this.onCloseButtonClick}
role="none"
/>
</div>
<div
className="panel-body"
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>
<ColorSchemeControl
description={t(
"Any color palette selected here will override the colors applied to this dashboard's individual charts",
)}
label={t('Color Scheme')}
name="color_scheme"
onChange={setColorSchemeAndUnsavedChanges}
value={colorScheme}
choices={this.getChoices}
schemes={this.getSchemes}
hovered={this.state.hovered}
/>
</div>
</div>
</div>
);
}
}
ColorComponentPane.propTypes = propTypes;
ColorComponentPane.defaultProps = defaultProps;
export default ColorComponentPane;

View File

@ -40,7 +40,7 @@ const propTypes = {
actions: PropTypes.shape({
addSliceToDashboard: PropTypes.func.isRequired,
removeSliceFromDashboard: PropTypes.func.isRequired,
runQuery: PropTypes.func.isRequired,
postChartFormData: PropTypes.func.isRequired,
logEvent: PropTypes.func.isRequired,
}).isRequired,
dashboardInfo: dashboardInfoPropShape.isRequired,
@ -153,10 +153,12 @@ class Dashboard extends React.PureComponent {
chart,
dashboardMetadata: this.props.dashboardInfo.metadata,
filters: this.props.dashboardState.filters,
colorScheme: this.props.dashboardState.colorScheme,
colorNamespace: this.props.dashboardState.colorNamespace,
sliceId: chart.id,
});
this.props.actions.runQuery(
this.props.actions.postChartFormData(
updatedFormData,
false,
this.props.timeout,

View File

@ -39,6 +39,7 @@ import getDragDropManager from '../util/getDragDropManager';
import findTabIndexByComponentId from '../util/findTabIndexByComponentId';
import {
BUILDER_PANE_TYPE,
DASHBOARD_GRID_ID,
DASHBOARD_ROOT_ID,
DASHBOARD_ROOT_DEPTH,
@ -52,7 +53,10 @@ const propTypes = {
dashboardLayout: PropTypes.object.isRequired,
deleteTopLevelTabs: PropTypes.func.isRequired,
editMode: PropTypes.bool.isRequired,
showBuilderPane: PropTypes.bool,
showBuilderPane: PropTypes.func.isRequired,
builderPaneType: PropTypes.string.isRequired,
setColorSchemeAndUnsavedChanges: PropTypes.func.isRequired,
colorScheme: PropTypes.string,
handleComponentDrop: PropTypes.func.isRequired,
toggleBuilderPane: PropTypes.func.isRequired,
directPathToChild: PropTypes.arrayOf(PropTypes.string),
@ -61,6 +65,7 @@ const propTypes = {
const defaultProps = {
showBuilderPane: false,
directPathToChild: [],
colorScheme: undefined,
};
class DashboardBuilder extends React.Component {
@ -116,7 +121,15 @@ class DashboardBuilder extends React.Component {
}
render() {
const { handleComponentDrop, dashboardLayout, editMode } = this.props;
const {
handleComponentDrop,
dashboardLayout,
editMode,
showBuilderPane,
builderPaneType,
setColorSchemeAndUnsavedChanges,
colorScheme,
} = this.props;
const { tabIndex } = this.state;
const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
const rootChildId = dashboardRoot.children[0];
@ -216,10 +229,13 @@ class DashboardBuilder extends React.Component {
)}
</ParentSize>
</div>
{this.props.editMode && this.props.showBuilderPane && (
{editMode && builderPaneType !== BUILDER_PANE_TYPE.NONE && (
<BuilderComponentPane
topOffset={HEADER_HEIGHT + (topLevelTabs ? TABS_HEIGHT : 0)}
toggleBuilderPane={this.props.toggleBuilderPane}
showBuilderPane={showBuilderPane}
builderPaneType={builderPaneType}
setColorSchemeAndUnsavedChanges={setColorSchemeAndUnsavedChanges}
colorScheme={colorScheme}
/>
)}
</div>

View File

@ -19,6 +19,7 @@
/* eslint-env browser */
import React from 'react';
import PropTypes from 'prop-types';
import { CategoricalColorNamespace } from '@superset-ui/color';
import { t } from '@superset-ui/translation';
import HeaderActionsDropdown from './HeaderActionsDropdown';
@ -29,6 +30,7 @@ import UndoRedoKeylisteners from './UndoRedoKeylisteners';
import { chartPropShape } from '../util/propShapes';
import {
BUILDER_PANE_TYPE,
UNDO_LIMIT,
SAVE_TYPE_OVERWRITE,
DASHBOARD_POSITION_DATA_LIMIT,
@ -52,6 +54,8 @@ const propTypes = {
filters: PropTypes.object.isRequired,
expandedSlices: PropTypes.object.isRequired,
css: PropTypes.string.isRequired,
colorNamespace: PropTypes.string,
colorScheme: PropTypes.string,
isStarred: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired,
onSave: PropTypes.func.isRequired,
@ -63,8 +67,8 @@ const propTypes = {
updateDashboardTitle: PropTypes.func.isRequired,
editMode: PropTypes.bool.isRequired,
setEditMode: PropTypes.func.isRequired,
showBuilderPane: PropTypes.bool.isRequired,
toggleBuilderPane: PropTypes.func.isRequired,
showBuilderPane: PropTypes.func.isRequired,
builderPaneType: PropTypes.string.isRequired,
updateCss: PropTypes.func.isRequired,
logEvent: PropTypes.func.isRequired,
hasUnsavedChanges: PropTypes.bool.isRequired,
@ -81,6 +85,11 @@ const propTypes = {
setRefreshFrequency: PropTypes.func.isRequired,
};
const defaultProps = {
colorNamespace: undefined,
colorScheme: undefined,
};
class Header extends React.PureComponent {
static discardChanges() {
window.location.reload();
@ -96,6 +105,10 @@ class Header extends React.PureComponent {
this.handleChangeText = this.handleChangeText.bind(this);
this.handleCtrlZ = this.handleCtrlZ.bind(this);
this.handleCtrlY = this.handleCtrlY.bind(this);
this.onInsertComponentsButtonClick = this.onInsertComponentsButtonClick.bind(
this,
);
this.onColorsButtonClick = this.onColorsButtonClick.bind(this);
this.toggleEditMode = this.toggleEditMode.bind(this);
this.forceRefresh = this.forceRefresh.bind(this);
this.startPeriodicRender = this.startPeriodicRender.bind(this);
@ -128,25 +141,12 @@ class Header extends React.PureComponent {
clearTimeout(this.ctrlZTimeout);
}
forceRefresh() {
if (!this.props.isLoading) {
const chartList = Object.values(this.props.charts);
this.props.logEvent(LOG_ACTIONS_FORCE_REFRESH_DASHBOARD, {
force: true,
interval: 0,
chartCount: chartList.length,
});
return this.props.fetchCharts(chartList, true);
}
return false;
onInsertComponentsButtonClick() {
this.props.showBuilderPane(BUILDER_PANE_TYPE.ADD_COMPONENTS);
}
startPeriodicRender(interval) {
this.props.logEvent(LOG_ACTIONS_PERIODIC_RENDER_DASHBOARD, {
force: true,
interval,
});
return this.props.startPeriodicRender(interval);
onColorsButtonClick() {
this.props.showBuilderPane(BUILDER_PANE_TYPE.COLORS);
}
handleChangeText(nextText) {
@ -177,6 +177,27 @@ class Header extends React.PureComponent {
});
}
forceRefresh() {
if (!this.props.isLoading) {
const chartList = Object.values(this.props.charts);
this.props.logEvent(LOG_ACTIONS_FORCE_REFRESH_DASHBOARD, {
force: true,
interval: 0,
chartCount: chartList.length,
});
return this.props.fetchCharts(chartList, true);
}
return false;
}
startPeriodicRender(interval) {
this.props.logEvent(LOG_ACTIONS_PERIODIC_RENDER_DASHBOARD, {
force: true,
interval,
});
return this.props.startPeriodicRender(interval);
}
toggleEditMode() {
this.props.logEvent(LOG_ACTIONS_TOGGLE_EDIT_DASHBOARD, {
edit_mode: !this.props.editMode,
@ -190,16 +211,28 @@ class Header extends React.PureComponent {
layout: positions,
expandedSlices,
css,
colorNamespace,
colorScheme,
filters,
dashboardInfo,
refreshFrequency,
} = this.props;
const scale = CategoricalColorNamespace.getScale(
colorScheme,
colorNamespace,
);
const labelColors = scale.getColorMap();
const data = {
positions,
expanded_slices: expandedSlices,
css,
color_namespace: colorNamespace,
color_scheme: colorScheme,
label_colors: labelColors,
dashboard_title: dashboardTitle,
default_filters: safeStringify(filters),
refresh_frequency: refreshFrequency,
};
// make sure positions data less than DB storage limitation:
@ -229,6 +262,8 @@ class Header extends React.PureComponent {
filters,
expandedSlices,
css,
colorNamespace,
colorScheme,
onUndo,
onRedo,
undoLength,
@ -237,7 +272,7 @@ class Header extends React.PureComponent {
onSave,
updateCss,
editMode,
showBuilderPane,
builderPaneType,
dashboardInfo,
hasUnsavedChanges,
isLoading,
@ -294,10 +329,22 @@ class Header extends React.PureComponent {
)}
{editMode && (
<Button bsSize="small" onClick={this.props.toggleBuilderPane}>
{showBuilderPane
? t('Hide components')
: t('Insert components')}
<Button
active={builderPaneType === BUILDER_PANE_TYPE.ADD_COMPONENTS}
bsSize="small"
onClick={this.onInsertComponentsButtonClick}
>
{t('Insert components')}
</Button>
)}
{editMode && (
<Button
active={builderPaneType === BUILDER_PANE_TYPE.COLORS}
bsSize="small"
onClick={this.onColorsButtonClick}
>
{t('Colors')}
</Button>
)}
@ -351,6 +398,8 @@ class Header extends React.PureComponent {
filters={filters}
expandedSlices={expandedSlices}
css={css}
colorNamespace={colorNamespace}
colorScheme={colorScheme}
onSave={onSave}
onChange={onChange}
forceRefreshAllCharts={this.forceRefresh}
@ -371,5 +420,6 @@ class Header extends React.PureComponent {
}
Header.propTypes = propTypes;
Header.defaultProps = defaultProps;
export default Header;

View File

@ -37,6 +37,8 @@ const propTypes = {
dashboardTitle: PropTypes.string.isRequired,
hasUnsavedChanges: PropTypes.bool.isRequired,
css: PropTypes.string.isRequired,
colorNamespace: PropTypes.string,
colorScheme: PropTypes.string,
onChange: PropTypes.func.isRequired,
updateCss: PropTypes.func.isRequired,
forceRefreshAllCharts: PropTypes.func.isRequired,
@ -53,7 +55,10 @@ const propTypes = {
onSave: PropTypes.func.isRequired,
};
const defaultProps = {};
const defaultProps = {
colorNamespace: undefined,
colorScheme: undefined,
};
class HeaderActionsDropdown extends React.PureComponent {
static discardChanges() {
@ -111,6 +116,8 @@ class HeaderActionsDropdown extends React.PureComponent {
refreshFrequency,
editMode,
css,
colorNamespace,
colorScheme,
hasUnsavedChanges,
layout,
filters,
@ -145,6 +152,8 @@ class HeaderActionsDropdown extends React.PureComponent {
expandedSlices={expandedSlices}
refreshFrequency={refreshFrequency}
css={css}
colorNamespace={colorNamespace}
colorScheme={colorScheme}
onSave={onSave}
isMenuItem
triggerNode={<span>{t('Save as')}</span>}
@ -168,11 +177,13 @@ class HeaderActionsDropdown extends React.PureComponent {
<MenuItem onClick={forceRefreshAllCharts} disabled={isLoading}>
{t('Force refresh dashboard')}
</MenuItem>
<RefreshIntervalModal
refreshFrequency={refreshFrequency}
onChange={this.changeRefreshInterval}
triggerNode={<span>{t('Set auto-refresh interval')}</span>}
/>
{editMode && (
<RefreshIntervalModal
refreshFrequency={refreshFrequency}
onChange={this.changeRefreshInterval}
triggerNode={<span>{t('Set auto-refresh interval')}</span>}
/>
)}
{editMode && (
<MenuItem target="_blank" href={`/dashboard/edit/${dashboardId}`}>
{t('Edit dashboard metadata')}

View File

@ -0,0 +1,118 @@
/**
* 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.
*/
/* eslint-env browser */
import PropTypes from 'prop-types';
import React from 'react';
import cx from 'classnames';
import { t } from '@superset-ui/translation';
import NewColumn from './gridComponents/new/NewColumn';
import NewDivider from './gridComponents/new/NewDivider';
import NewHeader from './gridComponents/new/NewHeader';
import NewRow from './gridComponents/new/NewRow';
import NewTabs from './gridComponents/new/NewTabs';
import NewMarkdown from './gridComponents/new/NewMarkdown';
import SliceAdder from '../containers/SliceAdder';
import { BUILDER_PANE_TYPE } from '../util/constants';
export const SUPERSET_HEADER_HEIGHT = 59;
const propTypes = {
height: PropTypes.number.isRequired,
isSticky: PropTypes.bool.isRequired,
showBuilderPane: PropTypes.func.isRequired,
};
class InsertComponentPane extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
slideDirection: 'slide-out',
};
this.onCloseButtonClick = this.onCloseButtonClick.bind(this);
this.openSlicesPane = this.slide.bind(this, 'slide-in');
this.closeSlicesPane = this.slide.bind(this, 'slide-out');
}
onCloseButtonClick() {
this.props.showBuilderPane(BUILDER_PANE_TYPE.NONE);
}
slide(direction) {
this.setState({
slideDirection: direction,
});
}
render() {
return (
<div className={cx('slider-container', this.state.slideDirection)}>
<div className="component-layer slide-content">
<div className="dashboard-builder-sidepane-header">
<span>{t('Insert components')}</span>
<i
className="fa fa-times trigger"
onClick={this.onCloseButtonClick}
role="none"
/>
</div>
<div
className="new-component static"
role="none"
onClick={this.openSlicesPane}
>
<div className="new-component-placeholder fa fa-area-chart" />
<div className="new-component-label">
{t('Your charts & filters')}
</div>
<i className="fa fa-arrow-right trigger" />
</div>
<NewTabs />
<NewRow />
<NewColumn />
<NewHeader />
<NewMarkdown />
<NewDivider />
</div>
<div className="slices-layer slide-content">
<div
className="dashboard-builder-sidepane-header"
onClick={this.closeSlicesPane}
role="none"
>
<i className="fa fa-arrow-left trigger" />
<span>{t('Your charts and filters')}</span>
</div>
<SliceAdder
height={
this.props.height +
(this.props.isSticky ? SUPERSET_HEADER_HEIGHT : 0)
}
/>
</div>
</div>
);
}
}
InsertComponentPane.propTypes = propTypes;
export default InsertComponentPane;

View File

@ -20,6 +20,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, FormControl, FormGroup, Radio } from 'react-bootstrap';
import { CategoricalColorNamespace } from '@superset-ui/color';
import { t } from '@superset-ui/translation';
import ModalTrigger from '../../components/ModalTrigger';
@ -38,6 +39,8 @@ const propTypes = {
triggerNode: PropTypes.node.isRequired,
filters: PropTypes.object.isRequired,
css: PropTypes.string.isRequired,
colorNamespace: PropTypes.string,
colorScheme: PropTypes.string,
onSave: PropTypes.func.isRequired,
isMenuItem: PropTypes.bool,
canOverwrite: PropTypes.bool.isRequired,
@ -47,6 +50,8 @@ const propTypes = {
const defaultProps = {
isMenuItem: false,
saveType: SAVE_TYPE_OVERWRITE,
colorNamespace: undefined,
colorScheme: undefined,
};
class SaveModal extends React.PureComponent {
@ -93,15 +98,25 @@ class SaveModal extends React.PureComponent {
dashboardTitle,
layout: positions,
css,
colorNamespace,
colorScheme,
expandedSlices,
filters,
dashboardId,
refreshFrequency,
} = this.props;
const scale = CategoricalColorNamespace.getScale(
colorScheme,
colorNamespace,
);
const labelColors = scale.getColorMap();
const data = {
positions,
css,
color_namespace: colorNamespace,
color_scheme: colorScheme,
label_colors: labelColors,
expanded_slices: expandedSlices,
dashboard_title:
saveType === SAVE_TYPE_NEWDASHBOARD ? newDashName : dashboardTitle,

View File

@ -44,7 +44,7 @@ function mapStateToProps(
) {
const { id } = ownProps;
const chart = chartQueries[id] || {};
const { filters } = dashboardState;
const { filters, colorScheme, colorNamespace } = dashboardState;
return {
chart,
@ -59,6 +59,8 @@ function mapStateToProps(
chart,
dashboardMetadata: dashboardInfo.metadata,
filters,
colorScheme,
colorNamespace,
sliceId: id,
}),
editMode: dashboardState.editMode,

View File

@ -25,7 +25,7 @@ import {
addSliceToDashboard,
removeSliceFromDashboard,
} from '../actions/dashboardState';
import { runQuery } from '../../chart/chartAction';
import { postChartFormData } from '../../chart/chartAction';
import { logEvent } from '../../logger/actions';
import getLoadStatsPerTopLevelComponent from '../util/logging/getLoadStatsPerTopLevelComponent';
@ -64,7 +64,7 @@ function mapDispatchToProps(dispatch) {
{
addSliceToDashboard,
removeSliceFromDashboard,
runQuery,
postChartFormData,
logEvent,
},
dispatch,

View File

@ -20,7 +20,10 @@ import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import DashboardBuilder from '../components/DashboardBuilder';
import { toggleBuilderPane } from '../actions/dashboardState';
import {
setColorSchemeAndUnsavedChanges,
showBuilderPane,
} from '../actions/dashboardState';
import {
deleteTopLevelTabs,
handleComponentDrop,
@ -32,6 +35,8 @@ function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState }) {
editMode: dashboardState.editMode,
showBuilderPane: dashboardState.showBuilderPane,
directPathToChild: dashboardState.directPathToChild,
builderPaneType: dashboardState.builderPaneType,
colorScheme: dashboardState.colorScheme,
};
}
@ -40,7 +45,8 @@ function mapDispatchToProps(dispatch) {
{
deleteTopLevelTabs,
handleComponentDrop,
toggleBuilderPane,
showBuilderPane,
setColorSchemeAndUnsavedChanges,
},
dispatch,
);

View File

@ -24,7 +24,7 @@ import isDashboardLoading from '../util/isDashboardLoading';
import {
setEditMode,
toggleBuilderPane,
showBuilderPane,
fetchFaveStar,
saveFaveStar,
fetchCharts,
@ -71,6 +71,8 @@ function mapStateToProps({
expandedSlices: dashboardState.expandedSlices,
refreshFrequency: dashboardState.refreshFrequency,
css: dashboardState.css,
colorNamespace: dashboardState.colorNamespace,
colorScheme: dashboardState.colorScheme,
charts,
userId: dashboardInfo.userId,
isStarred: !!dashboardState.isStarred,
@ -78,7 +80,7 @@ function mapStateToProps({
hasUnsavedChanges: !!dashboardState.hasUnsavedChanges,
maxUndoHistoryExceeded: !!dashboardState.maxUndoHistoryExceeded,
editMode: !!dashboardState.editMode,
showBuilderPane: !!dashboardState.showBuilderPane,
builderPaneType: dashboardState.builderPaneType,
};
}
@ -91,7 +93,7 @@ function mapDispatchToProps(dispatch) {
onUndo: undoLayoutAction,
onRedo: redoLayoutAction,
setEditMode,
toggleBuilderPane,
showBuilderPane,
fetchFaveStar,
saveFaveStar,
fetchCharts,

View File

@ -23,15 +23,17 @@ import {
ON_CHANGE,
ON_SAVE,
REMOVE_SLICE,
SET_COLOR_SCHEME,
SET_EDIT_MODE,
SET_MAX_UNDO_HISTORY_EXCEEDED,
SET_UNSAVED_CHANGES,
TOGGLE_BUILDER_PANE,
SHOW_BUILDER_PANE,
TOGGLE_EXPAND_SLICE,
TOGGLE_FAVE_STAR,
UPDATE_CSS,
SET_REFRESH_FREQUENCY,
} from '../actions/dashboardState';
import { BUILDER_PANE_TYPE } from '../util/constants';
export default function dashboardStateReducer(state = {}, action) {
const actionHandlers = {
@ -73,15 +75,24 @@ export default function dashboardStateReducer(state = {}, action) {
return {
...state,
editMode: action.editMode,
showBuilderPane: !!action.editMode,
builderPaneType: action.editMode
? BUILDER_PANE_TYPE.ADD_COMPONENTS
: BUILDER_PANE_TYPE.NONE,
};
},
[SET_MAX_UNDO_HISTORY_EXCEEDED]() {
const { maxUndoHistoryExceeded = true } = action.payload;
return { ...state, maxUndoHistoryExceeded };
},
[TOGGLE_BUILDER_PANE]() {
return { ...state, showBuilderPane: !state.showBuilderPane };
[SHOW_BUILDER_PANE]() {
return { ...state, builderPaneType: action.builderPaneType };
},
[SET_COLOR_SCHEME]() {
return {
...state,
colorScheme: action.colorScheme,
updatedColorScheme: true,
};
},
[TOGGLE_EXPAND_SLICE]() {
const updatedExpandedSlices = { ...state.expandedSlices };
@ -102,6 +113,8 @@ export default function dashboardStateReducer(state = {}, action) {
hasUnsavedChanges: false,
maxUndoHistoryExceeded: false,
editMode: false,
builderPaneType: BUILDER_PANE_TYPE.NONE,
updatedColorScheme: false,
};
},
@ -149,7 +162,11 @@ export default function dashboardStateReducer(state = {}, action) {
return { ...state, hasUnsavedChanges };
},
[SET_REFRESH_FREQUENCY]() {
return { ...state, refreshFrequency: action.refreshFrequency };
return {
...state,
refreshFrequency: action.refreshFrequency,
hasUnsavedChanges: true,
};
},
};

View File

@ -17,6 +17,7 @@
* under the License.
*/
/* eslint-disable camelcase */
import { isString } from 'lodash';
import shortid from 'shortid';
import { CategoricalColorNamespace } from '@superset-ui/color';
@ -28,6 +29,7 @@ import findFirstParentContainerId from '../util/findFirstParentContainer';
import getEmptyLayout from '../util/getEmptyLayout';
import newComponentFactory from '../util/newComponentFactory';
import {
BUILDER_PANE_TYPE,
DASHBOARD_HEADER_ID,
GRID_DEFAULT_CHART_WIDTH,
GRID_COLUMN_COUNT,
@ -55,9 +57,16 @@ export default function(bootstrapData) {
// Priming the color palette with user's label-color mapping provided in
// the dashboard's JSON metadata
if (dashboard.metadata && dashboard.metadata.label_colors) {
const colorMap = dashboard.metadata.label_colors;
const scheme = dashboard.metadata.color_scheme;
const namespace = dashboard.metadata.color_namespace;
const colorMap = isString(dashboard.metadata.label_colors)
? JSON.parse(dashboard.metadata.label_colors)
: dashboard.metadata.label_colors;
Object.keys(colorMap).forEach(label => {
CategoricalColorNamespace.getScale().setColor(label, colorMap[label]);
CategoricalColorNamespace.getScale(scheme, namespace).setColor(
label,
colorMap[label],
);
});
}
@ -201,8 +210,13 @@ export default function(bootstrapData) {
expandedSlices: dashboard.metadata.expanded_slices || {},
refreshFrequency: dashboard.metadata.refresh_frequency || 0,
css: dashboard.css || '',
colorNamespace: dashboard.metadata.color_namespace,
colorScheme: dashboard.metadata.color_scheme,
editMode: dashboard.dash_edit_perm && editMode,
showBuilderPane: dashboard.dash_edit_perm && editMode,
builderPaneType:
dashboard.dash_edit_perm && editMode
? BUILDER_PANE_TYPE.ADD_COMPONENTS
: BUILDER_PANE_TYPE.NONE,
hasUnsavedChanges: false,
maxUndoHistoryExceeded: false,
},

View File

@ -185,4 +185,18 @@
outline: none;
}
}
.color-scheme-container {
list-style: none;
margin: 0;
padding: 0;
display: flex;
align-items: center;
}
.color-scheme-container li {
flex-basis: 9px;
height: 10px;
margin: 9px 1px;
}
}

View File

@ -120,7 +120,14 @@ body {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
& > :not(:last-child) {
& > :nth-child(3) {
border-radius: 2px 0px 0px 2px;
border-right: none;
}
& > :nth-child(4) {
border-radius: 0px 2px 2px 0px;
}
& > :not(:nth-child(3)):not(:last-child) {
margin-right: 8px;
}
}

View File

@ -16,6 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
import { isEqual } from 'lodash';
import { CategoricalColorNamespace } from '@superset-ui/color';
import getEffectiveExtraFilters from './getEffectiveExtraFilters';
// We cache formData objects so that our connected container components don't always trigger
@ -28,12 +30,22 @@ export default function getFormDataWithExtraFilters({
chart = {},
dashboardMetadata,
filters,
colorScheme,
colorNamespace,
sliceId,
}) {
// Propagate color mapping to chart
const scale = CategoricalColorNamespace.getScale(colorScheme, colorNamespace);
const labelColors = scale.getColorMap();
// if dashboard metadata + filters have not changed, use cache if possible
if (
(cachedDashboardMetadataByChart[sliceId] || {}) === dashboardMetadata &&
(cachedFiltersByChart[sliceId] || {}) === filters &&
(colorScheme == null ||
cachedFormdataByChart[sliceId].color_scheme === colorScheme) &&
cachedFormdataByChart[sliceId].color_namespace === colorNamespace &&
isEqual(cachedFormdataByChart[sliceId].label_colors, labelColors) &&
!!cachedFormdataByChart[sliceId]
) {
return cachedFormdataByChart[sliceId];
@ -41,6 +53,8 @@ export default function getFormDataWithExtraFilters({
const formData = {
...chart.formData,
...(colorScheme && { color_scheme: colorScheme }),
label_colors: labelColors,
extra_filters: getEffectiveExtraFilters({
dashboardMetadata,
filters,

View File

@ -62,3 +62,10 @@ export const SAVE_TYPE_NEWDASHBOARD = 'newDashboard';
// default dashboard layout data size limit
// could be overwritten by server-side config
export const DASHBOARD_POSITION_DATA_LIMIT = 65535;
// Dashboard pane types
export const BUILDER_PANE_TYPE = {
NONE: 'NONE',
ADD_COMPONENTS: 'ADD_COMPONENTS',
COLORS: 'COLORS',
};

View File

@ -72,7 +72,10 @@ export const dashboardStatePropShape = PropTypes.shape({
filters: PropTypes.object.isRequired,
expandedSlices: PropTypes.object,
editMode: PropTypes.bool,
showBuilderPane: PropTypes.bool,
builderPaneType: PropTypes.string.isRequired,
colorNamespace: PropTypes.string,
colorScheme: PropTypes.string,
updatedColorScheme: PropTypes.bool,
hasUnsavedChanges: PropTypes.bool,
});

View File

@ -37,7 +37,6 @@ const propTypes = {
description: PropTypes.string,
tooltipOnClick: PropTypes.func,
places: PropTypes.number,
validators: PropTypes.array,
validationErrors: PropTypes.array,
renderTrigger: PropTypes.bool,
rightNode: PropTypes.node,
@ -54,7 +53,6 @@ const propTypes = {
const defaultProps = {
renderTrigger: false,
validators: [],
hidden: false,
validationErrors: [],
};
@ -63,45 +61,14 @@ export default class Control extends React.PureComponent {
constructor(props) {
super(props);
this.state = { hovered: false };
this.validate = this.validate.bind(this);
this.onChange = this.onChange.bind(this);
}
componentDidMount() {
this.validateAndSetValue(this.props.value, []);
}
onChange(value, errors) {
this.validateAndSetValue(value, errors);
this.props.actions.setControlValue(this.props.name, value, errors);
}
setHover(hovered) {
this.setState({ hovered });
}
validateAndSetValue(value, errors) {
let validationErrors = this.props.validationErrors;
let currentErrors = this.validate(value);
if (errors && errors.length > 0) {
currentErrors = validationErrors.concat(errors);
}
if (validationErrors.length + currentErrors.length > 0) {
validationErrors = currentErrors;
}
if (value !== this.props.value || validationErrors !== this.props.validationErrors) {
this.props.actions.setControlValue(this.props.name, value, validationErrors);
}
}
validate(value) {
const validators = this.props.validators;
const validationErrors = [];
if (validators && validators.length > 0) {
validators.forEach((f) => {
const v = f(value);
if (v) {
validationErrors.push(v);
}
});
}
return validationErrors;
}
render() {
const ControlType = controlMap[this.props.type];
const divStyle = this.props.hidden ? { display: 'none' } : null;

View File

@ -50,8 +50,8 @@ const propTypes = {
};
class ExploreChartHeader extends React.PureComponent {
runQuery() {
this.props.actions.runQuery(this.props.form_data, true,
postChartFormData() {
this.props.actions.postChartFormData(this.props.form_data, true,
this.props.timeout, this.props.chart.id);
}
@ -142,7 +142,7 @@ class ExploreChartHeader extends React.PureComponent {
/>}
{chartFinished && queryResponse && queryResponse.is_cached &&
<CachedLabel
onClick={this.runQuery.bind(this)}
onClick={this.postChartFormData.bind(this)}
cachedTimestamp={queryResponse.cached_dttm}
/>}
<Timer

View File

@ -21,6 +21,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { t } from '@superset-ui/translation';
import ExploreChartPanel from './ExploreChartPanel';
import ControlPanelsContainer from './ControlPanelsContainer';
@ -94,6 +95,12 @@ class ExploreViewContainer extends React.Component {
document.addEventListener('keydown', this.handleKeydown);
this.addHistory({ isReplace: true });
this.props.actions.logEvent(LOG_ACTIONS_MOUNT_EXPLORER);
// Trigger the chart if there are no errors
const { chart } = this.props;
if (!this.hasErrors()) {
this.props.actions.triggerQuery(true, this.props.chart.id);
}
}
componentWillReceiveProps(nextProps) {
@ -209,7 +216,7 @@ class ExploreViewContainer extends React.Component {
triggerQueryIfNeeded() {
if (this.props.chart.triggerQuery && !this.hasErrors()) {
this.props.actions.runQuery(
this.props.actions.postChartFormData(
this.props.form_data,
false,
this.props.timeout,
@ -249,7 +256,8 @@ class ExploreViewContainer extends React.Component {
const formData = history.state;
if (formData && Object.keys(formData).length) {
this.props.actions.setExploreControls(formData);
this.props.actions.runQuery(formData, false, this.props.timeout, this.props.chart.id);
this.props.actions.postChartFormData(
formData, false, this.props.timeout, this.props.chart.id);
}
}
@ -265,12 +273,13 @@ class ExploreViewContainer extends React.Component {
renderErrorMessage() {
// Returns an error message as a node if any errors are in the store
const errors = [];
const ctrls = this.props.controls;
for (const controlName in this.props.controls) {
const control = this.props.controls[controlName];
if (control.validationErrors && control.validationErrors.length > 0) {
errors.push(
<div key={controlName}>
<strong>{`[ ${control.label} ] `}</strong>
{t('Control labeled ')}<strong>{` "${control.label}" `}</strong>
{control.validationErrors.join('. ')}
</div>,
);

View File

@ -0,0 +1,54 @@
/**
* 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 PropTypes from 'prop-types';
import React from 'react';
import { CategoricalColorNamespace } from '@superset-ui/color';
const propTypes = {
onChange: PropTypes.func,
value: PropTypes.object,
colorScheme: PropTypes.string,
colorNamespace: PropTypes.string,
};
const defaultProps = {
onChange: () => {},
value: {},
colorScheme: undefined,
colorNamespace: undefined,
};
export default class ColorMapControl extends React.PureComponent {
constructor(props) {
super(props);
Object.keys(this.props.value).forEach((label) => {
CategoricalColorNamespace.getScale(
this.props.colorScheme,
this.props.colorNamespace,
).setColor(label, this.props.value[label]);
});
}
render() {
return null;
}
}
ColorMapControl.propTypes = propTypes;
ColorMapControl.defaultProps = defaultProps;

View File

@ -21,6 +21,7 @@ import PropTypes from 'prop-types';
import { isFunction } from 'lodash';
import { Creatable } from 'react-select';
import ControlHeader from '../ControlHeader';
import TooltipWrapper from '../../../components/TooltipWrapper';
const propTypes = {
description: PropTypes.string,
@ -77,17 +78,22 @@ export default class ColorSchemeControl extends React.PureComponent {
}
return (
<ul className="color-scheme-container">
{colors.map((color, i) => (
<li
key={`${currentScheme.name}-${i}`}
style={{
backgroundColor: color,
border: `1px solid ${color === 'white' ? 'black' : color}`,
}}
>&nbsp;</li>
))}
</ul>
<TooltipWrapper
label={`${currentScheme.id}-tooltip`}
tooltip={currentScheme.label}
>
<ul className="color-scheme-container">
{colors.map((color, i) => (
<li
key={`${currentScheme.id}-${i}`}
style={{
backgroundColor: color,
border: `1px solid ${color === 'white' ? 'black' : color}`,
}}
>&nbsp;</li>
))}
</ul>
</TooltipWrapper>
);
}

View File

@ -32,6 +32,7 @@ import {
Radio,
Tab,
Tabs,
Tooltip,
} from 'react-bootstrap';
import Datetime from 'react-datetime';
import 'react-datetime/css/react-datetime.css';
@ -311,15 +312,30 @@ export default class DateFilterControl extends React.Component {
{grain}
</MenuItem>
));
const timeFrames = COMMON_TIME_FRAMES.map(timeFrame => (
<Radio
key={timeFrame.replace(' ', '').toLowerCase()}
checked={this.state.common === timeFrame}
onChange={() => this.setState(getStateFromCommonTimeFrame(timeFrame))}
>
{timeFrame}
</Radio>
));
const timeFrames = COMMON_TIME_FRAMES.map((timeFrame) => {
const nextState = getStateFromCommonTimeFrame(timeFrame);
return (
<OverlayTrigger
key={timeFrame}
placement="left"
overlay={
<Tooltip id={`tooltip-${timeFrame}`}>
{nextState.since}<br />{nextState.until}
</Tooltip>
}
>
<div>
<Radio
key={timeFrame.replace(' ', '').toLowerCase()}
checked={this.state.common === timeFrame}
onChange={() => this.setState(nextState)}
>
{timeFrame}
</Radio>
</div>
</OverlayTrigger>
);
});
return (
<Popover id="filter-popover" placement="top" positionTop={0}>
<div style={{ width: '250px' }}>

View File

@ -20,6 +20,7 @@ import AnnotationLayerControl from './AnnotationLayerControl';
import BoundsControl from './BoundsControl';
import CheckboxControl from './CheckboxControl';
import CollectionControl from './CollectionControl';
import ColorMapControl from './ColorMapControl';
import ColorPickerControl from './ColorPickerControl';
import ColorSchemeControl from './ColorSchemeControl';
import DatasourceControl from './DatasourceControl';
@ -45,6 +46,7 @@ const controlMap = {
BoundsControl,
CheckboxControl,
CollectionControl,
ColorMapControl,
ColorPickerControl,
ColorSchemeControl,
DatasourceControl,

View File

@ -30,7 +30,7 @@ export default {
controlSetRows: [
['show_brush', 'show_legend'],
['line_interpolation', 'stacked_style'],
['color_scheme'],
['color_scheme', 'label_colors'],
['rich_tooltip', 'show_controls'],
],
},

View File

@ -28,7 +28,7 @@ export default {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
['color_scheme', 'label_colors'],
['show_brush', 'show_legend', 'show_bar_value'],
['rich_tooltip', 'bar_stacked'],
['line_interpolation', 'show_controls'],

View File

@ -29,13 +29,21 @@ export default {
],
},
{
label: t('Chart Options'),
label: t('Options'),
expanded: true,
controlSetRows: [
['compare_lag', 'compare_suffix'],
['y_axis_format', null],
['show_trend_line', 'start_y_axis_at_zero'],
],
},
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_picker', null],
['header_font_size'],
['subheader_font_size'],
],
},
],
@ -43,5 +51,8 @@ export default {
y_axis_format: {
label: t('Number format'),
},
header_font_size: {
label: t('Big Number Font Size'),
},
},
};

View File

@ -29,17 +29,28 @@ export default {
],
},
{
label: t('Chart Options'),
label: t('Options'),
expanded: true,
controlSetRows: [
['subheader'],
['y_axis_format'],
],
},
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['header_font_size'],
['subheader_font_size'],
],
},
],
controlOverrides: {
y_axis_format: {
label: t('Number format'),
},
header_font_size: {
label: t('Big Number Font Size'),
},
},
};

View File

@ -34,7 +34,7 @@ export default {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
['color_scheme', 'label_colors'],
['whisker_options', 'x_ticks_layout'],
],
},

View File

@ -38,7 +38,7 @@ export default {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
['color_scheme', 'label_colors'],
['show_legend', null],
],
},
@ -62,9 +62,6 @@ export default {
},
],
controlOverrides: {
x_axis_format: {
default: '.3s',
},
color_scheme: {
renderTrigger: false,
},

View File

@ -37,7 +37,7 @@ export default {
expanded: true,
controlSetRows: [
['y_axis_format', null],
['color_scheme'],
['color_scheme', 'label_colors'],
],
},
],

View File

@ -28,7 +28,7 @@ export default {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
['color_scheme', 'label_colors'],
],
},
{

View File

@ -42,7 +42,7 @@ export default {
label: t('Arc'),
controlSetRows: [
['color_picker', 'target_color_picker'],
['dimension', 'color_scheme'],
['dimension', 'color_scheme', 'label_colors'],
['stroke_width', 'legend_position'],
],
},

View File

@ -62,7 +62,7 @@ export default {
label: t('Point Color'),
controlSetRows: [
['color_picker', 'legend_position'],
['dimension', 'color_scheme'],
['dimension', 'color_scheme', 'label_colors'],
],
},
{

View File

@ -36,7 +36,7 @@ export default {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
['color_scheme', 'label_colors'],
['show_legend', 'show_bar_value'],
['bar_stacked', 'order_bars'],
['y_axis_format', 'y_axis_label'],

View File

@ -27,7 +27,7 @@ export default {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
['color_scheme', 'label_colors'],
['x_axis_format'],
],
},

View File

@ -35,7 +35,7 @@ export default {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
['color_scheme', 'label_colors'],
['link_length'],
['x_axis_label', 'y_axis_label'],
['global_opacity'],

View File

@ -28,7 +28,7 @@ export default {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
['color_scheme', 'label_colors'],
['show_brush', 'send_time_range', 'show_legend'],
['rich_tooltip', 'show_markers'],
['line_interpolation'],

View File

@ -27,7 +27,7 @@ export default {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
['color_scheme', 'label_colors'],
['prefix_metric_with_slice_name', null],
['show_legend', 'show_markers'],
['line_interpolation', null],

View File

@ -33,7 +33,7 @@ export default {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
['color_scheme', 'label_colors'],
['number_format', 'date_time_format'],
['partition_limit', 'partition_threshold'],
['log_scale', 'equal_date_size'],

View File

@ -37,7 +37,7 @@ export default {
['pie_label_type', 'number_format'],
['donut', 'show_legend'],
['show_labels', 'labels_outside'],
['color_scheme'],
['color_scheme', 'label_colors'],
],
},
],

View File

@ -27,7 +27,7 @@ export default {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
['color_scheme', 'label_colors'],
['number_format', 'date_time_format'],
['rich_tooltip', 'rose_area_proportion'],
],

View File

@ -34,7 +34,7 @@ export default {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
['color_scheme', 'label_colors'],
],
},
],

View File

@ -35,7 +35,7 @@ export default {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
['color_scheme', 'label_colors'],
],
},
],

View File

@ -34,7 +34,7 @@ export default {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
['color_scheme', 'label_colors'],
['treemap_ratio'],
['number_format'],
],

View File

@ -37,7 +37,7 @@ export default {
controlSetRows: [
['size_from', 'size_to'],
['rotation'],
['color_scheme'],
['color_scheme', 'label_colors'],
],
},
],

View File

@ -42,7 +42,7 @@ export const datasourceAndVizType = {
export const colorScheme = {
label: t('Color Scheme'),
controlSetRows: [
['color_scheme'],
['color_scheme', 'label_colors'],
],
};

View File

@ -79,6 +79,7 @@ const D3_FORMAT_DOCS = 'D3 format syntax: https://github.com/d3/d3-format';
// input choices & options
const D3_FORMAT_OPTIONS = [
['SMART_NUMBER', 'Adaptative formating'],
['.1s', '.1s (12345.432 => 10k)'],
['.3s', '.3s (12345.432 => 12.3k)'],
[',.1%', ',.1% (12345.432 => 1,234,543.2%)'],
@ -989,7 +990,7 @@ export const controls = {
freeForm: true,
label: t('Number format'),
renderTrigger: true,
default: '.3s',
default: 'SMART_NUMBER',
choices: D3_FORMAT_OPTIONS,
description: D3_FORMAT_DOCS,
},
@ -1232,7 +1233,7 @@ export const controls = {
freeForm: true,
label: t('X Axis Format'),
renderTrigger: true,
default: '.3s',
default: 'SMART_NUMBER',
choices: D3_FORMAT_OPTIONS,
description: D3_FORMAT_DOCS,
},
@ -1252,7 +1253,7 @@ export const controls = {
freeForm: true,
label: t('Y Axis Format'),
renderTrigger: true,
default: '.3s',
default: 'SMART_NUMBER',
choices: D3_FORMAT_OPTIONS,
description: D3_FORMAT_DOCS,
mapStateToProps: (state) => {
@ -1273,7 +1274,7 @@ export const controls = {
type: 'SelectControl',
freeForm: true,
label: t('Right Axis Format'),
default: '.3s',
default: 'SMART_NUMBER',
choices: D3_FORMAT_OPTIONS,
description: D3_FORMAT_DOCS,
},
@ -1401,6 +1402,68 @@ export const controls = {
description: t('Font size for the biggest value in the list'),
},
header_font_size: {
type: 'SelectControl',
label: t('Header Font Size'),
renderTrigger: true,
clearable: false,
default: 0.3,
// Values represent the percentage of space a header should take
options: [
{
label: t('Tiny'),
value: 0.125,
},
{
label: t('Small'),
value: 0.2,
},
{
label: t('Normal'),
value: 0.3,
},
{
label: t('Large'),
value: 0.4,
},
{
label: t('Huge'),
value: 0.5,
},
],
},
subheader_font_size: {
type: 'SelectControl',
label: t('Subheader Font Size'),
renderTrigger: true,
clearable: false,
default: 0.125,
// Values represent the percentage of space a subheader should take
options: [
{
label: t('Tiny'),
value: 0.125,
},
{
label: t('Small'),
value: 0.2,
},
{
label: t('Normal'),
value: 0.3,
},
{
label: t('Large'),
value: 0.4,
},
{
label: t('Huge'),
value: 0.5,
},
],
},
instant_filtering: {
type: 'CheckboxControl',
label: t('Instant Filtering'),
@ -2028,6 +2091,16 @@ export const controls = {
schemes: () => categoricalSchemeRegistry.getMap(),
},
label_colors: {
type: 'ColorMapControl',
label: t('Color Map'),
default: {},
mapStateToProps: state => ({
colorNamespace: state.form_data.color_namespace,
colorScheme: state.form_data.color_scheme,
}),
},
significance_level: {
type: 'TextControl',
label: t('Significance Level'),

View File

@ -95,6 +95,7 @@ export function getExploreUrlAndPayload({
curUrl = null,
requestParams = {},
allowDomainSharding = false,
method = 'POST',
}) {
if (!formData.datasource) {
return null;
@ -118,8 +119,19 @@ export function getExploreUrlAndPayload({
// Building the querystring (search) part of the URI
const search = uri.search(true);
if (formData.slice_id) {
search.form_data = safeStringify({ slice_id: formData.slice_id });
const { slice_id, extra_filters, adhoc_filters, viz_type } = formData;
if (slice_id) {
const form_data = { slice_id };
if (method === 'GET') {
form_data.viz_type = viz_type;
if (extra_filters && extra_filters.length) {
form_data.extra_filters = extra_filters;
}
if (adhoc_filters && adhoc_filters.length) {
form_data.adhoc_filters = adhoc_filters;
}
}
search.form_data = safeStringify(form_data);
}
if (force) {
search.force = 'true';

View File

@ -17,7 +17,8 @@
* under the License.
*/
/* eslint camelcase: 0 */
import { getControlsState, getFormDataFromControls } from '../store';
import { validateControl, getControlsState, getFormDataFromControls } from '../store';
import controls from '../controls';
import * as actions from '../actions/exploreActions';
export default function exploreReducer(state = {}, action) {
@ -75,24 +76,28 @@ export default function exploreReducer(state = {}, action) {
};
},
[actions.SET_FIELD_VALUE]() {
const controls = Object.assign({}, state.controls);
const control = Object.assign({}, controls[action.controlName]);
control.value = action.value;
control.validationErrors = action.validationErrors;
controls[action.controlName] = control;
const changes = {
controls,
// These errors are reported from the Control components
let errors = action.validationErrors || [];
let control = {
...controls[action.controlName],
value: action.value,
};
if (control.renderTrigger) {
changes.triggerRender = true;
} else {
changes.triggerRender = false;
}
const newState = {
control = validateControl(control);
// These errors are based on control config `validators`
errors = errors.concat(control.validationErrors || []);
const hasErrors = errors && errors.length > 0;
return {
...state,
...changes,
triggerRender: control.renderTrigger && !hasErrors,
controls: {
...state.controls,
[action.controlName]: {
...control,
validationErrors: errors,
},
},
};
return newState;
},
[actions.SET_EXPLORE_CONTROLS]() {
return {

View File

@ -52,14 +52,14 @@ export default function getInitialState(bootstrapData) {
[chartKey]: {
id: chartKey,
chartAlert: null,
chartStatus: 'loading',
chartStatus: null,
chartUpdateEndTime: null,
chartUpdateStartTime: 0,
latestQueryFormData: getFormDataFromControls(controls),
sliceFormData,
queryController: null,
queryResponse: null,
triggerQuery: true,
triggerQuery: false,
lastRendered: 0,
},
},

View File

@ -29,6 +29,24 @@ export function getFormDataFromControls(controlsState) {
return formData;
}
export function validateControl(control) {
const validators = control.validators;
const validationErrors = [];
if (validators && validators.length > 0) {
validators.forEach((f) => {
const v = f(control.value);
if (v) {
validationErrors.push(v);
}
});
}
if (validationErrors.length > 0) {
return { ...control, validationErrors };
}
return control;
}
export function getControlNames(vizType, datasourceType) {
const controlNames = [];
sectionsToRender(vizType, datasourceType).forEach(
@ -109,7 +127,7 @@ export function getControlsState(state, form_data) {
) {
control.value = formData[k];
}
controlsState[k] = control;
controlsState[k] = validateControl(control);
});
if (viz.onInit) {
return viz.onInit(controlsState);

View File

@ -34,6 +34,9 @@ const propTypes = {
disabled: PropTypes.bool,
viewport: PropTypes.object.isRequired,
children: PropTypes.node,
mapStyle: PropTypes.string,
mapboxApiAccessToken: PropTypes.string.isRequired,
setControlValue: PropTypes.func,
onViewportChange: PropTypes.func,
onValuesChange: PropTypes.func,
};
@ -41,6 +44,8 @@ const propTypes = {
const defaultProps = {
aggregation: false,
disabled: false,
mapStyle: 'light',
setControlValue: () => {},
onViewportChange: () => {},
onValuesChange: () => {},
};
@ -48,9 +53,6 @@ const defaultProps = {
export default class AnimatableDeckGLContainer extends React.Component {
constructor(props) {
super(props);
const { getLayers, start, end, getStep, values, disabled, viewport, ...other } = props;
this.other = other;
this.onViewportChange = this.onViewportChange.bind(this);
}
onViewportChange(viewport) {
@ -71,6 +73,9 @@ export default class AnimatableDeckGLContainer extends React.Component {
values,
onValuesChange,
viewport,
setControlValue,
mapStyle,
mapboxApiAccessToken,
} = this.props;
const layers = getLayers(values);
@ -83,9 +88,11 @@ export default class AnimatableDeckGLContainer extends React.Component {
return (
<div>
<DeckGLContainer
{...this.other}
viewport={modifiedViewport}
layers={layers}
setControlValue={setControlValue}
mapStyle={mapStyle}
mapboxApiAccessToken={mapboxApiAccessToken}
onViewportChange={this.onViewportChange}
/>
{!disabled &&

View File

@ -31,9 +31,9 @@ import { fitViewport } from './layers/common';
const { getScale } = CategoricalColorNamespace;
function getCategories(fd, data) {
const c = fd.colorPicker || { r: 0, g: 0, b: 0, a: 1 };
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
const colorFn = getScale(fd.colorScheme);
const colorFn = getScale(fd.color_scheme);
const categories = {};
data.forEach((d) => {
if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
@ -108,7 +108,7 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
// the granularity has to be read from the payload form_data, not the
// props formData which comes from the instantaneous controls state
const granularity = (
props.payload.form_data.timeGrainSqla ||
props.payload.form_data.time_grain_sqla ||
props.payload.form_data.granularity ||
'P1D'
);
@ -154,8 +154,8 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
features = this.addColor(features, fd);
// Apply user defined data mutator if defined
if (fd.jsDataMutator) {
const jsFnMutator = sandboxedEval(fd.jsDataMutator);
if (fd.js_data_mutator) {
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
features = jsFnMutator(features);
}
@ -180,8 +180,8 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
return [getLayer(fd, filteredPayload, onAddFilter, setTooltip)];
}
addColor(data, fd) {
const c = fd.colorPicker || { r: 0, g: 0, b: 0, a: 1 };
const colorFn = getScale(fd.colorScheme);
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
const colorFn = getScale(fd.color_scheme);
return data.map((d) => {
let color;
if (fd.dimension) {
@ -229,14 +229,14 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
viewport={this.state.viewport}
onViewportChange={this.onViewportChange}
mapboxApiAccessToken={this.props.mapboxApiKey}
mapStyle={this.props.formData.mapboxStyle}
mapStyle={this.props.formData.mapbox_style}
setControlValue={this.props.setControlValue}
>
<Legend
categories={this.state.categories}
toggleCategory={this.toggleCategory}
showSingleCategory={this.showSingleCategory}
position={this.props.formData.legendPosition}
position={this.props.formData.legend_position}
/>
</AnimatableDeckGLContainer>
</div>

View File

@ -22,6 +22,7 @@ import MapGL from 'react-map-gl';
import DeckGL from 'deck.gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import { isEqual } from 'lodash';
import '../stylesheets/deckgl.css';
const TICK = 2000; // milliseconds

View File

@ -56,7 +56,7 @@ class DeckMulti extends React.PureComponent {
const filters = [
...(subslice.form_data.filters || []),
...(formData.filters || []),
...(formData.extraFilters || []),
...(formData.extra_filters || []),
];
const subsliceCopy = {
...subslice,
@ -70,7 +70,7 @@ class DeckMulti extends React.PureComponent {
endpoint: getExploreLongUrl(subsliceCopy.form_data, 'json'),
})
.then(({ json }) => {
const layer = layerGenerators[subsliceCopy.form_data.vizType](
const layer = layerGenerators[subsliceCopy.form_data.viz_type](
subsliceCopy.form_data,
json,
);
@ -96,7 +96,7 @@ class DeckMulti extends React.PureComponent {
mapboxApiAccessToken={payload.data.mapboxApiKey}
viewport={viewport}
layers={layers}
mapStyle={formData.mapboxStyle}
mapStyle={formData.mapbox_style}
setControlValue={setControlValue}
/>
);

View File

@ -0,0 +1,36 @@
/**
* 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 PropTypes from 'prop-types';
const propTypes = {
label: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
};
export default class TooltipRow extends React.PureComponent {
render() {
return (
<div>{this.props.label}<strong>{this.props.value}</strong></div>
);
}
}
TooltipRow.propTypes = propTypes;

View File

@ -90,7 +90,7 @@ export function createDeckGLComponent(getLayer, getPoints) {
mapboxApiAccessToken={payload.data.mapboxApiKey}
viewport={viewport}
layers={[layer]}
mapStyle={formData.mapboxStyle}
mapStyle={formData.mapbox_style}
setControlValue={setControlValue}
onViewportChange={this.onViewportChange}
/>);

View File

@ -17,8 +17,11 @@
* under the License.
*/
import { ArcLayer } from 'deck.gl';
import React from 'react';
import { t } from '@superset-ui/translation';
import { commonLayerProps } from '../common';
import { createCategoricalDeckGLComponent } from '../../factory';
import TooltipRow from '../../TooltipRow';
function getPoints(data) {
const points = [];
@ -29,17 +32,29 @@ function getPoints(data) {
return points;
}
function setTooltipContent(formData) {
return o => (
<div className="deckgl-tooltip">
<TooltipRow label={`${t('Start (Longitude, Latitude)')}: `} value={`${o.object.sourcePosition[0]}, ${o.object.sourcePosition[1]}`} />
<TooltipRow label={`${t('End (Longitude, Latitude)')}: `} value={`${o.object.targetPosition[0]}, ${o.object.targetPosition[1]}`} />
{
formData.dimension && <TooltipRow label={`${formData.dimension}: `} value={`${o.object.cat_color}`} />
}
</div>
);
}
export function getLayer(fd, payload, onAddFilter, setTooltip) {
const data = payload.data.features;
const sc = fd.colorPicker;
const tc = fd.targetColorPicker;
const sc = fd.color_picker;
const tc = fd.target_color_picker;
return new ArcLayer({
id: `path-layer-${fd.sliceId}`,
id: `path-layer-${fd.slice_id}`,
data,
getSourceColor: d => d.sourceColor || d.color || [sc.r, sc.g, sc.b, 255 * sc.a],
getTargetColor: d => d.targetColor || d.color || [tc.r, tc.g, tc.b, 255 * tc.a],
strokeWidth: (fd.strokeWidth) ? fd.strokeWidth : 3,
...commonLayerProps(fd, setTooltip),
strokeWidth: (fd.stroke_width) ? fd.stroke_width : 3,
...commonLayerProps(fd, setTooltip, setTooltipContent(fd)),
});
}

View File

@ -25,6 +25,7 @@ import DeckGLContainer from '../../DeckGLContainer';
import { hexToRGB } from '../../../../modules/colors';
import sandboxedEval from '../../../../modules/sandbox';
import { commonLayerProps } from '../common';
import TooltipRow from '../../TooltipRow';
const propertyMap = {
fillColor: 'fillColor',
@ -75,10 +76,23 @@ const recurseGeoJson = (node, propOverrides, extraProps) => {
}
};
function setTooltipContent(o) {
return (
o.object.extraProps &&
<div className="deckgl-tooltip">
{
Object.keys(o.object.extraProps).map((prop, index) =>
<TooltipRow key={`prop-${index}`} label={`${prop}: `} value={`${o.object.extraProps[prop]}`} />,
)
}
</div>
);
}
export function getLayer(formData, payload, onAddFilter, setTooltip) {
const fd = formData;
const fc = fd.fillColorPicker;
const sc = fd.strokeColorPicker;
const fc = fd.fill_color_picker;
const sc = fd.stroke_color_picker;
const fillColor = [fc.r, fc.g, fc.b, 255 * fc.a];
const strokeColor = [sc.r, sc.g, sc.b, 255 * sc.a];
const propOverrides = {};
@ -93,20 +107,20 @@ export function getLayer(formData, payload, onAddFilter, setTooltip) {
recurseGeoJson(payload.data, propOverrides);
let jsFnMutator;
if (fd.jsDataMutator) {
if (fd.js_data_mutator) {
// Applying user defined data mutator if defined
jsFnMutator = sandboxedEval(fd.jsDataMutator);
jsFnMutator = sandboxedEval(fd.js_data_mutator);
features = jsFnMutator(features);
}
return new GeoJsonLayer({
id: `geojson-layer-${fd.sliceId}`,
id: `geojson-layer-${fd.slice_id}`,
filled: fd.filled,
data: features,
stroked: fd.stroked,
extruded: fd.extruded,
pointRadiusScale: fd.pointRadiusScale,
...commonLayerProps(fd, setTooltip),
pointRadiusScale: fd.point_radius_scale,
...commonLayerProps(fd, setTooltip, setTooltipContent),
});
}
@ -145,7 +159,7 @@ function deckGeoJson(props) {
mapboxApiAccessToken={payload.data.mapboxApiKey}
viewport={viewport}
layers={[layer]}
mapStyle={formData.mapboxStyle}
mapStyle={formData.mapbox_style}
setControlValue={setControlValue}
/>
);

View File

@ -17,38 +17,50 @@
* under the License.
*/
import { GridLayer } from 'deck.gl';
import React from 'react';
import { t } from '@superset-ui/translation';
import { commonLayerProps, getAggFunc } from '../common';
import sandboxedEval from '../../../../modules/sandbox';
import { createDeckGLComponent } from '../../factory';
import TooltipRow from '../../TooltipRow';
function setTooltipContent(o) {
return (
<div className="deckgl-tooltip">
<TooltipRow label={`${t('Longitude and Latitude')}: `} value={`${o.object.position[0]}, ${o.object.position[1]}`} />
<TooltipRow label={`${t('Height')}: `} value={`${o.object.elevationValue}`} />
</div>
);
}
export function getLayer(formData, payload, onAddFilter, setTooltip) {
const fd = formData;
const c = fd.colorPicker;
const c = fd.color_picker;
let data = payload.data.features.map(d => ({
...d,
color: [c.r, c.g, c.b, 255 * c.a],
}));
if (fd.jsDataMutator) {
if (fd.js_data_mutator) {
// Applying user defined data mutator if defined
const jsFnMutator = sandboxedEval(fd.jsDataMutator);
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
data = jsFnMutator(data);
}
const aggFunc = getAggFunc(fd.jsAggFunction, p => p.weight);
const aggFunc = getAggFunc(fd.js_agg_function, p => p.weight);
return new GridLayer({
id: `grid-layer-${fd.sliceId}`,
id: `grid-layer-${fd.slice_id}`,
data,
pickable: true,
cellSize: fd.gridSize,
cellSize: fd.grid_size,
minColor: [0, 0, 0, 0],
extruded: fd.extruded,
maxColor: [c.r, c.g, c.b, 255 * c.a],
outline: false,
getElevationValue: aggFunc,
getColorValue: aggFunc,
...commonLayerProps(fd, setTooltip),
...commonLayerProps(fd, setTooltip, setTooltipContent),
});
}

View File

@ -17,37 +17,49 @@
* under the License.
*/
import { HexagonLayer } from 'deck.gl';
import React from 'react';
import { t } from '@superset-ui/translation';
import { commonLayerProps, getAggFunc } from '../common';
import sandboxedEval from '../../../../modules/sandbox';
import { createDeckGLComponent } from '../../factory';
import TooltipRow from '../../TooltipRow';
function setTooltipContent(o) {
return (
<div className="deckgl-tooltip">
<TooltipRow label={`${t('Centroid (Longitude and Latitude)')}: `} value={`(${o.object.centroid[0]}, ${o.object.centroid[1]})`} />
<TooltipRow label={`${t('Height')}: `} value={`${o.object.elevationValue}`} />
</div>
);
}
export function getLayer(formData, payload, onAddFilter, setTooltip) {
const fd = formData;
const c = fd.colorPicker;
const c = fd.color_picker;
let data = payload.data.features.map(d => ({
...d,
color: [c.r, c.g, c.b, 255 * c.a],
}));
if (fd.jsDataMutator) {
if (fd.js_data_mutator) {
// Applying user defined data mutator if defined
const jsFnMutator = sandboxedEval(fd.jsDataMutator);
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
data = jsFnMutator(data);
}
const aggFunc = getAggFunc(fd.jsAggFunction, p => p.weight);
const aggFunc = getAggFunc(fd.js_agg_function, p => p.weight);
return new HexagonLayer({
id: `hex-layer-${fd.sliceId}`,
id: `hex-layer-${fd.slice_id}`,
data,
pickable: true,
radius: fd.gridSize,
radius: fd.grid_size,
minColor: [0, 0, 0, 0],
extruded: fd.extruded,
maxColor: [c.r, c.g, c.b, 255 * c.a],
outline: false,
getElevationValue: aggFunc,
getColorValue: aggFunc,
...commonLayerProps(fd, setTooltip),
...commonLayerProps(fd, setTooltip, setTooltipContent),
});
}

View File

@ -17,32 +17,47 @@
* under the License.
*/
import { PathLayer } from 'deck.gl';
import React from 'react';
import { commonLayerProps } from '../common';
import sandboxedEval from '../../../../modules/sandbox';
import { createDeckGLComponent } from '../../factory';
import TooltipRow from '../../TooltipRow';
function setTooltipContent(o) {
return (
o.object.extraProps &&
<div className="deckgl-tooltip">
{
Object.keys(o.object.extraProps).map((prop, index) =>
<TooltipRow key={`prop-${index}`} label={`${prop}: `} value={`${o.object.extraProps[prop]}`} />,
)
}
</div>
);
}
export function getLayer(formData, payload, onAddFilter, setTooltip) {
const fd = formData;
const c = fd.colorPicker;
const c = fd.color_picker;
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
let data = payload.data.features.map(feature => ({
...feature,
path: feature.path,
width: fd.lineWidth,
width: fd.line_width,
color: fixedColor,
}));
if (fd.jsDataMutator) {
const jsFnMutator = sandboxedEval(fd.jsDataMutator);
if (fd.js_data_mutator) {
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
data = jsFnMutator(data);
}
return new PathLayer({
id: `path-layer-${fd.sliceId}`,
id: `path-layer-${fd.slice_id}`,
data,
rounded: true,
widthScale: 1,
...commonLayerProps(fd, setTooltip),
...commonLayerProps(fd, setTooltip, setTooltipContent),
});
}

View File

@ -25,6 +25,7 @@ import { PolygonLayer } from 'deck.gl';
import AnimatableDeckGLContainer from '../../AnimatableDeckGLContainer';
import Legend from '../../../Legend';
import TooltipRow from '../../TooltipRow';
import { getBuckets, getBreakPointColorScaler } from '../../utils';
import { commonLayerProps, fitViewport } from '../common';
@ -48,10 +49,22 @@ function getElevation(d, colorScaler) {
: d.elevation;
}
function setTooltipContent(formData) {
return (o) => {
const metricLabel = formData.metric.label || formData.metric;
return (
<div className="deckgl-tooltip">
<TooltipRow label={`${formData.line_column}: `} value={`${o.object[formData.line_column]}`} />
{formData.metric && <TooltipRow label={`${metricLabel}: `} value={`${o.object[metricLabel]}`} />}
</div>
);
};
}
export function getLayer(formData, payload, setTooltip, selected, onSelect, filters) {
const fd = formData;
const fc = fd.fillColorPicker;
const sc = fd.strokeColorPicker;
const fc = fd.fill_color_picker;
const sc = fd.stroke_color_picker;
let data = [...payload.data.features];
if (filters != null) {
@ -60,9 +73,9 @@ export function getLayer(formData, payload, setTooltip, selected, onSelect, filt
});
}
if (fd.jsDataMutator) {
if (fd.js_data_mutator) {
// Applying user defined data mutator if defined
const jsFnMutator = sandboxedEval(fd.jsDataMutator);
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
data = jsFnMutator(data);
}
@ -76,13 +89,16 @@ export function getLayer(formData, payload, setTooltip, selected, onSelect, filt
// when polygons are selected, reduce the opacity of non-selected polygons
const colorScaler = (d) => {
const baseColor = baseColorScaler(d);
if (selected.length > 0 && selected.indexOf(d[fd.lineColumn]) === -1) {
if (selected.length > 0 && selected.indexOf(d[fd.line_column]) === -1) {
baseColor[3] /= 2;
}
return baseColor;
};
const tooltipContentGenerator = (fd.line_column && fd.metric && ['geohash', 'zipcode'].indexOf(fd.line_type) >= 0)
? setTooltipContent(fd)
: undefined;
return new PolygonLayer({
id: `path-layer-${fd.sliceId}`,
id: `path-layer-${fd.slice_id}`,
data,
pickable: true,
filled: fd.filled,
@ -90,12 +106,12 @@ export function getLayer(formData, payload, setTooltip, selected, onSelect, filt
getPolygon: d => d.polygon,
getFillColor: colorScaler,
getLineColor: [sc.r, sc.g, sc.b, 255 * sc.a],
getLineWidth: fd.lineWidth,
getLineWidth: fd.line_width,
extruded: fd.extruded,
getElevation: d => getElevation(d, colorScaler),
elevationScale: fd.multiplier,
fp64: true,
...commonLayerProps(fd, setTooltip, onSelect),
...commonLayerProps(fd, setTooltip, tooltipContentGenerator, onSelect),
});
}
@ -138,7 +154,7 @@ class DeckGLPolygon extends React.Component {
// the granularity has to be read from the payload form_data, not the
// props formData which comes from the instantaneous controls state
const granularity = (
props.payload.form_data.timeGrainSqla ||
props.payload.form_data.time_grain_sqla ||
props.payload.form_data.granularity ||
'P1D'
);
@ -177,7 +193,7 @@ class DeckGLPolygon extends React.Component {
const selected = [...this.state.selected];
if (doubleClick) {
selected.splice(0, selected.length, polygon);
} else if (formData.togglePolygons) {
} else if (formData.toggle_polygons) {
const i = selected.indexOf(polygon);
if (i === -1) {
selected.push(polygon);
@ -189,8 +205,8 @@ class DeckGLPolygon extends React.Component {
}
this.setState({ selected, lastClick: now });
if (formData.tableFilter) {
onAddFilter(formData.lineColumn, selected, false, true);
if (formData.table_filter) {
onAddFilter(formData.line_column, selected, false, true);
}
}
onValuesChange(values) {
@ -249,14 +265,14 @@ class DeckGLPolygon extends React.Component {
viewport={viewport}
onViewportChange={this.onViewportChange}
mapboxApiAccessToken={payload.data.mapboxApiKey}
mapStyle={formData.mapboxStyle}
mapStyle={formData.mapbox_style}
setControlValue={setControlValue}
aggregation
>
{formData.metric !== null &&
<Legend
categories={buckets}
position={formData.legendPosition}
position={formData.legend_position}
/>}
</AnimatableDeckGLContainer>
</div>

View File

@ -17,36 +17,53 @@
* under the License.
*/
import { ScatterplotLayer } from 'deck.gl';
import React from 'react';
import { t } from '@superset-ui/translation';
import { commonLayerProps } from '../common';
import { createCategoricalDeckGLComponent } from '../../factory';
import TooltipRow from '../../TooltipRow';
import { unitToRadius } from '../../../../modules/geo';
function getPoints(data) {
return data.map(d => d.position);
}
function setTooltipContent(formData) {
return o => (
<div className="deckgl-tooltip">
<TooltipRow label={`${t('Longitude and Latitude')}: `} value={`${o.object.position[0]}, ${o.object.position[1]}`} />
{
o.object.cat_color && <TooltipRow label={`${t('Category')}: `} value={`${o.object.cat_color}`} />
}
{
o.object.metric && <TooltipRow label={`${formData.point_radius_fixed.value}: `} value={`${o.object.metric}`} />
}
</div>
);
}
export function getLayer(fd, payload, onAddFilter, setTooltip) {
const dataWithRadius = payload.data.features.map((d) => {
let radius = unitToRadius(fd.pointUnit, d.radius) || 10;
let radius = unitToRadius(fd.point_unit, d.radius) || 10;
if (fd.multiplier) {
radius *= fd.multiplier;
}
if (d.color) {
return { ...d, radius };
}
const c = fd.colorPicker || { r: 0, g: 0, b: 0, a: 1 };
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
const color = [c.r, c.g, c.b, c.a * 255];
return { ...d, radius, color };
});
return new ScatterplotLayer({
id: `scatter-layer-${fd.sliceId}`,
id: `scatter-layer-${fd.slice_id}`,
data: dataWithRadius,
fp64: true,
radiusMinPixels: fd.minRadius || null,
radiusMaxPixels: fd.maxRadius || null,
radiusMinPixels: fd.min_radius || null,
radiusMaxPixels: fd.max_radius || null,
outline: false,
...commonLayerProps(fd, setTooltip),
...commonLayerProps(fd, setTooltip, setTooltipContent(fd)),
});
}

View File

@ -21,26 +21,37 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ScreenGridLayer } from 'deck.gl';
import { t } from '@superset-ui/translation';
import AnimatableDeckGLContainer from '../../AnimatableDeckGLContainer';
import { getPlaySliderParams } from '../../../../modules/time';
import sandboxedEval from '../../../../modules/sandbox';
import { commonLayerProps, fitViewport } from '../common';
import TooltipRow from '../../TooltipRow';
function getPoints(data) {
return data.map(d => d.position);
}
function setTooltipContent(o) {
return (
<div className="deckgl-tooltip">
<TooltipRow label={`${t('Longitude and Latitude')}: `} value={`${o.object.position[0]}, ${o.object.position[1]}`} />
<TooltipRow label={`${t('Weight')}: `} value={`${o.object.weight}`} />
</div>
);
}
export function getLayer(formData, payload, onAddFilter, setTooltip, filters) {
const fd = formData;
const c = fd.colorPicker;
const c = fd.color_picker;
let data = payload.data.features.map(d => ({
...d,
color: [c.r, c.g, c.b, 255 * c.a],
}));
if (fd.jsDataMutator) {
if (fd.js_data_mutator) {
// Applying user defined data mutator if defined
const jsFnMutator = sandboxedEval(fd.jsDataMutator);
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
data = jsFnMutator(data);
}
@ -53,15 +64,15 @@ export function getLayer(formData, payload, onAddFilter, setTooltip, filters) {
// Passing a layer creator function instead of a layer since the
// layer needs to be regenerated at each render
return new ScreenGridLayer({
id: `screengrid-layer-${fd.sliceId}`,
id: `screengrid-layer-${fd.slice_id}`,
data,
pickable: true,
cellSizePixels: fd.gridSize,
cellSizePixels: fd.grid_size,
minColor: [c.r, c.g, c.b, 0],
maxColor: [c.r, c.g, c.b, 255 * c.a],
outline: false,
getWeight: d => d.weight || 0,
...commonLayerProps(fd, setTooltip),
...commonLayerProps(fd, setTooltip, setTooltipContent),
});
}
@ -102,7 +113,7 @@ class DeckGLScreenGrid extends React.PureComponent {
// the granularity has to be read from the payload form_data, not the
// props formData which comes from the instantaneous controls state
const granularity = (
props.payload.form_data.timeGrainSqla ||
props.payload.form_data.time_grain_sqla ||
props.payload.form_data.granularity ||
'P1D'
);
@ -176,7 +187,7 @@ class DeckGLScreenGrid extends React.PureComponent {
viewport={this.state.viewport}
onViewportChange={this.onViewportChange}
mapboxApiAccessToken={payload.data.mapboxApiKey}
mapStyle={formData.mapboxStyle}
mapStyle={formData.mapbox_style}
setControlValue={setControlValue}
aggregation
/>

Some files were not shown because too many files have changed in this diff Show More