diff --git a/.gitignore b/.gitignore index 929abbd5e..2c553ad1d 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ dump.rdb env env_py3 envpy3 +env36 local_config.py superset_config.py superset.egg-info/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 750059241..cdca9cbe2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/docs/installation.rst b/docs/installation.rst index 7bc9f61f3..a06da876d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -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 diff --git a/requirements.txt b/requirements.txt index 4b6b4f56e..bab2c19fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/superset/assets/backendSync.json b/superset/assets/backendSync.json index 3dfb57381..2285f50ba 100644 --- a/superset/assets/backendSync.json +++ b/superset/assets/backendSync.json @@ -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", diff --git a/superset/assets/cypress/integration/dashboard/controls.js b/superset/assets/cypress/integration/dashboard/controls.js index fb103bde4..fe5581590 100644 --- a/superset/assets/cypress/integration/dashboard/controls.js +++ b/superset/assets/cypress/integration/dashboard/controls.js @@ -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'); }); diff --git a/superset/assets/cypress/integration/dashboard/edit_mode.js b/superset/assets/cypress/integration/dashboard/edit_mode.js index e58e7df53..280b6aae4 100644 --- a/superset/assets/cypress/integration/dashboard/edit_mode.js +++ b/superset/assets/cypress/integration/dashboard/edit_mode.js @@ -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(); diff --git a/superset/assets/cypress/integration/dashboard/filter.js b/superset/assets/cypress/integration/dashboard/filter.js index 157cbe868..f37c8c849 100644 --- a/superset/assets/cypress/integration/dashboard/filter.js +++ b/superset/assets/cypress/integration/dashboard/filter.js @@ -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)) diff --git a/superset/assets/cypress/integration/dashboard/load.js b/superset/assets/cypress/integration/dashboard/load.js index 30c9d325d..79daa3042 100644 --- a/superset/assets/cypress/integration/dashboard/load.js +++ b/superset/assets/cypress/integration/dashboard/load.js @@ -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}`); }); }); diff --git a/superset/assets/cypress/integration/dashboard/save.js b/superset/assets/cypress/integration/dashboard/save.js index 1c673b0c7..1d26ac219 100644 --- a/superset/assets/cypress/integration/dashboard/save.js +++ b/superset/assets/cypress/integration/dashboard/save.js @@ -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'); diff --git a/superset/assets/cypress/integration/explore/control.test.js b/superset/assets/cypress/integration/explore/control.test.js index ba4636c47..d20cb46e2 100644 --- a/superset/assets/cypress/integration/explore/control.test.js +++ b/superset/assets/cypress/integration/explore/control.test.js @@ -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(); diff --git a/superset/assets/cypress/integration/explore/link.test.js b/superset/assets/cypress/integration/explore/link.test.js index 024612f53..9f5f82d59 100644 --- a/superset/assets/cypress/integration/explore/link.test.js +++ b/superset/assets/cypress/integration/explore/link.test.js @@ -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]}`); }); diff --git a/superset/assets/package-lock.json b/superset/assets/package-lock.json index a9df19722..704e6f3af 100644 --- a/superset/assets/package-lock.json +++ b/superset/assets/package-lock.json @@ -1378,7 +1378,7 @@ }, "@data-ui/event-flow": { "version": "0.0.54", - "resolved": "https://registry.npmjs.org/@data-ui/event-flow/-/event-flow-0.0.54.tgz", + "resolved": "http://registry.npmjs.org/@data-ui/event-flow/-/event-flow-0.0.54.tgz", "integrity": "sha1-uwPh/StWNCSGVbjfnTxsOKdH5l4=", "requires": { "@data-ui/forms": "0.0.50", @@ -1669,7 +1669,7 @@ }, "@data-ui/radial-chart": { "version": "0.0.54", - "resolved": "https://registry.npmjs.org/@data-ui/radial-chart/-/radial-chart-0.0.54.tgz", + "resolved": "http://registry.npmjs.org/@data-ui/radial-chart/-/radial-chart-0.0.54.tgz", "integrity": "sha1-DSiwdoHZtgJ9msI7cpJBgn1RMAE=", "requires": { "@data-ui/shared": "0.0.54", @@ -1684,7 +1684,7 @@ }, "@data-ui/shared": { "version": "0.0.54", - "resolved": "https://registry.npmjs.org/@data-ui/shared/-/shared-0.0.54.tgz", + "resolved": "http://registry.npmjs.org/@data-ui/shared/-/shared-0.0.54.tgz", "integrity": "sha1-L7DW3ukNrCC/jzwpE8aFCoIj1Zs=", "requires": { "@data-ui/theme": "0.0.48", @@ -1757,7 +1757,7 @@ }, "@data-ui/sparkline": { "version": "0.0.54", - "resolved": "https://registry.npmjs.org/@data-ui/sparkline/-/sparkline-0.0.54.tgz", + "resolved": "http://registry.npmjs.org/@data-ui/sparkline/-/sparkline-0.0.54.tgz", "integrity": "sha1-zj0WbZ4LI5oLoC84lMuejIQXHO8=", "requires": { "@data-ui/shared": "0.0.54", @@ -2549,7 +2549,7 @@ }, "@sinonjs/formatio": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", + "resolved": "http://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==", "dev": true, "requires": { @@ -2599,7 +2599,7 @@ "dependencies": { "whatwg-fetch": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", + "resolved": "http://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" } } @@ -3727,7 +3727,7 @@ }, "acorn-jsx": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "resolved": "http://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", "dev": true, "requires": { @@ -3736,7 +3736,7 @@ "dependencies": { "acorn": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "resolved": "http://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", "dev": true } @@ -4177,7 +4177,7 @@ }, "array-equal": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "resolved": "http://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", "dev": true }, @@ -4300,7 +4300,7 @@ }, "util": { "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "dev": true, "requires": { @@ -4790,7 +4790,7 @@ }, "babel-plugin-syntax-dynamic-import": { "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", + "resolved": "http://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=", "dev": true }, @@ -4987,9 +4987,9 @@ "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==" }, "bignumber.js": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-8.1.1.tgz", - "integrity": "sha512-QD46ppGintwPGuL1KqmwhR0O+N2cZUg8JG/VzwI2e28sM9TqHjQB10lI4QAaMHVbLzwVLLAwEglpKPViWX+5NQ==" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.4.0.tgz", + "integrity": "sha1-g4qZLan51zfg9LLbC+YrsJ3Qxeg=" }, "binary-extensions": { "version": "1.12.0", @@ -5074,7 +5074,7 @@ }, "brace": { "version": "0.11.1", - "resolved": "https://registry.npmjs.org/brace/-/brace-0.11.1.tgz", + "resolved": "http://registry.npmjs.org/brace/-/brace-0.11.1.tgz", "integrity": "sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg=" }, "brace-expansion": { @@ -5106,7 +5106,7 @@ }, "brfs": { "version": "1.6.1", - "resolved": "https://registry.npmjs.org/brfs/-/brfs-1.6.1.tgz", + "resolved": "http://registry.npmjs.org/brfs/-/brfs-1.6.1.tgz", "integrity": "sha512-OfZpABRQQf+Xsmju8XE9bDjs+uU4vLREGolP7bDgcpsI17QREyZ4Bl+2KLxxx1kCgA0fAIhKQBaBYh+PEcCqYQ==", "requires": { "quote-stream": "^1.0.1", @@ -5146,7 +5146,7 @@ }, "browserify-aes": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dev": true, "requires": { @@ -5183,7 +5183,7 @@ }, "browserify-rsa": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "dev": true, "requires": { @@ -5269,7 +5269,7 @@ }, "buffer": { "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "dev": true, "requires": { @@ -5419,7 +5419,7 @@ }, "callsites": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "resolved": "http://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", "dev": true }, @@ -5714,7 +5714,7 @@ }, "clean-webpack-plugin": { "version": "0.1.19", - "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-0.1.19.tgz", + "resolved": "http://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-0.1.19.tgz", "integrity": "sha512-M1Li5yLHECcN2MahoreuODul5LkjohJGFxLPTjl3j1ttKrF5rgjZET1SJduuqxLAuT1gAPOdkhg03qcaaU1KeA==", "dev": true, "requires": { @@ -6125,7 +6125,7 @@ }, "create-hash": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dev": true, "requires": { @@ -6138,7 +6138,7 @@ }, "create-hmac": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dev": true, "requires": { @@ -6274,7 +6274,7 @@ }, "css-in-js-utils": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz", + "resolved": "http://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz", "integrity": "sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==", "requires": { "hyphenate-style-name": "^1.0.2", @@ -6334,7 +6334,7 @@ }, "css-select": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", "dev": true, "requires": { @@ -6755,7 +6755,7 @@ }, "d3-geo-projection": { "version": "0.2.16", - "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-0.2.16.tgz", + "resolved": "http://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-0.2.16.tgz", "integrity": "sha1-SZTs0QM92xUztsTFUoocgdzClCc=", "requires": { "brfs": "^1.3.0" @@ -7173,7 +7173,7 @@ }, "diffie-hellman": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "dev": true, "requires": { @@ -7203,7 +7203,7 @@ }, "dnd-core": { "version": "2.6.0", - "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-2.6.0.tgz", + "resolved": "http://registry.npmjs.org/dnd-core/-/dnd-core-2.6.0.tgz", "integrity": "sha1-ErrWbVh0LG5ffPKUP7aFlED4CcQ=", "requires": { "asap": "^2.0.6", @@ -7333,7 +7333,7 @@ }, "duplexer": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", "dev": true }, @@ -7575,7 +7575,7 @@ }, "es6-promise": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "resolved": "http://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=" }, "es6bindall": { @@ -7622,7 +7622,7 @@ }, "eslint": { "version": "4.19.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz", + "resolved": "http://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz", "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==", "dev": true, "requires": { @@ -7726,7 +7726,7 @@ }, "fast-deep-equal": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", "dev": true }, @@ -7900,7 +7900,7 @@ "dependencies": { "doctrine": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "resolved": "http://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", "dev": true, "requires": { @@ -7910,7 +7910,7 @@ }, "load-json-file": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", "dev": true, "requires": { @@ -8048,7 +8048,7 @@ }, "espree": { "version": "3.5.4", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", + "resolved": "http://registry.npmjs.org/espree/-/espree-3.5.4.tgz", "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", "dev": true, "requires": { @@ -8226,7 +8226,7 @@ "dependencies": { "source-map": { "version": "0.5.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.0.tgz", + "resolved": "http://registry.npmjs.org/source-map/-/source-map-0.5.0.tgz", "integrity": "sha1-D+llA6yGpa213mP05BKuSHLNvoY=", "dev": true } @@ -8272,7 +8272,7 @@ "dependencies": { "array-flatten": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", "dev": true }, @@ -8300,7 +8300,7 @@ }, "external-editor": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "resolved": "http://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", "dev": true, "requires": { @@ -8522,7 +8522,7 @@ }, "file-loader": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.11.tgz", + "resolved": "http://registry.npmjs.org/file-loader/-/file-loader-1.1.11.tgz", "integrity": "sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==", "dev": true, "requires": { @@ -8532,7 +8532,7 @@ }, "file-type": { "version": "3.9.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "resolved": "http://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=" }, "fileset": { @@ -8565,7 +8565,7 @@ }, "finalhandler": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", "dev": true, "requires": { @@ -9135,6 +9135,16 @@ "readable-stream": "^2.0.0" } }, + "fs-minipass": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, "fs-readdir-recursive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", @@ -9171,7 +9181,8 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true, "optional": true }, @@ -9183,13 +9194,15 @@ }, "aproba": { "version": "1.2.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "dev": true, "optional": true }, "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", "dev": true, "optional": true, "requires": { @@ -9214,10 +9227,9 @@ } }, "chownr": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=" }, "code-point-at": { "version": "1.1.0", @@ -9239,13 +9251,15 @@ }, "core-util-is": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true, "optional": true }, "debug": { "version": "2.6.9", - "bundled": true, + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "optional": true, "requires": { @@ -9253,41 +9267,37 @@ } }, "deep-extend": { - "version": "0.6.0", - "bundled": true, + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", + "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", "dev": true, "optional": true }, "delegates": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true, "optional": true }, "detect-libc": { "version": "1.0.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "dev": true, "optional": true }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, "fs.realpath": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true, "optional": true }, "gauge": { "version": "2.7.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, "optional": true, "requires": { @@ -9302,8 +9312,9 @@ } }, "glob": { - "version": "7.1.3", - "bundled": true, + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "dev": true, "optional": true, "requires": { @@ -9317,22 +9328,25 @@ }, "has-unicode": { "version": "2.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "dev": true, "optional": true }, "iconv-lite": { - "version": "0.4.24", - "bundled": true, + "version": "0.4.21", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz", + "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", "dev": true, "optional": true, "requires": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": "^2.1.0" } }, "ignore-walk": { "version": "3.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "dev": true, "optional": true, "requires": { @@ -9341,7 +9355,8 @@ }, "inflight": { "version": "1.0.6", - "bundled": true, + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "optional": true, "requires": { @@ -9357,7 +9372,8 @@ }, "ini": { "version": "1.3.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true, "optional": true }, @@ -9372,7 +9388,8 @@ }, "isarray": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true, "optional": true }, @@ -9391,21 +9408,10 @@ "dev": true, "optional": true }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, "minizlib": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", + "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", "requires": { "minipass": "^2.2.1" } @@ -9421,13 +9427,15 @@ }, "ms": { "version": "2.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true, "optional": true }, "needle": { - "version": "2.2.4", - "bundled": true, + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.0.tgz", + "integrity": "sha512-eFagy6c+TYayorXw/qtAdSvaUpEbBsDwDyxYFgLZ0lTojfH7K+OdBqAF7TAFwDokJaGpubpSGG0wO3iC0XPi8w==", "dev": true, "optional": true, "requires": { @@ -9437,18 +9445,19 @@ } }, "node-pre-gyp": { - "version": "0.10.3", - "bundled": true, + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz", + "integrity": "sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ==", "dev": true, "optional": true, "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", - "needle": "^2.2.1", + "needle": "^2.2.0", "nopt": "^4.0.1", "npm-packlist": "^1.1.6", "npmlog": "^4.0.2", - "rc": "^1.2.7", + "rc": "^1.1.7", "rimraf": "^2.6.1", "semver": "^5.3.0", "tar": "^4" @@ -9456,7 +9465,8 @@ }, "nopt": { "version": "4.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "dev": true, "optional": true, "requires": { @@ -9465,14 +9475,16 @@ } }, "npm-bundled": { - "version": "1.0.5", - "bundled": true, + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.3.tgz", + "integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==", "dev": true, "optional": true }, "npm-packlist": { - "version": "1.2.0", - "bundled": true, + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.10.tgz", + "integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==", "dev": true, "optional": true, "requires": { @@ -9482,7 +9494,8 @@ }, "npmlog": { "version": "4.1.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, "optional": true, "requires": { @@ -9500,7 +9513,8 @@ }, "object-assign": { "version": "4.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true, "optional": true }, @@ -9515,19 +9529,22 @@ }, "os-homedir": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true, "optional": true }, "osenv": { "version": "0.1.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "dev": true, "optional": true, "requires": { @@ -9537,23 +9554,26 @@ }, "path-is-absolute": { "version": "1.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true, "optional": true }, "process-nextick-args": { "version": "2.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true, "optional": true }, "rc": { - "version": "1.2.8", - "bundled": true, + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.7.tgz", + "integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==", "dev": true, "optional": true, "requires": { - "deep-extend": "^0.6.0", + "deep-extend": "^0.5.1", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" @@ -9561,7 +9581,8 @@ "dependencies": { "minimist": { "version": "1.2.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true, "optional": true } @@ -9569,7 +9590,8 @@ }, "readable-stream": { "version": "2.3.6", - "bundled": true, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "optional": true, "requires": { @@ -9583,12 +9605,13 @@ } }, "rimraf": { - "version": "2.6.3", - "bundled": true, + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", "dev": true, "optional": true, "requires": { - "glob": "^7.1.3" + "glob": "^7.0.5" } }, "safe-buffer": { @@ -9599,31 +9622,36 @@ }, "safer-buffer": { "version": "2.1.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "optional": true }, "sax": { "version": "1.2.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true, "optional": true }, "semver": { - "version": "5.6.0", - "bundled": true, + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", "dev": true, "optional": true }, "set-blocking": { "version": "2.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true, "optional": true }, @@ -9640,7 +9668,8 @@ }, "string_decoder": { "version": "1.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "optional": true, "requires": { @@ -9658,38 +9687,26 @@ }, "strip-json-comments": { "version": "2.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true, "optional": true }, - "tar": { - "version": "4.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, "util-deprecate": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true, "optional": true }, "wide-align": { - "version": "1.1.3", - "bundled": true, + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", "dev": true, "optional": true, "requires": { - "string-width": "^1.0.2 || 2" + "string-width": "^1.0.2" } }, "wrappy": { @@ -9697,12 +9714,6 @@ "bundled": true, "dev": true, "optional": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "optional": true } } }, @@ -9811,7 +9822,7 @@ }, "gettext-parser": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/gettext-parser/-/gettext-parser-1.1.0.tgz", "integrity": "sha1-LFpmONiTk0ubVQN9CtgstwBLJnk=", "dev": true, "requires": { @@ -9903,7 +9914,7 @@ }, "globby": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "resolved": "http://registry.npmjs.org/globby/-/globby-6.1.0.tgz", "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", "dev": true, "requires": { @@ -10171,7 +10182,7 @@ }, "hoist-non-react-statics": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" }, "homedir-polyfill": { @@ -10275,7 +10286,7 @@ }, "http-errors": { "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "dev": true, "requires": { @@ -11023,7 +11034,7 @@ }, "is-accessor-descriptor": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "resolved": "http://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "dev": true, "requires": { @@ -11120,7 +11131,7 @@ }, "is-data-descriptor": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "resolved": "http://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "dev": true, "requires": { @@ -12749,13 +12760,6 @@ "stream-to-buffer": "^0.1.0", "tinycolor2": "^1.1.2", "url-regex": "^3.0.0" - }, - "dependencies": { - "bignumber.js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.4.0.tgz", - "integrity": "sha1-g4qZLan51zfg9LLbC+YrsJ3Qxeg=" - } } }, "jpeg-js": { @@ -13287,7 +13291,7 @@ }, "magic-string": { "version": "0.22.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", + "resolved": "http://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", "integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==", "requires": { "vlq": "^0.2.2" @@ -13410,7 +13414,7 @@ }, "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "quickselect": { @@ -13468,7 +13472,7 @@ }, "mathjs": { "version": "3.20.2", - "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-3.20.2.tgz", + "resolved": "http://registry.npmjs.org/mathjs/-/mathjs-3.20.2.tgz", "integrity": "sha512-3f6/+uf1cUtIz1rYFz775wekl/UEDSQ3mU6xdxW7qzpvvhc2v28i3UtLsGTRB+u8OqDWoSX6Dz8gehaGFs6tCA==", "requires": { "complex.js": "2.0.4", @@ -13518,7 +13522,7 @@ }, "media-typer": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, @@ -13718,9 +13722,35 @@ }, "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, + "minipass": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + }, + "dependencies": { + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + } + } + }, + "minizlib": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, "mississippi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", @@ -13770,7 +13800,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" @@ -13778,7 +13808,7 @@ "dependencies": { "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" } } @@ -14445,7 +14475,7 @@ }, "os-tmpdir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, @@ -14632,7 +14662,7 @@ }, "path-browserify": { "version": "0.0.0", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "resolved": "http://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", "dev": true }, @@ -14650,7 +14680,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { @@ -14791,7 +14821,7 @@ }, "po2json": { "version": "0.4.5", - "resolved": "https://registry.npmjs.org/po2json/-/po2json-0.4.5.tgz", + "resolved": "http://registry.npmjs.org/po2json/-/po2json-0.4.5.tgz", "integrity": "sha1-R7spUtoy1Yob4vJWpZjuvAt0URg=", "dev": true, "requires": { @@ -14853,7 +14883,7 @@ "dependencies": { "async": { "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz", "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", "dev": true } @@ -17261,7 +17291,7 @@ }, "react-ace": { "version": "5.10.0", - "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-5.10.0.tgz", + "resolved": "http://registry.npmjs.org/react-ace/-/react-ace-5.10.0.tgz", "integrity": "sha512-aEK/XZCowP8IXq91e2DYqOtGhabk1bbjt+fyeW0UBcIkzDzP/RX/MeJKeyW7wsZcwElACVwyy9nnwXBTqgky3A==", "requires": { "brace": "^0.11.0", @@ -17340,7 +17370,7 @@ }, "react-dnd": { "version": "2.6.0", - "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-2.6.0.tgz", + "resolved": "http://registry.npmjs.org/react-dnd/-/react-dnd-2.6.0.tgz", "integrity": "sha1-f6JWds+CfViokSk+PBq1naACVFo=", "requires": { "disposables": "^1.0.1", @@ -17360,7 +17390,7 @@ }, "react-dnd-html5-backend": { "version": "2.6.0", - "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-2.6.0.tgz", + "resolved": "http://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-2.6.0.tgz", "integrity": "sha1-WQzRzKeEQbsnTt1XH+9MCxbdz44=", "requires": { "lodash": "^4.2.0" @@ -17487,7 +17517,7 @@ }, "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "mjolnir.js": { @@ -17806,7 +17836,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -18157,7 +18187,7 @@ }, "reduce-css-calc": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", + "resolved": "http://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=", "requires": { "balanced-match": "^0.4.2", @@ -18280,7 +18310,7 @@ }, "regexpp": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==", "dev": true }, @@ -18312,7 +18342,7 @@ "dependencies": { "jsesc": { "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", "dev": true } @@ -18428,7 +18458,7 @@ }, "require-uncached": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "resolved": "http://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", "dev": true, "requires": { @@ -18595,7 +18625,7 @@ }, "safe-regex": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "requires": { @@ -18816,7 +18846,7 @@ }, "sha.js": { "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dev": true, "requires": { @@ -18851,7 +18881,7 @@ }, "iconv-lite": { "version": "0.2.11", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", + "resolved": "http://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", "integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=" } } @@ -18871,7 +18901,7 @@ "dependencies": { "minimist": { "version": "0.0.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.5.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.5.tgz", "integrity": "sha1-16oye87PUY+RBqxrjwA/o7zqhWY=" } } @@ -18930,7 +18960,7 @@ }, "sinon": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-4.5.0.tgz", + "resolved": "http://registry.npmjs.org/sinon/-/sinon-4.5.0.tgz", "integrity": "sha512-trdx+mB0VBBgoYucy6a9L7/jfQOmvGeaKZT4OOJ+lPAtI8623xyGr8wLiE4eojzBS8G9yXbhx42GHUOVLr4X2w==", "dev": true, "requires": { @@ -19322,7 +19352,7 @@ }, "split": { "version": "0.2.10", - "resolved": "https://registry.npmjs.org/split/-/split-0.2.10.tgz", + "resolved": "http://registry.npmjs.org/split/-/split-0.2.10.tgz", "integrity": "sha1-Zwl8YB1pfOE2j0GPBs0gHPBSGlc=", "requires": { "through": "2" @@ -19527,7 +19557,7 @@ }, "readable-stream": { "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "requires": { "core-util-is": "~1.0.0", @@ -19538,7 +19568,7 @@ }, "string_decoder": { "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" } } @@ -19617,7 +19647,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -19625,7 +19655,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -19640,7 +19670,7 @@ }, "strip-eof": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true }, @@ -19920,7 +19950,7 @@ }, "fast-deep-equal": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", "dev": true }, @@ -19977,10 +20007,35 @@ }, "tapable": { "version": "0.1.10", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz", + "resolved": "http://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz", "integrity": "sha1-KcNXB8K3DlDQdIK10gLo7URtr9Q=", "dev": true }, + "tar": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + }, + "dependencies": { + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "dev": true, + "optional": true + } + } + }, "terminal-char-width": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/terminal-char-width/-/terminal-char-width-1.0.8.tgz", @@ -20165,7 +20220,7 @@ }, "through": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "through2": { @@ -20861,7 +20916,7 @@ }, "tty-browserify": { "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "resolved": "http://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", "dev": true }, @@ -21394,7 +21449,7 @@ }, "vm-browserify": { "version": "0.0.4", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "resolved": "http://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", "dev": true, "requires": { @@ -22966,7 +23021,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "requires": { @@ -23051,7 +23106,7 @@ }, "xmlbuilder": { "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" }, "xregexp": { diff --git a/superset/assets/package.json b/superset/assets/package.json index b3df7acef..6914c7364 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -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", diff --git a/superset/assets/spec/javascripts/chart/chartActions_spec.js b/superset/assets/spec/javascripts/chart/chartActions_spec.js index 09f618c6c..a4a832632 100644 --- a/superset/assets/spec/javascripts/chart/chartActions_spec.js +++ b/superset/assets/spec/javascripts/chart/chartActions_spec.js @@ -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 diff --git a/superset/assets/spec/javascripts/chart/transformBigNumber_spec.js b/superset/assets/spec/javascripts/chart/transformBigNumber_spec.js deleted file mode 100644 index 1500a4243..000000000 --- a/superset/assets/spec/javascripts/chart/transformBigNumber_spec.js +++ /dev/null @@ -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] }, - }]); - }); -}); diff --git a/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx index bf24644e1..16dc33dea 100644 --- a/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx +++ b/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx @@ -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); }); diff --git a/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx index 523b835d4..de637cd2a 100644 --- a/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx +++ b/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx @@ -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); diff --git a/superset/assets/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx index cd1c0156c..f89c9ed94 100644 --- a/superset/assets/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx +++ b/superset/assets/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx @@ -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', () => { diff --git a/superset/assets/spec/javascripts/dashboard/components/Header_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/Header_spec.jsx index 2fff4638c..69e57da8d 100644 --- a/superset/assets/spec/javascripts/dashboard/components/Header_spec.jsx +++ b/superset/assets/spec/javascripts/dashboard/components/Header_spec.jsx @@ -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', () => { diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js index f326a76ee..3763ef41a 100644 --- a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js +++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js @@ -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, diff --git a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js index c3e385580..dadcf06c8 100644 --- a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js +++ b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js @@ -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, }); }); diff --git a/superset/assets/spec/javascripts/visualizations/deckgl/layers/common_spec.jsx b/superset/assets/spec/javascripts/visualizations/deckgl/layers/common_spec.jsx index a2a6ef3f5..c82eeeea4 100644 --- a/superset/assets/spec/javascripts/visualizations/deckgl/layers/common_spec.jsx +++ b/superset/assets/spec/javascripts/visualizations/deckgl/layers/common_spec.jsx @@ -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], + ]); + }); + }); }); diff --git a/superset/assets/spec/javascripts/visualizations/deckgl/utils_spec.js b/superset/assets/spec/javascripts/visualizations/deckgl/utils_spec.js index bcc328897..bd256a458 100644 --- a/superset/assets/spec/javascripts/visualizations/deckgl/utils_spec.js +++ b/superset/assets/spec/javascripts/visualizations/deckgl/utils_spec.js @@ -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 = []; diff --git a/superset/assets/src/SqlLab/actions/sqlLab.js b/superset/assets/src/SqlLab/actions/sqlLab.js index 677373daf..b3d61a8ee 100644 --- a/superset/assets/src/SqlLab/actions/sqlLab.js +++ b/superset/assets/src/SqlLab/actions/sqlLab.js @@ -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 => diff --git a/superset/assets/src/chart/Chart.jsx b/superset/assets/src/chart/Chart.jsx index bc19d63b6..c0d391673 100644 --- a/superset/assets/src/chart/Chart.jsx +++ b/superset/assets/src/chart/Chart.jsx @@ -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 {errorMessage}; + } return (
{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} /> diff --git a/superset/assets/src/chart/chartAction.js b/superset/assets/src/chart/chartAction.js index 93433c823..3909dadd8 100644 --- a/superset/assets/src/chart/chartAction.js +++ b/superset/assets/src/chart/chartAction.js @@ -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)); }; } diff --git a/superset/assets/src/chart/chartReducer.js b/superset/assets/src/chart/chartReducer.js index f626be701..e7ad6c6a5 100644 --- a/superset/assets/src/chart/chartReducer.js +++ b/superset/assets/src/chart/chartReducer.js @@ -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) { diff --git a/superset/assets/src/chart/transformBigNumber.js b/superset/assets/src/chart/transformBigNumber.js deleted file mode 100644 index dffa02a7f..000000000 --- a/superset/assets/src/chart/transformBigNumber.js +++ /dev/null @@ -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; -} diff --git a/superset/assets/src/dashboard/actions/dashboardLayout.js b/superset/assets/src/dashboard/actions/dashboardLayout.js index 23cb90e58..b76966c93 100644 --- a/superset/assets/src/dashboard/actions/dashboardLayout.js +++ b/superset/assets/src/dashboard/actions/dashboardLayout.js @@ -209,7 +209,8 @@ export function undoLayoutAction() { if ( dashboardLayout.past.length === 0 && - !dashboardState.maxUndoHistoryExceeded + !dashboardState.maxUndoHistoryExceeded && + !dashboardState.updatedColorScheme ) { dispatch(setUnsavedChanges(false)); } diff --git a/superset/assets/src/dashboard/actions/dashboardState.js b/superset/assets/src/dashboard/actions/dashboardState.js index adf90c609..d3a97cd45 100644 --- a/superset/assets/src/dashboard/actions/dashboardState.js +++ b/superset/assets/src/dashboard/actions/dashboardState.js @@ -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) { diff --git a/superset/assets/src/dashboard/components/BuilderComponentPane.jsx b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx index 4c2e92ce3..2d2ab0899 100644 --- a/superset/assets/src/dashboard/components/BuilderComponentPane.jsx +++ b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx @@ -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 (
-
-
-
- {t('Insert components')} - -
-
-
-
- {t('Your charts & filters')} -
- - -
- - - - - - -
-
-
- - {t('Your charts and filters')} -
- -
-
+ {builderPaneType === BUILDER_PANE_TYPE.ADD_COMPONENTS && ( + + )} + {builderPaneType === BUILDER_PANE_TYPE.COLORS && ( + + )}
)} diff --git a/superset/assets/src/dashboard/components/ColorComponentPane.jsx b/superset/assets/src/dashboard/components/ColorComponentPane.jsx new file mode 100644 index 000000000..ee6aec585 --- /dev/null +++ b/superset/assets/src/dashboard/components/ColorComponentPane.jsx @@ -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 ( +
+
+
+ {'Color Settings'} + +
+
+ +
+
+
+ ); + } +} + +ColorComponentPane.propTypes = propTypes; +ColorComponentPane.defaultProps = defaultProps; + +export default ColorComponentPane; diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx index dc1f05479..b26cde6f2 100644 --- a/superset/assets/src/dashboard/components/Dashboard.jsx +++ b/superset/assets/src/dashboard/components/Dashboard.jsx @@ -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, diff --git a/superset/assets/src/dashboard/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx index 345807dce..12c8ff336 100644 --- a/superset/assets/src/dashboard/components/DashboardBuilder.jsx +++ b/superset/assets/src/dashboard/components/DashboardBuilder.jsx @@ -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 { )}
- {this.props.editMode && this.props.showBuilderPane && ( + {editMode && builderPaneType !== BUILDER_PANE_TYPE.NONE && ( )}
diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx index 796a2df09..92d3b1df6 100644 --- a/superset/assets/src/dashboard/components/Header.jsx +++ b/superset/assets/src/dashboard/components/Header.jsx @@ -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 && ( - + )} + + {editMode && ( + )} @@ -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; diff --git a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx index 7e10d4c76..b96096517 100644 --- a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx +++ b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx @@ -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={{t('Save as')}} @@ -168,11 +177,13 @@ class HeaderActionsDropdown extends React.PureComponent { {t('Force refresh dashboard')} - {t('Set auto-refresh interval')}} - /> + {editMode && ( + {t('Set auto-refresh interval')}} + /> + )} {editMode && ( {t('Edit dashboard metadata')} diff --git a/superset/assets/src/dashboard/components/InsertComponentPane.jsx b/superset/assets/src/dashboard/components/InsertComponentPane.jsx new file mode 100644 index 000000000..31413471f --- /dev/null +++ b/superset/assets/src/dashboard/components/InsertComponentPane.jsx @@ -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 ( +
+
+
+ {t('Insert components')} + +
+
+
+
+ {t('Your charts & filters')} +
+ + +
+ + + + + + +
+
+
+ + {t('Your charts and filters')} +
+ +
+
+ ); + } +} + +InsertComponentPane.propTypes = propTypes; + +export default InsertComponentPane; diff --git a/superset/assets/src/dashboard/components/SaveModal.jsx b/superset/assets/src/dashboard/components/SaveModal.jsx index aa5436979..1873f0c31 100644 --- a/superset/assets/src/dashboard/components/SaveModal.jsx +++ b/superset/assets/src/dashboard/components/SaveModal.jsx @@ -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, diff --git a/superset/assets/src/dashboard/containers/Chart.jsx b/superset/assets/src/dashboard/containers/Chart.jsx index 5b27b13d3..d47c7c97a 100644 --- a/superset/assets/src/dashboard/containers/Chart.jsx +++ b/superset/assets/src/dashboard/containers/Chart.jsx @@ -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, diff --git a/superset/assets/src/dashboard/containers/Dashboard.jsx b/superset/assets/src/dashboard/containers/Dashboard.jsx index 865ec40ac..e5cf4fb59 100644 --- a/superset/assets/src/dashboard/containers/Dashboard.jsx +++ b/superset/assets/src/dashboard/containers/Dashboard.jsx @@ -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, diff --git a/superset/assets/src/dashboard/containers/DashboardBuilder.jsx b/superset/assets/src/dashboard/containers/DashboardBuilder.jsx index 9e1804f52..cb3ca3ee8 100644 --- a/superset/assets/src/dashboard/containers/DashboardBuilder.jsx +++ b/superset/assets/src/dashboard/containers/DashboardBuilder.jsx @@ -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, ); diff --git a/superset/assets/src/dashboard/containers/DashboardHeader.jsx b/superset/assets/src/dashboard/containers/DashboardHeader.jsx index 570d790ae..05e90fb92 100644 --- a/superset/assets/src/dashboard/containers/DashboardHeader.jsx +++ b/superset/assets/src/dashboard/containers/DashboardHeader.jsx @@ -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, diff --git a/superset/assets/src/dashboard/reducers/dashboardState.js b/superset/assets/src/dashboard/reducers/dashboardState.js index 24066ebca..830e2a751 100644 --- a/superset/assets/src/dashboard/reducers/dashboardState.js +++ b/superset/assets/src/dashboard/reducers/dashboardState.js @@ -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, + }; }, }; diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js index 44a491ccb..fb5ff7523 100644 --- a/superset/assets/src/dashboard/reducers/getInitialState.js +++ b/superset/assets/src/dashboard/reducers/getInitialState.js @@ -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, }, diff --git a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less index 3b850c84f..6bf6f6faf 100644 --- a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less +++ b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less @@ -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; + } } diff --git a/superset/assets/src/dashboard/stylesheets/dashboard.less b/superset/assets/src/dashboard/stylesheets/dashboard.less index 3dc9672b1..16541db1e 100644 --- a/superset/assets/src/dashboard/stylesheets/dashboard.less +++ b/superset/assets/src/dashboard/stylesheets/dashboard.less @@ -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; } } diff --git a/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js b/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js index f397a937f..5869a095a 100644 --- a/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js +++ b/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js @@ -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, diff --git a/superset/assets/src/dashboard/util/constants.js b/superset/assets/src/dashboard/util/constants.js index 5cce3ae37..9b33ca899 100644 --- a/superset/assets/src/dashboard/util/constants.js +++ b/superset/assets/src/dashboard/util/constants.js @@ -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', +}; diff --git a/superset/assets/src/dashboard/util/propShapes.jsx b/superset/assets/src/dashboard/util/propShapes.jsx index c433de924..c50ffc6a0 100644 --- a/superset/assets/src/dashboard/util/propShapes.jsx +++ b/superset/assets/src/dashboard/util/propShapes.jsx @@ -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, }); diff --git a/superset/assets/src/explore/components/Control.jsx b/superset/assets/src/explore/components/Control.jsx index 31942e9f2..b8babc709 100644 --- a/superset/assets/src/explore/components/Control.jsx +++ b/superset/assets/src/explore/components/Control.jsx @@ -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; diff --git a/superset/assets/src/explore/components/ExploreChartHeader.jsx b/superset/assets/src/explore/components/ExploreChartHeader.jsx index cbaedb67e..c361367b6 100644 --- a/superset/assets/src/explore/components/ExploreChartHeader.jsx +++ b/superset/assets/src/explore/components/ExploreChartHeader.jsx @@ -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 && } 0) { errors.push(
- {`[ ${control.label} ] `} + {t('Control labeled ')}{` "${control.label}" `} {control.validationErrors.join('. ')}
, ); diff --git a/superset/assets/src/explore/components/controls/ColorMapControl.jsx b/superset/assets/src/explore/components/controls/ColorMapControl.jsx new file mode 100644 index 000000000..08cddef1f --- /dev/null +++ b/superset/assets/src/explore/components/controls/ColorMapControl.jsx @@ -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; diff --git a/superset/assets/src/explore/components/controls/ColorSchemeControl.jsx b/superset/assets/src/explore/components/controls/ColorSchemeControl.jsx index 1e1e67718..34a4d3cf1 100644 --- a/superset/assets/src/explore/components/controls/ColorSchemeControl.jsx +++ b/superset/assets/src/explore/components/controls/ColorSchemeControl.jsx @@ -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 ( -
    - {colors.map((color, i) => ( -
  •  
  • - ))} -
+ +
    + {colors.map((color, i) => ( +
  •  
  • + ))} +
+
); } diff --git a/superset/assets/src/explore/components/controls/DateFilterControl.jsx b/superset/assets/src/explore/components/controls/DateFilterControl.jsx index 7fe0ce10c..0a8579620 100644 --- a/superset/assets/src/explore/components/controls/DateFilterControl.jsx +++ b/superset/assets/src/explore/components/controls/DateFilterControl.jsx @@ -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} )); - const timeFrames = COMMON_TIME_FRAMES.map(timeFrame => ( - this.setState(getStateFromCommonTimeFrame(timeFrame))} - > - {timeFrame} - - )); + const timeFrames = COMMON_TIME_FRAMES.map((timeFrame) => { + const nextState = getStateFromCommonTimeFrame(timeFrame); + return ( + + {nextState.since}
{nextState.until} + + } + > +
+ this.setState(nextState)} + > + {timeFrame} + +
+
+ ); + }); return (
diff --git a/superset/assets/src/explore/components/controls/index.js b/superset/assets/src/explore/components/controls/index.js index d1a2f2c27..32a8d449c 100644 --- a/superset/assets/src/explore/components/controls/index.js +++ b/superset/assets/src/explore/components/controls/index.js @@ -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, diff --git a/superset/assets/src/explore/controlPanels/Area.js b/superset/assets/src/explore/controlPanels/Area.js index 73b5994a8..b978cd15e 100644 --- a/superset/assets/src/explore/controlPanels/Area.js +++ b/superset/assets/src/explore/controlPanels/Area.js @@ -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'], ], }, diff --git a/superset/assets/src/explore/controlPanels/Bar.js b/superset/assets/src/explore/controlPanels/Bar.js index c3cbc733e..b21fb4727 100644 --- a/superset/assets/src/explore/controlPanels/Bar.js +++ b/superset/assets/src/explore/controlPanels/Bar.js @@ -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'], diff --git a/superset/assets/src/explore/controlPanels/BigNumber.js b/superset/assets/src/explore/controlPanels/BigNumber.js index b931c7100..e621ee1f7 100644 --- a/superset/assets/src/explore/controlPanels/BigNumber.js +++ b/superset/assets/src/explore/controlPanels/BigNumber.js @@ -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'), + }, }, }; diff --git a/superset/assets/src/explore/controlPanels/BigNumberTotal.js b/superset/assets/src/explore/controlPanels/BigNumberTotal.js index 22a7d6980..7b3e730ce 100644 --- a/superset/assets/src/explore/controlPanels/BigNumberTotal.js +++ b/superset/assets/src/explore/controlPanels/BigNumberTotal.js @@ -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'), + }, }, }; diff --git a/superset/assets/src/explore/controlPanels/BoxPlot.js b/superset/assets/src/explore/controlPanels/BoxPlot.js index 142c1d663..1cdf320d9 100644 --- a/superset/assets/src/explore/controlPanels/BoxPlot.js +++ b/superset/assets/src/explore/controlPanels/BoxPlot.js @@ -34,7 +34,7 @@ export default { label: t('Chart Options'), expanded: true, controlSetRows: [ - ['color_scheme'], + ['color_scheme', 'label_colors'], ['whisker_options', 'x_ticks_layout'], ], }, diff --git a/superset/assets/src/explore/controlPanels/Bubble.js b/superset/assets/src/explore/controlPanels/Bubble.js index d3cdaca64..891fe29cf 100644 --- a/superset/assets/src/explore/controlPanels/Bubble.js +++ b/superset/assets/src/explore/controlPanels/Bubble.js @@ -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, }, diff --git a/superset/assets/src/explore/controlPanels/Chord.js b/superset/assets/src/explore/controlPanels/Chord.js index 18ae6eb67..b932c3f0e 100644 --- a/superset/assets/src/explore/controlPanels/Chord.js +++ b/superset/assets/src/explore/controlPanels/Chord.js @@ -37,7 +37,7 @@ export default { expanded: true, controlSetRows: [ ['y_axis_format', null], - ['color_scheme'], + ['color_scheme', 'label_colors'], ], }, ], diff --git a/superset/assets/src/explore/controlPanels/Compare.js b/superset/assets/src/explore/controlPanels/Compare.js index 091e8db49..3c50834ae 100644 --- a/superset/assets/src/explore/controlPanels/Compare.js +++ b/superset/assets/src/explore/controlPanels/Compare.js @@ -28,7 +28,7 @@ export default { label: t('Chart Options'), expanded: true, controlSetRows: [ - ['color_scheme'], + ['color_scheme', 'label_colors'], ], }, { diff --git a/superset/assets/src/explore/controlPanels/DeckArc.js b/superset/assets/src/explore/controlPanels/DeckArc.js index d54d59dc5..e0bee756f 100644 --- a/superset/assets/src/explore/controlPanels/DeckArc.js +++ b/superset/assets/src/explore/controlPanels/DeckArc.js @@ -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'], ], }, diff --git a/superset/assets/src/explore/controlPanels/DeckScatter.js b/superset/assets/src/explore/controlPanels/DeckScatter.js index 79476228c..8e60029fe 100644 --- a/superset/assets/src/explore/controlPanels/DeckScatter.js +++ b/superset/assets/src/explore/controlPanels/DeckScatter.js @@ -62,7 +62,7 @@ export default { label: t('Point Color'), controlSetRows: [ ['color_picker', 'legend_position'], - ['dimension', 'color_scheme'], + ['dimension', 'color_scheme', 'label_colors'], ], }, { diff --git a/superset/assets/src/explore/controlPanels/DistBar.js b/superset/assets/src/explore/controlPanels/DistBar.js index fff22d688..41b944e1c 100644 --- a/superset/assets/src/explore/controlPanels/DistBar.js +++ b/superset/assets/src/explore/controlPanels/DistBar.js @@ -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'], diff --git a/superset/assets/src/explore/controlPanels/DualLine.js b/superset/assets/src/explore/controlPanels/DualLine.js index 4792b4a2c..11d930fd3 100644 --- a/superset/assets/src/explore/controlPanels/DualLine.js +++ b/superset/assets/src/explore/controlPanels/DualLine.js @@ -27,7 +27,7 @@ export default { label: t('Chart Options'), expanded: true, controlSetRows: [ - ['color_scheme'], + ['color_scheme', 'label_colors'], ['x_axis_format'], ], }, diff --git a/superset/assets/src/explore/controlPanels/Histogram.js b/superset/assets/src/explore/controlPanels/Histogram.js index 1df41842f..d95ff80a2 100644 --- a/superset/assets/src/explore/controlPanels/Histogram.js +++ b/superset/assets/src/explore/controlPanels/Histogram.js @@ -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'], diff --git a/superset/assets/src/explore/controlPanels/Line.js b/superset/assets/src/explore/controlPanels/Line.js index 012976a6e..a92427dee 100644 --- a/superset/assets/src/explore/controlPanels/Line.js +++ b/superset/assets/src/explore/controlPanels/Line.js @@ -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'], diff --git a/superset/assets/src/explore/controlPanels/LineMulti.js b/superset/assets/src/explore/controlPanels/LineMulti.js index 0966712bd..71c0182ed 100644 --- a/superset/assets/src/explore/controlPanels/LineMulti.js +++ b/superset/assets/src/explore/controlPanels/LineMulti.js @@ -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], diff --git a/superset/assets/src/explore/controlPanels/Partition.js b/superset/assets/src/explore/controlPanels/Partition.js index 2bee2180a..a450da2ba 100644 --- a/superset/assets/src/explore/controlPanels/Partition.js +++ b/superset/assets/src/explore/controlPanels/Partition.js @@ -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'], diff --git a/superset/assets/src/explore/controlPanels/Pie.js b/superset/assets/src/explore/controlPanels/Pie.js index ee9481b93..e3b120394 100644 --- a/superset/assets/src/explore/controlPanels/Pie.js +++ b/superset/assets/src/explore/controlPanels/Pie.js @@ -37,7 +37,7 @@ export default { ['pie_label_type', 'number_format'], ['donut', 'show_legend'], ['show_labels', 'labels_outside'], - ['color_scheme'], + ['color_scheme', 'label_colors'], ], }, ], diff --git a/superset/assets/src/explore/controlPanels/Rose.js b/superset/assets/src/explore/controlPanels/Rose.js index 5939d0cbe..135b0d5ad 100644 --- a/superset/assets/src/explore/controlPanels/Rose.js +++ b/superset/assets/src/explore/controlPanels/Rose.js @@ -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'], ], diff --git a/superset/assets/src/explore/controlPanels/Sankey.js b/superset/assets/src/explore/controlPanels/Sankey.js index dfb337ab7..c7c2f21b1 100644 --- a/superset/assets/src/explore/controlPanels/Sankey.js +++ b/superset/assets/src/explore/controlPanels/Sankey.js @@ -34,7 +34,7 @@ export default { label: t('Chart Options'), expanded: true, controlSetRows: [ - ['color_scheme'], + ['color_scheme', 'label_colors'], ], }, ], diff --git a/superset/assets/src/explore/controlPanels/Sunburst.js b/superset/assets/src/explore/controlPanels/Sunburst.js index 1272c83bc..6d84b2d47 100644 --- a/superset/assets/src/explore/controlPanels/Sunburst.js +++ b/superset/assets/src/explore/controlPanels/Sunburst.js @@ -35,7 +35,7 @@ export default { label: t('Chart Options'), expanded: true, controlSetRows: [ - ['color_scheme'], + ['color_scheme', 'label_colors'], ], }, ], diff --git a/superset/assets/src/explore/controlPanels/Treemap.js b/superset/assets/src/explore/controlPanels/Treemap.js index 9ed05d958..8342ee55b 100644 --- a/superset/assets/src/explore/controlPanels/Treemap.js +++ b/superset/assets/src/explore/controlPanels/Treemap.js @@ -34,7 +34,7 @@ export default { label: t('Chart Options'), expanded: true, controlSetRows: [ - ['color_scheme'], + ['color_scheme', 'label_colors'], ['treemap_ratio'], ['number_format'], ], diff --git a/superset/assets/src/explore/controlPanels/WordCloud.js b/superset/assets/src/explore/controlPanels/WordCloud.js index 35c396baa..79c8fc1ae 100644 --- a/superset/assets/src/explore/controlPanels/WordCloud.js +++ b/superset/assets/src/explore/controlPanels/WordCloud.js @@ -37,7 +37,7 @@ export default { controlSetRows: [ ['size_from', 'size_to'], ['rotation'], - ['color_scheme'], + ['color_scheme', 'label_colors'], ], }, ], diff --git a/superset/assets/src/explore/controlPanels/sections.jsx b/superset/assets/src/explore/controlPanels/sections.jsx index e3b32ca28..e4ad95c8f 100644 --- a/superset/assets/src/explore/controlPanels/sections.jsx +++ b/superset/assets/src/explore/controlPanels/sections.jsx @@ -42,7 +42,7 @@ export const datasourceAndVizType = { export const colorScheme = { label: t('Color Scheme'), controlSetRows: [ - ['color_scheme'], + ['color_scheme', 'label_colors'], ], }; diff --git a/superset/assets/src/explore/controls.jsx b/superset/assets/src/explore/controls.jsx index 913d7fdc2..95e2b64e9 100644 --- a/superset/assets/src/explore/controls.jsx +++ b/superset/assets/src/explore/controls.jsx @@ -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'), diff --git a/superset/assets/src/explore/exploreUtils.js b/superset/assets/src/explore/exploreUtils.js index 48bb60a4f..eabedbabb 100644 --- a/superset/assets/src/explore/exploreUtils.js +++ b/superset/assets/src/explore/exploreUtils.js @@ -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'; diff --git a/superset/assets/src/explore/reducers/exploreReducer.js b/superset/assets/src/explore/reducers/exploreReducer.js index 382388881..6f6a1a9e6 100644 --- a/superset/assets/src/explore/reducers/exploreReducer.js +++ b/superset/assets/src/explore/reducers/exploreReducer.js @@ -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 { diff --git a/superset/assets/src/explore/reducers/getInitialState.js b/superset/assets/src/explore/reducers/getInitialState.js index 48c85c757..98b979914 100644 --- a/superset/assets/src/explore/reducers/getInitialState.js +++ b/superset/assets/src/explore/reducers/getInitialState.js @@ -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, }, }, diff --git a/superset/assets/src/explore/store.js b/superset/assets/src/explore/store.js index be2be7b32..df456c29e 100644 --- a/superset/assets/src/explore/store.js +++ b/superset/assets/src/explore/store.js @@ -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); diff --git a/superset/assets/src/visualizations/deckgl/AnimatableDeckGLContainer.jsx b/superset/assets/src/visualizations/deckgl/AnimatableDeckGLContainer.jsx index f206522cf..fe2c7165d 100644 --- a/superset/assets/src/visualizations/deckgl/AnimatableDeckGLContainer.jsx +++ b/superset/assets/src/visualizations/deckgl/AnimatableDeckGLContainer.jsx @@ -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 (
{!disabled && diff --git a/superset/assets/src/visualizations/deckgl/CategoricalDeckGLContainer.jsx b/superset/assets/src/visualizations/deckgl/CategoricalDeckGLContainer.jsx index 3765f7d49..e1933a4b1 100644 --- a/superset/assets/src/visualizations/deckgl/CategoricalDeckGLContainer.jsx +++ b/superset/assets/src/visualizations/deckgl/CategoricalDeckGLContainer.jsx @@ -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} >
diff --git a/superset/assets/src/visualizations/deckgl/DeckGLContainer.jsx b/superset/assets/src/visualizations/deckgl/DeckGLContainer.jsx index c10f16ac9..ff414bfc0 100644 --- a/superset/assets/src/visualizations/deckgl/DeckGLContainer.jsx +++ b/superset/assets/src/visualizations/deckgl/DeckGLContainer.jsx @@ -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 diff --git a/superset/assets/src/visualizations/deckgl/Multi/Multi.jsx b/superset/assets/src/visualizations/deckgl/Multi/Multi.jsx index 792b94ad0..e9da1f772 100644 --- a/superset/assets/src/visualizations/deckgl/Multi/Multi.jsx +++ b/superset/assets/src/visualizations/deckgl/Multi/Multi.jsx @@ -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} /> ); diff --git a/superset/assets/src/visualizations/deckgl/TooltipRow.jsx b/superset/assets/src/visualizations/deckgl/TooltipRow.jsx new file mode 100644 index 000000000..cc85bfd98 --- /dev/null +++ b/superset/assets/src/visualizations/deckgl/TooltipRow.jsx @@ -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 ( +
{this.props.label}{this.props.value}
+ ); + } +} + +TooltipRow.propTypes = propTypes; diff --git a/superset/assets/src/visualizations/deckgl/factory.jsx b/superset/assets/src/visualizations/deckgl/factory.jsx index 348bdeeb7..1ad8bd373 100644 --- a/superset/assets/src/visualizations/deckgl/factory.jsx +++ b/superset/assets/src/visualizations/deckgl/factory.jsx @@ -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} />); diff --git a/superset/assets/src/visualizations/deckgl/layers/Arc/Arc.jsx b/superset/assets/src/visualizations/deckgl/layers/Arc/Arc.jsx index 75894b089..2c0a99b09 100644 --- a/superset/assets/src/visualizations/deckgl/layers/Arc/Arc.jsx +++ b/superset/assets/src/visualizations/deckgl/layers/Arc/Arc.jsx @@ -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 => ( +
+ + + { + formData.dimension && + } +
+ ); +} + 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)), }); } diff --git a/superset/assets/src/visualizations/deckgl/layers/Geojson/Geojson.jsx b/superset/assets/src/visualizations/deckgl/layers/Geojson/Geojson.jsx index d698a0b8a..7488a3ddf 100644 --- a/superset/assets/src/visualizations/deckgl/layers/Geojson/Geojson.jsx +++ b/superset/assets/src/visualizations/deckgl/layers/Geojson/Geojson.jsx @@ -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 && +
+ { + Object.keys(o.object.extraProps).map((prop, index) => + , + ) + } +
+ ); +} + 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} /> ); diff --git a/superset/assets/src/visualizations/deckgl/layers/Grid/Grid.jsx b/superset/assets/src/visualizations/deckgl/layers/Grid/Grid.jsx index 2baba1e7a..a0cc8613f 100644 --- a/superset/assets/src/visualizations/deckgl/layers/Grid/Grid.jsx +++ b/superset/assets/src/visualizations/deckgl/layers/Grid/Grid.jsx @@ -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 ( +
+ + +
+ ); +} 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), }); } diff --git a/superset/assets/src/visualizations/deckgl/layers/Hex/Hex.jsx b/superset/assets/src/visualizations/deckgl/layers/Hex/Hex.jsx index b17116f21..9901b2262 100644 --- a/superset/assets/src/visualizations/deckgl/layers/Hex/Hex.jsx +++ b/superset/assets/src/visualizations/deckgl/layers/Hex/Hex.jsx @@ -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 ( +
+ + +
+ ); +} 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), }); } diff --git a/superset/assets/src/visualizations/deckgl/layers/Path/Path.jsx b/superset/assets/src/visualizations/deckgl/layers/Path/Path.jsx index bd1b44979..7bf098241 100644 --- a/superset/assets/src/visualizations/deckgl/layers/Path/Path.jsx +++ b/superset/assets/src/visualizations/deckgl/layers/Path/Path.jsx @@ -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 && +
+ { + Object.keys(o.object.extraProps).map((prop, index) => + , + ) + } +
+ ); +} 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), }); } diff --git a/superset/assets/src/visualizations/deckgl/layers/Polygon/Polygon.jsx b/superset/assets/src/visualizations/deckgl/layers/Polygon/Polygon.jsx index b69bbf782..7e30668e2 100644 --- a/superset/assets/src/visualizations/deckgl/layers/Polygon/Polygon.jsx +++ b/superset/assets/src/visualizations/deckgl/layers/Polygon/Polygon.jsx @@ -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 ( +
+ + {formData.metric && } +
+ ); + }; +} + 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 && }
diff --git a/superset/assets/src/visualizations/deckgl/layers/Scatter/Scatter.jsx b/superset/assets/src/visualizations/deckgl/layers/Scatter/Scatter.jsx index ebc26c899..8ca13aac4 100644 --- a/superset/assets/src/visualizations/deckgl/layers/Scatter/Scatter.jsx +++ b/superset/assets/src/visualizations/deckgl/layers/Scatter/Scatter.jsx @@ -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 => ( +
+ + { + o.object.cat_color && + } + { + o.object.metric && + } +
+ ); +} + 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)), }); } diff --git a/superset/assets/src/visualizations/deckgl/layers/Screengrid/Screengrid.jsx b/superset/assets/src/visualizations/deckgl/layers/Screengrid/Screengrid.jsx index cc309bbaa..1efefae21 100644 --- a/superset/assets/src/visualizations/deckgl/layers/Screengrid/Screengrid.jsx +++ b/superset/assets/src/visualizations/deckgl/layers/Screengrid/Screengrid.jsx @@ -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 ( +
+ + +
+ ); +} + 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 /> diff --git a/superset/assets/src/visualizations/deckgl/layers/common.jsx b/superset/assets/src/visualizations/deckgl/layers/common.jsx index 2e5aeede2..aaee55361 100644 --- a/superset/assets/src/visualizations/deckgl/layers/common.jsx +++ b/superset/assets/src/visualizations/deckgl/layers/common.jsx @@ -16,18 +16,54 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react'; - import { fitBounds } from 'viewport-mercator-project'; import * as d3array from 'd3-array'; import sandboxedEval from '../../../modules/sandbox'; +const PADDING = 0.25; +const GEO_BOUNDS = { + LAT_MIN: -90, + LAT_MAX: 90, + LNG_MIN: -180, + LNG_MAX: 180, +}; + +/** + * Get the latitude bounds if latitude is a single coordinate + * @param latExt Latitude range + */ +function getLatBoundsForSingleCoordinate(latExt) { + const latMin = latExt[0] - PADDING < GEO_BOUNDS.LAT_MIN + ? GEO_BOUNDS.LAT_MIN + : latExt[0] - PADDING; + const latMax = latExt[1] + PADDING > GEO_BOUNDS.LAT_MAX + ? GEO_BOUNDS.LAT_MAX + : latExt[1] + PADDING; + return [latMin, latMax]; +} + +/** + * Get the longitude bounds if longitude is a single coordinate + * @param lngExt Longitude range + */ +function getLngBoundsForSingleCoordinate(lngExt) { + const lngMin = lngExt[0] - PADDING < GEO_BOUNDS.LNG_MIN + ? GEO_BOUNDS.LNG_MIN + : lngExt[0] - PADDING; + const lngMax = lngExt[1] + PADDING > GEO_BOUNDS.LNG_MAX + ? GEO_BOUNDS.LNG_MAX + : lngExt[1] + PADDING; + return [lngMin, lngMax]; +} + export function getBounds(points) { const latExt = d3array.extent(points, d => d[1]); const lngExt = d3array.extent(points, d => d[0]); + const latBounds = latExt[0] === latExt[1] ? getLatBoundsForSingleCoordinate(latExt) : latExt; + const lngBounds = lngExt[0] === lngExt[1] ? getLngBoundsForSingleCoordinate(lngExt) : lngExt; return [ - [lngExt[0], latExt[0]], - [lngExt[1], latExt[1]], + [lngBounds[0], latBounds[0]], + [lngBounds[1], latBounds[1]], ]; } @@ -50,20 +86,12 @@ export function fitViewport(viewport, points, padding = 10) { } } -export function commonLayerProps(formData, setTooltip, onSelect) { +export function commonLayerProps(formData, setTooltip, setTooltipContent, onSelect) { const fd = formData; let onHover; - let tooltipContentGenerator; - if (fd.jsTooltip) { - tooltipContentGenerator = sandboxedEval(fd.jsTooltip); - } else if (fd.lineColumn && fd.metric && ['geohash', 'zipcode'].indexOf(fd.lineType) >= 0) { - const metricLabel = fd.metric.label || fd.metric; - tooltipContentGenerator = o => ( -
-
{fd.lineColumn}: {o.object[fd.lineColumn]}
- {fd.metric && -
{metricLabel}: {o.object[metricLabel]}
} -
); + let tooltipContentGenerator = setTooltipContent; + if (fd.js_tooltip) { + tooltipContentGenerator = sandboxedEval(fd.js_tooltip); } if (tooltipContentGenerator) { onHover = (o) => { @@ -79,13 +107,13 @@ export function commonLayerProps(formData, setTooltip, onSelect) { }; } let onClick; - if (fd.jsOnclickHref) { + if (fd.js_onclick_href) { onClick = (o) => { - const href = sandboxedEval(fd.jsOnclickHref)(o); + const href = sandboxedEval(fd.js_onclick_href)(o); window.open(href); }; - } else if (fd.tableFilter && onSelect !== undefined) { - onClick = o => onSelect(o.object[fd.lineColumn]); + } else if (fd.table_filter && onSelect !== undefined) { + onClick = o => onSelect(o.object[fd.line_column]); } return { onClick, diff --git a/superset/assets/src/visualizations/deckgl/utils.js b/superset/assets/src/visualizations/deckgl/utils.js index ef8ae4120..b2b130a48 100644 --- a/superset/assets/src/visualizations/deckgl/utils.js +++ b/superset/assets/src/visualizations/deckgl/utils.js @@ -24,8 +24,8 @@ import { hexToRGB } from '../../modules/colors'; const DEFAULT_NUM_BUCKETS = 10; export function getBreakPoints({ - breakPoints: formDataBreakPoints, - numBuckets: formDataNumBuckets, + break_points: formDataBreakPoints, + num_buckets: formDataNumBuckets, }, features, accessor) { if (!features) { return []; @@ -38,7 +38,8 @@ export function getBreakPoints({ const precision = delta === 0 ? 0 : Math.max(0, Math.ceil(Math.log10(1 / delta))); - return Array(numBuckets + 1) + const extraBucket = maxValue > maxValue.toFixed(precision) ? 1 : 0; + return Array(numBuckets + 1 + extraBucket) .fill() .map((_, i) => (minValue + i * delta).toFixed(precision)); } @@ -46,15 +47,15 @@ export function getBreakPoints({ } export function getBreakPointColorScaler({ - breakPoints: formDataBreakPoints, - numBuckets: formDataNumBuckets, - linearColorScheme, + break_points: formDataBreakPoints, + num_buckets: formDataNumBuckets, + linear_color_scheme: linearColorScheme, opacity, }, features, accessor) { const breakPoints = formDataBreakPoints || formDataNumBuckets ? getBreakPoints({ - breakPoints: formDataBreakPoints, - numBuckets: formDataNumBuckets, + break_points: formDataBreakPoints, + num_buckets: formDataNumBuckets, }, features, accessor) : null; const colorScheme = Array.isArray(linearColorScheme) diff --git a/superset/assets/src/visualizations/stylesheets/deckgl.css b/superset/assets/src/visualizations/stylesheets/deckgl.css new file mode 100644 index 000000000..b085d1c6b --- /dev/null +++ b/superset/assets/src/visualizations/stylesheets/deckgl.css @@ -0,0 +1,22 @@ +/** + * 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. + */ + .deckgl-tooltip > div { + overflow: hidden; + text-overflow: ellipsis; + } diff --git a/superset/config.py b/superset/config.py index 96ccb09d0..b402fec9a 100644 --- a/superset/config.py +++ b/superset/config.py @@ -592,6 +592,8 @@ WEBDRIVER_BASEURL = 'http://0.0.0.0:8080/' # Send user to a link where they can report bugs BUG_REPORT_URL = None +# Send user to a link where they can read more about Superset +DOCUMENTATION_URL = None # What is the Last N days relative in the time selector to: # 'today' means it is midnight (00:00:00) of today in the local timezone diff --git a/superset/db_engine_specs.py b/superset/db_engine_specs.py index 8be1a216d..620ac4ee9 100644 --- a/superset/db_engine_specs.py +++ b/superset/db_engine_specs.py @@ -832,6 +832,8 @@ class PrestoEngineSpec(BaseEngineSpec): return "from_iso8601_date('{}')".format(dttm.isoformat()[:10]) if tt == 'TIMESTAMP': return "from_iso8601_timestamp('{}')".format(dttm.isoformat()) + if tt == 'BIGINT': + return "to_unixtime(from_iso8601_timestamp('{}'))".format(dttm.isoformat()) return "'{}'".format(dttm.strftime('%Y-%m-%d %H:%M:%S')) @classmethod @@ -951,7 +953,6 @@ class PrestoEngineSpec(BaseEngineSpec): that determines if that field should be sorted in descending order :type order_by: list of (str, bool) tuples - :param filters: a list of filters to apply :param filters: dict of field name and filter value combinations """ limit_clause = 'LIMIT {}'.format(limit) if limit else '' @@ -970,7 +971,8 @@ class PrestoEngineSpec(BaseEngineSpec): where_clause = 'WHERE ' + ' AND '.join(l) sql = textwrap.dedent(f"""\ - SHOW PARTITIONS FROM {table_name} + SELECT * FROM "{table_name}$partitions" + {where_clause} {order_by_clause} {limit_clause} diff --git a/superset/models/tags.py b/superset/models/tags.py index 897c18928..9c30f55e5 100644 --- a/superset/models/tags.py +++ b/superset/models/tags.py @@ -123,7 +123,7 @@ class ObjectUpdater(object): tagged_object = TaggedObject( tag_id=tag.id, object_id=target.id, - object_type=ObjectTypes.chart, + object_type=cls.object_type, ) session.add(tagged_object) @@ -141,7 +141,7 @@ class ObjectUpdater(object): tagged_object = TaggedObject( tag_id=tag.id, object_id=target.id, - object_type=ObjectTypes.query, + object_type=cls.object_type, ) session.add(tagged_object) diff --git a/superset/security.py b/superset/security.py index e5ca963cf..df835b096 100644 --- a/superset/security.py +++ b/superset/security.py @@ -118,10 +118,8 @@ class SupersetSecurityManager(SecurityManager): ) def all_datasource_access(self): - return self.can_access('all_datasource_access', 'all_datasource_access') - - def all_database_access(self): - return self.can_access('all_database_access', 'all_database_access') + return self.can_access( + 'all_datasource_access', 'all_datasource_access') def database_access(self, database): return ( @@ -424,12 +422,8 @@ class SupersetSecurityManager(SecurityManager): .values(perm=target.get_perm()), ) - permission_name = 'datasource_access' - from superset.models.core import Database - if mapper.class_ == Database: - permission_name = 'database_access' - # add to view menu if not already exists + permission_name = 'datasource_access' view_menu_name = target.get_perm() permission = self.find_permission(permission_name) view_menu = self.find_view_menu(view_menu_name) diff --git a/superset/tasks/__init__.py b/superset/tasks/__init__.py index 58702ac4c..c2be5085f 100644 --- a/superset/tasks/__init__.py +++ b/superset/tasks/__init__.py @@ -16,3 +16,4 @@ # specific language governing permissions and limitations # under the License. from . import schedules # noqa +from . import cache # noqa diff --git a/superset/tasks/cache.py b/superset/tasks/cache.py new file mode 100644 index 000000000..831bb6642 --- /dev/null +++ b/superset/tasks/cache.py @@ -0,0 +1,316 @@ +# 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. +# pylint: disable=too-few-public-methods + +import json +import logging +import urllib.parse + +from celery.utils.log import get_task_logger +from flask import url_for +import requests +from requests.exceptions import RequestException +from sqlalchemy import and_, func + +from superset import app, db +from superset.models.core import Dashboard, Log, Slice +from superset.models.tags import Tag, TaggedObject +from superset.tasks.celery_app import app as celery_app +from superset.utils.core import parse_human_datetime + + +logger = get_task_logger(__name__) +logger.setLevel(logging.INFO) + + +def get_form_data(chart_id, dashboard=None): + """ + Build `form_data` for chart GET request from dashboard's `default_filters`. + + When a dashboard has `default_filters` they need to be added as extra + filters in the GET request for charts. + + """ + form_data = {'slice_id': chart_id} + + if dashboard is None or not dashboard.json_metadata: + return form_data + + json_metadata = json.loads(dashboard.json_metadata) + + # do not apply filters if chart is immune to them + if chart_id in json_metadata.get('filter_immune_slices', []): + return form_data + + default_filters = json.loads(json_metadata.get('default_filters', 'null')) + if not default_filters: + return form_data + + # are some of the fields in the chart immune to filters? + filter_immune_slice_fields = json_metadata.get('filter_immune_slice_fields', {}) + immune_fields = filter_immune_slice_fields.get(str(chart_id), []) + + extra_filters = [] + for filters in default_filters.values(): + for col, val in filters.items(): + if col not in immune_fields: + extra_filters.append({'col': col, 'op': 'in', 'val': val}) + if extra_filters: + form_data['extra_filters'] = extra_filters + + return form_data + + +def get_url(params): + """Return external URL for warming up a given chart/table cache.""" + baseurl = 'http://{SUPERSET_WEBSERVER_ADDRESS}:{SUPERSET_WEBSERVER_PORT}/'.format( + **app.config) + with app.test_request_context(): + return urllib.parse.urljoin( + baseurl, + url_for('Superset.explore_json', **params), + ) + + +class Strategy: + """ + A cache warm up strategy. + + Each strategy defines a `get_urls` method that returns a list of URLs to + be fetched from the `/superset/warm_up_cache/` endpoint. + + Strategies can be configured in `superset/config.py`: + + CELERYBEAT_SCHEDULE = { + 'cache-warmup-hourly': { + 'task': 'cache-warmup', + 'schedule': crontab(minute=1, hour='*'), # @hourly + 'kwargs': { + 'strategy_name': 'top_n_dashboards', + 'top_n': 10, + 'since': '7 days ago', + }, + }, + } + + """ + def __init__(self): + pass + + def get_urls(self): + raise NotImplementedError('Subclasses must implement get_urls!') + + +class DummyStrategy(Strategy): + """ + Warm up all charts. + + This is a dummy strategy that will fetch all charts. Can be configured by: + + CELERYBEAT_SCHEDULE = { + 'cache-warmup-hourly': { + 'task': 'cache-warmup', + 'schedule': crontab(minute=1, hour='*'), # @hourly + 'kwargs': {'strategy_name': 'dummy'}, + }, + } + + """ + + name = 'dummy' + + def get_urls(self): + session = db.create_scoped_session() + charts = session.query(Slice).all() + + return [get_url({'form_data': get_form_data(chart.id)}) for chart in charts] + + +class TopNDashboardsStrategy(Strategy): + """ + Warm up charts in the top-n dashboards. + + CELERYBEAT_SCHEDULE = { + 'cache-warmup-hourly': { + 'task': 'cache-warmup', + 'schedule': crontab(minute=1, hour='*'), # @hourly + 'kwargs': { + 'strategy_name': 'top_n_dashboards', + 'top_n': 5, + 'since': '7 days ago', + }, + }, + } + + """ + + name = 'top_n_dashboards' + + def __init__(self, top_n=5, since='7 days ago'): + super(TopNDashboardsStrategy, self).__init__() + self.top_n = top_n + self.since = parse_human_datetime(since) + + def get_urls(self): + urls = [] + session = db.create_scoped_session() + + records = ( + session + .query(Log.dashboard_id, func.count(Log.dashboard_id)) + .filter(and_( + Log.dashboard_id.isnot(None), + Log.dttm >= self.since, + )) + .group_by(Log.dashboard_id) + .order_by(func.count(Log.dashboard_id).desc()) + .limit(self.top_n) + .all() + ) + dash_ids = [record.dashboard_id for record in records] + dashboards = ( + session + .query(Dashboard) + .filter(Dashboard.id.in_(dash_ids)) + .all() + ) + for dashboard in dashboards: + for chart in dashboard.slices: + urls.append( + get_url({'form_data': get_form_data(chart.id, dashboard)})) + + return urls + + +class DashboardTagsStrategy(Strategy): + """ + Warm up charts in dashboards with custom tags. + + CELERYBEAT_SCHEDULE = { + 'cache-warmup-hourly': { + 'task': 'cache-warmup', + 'schedule': crontab(minute=1, hour='*'), # @hourly + 'kwargs': { + 'strategy_name': 'dashboard_tags', + 'tags': ['core', 'warmup'], + }, + }, + } + """ + + name = 'dashboard_tags' + + def __init__(self, tags=None): + super(DashboardTagsStrategy, self).__init__() + self.tags = tags or [] + + def get_urls(self): + urls = [] + session = db.create_scoped_session() + + tags = ( + session + .query(Tag) + .filter(Tag.name.in_(self.tags)) + .all() + ) + tag_ids = [tag.id for tag in tags] + + # add dashboards that are tagged + tagged_objects = ( + session + .query(TaggedObject) + .filter(and_( + TaggedObject.object_type == 'dashboard', + TaggedObject.tag_id.in_(tag_ids), + )) + .all() + ) + dash_ids = [tagged_object.object_id for tagged_object in tagged_objects] + tagged_dashboards = ( + session + .query(Dashboard) + .filter(Dashboard.id.in_(dash_ids)) + ) + for dashboard in tagged_dashboards: + for chart in dashboard.slices: + urls.append( + get_url({'form_data': get_form_data(chart.id, dashboard)})) + + # add charts that are tagged + tagged_objects = ( + session + .query(TaggedObject) + .filter(and_( + TaggedObject.object_type == 'chart', + TaggedObject.tag_id.in_(tag_ids), + )) + .all() + ) + chart_ids = [tagged_object.object_id for tagged_object in tagged_objects] + tagged_charts = ( + session + .query(Slice) + .filter(Slice.id.in_(chart_ids)) + ) + for chart in tagged_charts: + urls.append(get_url({'form_data': get_form_data(chart.id)})) + + return urls + + +strategies = [DummyStrategy, TopNDashboardsStrategy, DashboardTagsStrategy] + + +@celery_app.task(name='cache-warmup') +def cache_warmup(strategy_name, *args, **kwargs): + """ + Warm up cache. + + This task periodically hits charts to warm up the cache. + + """ + logger.info('Loading strategy') + class_ = None + for class_ in strategies: + if class_.name == strategy_name: + break + else: + message = f'No strategy {strategy_name} found!' + logger.error(message) + return message + + logger.info(f'Loading {class_.__name__}') + try: + strategy = class_(*args, **kwargs) + logger.info('Success!') + except TypeError: + message = 'Error loading strategy!' + logger.exception(message) + return message + + results = {'success': [], 'errors': []} + for url in strategy.get_urls(): + try: + logger.info(f'Fetching {url}') + requests.get(url) + results['success'].append(url) + except RequestException: + logger.exception('Error warming up cache!') + results['errors'].append(url) + + return results diff --git a/superset/templates/appbuilder/navbar_right.html b/superset/templates/appbuilder/navbar_right.html index 878e93764..73c545e39 100644 --- a/superset/templates/appbuilder/navbar_right.html +++ b/superset/templates/appbuilder/navbar_right.html @@ -18,6 +18,7 @@ #} {% set bug_report_url = appbuilder.app.config.get('BUG_REPORT_URL') %} +{% set documentation_url = appbuilder.app.config.get('DOCUMENTATION_URL') %} {% set locale = session['locale'] %} {% if not locale %} {% set locale = 'en' %} @@ -35,6 +36,17 @@ {% endif %} +{% if documentation_url %} +
  • + +   + +
  • +{% endif %} {% if bug_report_url %}
  • /') def slice(self, slice_id): - form_data, slc = self.get_form_data(slice_id, use_slice_data=True) + form_data, slc = get_form_data(slice_id, use_slice_data=True) if not slc: abort(404) endpoint = '/superset/explore/?form_data={}'.format( @@ -1156,18 +1165,7 @@ class Superset(BaseSupersetView): }) def generate_json( - self, datasource_type, datasource_id, form_data, - csv=False, query=False, force=False, results=False, - samples=False, - ): - viz_obj = self.get_viz( - datasource_type=datasource_type, - datasource_id=datasource_id, - form_data=form_data, - force=force, - ) - security_manager.assert_datasource_permission(viz_obj.datasource) - + self, viz_obj, csv=False, query=False, results=False, samples=False): if csv: return CsvResponse( viz_obj.get_csv(), @@ -1191,21 +1189,25 @@ class Superset(BaseSupersetView): @api @has_access_api @expose('/slice_json/') + @etag_cache(CACHE_DEFAULT_TIMEOUT, check_perms=check_slice_perms) def slice_json(self, slice_id): - form_data, slc = self.get_form_data(slice_id, use_slice_data=True) + form_data, slc = get_form_data(slice_id, use_slice_data=True) datasource_type = slc.datasource.type datasource_id = slc.datasource.id - - return self.generate_json(datasource_type=datasource_type, - datasource_id=datasource_id, - form_data=form_data) + viz_obj = get_viz( + datasource_type=datasource_type, + datasource_id=datasource_id, + form_data=form_data, + force=False, + ) + return self.generate_json(viz_obj) @log_this @api @has_access_api @expose('/annotation_json/') def annotation_json(self, layer_id): - form_data = self.get_form_data()[0] + form_data = get_form_data()[0] form_data['layer_id'] = layer_id form_data['filters'] = [{'col': 'layer_id', 'op': '==', @@ -1225,6 +1227,7 @@ class Superset(BaseSupersetView): @handle_api_exception @expose('/explore_json///', methods=['GET', 'POST']) @expose('/explore_json/', methods=['GET', 'POST']) + @etag_cache(CACHE_DEFAULT_TIMEOUT, check_perms=check_datasource_perms) def explore_json(self, datasource_type=None, datasource_id=None): """Serves all request that GET or POST form_data @@ -1241,18 +1244,21 @@ class Superset(BaseSupersetView): samples = request.args.get('samples') == 'true' force = request.args.get('force') == 'true' - form_data = self.get_form_data()[0] - datasource_id, datasource_type = self.datasource_info( + form_data = get_form_data()[0] + datasource_id, datasource_type = get_datasource_info( datasource_id, datasource_type, form_data) - - return self.generate_json( + viz_obj = get_viz( datasource_type=datasource_type, datasource_id=datasource_id, form_data=form_data, + force=force, + ) + + return self.generate_json( + viz_obj, csv=csv, query=query, results=results, - force=force, samples=samples, ) @@ -1278,34 +1284,15 @@ class Superset(BaseSupersetView): datasource_id=datasource_id, **request.args)) - @staticmethod - def datasource_info(datasource_id, datasource_type, form_data): - """Compatibility layer for handling of datasource info - - datasource_id & datasource_type used to be passed in the URL - directory, now they should come as part of the form_data, - This function allows supporting both without duplicating code""" - datasource = form_data.get('datasource', '') - if '__' in datasource: - datasource_id, datasource_type = datasource.split('__') - # The case where the datasource has been deleted - datasource_id = None if datasource_id == 'None' else datasource_id - - if not datasource_id: - raise Exception( - 'The datasource associated with this chart no longer exists') - datasource_id = int(datasource_id) - return datasource_id, datasource_type - @log_this @has_access @expose('/explore///', methods=['GET', 'POST']) @expose('/explore/', methods=['GET', 'POST']) def explore(self, datasource_type=None, datasource_id=None): user_id = g.user.get_id() if g.user else None - form_data, slc = self.get_form_data(use_slice_data=True) + form_data, slc = get_form_data(use_slice_data=True) - datasource_id, datasource_type = self.datasource_info( + datasource_id, datasource_type = get_datasource_info( datasource_id, datasource_type, form_data) error_redirect = '/chart/list/' @@ -1431,7 +1418,7 @@ class Superset(BaseSupersetView): """Save or overwrite a slice""" slice_name = args.get('slice_name') action = args.get('action') - form_data, unused_slc = self.get_form_data() + form_data = get_form_data()[0] if action in ('saveas'): if 'slice_id' in form_data: @@ -1735,6 +1722,12 @@ class Superset(BaseSupersetView): {key: v for key, v in default_filters_data.items() if int(key) in slice_ids} md['default_filters'] = json.dumps(applicable_filters) + if data.get('color_namespace'): + md['color_namespace'] = data.get('color_namespace') + if data.get('color_scheme'): + md['color_scheme'] = data.get('color_scheme') + if data.get('label_colors'): + md['label_colors'] = data.get('label_colors') dashboard.json_metadata = json.dumps(md) @api @@ -2117,8 +2110,8 @@ class Superset(BaseSupersetView): for slc in slices: try: - form_data = self.get_form_data(slc.id, use_slice_data=True)[0] - obj = self.get_viz( + form_data = get_form_data(slc.id, use_slice_data=True)[0] + obj = get_viz( datasource_type=slc.datasource.type, datasource_id=slc.datasource.id, form_data=form_data, @@ -2552,10 +2545,8 @@ class Superset(BaseSupersetView): ) client_id = request.form.get('client_id') or utils.shortid()[:10] - limits = [mydb.db_engine_spec.get_limit_from_sql(sql), limit] query = Query( database_id=int(database_id), - limit=min(lim for lim in limits if lim is not None), sql=sql, schema=schema, select_as_cta=select_as_cta, @@ -2585,6 +2576,10 @@ class Superset(BaseSupersetView): return json_error_response( 'Template rendering failed: {}'.format(utils.error_msg_from_exception(e))) + # set LIMIT after template processing + limits = [mydb.db_engine_spec.get_limit_from_sql(rendered_query), limit] + query.limit = min(lim for lim in limits if lim is not None) + # Async request. if async_: logging.info('Running query on a Celery worker') @@ -2895,7 +2890,7 @@ class Superset(BaseSupersetView): This method exposes an API endpoint to get the database query string for this slice """ - viz_obj = self.get_viz(slice_id) + viz_obj = get_viz(slice_id) security_manager.assert_datasource_permission(viz_obj.datasource) return self.get_query_string_response(viz_obj) diff --git a/superset/views/tags.py b/superset/views/tags.py index fc34490c0..cffec8748 100644 --- a/superset/views/tags.py +++ b/superset/views/tags.py @@ -30,7 +30,7 @@ from werkzeug.routing import BaseConverter from superset import app, appbuilder, db, utils from superset.jinja_context import current_user_id, current_username -import superset.models.core +from superset.models.core import Dashboard, Slice from superset.models.sql_lab import SavedQuery from superset.models.tags import ObjectTypes, Tag, TaggedObject, TagTypes from .base import BaseSupersetView, json_success @@ -57,33 +57,6 @@ def process_template(content): return template.render(context) -def get_name(obj): - if obj.Dashboard: - return obj.Dashboard.dashboard_title - elif obj.Slice: - return obj.Slice.slice_name - elif obj.SavedQuery: - return obj.SavedQuery.label - - -def get_creator(obj): - if obj.Dashboard: - return obj.Dashboard.creator() - elif obj.Slice: - return obj.Slice.creator() - elif obj.SavedQuery: - return obj.SavedQuery.creator() - - -def get_attribute(obj, attr): - if obj.Dashboard: - return getattr(obj.Dashboard, attr) - elif obj.Slice: - return getattr(obj.Slice, attr) - elif obj.SavedQuery: - return getattr(obj.SavedQuery, attr) - - class TagView(BaseSupersetView): @has_access_api @@ -91,17 +64,22 @@ class TagView(BaseSupersetView): def suggestions(self): query = ( db.session.query(TaggedObject) - .group_by(TaggedObject.tag_id) + .join(Tag) + .with_entities(TaggedObject.tag_id, Tag.name) + .group_by(TaggedObject.tag_id, Tag.name) .order_by(func.count().desc()) .all() ) - tags = [{'id': obj.tag.id, 'name': obj.tag.name} for obj in query] + tags = [{'id': id, 'name': name} for id, name in query] return json_success(json.dumps(tags)) @has_access_api @expose('/tags///', methods=['GET']) def get(self, object_type, object_id): """List all tags a given object has.""" + if object_id == 0: + return json_success(json.dumps([])) + query = db.session.query(TaggedObject).filter(and_( TaggedObject.object_type == object_type, TaggedObject.object_id == object_id)) @@ -112,6 +90,9 @@ class TagView(BaseSupersetView): @expose('/tags///', methods=['POST']) def post(self, object_type, object_id): """Add new tags to an object.""" + if object_id == 0: + return Response(status=404) + tagged_objects = [] for name in request.get_json(force=True): if ':' in name: @@ -157,60 +138,101 @@ class TagView(BaseSupersetView): @has_access_api @expose('/tagged_objects/', methods=['GET', 'POST']) def tagged_objects(self): - query = db.session.query( - TaggedObject, - superset.models.core.Dashboard, - superset.models.core.Slice, - SavedQuery, - ).join(Tag) - - tags = request.args.get('tags') + tags = [ + process_template(tag) + for tag in request.args.get('tags', '').split(',') if tag + ] if not tags: return json_success(json.dumps([])) - tags = [process_template(tag) for tag in tags.split(',')] - query = query.filter(Tag.name.in_(tags)) - # filter types - types = request.args.get('types') - if types: - query = query.filter(TaggedObject.object_type.in_(types.split(','))) - - # get names - query = query.outerjoin( - superset.models.core.Dashboard, - and_( - TaggedObject.object_id == superset.models.core.Dashboard.id, - TaggedObject.object_type == ObjectTypes.dashboard, - ), - ).outerjoin( - superset.models.core.Slice, - and_( - TaggedObject.object_id == superset.models.core.Slice.id, - TaggedObject.object_type == ObjectTypes.chart, - ), - ).outerjoin( - SavedQuery, - and_( - TaggedObject.object_id == SavedQuery.id, - TaggedObject.object_type == ObjectTypes.query, - ), - ).group_by(TaggedObject.object_id, TaggedObject.object_type) - - objects = [ - { - 'id': get_attribute(obj, 'id'), - 'type': obj.TaggedObject.object_type.name, - 'name': get_name(obj), - 'url': get_attribute(obj, 'url'), - 'changed_on': get_attribute(obj, 'changed_on'), - 'created_by': get_attribute(obj, 'created_by_fk'), - 'creator': get_creator(obj), - } - for obj in query if get_attribute(obj, 'id') + types = [ + type_ + for type_ in request.args.get('types', '').split(',') + if type_ ] - return json_success(json.dumps(objects, default=utils.core.json_int_dttm_ser)) + results = [] + + # dashboards + if not types or 'dashboard' in types: + dashboards = ( + db.session.query(Dashboard) + .join( + TaggedObject, + and_( + TaggedObject.object_id == Dashboard.id, + TaggedObject.object_type == ObjectTypes.dashboard, + ), + ) + .join(Tag, TaggedObject.tag_id == Tag.id) + .filter(Tag.name.in_(tags)) + ) + results.extend( + { + 'id': obj.id, + 'type': ObjectTypes.dashboard.name, + 'name': obj.dashboard_title, + 'url': obj.url, + 'changed_on': obj.changed_on, + 'created_by': obj.created_by_fk, + 'creator': obj.creator(), + } for obj in dashboards + ) + + # charts + if not types or 'chart' in types: + charts = ( + db.session.query(Slice) + .join( + TaggedObject, + and_( + TaggedObject.object_id == Slice.id, + TaggedObject.object_type == ObjectTypes.chart, + ), + ) + .join(Tag, TaggedObject.tag_id == Tag.id) + .filter(Tag.name.in_(tags)) + ) + results.extend( + { + 'id': obj.id, + 'type': ObjectTypes.chart.name, + 'name': obj.slice_name, + 'url': obj.url, + 'changed_on': obj.changed_on, + 'created_by': obj.created_by_fk, + 'creator': obj.creator(), + } for obj in charts + ) + + # saved queries + if not types or 'query' in types: + saved_queries = ( + db.session.query(SavedQuery) + .join( + TaggedObject, + and_( + TaggedObject.object_id == SavedQuery.id, + TaggedObject.object_type == ObjectTypes.query, + ), + ) + .join(Tag, TaggedObject.tag_id == Tag.id) + .filter(Tag.name.in_(tags)) + ) + results.extend( + { + 'id': obj.id, + 'type': ObjectTypes.query.name, + 'name': obj.label, + 'url': obj.url(), + 'changed_on': obj.changed_on, + 'created_by': obj.created_by_fk, + 'creator': obj.creator(), + } for obj in saved_queries + ) + + return json_success(json.dumps(results, default=utils.core.json_int_dttm_ser)) app.url_map.converters['object_type'] = ObjectTypeConverter diff --git a/superset/views/utils.py b/superset/views/utils.py index eb68316ba..bb934a112 100644 --- a/superset/views/utils.py +++ b/superset/views/utils.py @@ -16,11 +16,25 @@ # under the License. # pylint: disable=C,R,W from collections import defaultdict +from urllib import parse -from flask import g +from flask import g, request from flask_appbuilder.security.sqla import models as ab_models +import simplejson as json -from superset import db +from superset import app, db, viz +from superset.connectors.connector_registry import ConnectorRegistry +from superset.legacy import update_time_range +import superset.models.core as models + + +FORM_DATA_KEY_BLACKLIST = [] +if not app.config.get('ENABLE_JAVASCRIPT_CONTROLS'): + FORM_DATA_KEY_BLACKLIST = [ + 'js_tooltip', + 'js_onclick_href', + 'js_data_mutator', + ] def bootstrap_user_data(username=None, include_perms=False): @@ -74,3 +88,99 @@ def get_permissions(user): ] return roles, permissions + + +def get_viz( + slice_id=None, + form_data=None, + datasource_type=None, + datasource_id=None, + force=False, +): + if slice_id: + slc = ( + db.session.query(models.Slice) + .filter_by(id=slice_id) + .one() + ) + return slc.get_viz() + else: + viz_type = form_data.get('viz_type', 'table') + datasource = ConnectorRegistry.get_datasource( + datasource_type, datasource_id, db.session) + viz_obj = viz.viz_types[viz_type]( + datasource, + form_data=form_data, + force=force, + ) + return viz_obj + + +def get_form_data(slice_id=None, use_slice_data=False): + form_data = {} + post_data = request.form.get('form_data') + request_args_data = request.args.get('form_data') + # Supporting POST + if post_data: + form_data.update(json.loads(post_data)) + # request params can overwrite post body + if request_args_data: + form_data.update(json.loads(request_args_data)) + + url_id = request.args.get('r') + if url_id: + saved_url = db.session.query(models.Url).filter_by(id=url_id).first() + if saved_url: + url_str = parse.unquote_plus( + saved_url.url.split('?')[1][10:], encoding='utf-8', errors=None) + url_form_data = json.loads(url_str) + # allow form_date in request override saved url + url_form_data.update(form_data) + form_data = url_form_data + + form_data = { + k: v + for k, v in form_data.items() + if k not in FORM_DATA_KEY_BLACKLIST + } + + # When a slice_id is present, load from DB and override + # the form_data from the DB with the other form_data provided + slice_id = form_data.get('slice_id') or slice_id + slc = None + + # Check if form data only contains slice_id, additional filters and viz type + valid_keys = ['slice_id', 'extra_filters', 'adhoc_filters', 'viz_type'] + valid_slice_id = all(key in valid_keys for key in form_data) + + # Include the slice_form_data if request from explore or slice calls + # or if form_data only contains slice_id and additional filters + if slice_id and (use_slice_data or valid_slice_id): + slc = db.session.query(models.Slice).filter_by(id=slice_id).one_or_none() + if slc: + slice_form_data = slc.form_data.copy() + slice_form_data.update(form_data) + form_data = slice_form_data + + update_time_range(form_data) + + return form_data, slc + + +def get_datasource_info(datasource_id, datasource_type, form_data): + """Compatibility layer for handling of datasource info + + datasource_id & datasource_type used to be passed in the URL + directory, now they should come as part of the form_data, + This function allows supporting both without duplicating code""" + datasource = form_data.get('datasource', '') + if '__' in datasource: + datasource_id, datasource_type = datasource.split('__') + # The case where the datasource has been deleted + datasource_id = None if datasource_id == 'None' else datasource_id + + if not datasource_id: + raise Exception( + 'The datasource associated with this chart no longer exists') + datasource_id = int(datasource_id) + return datasource_id, datasource_type diff --git a/tests/dashboard_tests.py b/tests/dashboard_tests.py index c436753dd..04dcd5907 100644 --- a/tests/dashboard_tests.py +++ b/tests/dashboard_tests.py @@ -191,17 +191,60 @@ class DashboardTests(SupersetTestCase): data['dashboard_title'] = origin_title self.get_resp(url, data=dict(data=json.dumps(data))) + def test_save_dash_with_colors(self, username='admin'): + self.login(username=username) + dash = ( + db.session.query(models.Dashboard) + .filter_by(slug='births') + .first() + ) + positions = self.get_mock_positions(dash) + new_label_colors = { + 'data value': 'random color', + } + data = { + 'css': '', + 'expanded_slices': {}, + 'positions': positions, + 'dashboard_title': dash.dashboard_title, + 'color_namespace': 'Color Namespace Test', + 'color_scheme': 'Color Scheme Test', + 'label_colors': new_label_colors, + + } + url = '/superset/save_dash/{}/'.format(dash.id) + self.get_resp(url, data=dict(data=json.dumps(data))) + updatedDash = ( + db.session.query(models.Dashboard) + .filter_by(slug='births') + .first() + ) + self.assertIn('color_namespace', updatedDash.json_metadata) + self.assertIn('color_scheme', updatedDash.json_metadata) + self.assertIn('label_colors', updatedDash.json_metadata) + # bring back original dashboard + del data['color_namespace'] + del data['color_scheme'] + del data['label_colors'] + self.get_resp(url, data=dict(data=json.dumps(data))) + def test_copy_dash(self, username='admin'): self.login(username=username) dash = db.session.query(models.Dashboard).filter_by( slug='births').first() positions = self.get_mock_positions(dash) + new_label_colors = { + 'data value': 'random color', + } data = { 'css': '', 'duplicate_slices': False, 'expanded_slices': {}, 'positions': positions, 'dashboard_title': 'Copy Of Births', + 'color_namespace': 'Color Namespace Test', + 'color_scheme': 'Color Scheme Test', + 'label_colors': new_label_colors, } # Save changes to Births dashboard and retrieve updated dash diff --git a/tests/strategy_tests.py b/tests/strategy_tests.py new file mode 100644 index 000000000..204f9547c --- /dev/null +++ b/tests/strategy_tests.py @@ -0,0 +1,236 @@ +# 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. +"""Unit tests for Superset cache warmup""" +import json +from unittest.mock import MagicMock + +from superset import db +from superset.models.core import Log +from superset.models.tags import ( + get_tag, + ObjectTypes, + TaggedObject, + TagTypes, +) +from superset.tasks.cache import ( + DashboardTagsStrategy, + DummyStrategy, + get_form_data, + TopNDashboardsStrategy, +) +from .base_tests import SupersetTestCase + + +TEST_URL = 'http://0.0.0.0:8081/superset/explore_json' + + +class CacheWarmUpTests(SupersetTestCase): + + def __init__(self, *args, **kwargs): + super(CacheWarmUpTests, self).__init__(*args, **kwargs) + + def test_get_form_data_chart_only(self): + chart_id = 1 + result = get_form_data(chart_id, None) + expected = {'slice_id': chart_id} + self.assertEqual(result, expected) + + def test_get_form_data_no_dashboard_metadata(self): + chart_id = 1 + dashboard = MagicMock() + dashboard.json_metadata = None + result = get_form_data(chart_id, dashboard) + expected = {'slice_id': chart_id} + self.assertEqual(result, expected) + + def test_get_form_data_immune_slice(self): + chart_id = 1 + filter_box_id = 2 + dashboard = MagicMock() + dashboard.json_metadata = json.dumps({ + 'filter_immune_slices': [chart_id], + 'default_filters': json.dumps({ + str(filter_box_id): {'name': ['Alice', 'Bob']}, + }), + }) + result = get_form_data(chart_id, dashboard) + expected = {'slice_id': chart_id} + self.assertEqual(result, expected) + + def test_get_form_data_no_default_filters(self): + chart_id = 1 + dashboard = MagicMock() + dashboard.json_metadata = json.dumps({}) + result = get_form_data(chart_id, dashboard) + expected = {'slice_id': chart_id} + self.assertEqual(result, expected) + + def test_get_form_data_immune_fields(self): + chart_id = 1 + filter_box_id = 2 + dashboard = MagicMock() + dashboard.json_metadata = json.dumps({ + 'default_filters': json.dumps({ + str(filter_box_id): { + 'name': ['Alice', 'Bob'], + '__time_range': '100 years ago : today', + }, + }), + 'filter_immune_slice_fields': {chart_id: ['__time_range']}, + }) + result = get_form_data(chart_id, dashboard) + expected = { + 'slice_id': chart_id, + 'extra_filters': [ + { + 'col': 'name', + 'op': 'in', + 'val': ['Alice', 'Bob'], + }, + ], + } + self.assertEqual(result, expected) + + def test_get_form_data_no_extra_filters(self): + chart_id = 1 + filter_box_id = 2 + dashboard = MagicMock() + dashboard.json_metadata = json.dumps({ + 'default_filters': json.dumps({ + str(filter_box_id): { + '__time_range': '100 years ago : today', + }, + }), + 'filter_immune_slice_fields': {chart_id: ['__time_range']}, + }) + result = get_form_data(chart_id, dashboard) + expected = {'slice_id': chart_id} + self.assertEqual(result, expected) + + def test_get_form_data(self): + chart_id = 1 + filter_box_id = 2 + dashboard = MagicMock() + dashboard.json_metadata = json.dumps({ + 'default_filters': json.dumps({ + str(filter_box_id): { + 'name': ['Alice', 'Bob'], + '__time_range': '100 years ago : today', + }, + }), + }) + result = get_form_data(chart_id, dashboard) + expected = { + 'slice_id': chart_id, + 'extra_filters': [ + { + 'col': 'name', + 'op': 'in', + 'val': ['Alice', 'Bob'], + }, + { + 'col': '__time_range', + 'op': 'in', + 'val': '100 years ago : today', + }, + ], + } + self.assertEqual(result, expected) + + def test_dummy_strategy(self): + strategy = DummyStrategy() + result = sorted(strategy.get_urls()) + expected = [ + f'{TEST_URL}/?form_data=%7B%27slice_id%27%3A+1%7D', + f'{TEST_URL}/?form_data=%7B%27slice_id%27%3A+17%7D', + f'{TEST_URL}/?form_data=%7B%27slice_id%27%3A+18%7D', + f'{TEST_URL}/?form_data=%7B%27slice_id%27%3A+19%7D', + f'{TEST_URL}/?form_data=%7B%27slice_id%27%3A+30%7D', + f'{TEST_URL}/?form_data=%7B%27slice_id%27%3A+31%7D', + f'{TEST_URL}/?form_data=%7B%27slice_id%27%3A+8%7D', + ] + self.assertEqual(result, expected) + + def test_top_n_dashboards_strategy(self): + # create a top visited dashboard + db.session.query(Log).delete() + self.login(username='admin') + for _ in range(10): + self.client.get('/superset/dashboard/3/') + + strategy = TopNDashboardsStrategy(1) + result = sorted(strategy.get_urls()) + expected = [ + f'{TEST_URL}/?form_data=%7B%27slice_id%27%3A+31%7D', + ] + self.assertEqual(result, expected) + + def test_dashboard_tags(self): + strategy = DashboardTagsStrategy(['tag1']) + + result = sorted(strategy.get_urls()) + expected = [] + self.assertEqual(result, expected) + + # tag dashboard 3 with `tag1` + tag1 = get_tag('tag1', db.session, TagTypes.custom) + object_id = 3 + tagged_object = TaggedObject( + tag_id=tag1.id, + object_id=object_id, + object_type=ObjectTypes.dashboard, + ) + db.session.add(tagged_object) + db.session.commit() + + result = sorted(strategy.get_urls()) + expected = [ + f'{TEST_URL}/?form_data=%7B%27slice_id%27%3A+31%7D', + ] + self.assertEqual(result, expected) + + strategy = DashboardTagsStrategy(['tag2']) + + result = sorted(strategy.get_urls()) + expected = [] + self.assertEqual(result, expected) + + # tag chart 30 with `tag2` + tag2 = get_tag('tag2', db.session, TagTypes.custom) + object_id = 30 + tagged_object = TaggedObject( + tag_id=tag2.id, + object_id=object_id, + object_type=ObjectTypes.chart, + ) + db.session.add(tagged_object) + db.session.commit() + + result = sorted(strategy.get_urls()) + expected = [ + f'{TEST_URL}/?form_data=%7B%27slice_id%27%3A+30%7D', + ] + self.assertEqual(result, expected) + + strategy = DashboardTagsStrategy(['tag1', 'tag2']) + + result = sorted(strategy.get_urls()) + expected = [ + f'{TEST_URL}/?form_data=%7B%27slice_id%27%3A+30%7D', + f'{TEST_URL}/?form_data=%7B%27slice_id%27%3A+31%7D', + ] + self.assertEqual(result, expected)