diff --git a/.pylintrc b/.pylintrc index 0835b7afc..2e05f9320 100644 --- a/.pylintrc +++ b/.pylintrc @@ -362,7 +362,7 @@ ignored-argument-names=_.* max-locals=15 # Maximum number of return / yield for function / method body -max-returns=6 +max-returns=10 # Maximum number of branch for function / method body max-branches=12 diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/load.test.js b/superset-frontend/cypress-base/cypress/integration/dashboard/load.test.ts similarity index 91% rename from superset-frontend/cypress-base/cypress/integration/dashboard/load.test.js rename to superset-frontend/cypress-base/cypress/integration/dashboard/load.test.ts index 7e79fa47a..0b38a8378 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/load.test.js +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/load.test.ts @@ -20,19 +20,20 @@ import { getChartAliases, isLegacyResponse, getSliceIdFromRequestUrl, + JsonObject, } from '../../utils/vizPlugins'; import { WORLD_HEALTH_DASHBOARD } from './dashboard.helper'; describe('Dashboard load', () => { let dashboard; - let aliases; + let aliases: string[]; beforeEach(() => { cy.login(); cy.visit(WORLD_HEALTH_DASHBOARD); - cy.get('#app').then(data => { - const bootstrapData = JSON.parse(data[0].dataset.bootstrap); + cy.get('#app').then(nodes => { + const bootstrapData = JSON.parse(nodes[0].dataset.bootstrap || ''); dashboard = bootstrapData.dashboard_data; const { slices } = dashboard; // then define routes and create alias for each requests @@ -53,7 +54,7 @@ describe('Dashboard load', () => { sliceId = responseBody.form_data.slice_id; } else { sliceId = getSliceIdFromRequestUrl(request.url); - responseBody.result.forEach(element => { + responseBody.result.forEach((element: JsonObject) => { expect(element).to.have.property('error', null); expect(element).to.have.property('status', 'success'); }); diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/tabs.test.js b/superset-frontend/cypress-base/cypress/integration/dashboard/tabs.test.ts similarity index 89% rename from superset-frontend/cypress-base/cypress/integration/dashboard/tabs.test.js rename to superset-frontend/cypress-base/cypress/integration/dashboard/tabs.test.ts index fbe3499d0..2ceed512f 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/tabs.test.js +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/tabs.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { interceptChart, parsePostForm } from 'cypress/utils'; +import { interceptChart, parsePostForm, Slice } from 'cypress/utils'; import { TABBED_DASHBOARD } from './dashboard.helper'; describe('Dashboard tabs', () => { @@ -40,24 +40,28 @@ describe('Dashboard tabs', () => { cy.visit(TABBED_DASHBOARD); cy.get('#app').then(data => { - const bootstrapData = JSON.parse(data[0].dataset.bootstrap); - const dashboard = bootstrapData.dashboard_data; + const bootstrapData = JSON.parse(data[0].dataset.bootstrap || ''); + const dashboard = bootstrapData.dashboard_data as { slices: Slice[] }; filterId = dashboard.slices.find( slice => slice.form_data.viz_type === 'filter_box', - ).slice_id; + )?.slice_id; boxplotId = dashboard.slices.find( slice => slice.form_data.viz_type === 'box_plot', - ).slice_id; + )?.slice_id; treemapId = dashboard.slices.find( slice => slice.form_data.viz_type === 'treemap', - ).slice_id; + )?.slice_id; linechartId = dashboard.slices.find( slice => slice.form_data.viz_type === 'line', - ).slice_id; - interceptChart(filterId).as('filterRequest'); - interceptChart(treemapId).as('treemapRequest'); - interceptChart(linechartId).as('linechartRequest'); - interceptChart(boxplotId, false).as('boxplotRequest'); + )?.slice_id; + interceptChart({ sliceId: filterId, legacy: true }).as('filterRequest'); + interceptChart({ sliceId: treemapId, legacy: true }).as('treemapRequest'); + interceptChart({ sliceId: linechartId, legacy: true }).as( + 'linechartRequest', + ); + interceptChart({ sliceId: boxplotId, legacy: false }).as( + 'boxplotRequest', + ); }); }); @@ -140,7 +144,7 @@ describe('Dashboard tabs', () => { // send new query from same tab cy.wait('@treemapRequest').then(({ request }) => { const requestBody = parsePostForm(request.body); - const requestParams = JSON.parse(requestBody.form_data); + const requestParams = JSON.parse(requestBody.form_data as string); expect(requestParams.extra_filters[0]).deep.eq({ col: 'region', op: '==', @@ -153,7 +157,7 @@ describe('Dashboard tabs', () => { cy.wait('@linechartRequest').then(({ request }) => { const requestBody = parsePostForm(request.body); - const requestParams = JSON.parse(requestBody.form_data); + const requestParams = JSON.parse(requestBody.form_data as string); expect(requestParams.extra_filters[0]).deep.eq({ col: 'region', op: '==', diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/url_params.test.js b/superset-frontend/cypress-base/cypress/integration/dashboard/url_params.test.ts similarity index 83% rename from superset-frontend/cypress-base/cypress/integration/dashboard/url_params.test.js rename to superset-frontend/cypress-base/cypress/integration/dashboard/url_params.test.ts index 0ff138755..72ce9b4b1 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/url_params.test.js +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/url_params.test.ts @@ -20,12 +20,14 @@ import { isLegacyResponse, getChartAliases, parsePostForm, + Dashboard, + JsonObject, } from 'cypress/utils'; import { WORLD_HEALTH_DASHBOARD } from './dashboard.helper'; describe('Dashboard form data', () => { const urlParams = { param1: '123', param2: 'abc' }; - let dashboard; + let dashboard: Dashboard; beforeEach(() => { cy.login(); @@ -33,7 +35,7 @@ describe('Dashboard form data', () => { cy.visit(WORLD_HEALTH_DASHBOARD, { qs: urlParams }); cy.get('#app').then(data => { - const bootstrapData = JSON.parse(data[0].dataset.bootstrap); + const bootstrapData = JSON.parse(data[0].dataset.bootstrap || ''); dashboard = bootstrapData.dashboard_data; }); }); @@ -47,13 +49,16 @@ describe('Dashboard form data', () => { const responseBody = response?.body; if (isLegacyResponse(responseBody)) { const requestParams = JSON.parse( - parsePostForm(request.body).form_data, + parsePostForm(request.body).form_data as string, ); expect(requestParams.url_params).deep.eq(urlParams); } else { - request.body.queries.forEach(query => { - expect(query.url_params).deep.eq(urlParams); - }); + // TODO: export url params to chart data API + request.body.queries.forEach( + (query: { url_params: JsonObject }) => { + expect(query.url_params).deep.eq(urlParams); + }, + ); } }), ), diff --git a/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts index b99cd7b7c..70b01351e 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts @@ -19,6 +19,7 @@ // *********************************************** // Tests for setting controls in the UI // *********************************************** +import { interceptChart } from 'cypress/utils'; import { FORM_DATA_DEFAULTS, NUM_METRIC } from './visualizations/shared.helper'; describe('Datasource control', () => { @@ -29,10 +30,10 @@ describe('Datasource control', () => { let numScripts = 0; cy.login(); - cy.intercept('GET', '/superset/explore_json/**').as('getJson'); - cy.intercept('POST', '/superset/explore_json/**').as('postJson'); + interceptChart({ legacy: false }).as('chartData'); + cy.visitChartByName('Num Births Trend'); - cy.verifySliceSuccess({ waitAlias: '@postJson' }); + cy.verifySliceSuccess({ waitAlias: '@chartData' }); cy.get('[data-test="open-datasource-tab').click({ force: true }); cy.get('[data-test="datasource-menu-trigger"]').click(); @@ -90,13 +91,13 @@ describe('Datasource control', () => { describe('VizType control', () => { beforeEach(() => { cy.login(); - cy.intercept('GET', '/superset/explore_json/**').as('getJson'); - cy.intercept('POST', '/superset/explore_json/**').as('postJson'); + interceptChart({ legacy: false }).as('tableChartData'); + interceptChart({ legacy: true }).as('lineChartData'); }); it('Can change vizType', () => { cy.visitChartByName('Daily Totals'); - cy.verifySliceSuccess({ waitAlias: '@postJson' }); + cy.verifySliceSuccess({ waitAlias: '@tableChartData' }); let numScripts = 0; cy.get('script').then(nodes => { @@ -114,27 +115,29 @@ describe('VizType control', () => { }); cy.get('button[data-test="run-query-button"]').click(); - cy.verifySliceSuccess({ waitAlias: '@postJson', chartSelector: 'svg' }); + cy.verifySliceSuccess({ + waitAlias: '@lineChartData', + chartSelector: 'svg', + }); }); }); describe('Time range filter', () => { beforeEach(() => { cy.login(); - cy.intercept('GET', '/superset/explore_json/**').as('getJson'); - cy.intercept('POST', '/superset/explore_json/**').as('postJson'); + interceptChart({ legacy: true }).as('chartData'); }); it('Advanced time_range params', () => { const formData = { ...FORM_DATA_DEFAULTS, - metrics: [NUM_METRIC], viz_type: 'line', time_range: '100 years ago : now', + metrics: [NUM_METRIC], }; cy.visitChartByParams(JSON.stringify(formData)); - cy.verifySliceSuccess({ waitAlias: '@postJson' }); + cy.verifySliceSuccess({ waitAlias: '@chartData' }); cy.get('[data-test=time-range-trigger]') .click() @@ -152,13 +155,13 @@ describe('Time range filter', () => { it('Common time_range params', () => { const formData = { ...FORM_DATA_DEFAULTS, - metrics: [NUM_METRIC], viz_type: 'line', + metrics: [NUM_METRIC], time_range: 'Last year', }; cy.visitChartByParams(JSON.stringify(formData)); - cy.verifySliceSuccess({ waitAlias: '@postJson' }); + cy.verifySliceSuccess({ waitAlias: '@chartData' }); cy.get('[data-test=time-range-trigger]') .click() @@ -172,13 +175,13 @@ describe('Time range filter', () => { it('Previous time_range params', () => { const formData = { ...FORM_DATA_DEFAULTS, - metrics: [NUM_METRIC], viz_type: 'line', + metrics: [NUM_METRIC], time_range: 'previous calendar month', }; cy.visitChartByParams(JSON.stringify(formData)); - cy.verifySliceSuccess({ waitAlias: '@postJson' }); + cy.verifySliceSuccess({ waitAlias: '@chartData' }); cy.get('[data-test=time-range-trigger]') .click() @@ -192,13 +195,13 @@ describe('Time range filter', () => { it('Custom time_range params', () => { const formData = { ...FORM_DATA_DEFAULTS, - metrics: [NUM_METRIC], viz_type: 'line', + metrics: [NUM_METRIC], time_range: 'DATEADD(DATETIME("today"), -7, day) : today', }; cy.visitChartByParams(JSON.stringify(formData)); - cy.verifySliceSuccess({ waitAlias: '@postJson' }); + cy.verifySliceSuccess({ waitAlias: '@chartData' }); cy.get('[data-test=time-range-trigger]') .click() @@ -215,13 +218,13 @@ describe('Time range filter', () => { it('No filter time_range params', () => { const formData = { ...FORM_DATA_DEFAULTS, - metrics: [NUM_METRIC], viz_type: 'line', + metrics: [NUM_METRIC], time_range: 'No filter', }; cy.visitChartByParams(JSON.stringify(formData)); - cy.verifySliceSuccess({ waitAlias: '@postJson' }); + cy.verifySliceSuccess({ waitAlias: '@chartData' }); cy.get('[data-test=time-range-trigger]') .click() @@ -235,16 +238,16 @@ describe('Time range filter', () => { describe('Groupby control', () => { it('Set groupby', () => { cy.login(); - cy.intercept('GET', '/superset/explore_json/**').as('getJson'); - cy.intercept('POST', '/superset/explore_json/**').as('postJson'); + interceptChart({ legacy: true }).as('chartData'); + cy.visitChartByName('Num Births Trend'); - cy.verifySliceSuccess({ waitAlias: '@postJson' }); + cy.verifySliceSuccess({ waitAlias: '@chartData' }); cy.get('[data-test=groupby]').within(() => { cy.get('.Select__control').click(); cy.get('input[type=text]').type('state{enter}'); }); cy.get('button[data-test="run-query-button"]').click(); - cy.verifySliceSuccess({ waitAlias: '@postJson', chartSelector: 'svg' }); + cy.verifySliceSuccess({ waitAlias: '@chartData', chartSelector: 'svg' }); }); }); diff --git a/superset-frontend/cypress-base/cypress/integration/explore/link.test.js b/superset-frontend/cypress-base/cypress/integration/explore/link.test.ts similarity index 85% rename from superset-frontend/cypress-base/cypress/integration/explore/link.test.js rename to superset-frontend/cypress-base/cypress/integration/explore/link.test.ts index 90628441a..fdbf6f5f9 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/link.test.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/link.test.ts @@ -22,25 +22,25 @@ import rison from 'rison'; import shortid from 'shortid'; +import { interceptChart } from 'cypress/utils'; import { HEALTH_POP_FORM_DATA_DEFAULTS } from './visualizations/shared.helper'; -const apiURL = (endpoint, queryObject) => +const apiURL = (endpoint: string, queryObject: Record) => `${endpoint}?q=${rison.encode(queryObject)}`; describe('Test explore links', () => { beforeEach(() => { cy.login(); - cy.intercept('GET', '/superset/explore_json/**').as('getJson'); - cy.intercept('POST', '/superset/explore_json/**').as('postJson'); + interceptChart({ legacy: true }).as('chartData'); }); it('Open and close view query modal', () => { cy.visitChartByName('Growth Rate'); - cy.verifySliceSuccess({ waitAlias: '@postJson' }); + cy.verifySliceSuccess({ waitAlias: '@chartData' }); cy.get('button#query').click(); cy.get('span').contains('View query').parent().click(); - cy.wait('@postJson').then(() => { + cy.wait('@chartData').then(() => { cy.get('code'); }); cy.get('.ant-modal-content').within(() => { @@ -52,7 +52,7 @@ describe('Test explore links', () => { cy.intercept('POST', 'r/shortner/').as('getShortUrl'); cy.visitChartByName('Growth Rate'); - cy.verifySliceSuccess({ waitAlias: '@postJson' }); + cy.verifySliceSuccess({ waitAlias: '@chartData' }); cy.get('[data-test=short-link-button]').click(); @@ -64,12 +64,12 @@ describe('Test explore links', () => { .then(text => { cy.visit(text); }); - cy.verifySliceSuccess({ waitAlias: '@postJson' }); + cy.verifySliceSuccess({ waitAlias: '@chartData' }); }); it('Test iframe link', () => { cy.visitChartByName('Growth Rate'); - cy.verifySliceSuccess({ waitAlias: '@postJson' }); + cy.verifySliceSuccess({ waitAlias: '@chartData' }); cy.get('[data-test=embed-code-button]').click(); cy.get('#embed-code-popover').within(() => { @@ -78,6 +78,8 @@ describe('Test explore links', () => { }); it('Test chart save as AND overwrite', () => { + interceptChart({ legacy: false }).as('tableChartData'); + const formData = { ...HEALTH_POP_FORM_DATA_DEFAULTS, viz_type: 'table', @@ -87,20 +89,20 @@ describe('Test explore links', () => { const newChartName = `Test chart [${shortid.generate()}]`; cy.visitChartByParams(JSON.stringify(formData)); - cy.verifySliceSuccess({ waitAlias: '@postJson' }); + cy.verifySliceSuccess({ waitAlias: '@tableChartData' }); cy.url().then(() => { cy.get('[data-test="query-save-button"]').click(); cy.get('[data-test="saveas-radio"]').check(); cy.get('[data-test="new-chart-name"]').type(newChartName); cy.get('[data-test="btn-modal-save"]').click(); - cy.verifySliceSuccess({ waitAlias: '@postJson' }); + cy.verifySliceSuccess({ waitAlias: '@tableChartData' }); cy.visitChartByName(newChartName); // Overwriting! cy.get('[data-test="query-save-button"]').click(); cy.get('[data-test="save-overwrite-radio"]').check(); cy.get('[data-test="btn-modal-save"]').click(); - cy.verifySliceSuccess({ waitAlias: '@postJson' }); + cy.verifySliceSuccess({ waitAlias: '@tableChartData' }); const query = { filters: [ { @@ -110,6 +112,7 @@ describe('Test explore links', () => { }, ], }; + cy.request(apiURL('/api/v1/chart/', query)).then(response => { expect(response.body.count).equals(1); cy.request('DELETE', `/api/v1/chart/${response.body.ids[0]}`); @@ -123,7 +126,7 @@ describe('Test explore links', () => { const dashboardTitle = `Test dashboard [${shortid.generate()}]`; cy.visitChartByName(chartName); - cy.verifySliceSuccess({ waitAlias: '@postJson' }); + cy.verifySliceSuccess({ waitAlias: '@chartData' }); cy.get('[data-test="query-save-button"]').click(); cy.get('[data-test="saveas-radio"]').check(); @@ -134,7 +137,7 @@ describe('Test explore links', () => { .type(`${dashboardTitle}{enter}{enter}`); cy.get('[data-test="btn-modal-save"]').click(); - cy.verifySliceSuccess({ waitAlias: '@postJson' }); + cy.verifySliceSuccess({ waitAlias: '@chartData' }); let query = { filters: [ { @@ -149,7 +152,7 @@ describe('Test explore links', () => { }); cy.visitChartByName(newChartName); - cy.verifySliceSuccess({ waitAlias: '@postJson' }); + cy.verifySliceSuccess({ waitAlias: '@chartData' }); cy.get('[data-test="query-save-button"]').click(); cy.get('[data-test="save-overwrite-radio"]').check(); @@ -161,7 +164,7 @@ describe('Test explore links', () => { .type(`${dashboardTitle}{enter}{enter}`); cy.get('[data-test="btn-modal-save"]').click(); - cy.verifySliceSuccess({ waitAlias: '@postJson' }); + cy.verifySliceSuccess({ waitAlias: '@chartData' }); query = { filters: [ { diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/table.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/table.test.ts index 91a0367e5..868d2fe27 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/table.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/table.test.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { interceptChart } from 'cypress/utils'; import { FORM_DATA_DEFAULTS, NUM_METRIC, @@ -34,7 +35,7 @@ describe('Visualization > Table', () => { const PERCENT_METRIC = { expressionType: 'SQL', - sqlExpression: 'CAST(SUM(num_girls)+AS+FLOAT)/SUM(num)', + sqlExpression: 'CAST(SUM(num_girls) AS FLOAT)/SUM(num)', column: null, aggregate: null, hasCustomLabel: true, @@ -44,7 +45,7 @@ describe('Visualization > Table', () => { beforeEach(() => { cy.login(); - cy.intercept('POST', '/superset/explore_json/**').as('getJson'); + interceptChart({ legacy: false }).as('chartData'); }); it('Use default time column', () => { @@ -66,8 +67,8 @@ describe('Visualization > Table', () => { }); // when format with smart_date, time column use format by granularity cy.get('.chart-container td:nth-child(1)').contains('2008 Q1'); - // other column with timestamp use raw timestamp - cy.get('.chart-container td:nth-child(3)').contains('2008-01-01T00:00:00'); + // other column with timestamp use adaptive formatting + cy.get('.chart-container td:nth-child(3)').contains('2008'); cy.get('.chart-container td:nth-child(4)').contains('TX'); }); @@ -99,7 +100,7 @@ describe('Visualization > Table', () => { groupby: ['name'], }); cy.verifySliceSuccess({ - waitAlias: '@getJson', + waitAlias: '@chartData', querySubstring: /group by.*name/i, chartSelector: 'table', }); @@ -115,14 +116,14 @@ describe('Visualization > Table', () => { groupby: ['name'], }); cy.verifySliceSuccess({ - waitAlias: '@getJson', + waitAlias: '@chartData', querySubstring: /group by.*name/i, chartSelector: 'table', }); // should handle sorting correctly cy.get('.chart-container th').contains('name').click(); - cy.get('.chart-container td:nth-child(2):eq(0)').contains('Abigail'); + cy.get('.chart-container td:nth-child(2):eq(0)').contains('Aaron'); cy.get('.chart-container th').contains('Time').click().click(); cy.get('.chart-container td:nth-child(1):eq(0)').contains('2008'); }); @@ -134,7 +135,7 @@ describe('Visualization > Table', () => { metrics: [], groupby: ['name'], }); - cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'table' }); + cy.verifySliceSuccess({ waitAlias: '@chartData', chartSelector: 'table' }); }); it('Test table with groupby order desc', () => { @@ -144,7 +145,7 @@ describe('Visualization > Table', () => { groupby: ['name'], order_desc: true, }); - cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'table' }); + cy.verifySliceSuccess({ waitAlias: '@chartData', chartSelector: 'table' }); }); it('Test table with groupby and limit', () => { @@ -156,9 +157,9 @@ describe('Visualization > Table', () => { row_limit: limit, }; cy.visitChartByParams(JSON.stringify(formData)); - cy.wait('@getJson').then(({ response }) => { + cy.wait('@chartData').then(({ response }) => { cy.verifySliceContainer('table'); - expect(response?.body.data.records.length).to.eq(limit); + expect(response?.body.result[0].data.length).to.eq(limit); }); cy.get('span.label-danger').contains('10 rows'); }); @@ -178,7 +179,7 @@ describe('Visualization > Table', () => { cy.get('div[data-test="all_columns"]').should('be.visible'); cy.get('div[data-test="groupby"]').should('not.exist'); - cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'table' }); + cy.verifySliceSuccess({ waitAlias: '@chartData', chartSelector: 'table' }); // should allow switch to aggregate mode cy.get('div[data-test="query_mode"] .btn').contains('Aggregate').click(); @@ -196,14 +197,13 @@ describe('Visualization > Table', () => { all_columns: ['name', 'state', 'ds', 'num'], metrics: [], row_limit: limit, - order_by_cols: ['["num",+false]'], + order_by_cols: ['["num", false]'], }; cy.visitChartByParams(JSON.stringify(formData)); - cy.wait('@getJson').then(({ response }) => { + cy.wait('@chartData').then(({ response }) => { cy.verifySliceContainer('table'); - const responseBody = response?.body; - const { records } = responseBody.data; + const records = response?.body.result[0].data; expect(records[0].num).greaterThan(records[records.length - 1].num); }); }); @@ -215,7 +215,7 @@ describe('Visualization > Table', () => { const formData = { ...VIZ_DEFAULTS, metrics, adhoc_filters: filters }; cy.visitChartByParams(JSON.stringify(formData)); - cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'table' }); + cy.verifySliceSuccess({ waitAlias: '@chartData', chartSelector: 'table' }); }); it('Tests table number formatting with % in metric name', () => { @@ -227,7 +227,7 @@ describe('Visualization > Table', () => { cy.visitChartByParams(JSON.stringify(formData)); cy.verifySliceSuccess({ - waitAlias: '@getJson', + waitAlias: '@chartData', querySubstring: /group by.*state/i, chartSelector: 'table', }); diff --git a/superset-frontend/cypress-base/cypress/support/index.ts b/superset-frontend/cypress-base/cypress/support/index.ts index e2c80b873..e22f69975 100644 --- a/superset-frontend/cypress-base/cypress/support/index.ts +++ b/superset-frontend/cypress-base/cypress/support/index.ts @@ -90,9 +90,8 @@ Cypress.Commands.add( cy.verifySliceContainer(chartSelector); const responseBody = response?.body; if (querySubstring) { - const query = responseBody - ? (responseBody as { query: string }).query - : ''; + const query: string = + responseBody.query || responseBody.result[0].query || ''; if (querySubstring instanceof RegExp) { expect(query).to.match(querySubstring); } else { diff --git a/superset-frontend/cypress-base/cypress/utils/vizPlugins.ts b/superset-frontend/cypress-base/cypress/utils/vizPlugins.ts index 4a60ef08a..9450a303b 100644 --- a/superset-frontend/cypress-base/cypress/utils/vizPlugins.ts +++ b/superset-frontend/cypress-base/cypress/utils/vizPlugins.ts @@ -17,7 +17,30 @@ * under the License. */ -const V1_PLUGINS = ['box_plot', 'echarts_timeseries', 'word_cloud', 'pie']; +export type JsonPrimitive = string | number | boolean | null; +export type JsonValue = JsonPrimitive | JsonObject | JsonArray; +export type JsonArray = JsonValue[]; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type JsonObject = { [member: string]: any }; + +export interface Slice { + slice_id: number; + form_data: { + viz_type: string; + }; +} + +export interface Dashboard { + slices: Slice[]; +} + +const V1_PLUGINS = [ + 'box_plot', + 'echarts_timeseries', + 'word_cloud', + 'pie', + 'table', +]; export const DASHBOARD_CHART_ALIAS_PREFIX = 'getJson_'; export function isLegacyChart(vizType: string): boolean { @@ -34,7 +57,7 @@ export function getSliceIdFromRequestUrl(url: string) { return query?.match(/\d+/)?.[0]; } -export function getChartAliases(slices: any[]): string[] { +export function getChartAliases(slices: Slice[]): string[] { const aliases: string[] = []; Array.from(slices).forEach(slice => { const vizType = slice.form_data.viz_type; @@ -54,11 +77,24 @@ export function getChartAliases(slices: any[]): string[] { return aliases; } -export function interceptChart(sliceId: number, isLegacy = true) { - const formData = { slice_id: sliceId }; - const encodedFormData = encodeURIComponent(JSON.stringify(formData)); - const url = isLegacy - ? `**/superset/explore_json/?form_data=${encodedFormData}*` - : `**/api/v1/chart/data?form_data=${encodedFormData}*`; - return cy.intercept('POST', url); +export function interceptChart({ + sliceId, + legacy = false, + method = 'POST', +}: { + sliceId?: number; + legacy?: boolean; + method?: 'POST' | 'GET'; +}) { + const urlBase = legacy ? '**/superset/explore_json/' : '**/api/v1/chart/data'; + let url; + if (sliceId) { + const encodedFormData = encodeURIComponent( + JSON.stringify({ slice_id: sliceId }), + ); + url = `${urlBase}?form_data=${encodedFormData}*`; + } else { + url = `${urlBase}**`; + } + return cy.intercept(method, url); } diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index bebf63b34..afbfa1ca5 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -6072,9 +6072,12 @@ } }, "d3-array": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", - "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.11.0.tgz", + "integrity": "sha512-26clcwmHQEdsLv34oNKq5Ia9tQ26Y/4HqS3dQzF42QBUqymZJ+9PORcN1G52bt37NsL2ABoX4lvyYZc+A9Y0zw==", + "requires": { + "internmap": "^1.0.0" + } }, "d3-interpolate": { "version": "2.0.1", @@ -18549,19 +18552,19 @@ } }, "@superset-ui/chart-controls": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.16.9.tgz", - "integrity": "sha512-GTwnJx5AhiYqwed3F3FCz+8Yuc56jlLM/g872zoHYQUejnAXGs/Iomeznga6+281DKfsbCO6ptH6qiOZYDH8PA==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.17.1.tgz", + "integrity": "sha512-dfJRoVH0WbG5FQ8smszVtiYLI3NvvLAQxW6HRgOqTLegiKocIIB8hjpMpGrOPxx2G0mBigAubeQowC1VOlpAZQ==", "requires": { - "@superset-ui/core": "0.16.7", + "@superset-ui/core": "0.17.1", "lodash": "^4.17.15", "prop-types": "^15.7.2" } }, "@superset-ui/core": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.16.7.tgz", - "integrity": "sha512-9i/o9ZC+dJibhoWZnoKGvxMMFcz65LjHuYk+hRspuRWA4qwobcdu64piQpfwuFVhw1yh3cbdxq+OSsNmNX9A9g==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.17.1.tgz", + "integrity": "sha512-VnWhb5FjMOrAF2+PJG4WkvscmgtRnnFZBEqG+2g8TSSby2RfIrGB390Dq6abqc9SmBmjNPLj6zkzsvJZv8DsOA==", "requires": { "@babel/runtime": "^7.1.2", "@emotion/core": "^10.0.28", @@ -18574,7 +18577,7 @@ "@types/lodash": "^4.14.149", "@types/rison": "0.0.6", "@types/seedrandom": "^2.4.28", - "@vx/responsive": "^0.0.197", + "@vx/responsive": "^0.0.199", "csstype": "^2.6.4", "d3-format": "^1.3.2", "d3-interpolate": "^1.4.0", @@ -18594,9 +18597,9 @@ }, "dependencies": { "@vx/responsive": { - "version": "0.0.197", - "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", - "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "version": "0.0.199", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.199.tgz", + "integrity": "sha512-ONrmLUAG+8wzD3cn/EmsuZh6JHeyejqup3ZsV25t04VaVJAVQAJukAfNdH8YiwSJu0zSo+txkBTfrnOmFyQLOw==", "requires": { "@types/lodash": "^4.14.146", "@types/react": "*", @@ -18606,9 +18609,12 @@ } }, "d3-array": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", - "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.11.0.tgz", + "integrity": "sha512-26clcwmHQEdsLv34oNKq5Ia9tQ26Y/4HqS3dQzF42QBUqymZJ+9PORcN1G52bt37NsL2ABoX4lvyYZc+A9Y0zw==", + "requires": { + "internmap": "^1.0.0" + } }, "d3-interpolate": { "version": "1.4.0", @@ -18641,12 +18647,12 @@ } }, "@superset-ui/legacy-plugin-chart-calendar": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-calendar/-/legacy-plugin-chart-calendar-0.16.9.tgz", - "integrity": "sha512-8SbUVhuyXYB4Jh4ucFEonhWwF9/rEVOmR/E28bfS3dVJ3lChRx2T8ijv0pRxXNuPSl619Qm61/ec4637V7I//g==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-calendar/-/legacy-plugin-chart-calendar-0.17.1.tgz", + "integrity": "sha512-v9Hh2hNdxsu3vSRtx1KjqsDhDYlCStbEQekp2+BKRH55RKitbJzbw+6GgXxA09s/6VgIxji8oEaZVYAWylzJtQ==", "requires": { - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "d3-array": "^2.0.3", "d3-selection": "^1.4.0", "d3-tip": "^0.9.1", @@ -18654,72 +18660,78 @@ }, "dependencies": { "d3-array": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", - "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.11.0.tgz", + "integrity": "sha512-26clcwmHQEdsLv34oNKq5Ia9tQ26Y/4HqS3dQzF42QBUqymZJ+9PORcN1G52bt37NsL2ABoX4lvyYZc+A9Y0zw==", + "requires": { + "internmap": "^1.0.0" + } } } }, "@superset-ui/legacy-plugin-chart-chord": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-chord/-/legacy-plugin-chart-chord-0.16.9.tgz", - "integrity": "sha512-QK3yJLDzkrIDYAfvAXmZI/nms0fl54WPSQlEN42e17IRe8Z/UNMd7Un4IyAEPjNJqt53uBJq5CEKIBAIB2eTng==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-chord/-/legacy-plugin-chart-chord-0.17.1.tgz", + "integrity": "sha512-xJbr9oyHBOBRp1IWQn1HuLsrArJtADVk2FE6r4QZTVYCKzJoKrQTqQEfkA2roHOHfOZtlcHcD1rlOMt8KpdmDw==", "requires": { - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "d3": "^3.5.17", "prop-types": "^15.6.2", "react": "^16.13.1" } }, "@superset-ui/legacy-plugin-chart-country-map": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-country-map/-/legacy-plugin-chart-country-map-0.16.9.tgz", - "integrity": "sha512-h58JI45Gg/Y7FQEY3v6Wsh07XvYSrH88d+6MJN0Iv72nv9X2iC5h9QVyN0M0jYDpqnfMkoPzsP90Ana8pYqjCw==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-country-map/-/legacy-plugin-chart-country-map-0.17.1.tgz", + "integrity": "sha512-CCJPFGp0P1lEX4W0JqcSC0Lq43gx8BSUNeE//xz+ZKc9JoERttVCKjwEyFCov6Y++iFM/EfDpg1bRS0XpJxD4A==", "requires": { - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "d3": "^3.5.17", "d3-array": "^2.0.3", "prop-types": "^15.6.2" }, "dependencies": { "d3-array": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", - "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.11.0.tgz", + "integrity": "sha512-26clcwmHQEdsLv34oNKq5Ia9tQ26Y/4HqS3dQzF42QBUqymZJ+9PORcN1G52bt37NsL2ABoX4lvyYZc+A9Y0zw==", + "requires": { + "internmap": "^1.0.0" + } } } }, "@superset-ui/legacy-plugin-chart-event-flow": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-event-flow/-/legacy-plugin-chart-event-flow-0.16.9.tgz", - "integrity": "sha512-FWaaXmZXaslb0XlJS3xRcZlFpvGcWtNlGc4x01jrW8vSUzZ16wnQqqPO7b46K6LDJYfgaglXEzNK4gwPWLHKoA==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-event-flow/-/legacy-plugin-chart-event-flow-0.17.1.tgz", + "integrity": "sha512-pGuo5cVjLRJILKbE2oc7mFoWUTrOIf2ChNLHpULZZeNtpG8H3gXGA97qK+5KgXtslfv2BVi1sbR97VV9IH3gTw==", "requires": { "@data-ui/event-flow": "^0.0.84", - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "prop-types": "^15.6.2" } }, "@superset-ui/legacy-plugin-chart-force-directed": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-force-directed/-/legacy-plugin-chart-force-directed-0.16.9.tgz", - "integrity": "sha512-2kryYHT1HqzpYLXkMW5ocCVEJ57K1zGII6mP+piIhqkEgzfSUL1+mUjfzUypuvc+zpKOuWiDxaGwVJqE8gGSbA==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-force-directed/-/legacy-plugin-chart-force-directed-0.17.1.tgz", + "integrity": "sha512-F8aV/iGBeHOh+9ewE8rfpWN2J/avAvlWl1zIM/PZTMlwimVwBXj8voTWk32LVL+Cb+9DwntB5KA093r8yWHK5Q==", "requires": { - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "d3": "^3.5.17", "prop-types": "^15.7.2" } }, "@superset-ui/legacy-plugin-chart-heatmap": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-heatmap/-/legacy-plugin-chart-heatmap-0.16.9.tgz", - "integrity": "sha512-sw1/gOzAyZq6ki7ZocM9KH6BfYuT/yO2zfxW8OMB/8HeMVrM2gPkljSgIX6GYwY5Y83g3ZI+dVPXcp0HNTR0yQ==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-heatmap/-/legacy-plugin-chart-heatmap-0.17.1.tgz", + "integrity": "sha512-IlItjyVT9Y3aE3qYml+CEbGpwVrPJu68MYb5UNOp+ms5DEETRH0Z61kKvX/egCVouYznyVrxjdc8SvL8tHqQYg==", "requires": { - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "d3": "^3.5.17", "d3-svg-legend": "^1.x", "d3-tip": "^0.9.1", @@ -18727,16 +18739,16 @@ } }, "@superset-ui/legacy-plugin-chart-histogram": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-histogram/-/legacy-plugin-chart-histogram-0.16.9.tgz", - "integrity": "sha512-8g4NXTxRjojIntF/ovDoXKaDws/cg6G4CHtll3fLmr85eXSvJATtYODF+KsjefBmGmfIXNUr0kdIPAjfyprizA==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-histogram/-/legacy-plugin-chart-histogram-0.17.1.tgz", + "integrity": "sha512-awZCVmpXH5LCOOCktZdaj5PrPUBeAWdK/60A6n/oKufZ2x4eoXVLgBpsj3JCS73XcKu3FwhWRHLujE0pZthKhA==", "requires": { "@data-ui/histogram": "^0.0.84", "@data-ui/theme": "^0.0.84", - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "@vx/legend": "^0.0.198", - "@vx/responsive": "^0.0.197", + "@vx/responsive": "^0.0.199", "@vx/scale": "^0.0.197", "prop-types": "^15.6.2" }, @@ -18766,9 +18778,9 @@ } }, "@vx/responsive": { - "version": "0.0.197", - "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", - "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "version": "0.0.199", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.199.tgz", + "integrity": "sha512-ONrmLUAG+8wzD3cn/EmsuZh6JHeyejqup3ZsV25t04VaVJAVQAJukAfNdH8YiwSJu0zSo+txkBTfrnOmFyQLOw==", "requires": { "@types/lodash": "^4.14.146", "@types/react": "*", @@ -18802,21 +18814,24 @@ } }, "@superset-ui/legacy-plugin-chart-horizon": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-horizon/-/legacy-plugin-chart-horizon-0.16.9.tgz", - "integrity": "sha512-l5uCO0w1cCcMeI5lJtwhOLZO1509Fj+OCZuvsRCe/pjMncwH8tAwxVljGZ54YLLIWi9Dqo1WFQvA2BLsjQcv6w==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-horizon/-/legacy-plugin-chart-horizon-0.17.1.tgz", + "integrity": "sha512-1omJPgUSktLCqBXqDMMqb9dFfflxWGmEdl6lxVPMqqCrlRILdNCd1rdsQFsu1cN90FmZav7AMVj7SLvIHWvNnQ==", "requires": { - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "d3-array": "^2.0.3", "d3-scale": "^3.0.1", "prop-types": "^15.6.2" }, "dependencies": { "d3-array": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", - "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.11.0.tgz", + "integrity": "sha512-26clcwmHQEdsLv34oNKq5Ia9tQ26Y/4HqS3dQzF42QBUqymZJ+9PORcN1G52bt37NsL2ABoX4lvyYZc+A9Y0zw==", + "requires": { + "internmap": "^1.0.0" + } }, "d3-scale": { "version": "3.2.3", @@ -18833,12 +18848,12 @@ } }, "@superset-ui/legacy-plugin-chart-map-box": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-map-box/-/legacy-plugin-chart-map-box-0.16.9.tgz", - "integrity": "sha512-cf7QdN64rAo4UAmzirhHcJMO7IBYvMX+Slu7/LsCX4CnrVmJtO4d+vGFlNS5FaWU+t2A6lVH5QDpSI1WBgcfAA==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-map-box/-/legacy-plugin-chart-map-box-0.17.1.tgz", + "integrity": "sha512-RHI9k3ulGodGjKgX2kBF3muMyTZKCQGPXV6BbNRzV8DJCc6eGrcE1eznC0ipHiy4yBYeKHfMwcWX3jh9dCI/kg==", "requires": { - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "immutable": "^3.8.2", "mapbox-gl": "^0.53.0", "prop-types": "^15.6.2", @@ -18855,118 +18870,118 @@ } }, "@superset-ui/legacy-plugin-chart-paired-t-test": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-paired-t-test/-/legacy-plugin-chart-paired-t-test-0.16.9.tgz", - "integrity": "sha512-YEVJtqn25SWLr0N33BHzm5CjAOuSkwkOrxl05Lmceyf2z2PwHhrCnnLWXRzEEszQ9IXTsbKT8HYJtPkKNFVIWw==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-paired-t-test/-/legacy-plugin-chart-paired-t-test-0.17.1.tgz", + "integrity": "sha512-WExiHSMvByu8+weNJoKll82cPrhF4zNRnMGzdiYMewJ6TZLN4Ws3ZLR+b2c26BnWQFyA8qXPpsCcQzX9c9WODw==", "requires": { - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "distributions": "^1.0.0", "prop-types": "^15.6.2", "reactable-arc": "0.15.0" } }, "@superset-ui/legacy-plugin-chart-parallel-coordinates": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-parallel-coordinates/-/legacy-plugin-chart-parallel-coordinates-0.16.9.tgz", - "integrity": "sha512-zX6uToyBEV5fKh+cpELDQ+xD9c+TtAGqKsc7r1mpY/R2Vfzw7oD5aOrwcxBshtMoJxx72jo9EuC5nzgNQf4+ug==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-parallel-coordinates/-/legacy-plugin-chart-parallel-coordinates-0.17.1.tgz", + "integrity": "sha512-v6HPEyQRKEOhVpydKzVo+HWUDNaYJhgWL/mrr/kDhT+htUPOUqtTqQZ2BsvgpTQiX4qXHUMnAOXAN7eBa4Xf3w==", "requires": { - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "d3": "^3.5.17", "prop-types": "^15.7.2" } }, "@superset-ui/legacy-plugin-chart-partition": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-partition/-/legacy-plugin-chart-partition-0.16.9.tgz", - "integrity": "sha512-OEmirurMTPAInqMEjGkMQqJDUsM0TMJ4OCFaJ2vtwbvlwk3Fgar/fm/m/qhaLzDLXfTIgb6TyUkxiBCWmmF0iA==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-partition/-/legacy-plugin-chart-partition-0.17.1.tgz", + "integrity": "sha512-Zx7lEjgz0N/MQBmXFrhAsjryIl7QzZc5Gi5bXEi9GYiXcDUaZJmLW0cUnJT5Maqgwt3HCtKgCDZEh/SRj+XBsQ==", "requires": { - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "d3": "^3.5.17", "d3-hierarchy": "^1.1.8", "prop-types": "^15.6.2" } }, "@superset-ui/legacy-plugin-chart-pivot-table": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-pivot-table/-/legacy-plugin-chart-pivot-table-0.16.9.tgz", - "integrity": "sha512-gpEXyC/WlBVeRD2INPx1IfOi3QwmPkr9bdwLfhXe34xU620uj85jO8VKlSU+tGppiyZz6aNaC2WBU4nTRRXUgg==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-pivot-table/-/legacy-plugin-chart-pivot-table-0.17.1.tgz", + "integrity": "sha512-qlXdtaKNQsMibpyJIeQUqaJTLE4Uce9Kn47t9mgB6lCLQc5c+egM9hVwB57P8tUxMBCJePQcjiRIJRnyLKjOTA==", "requires": { - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "d3": "^3.5.17", "datatables.net-bs": "^1.10.15", "prop-types": "^15.6.2" } }, "@superset-ui/legacy-plugin-chart-rose": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-rose/-/legacy-plugin-chart-rose-0.16.9.tgz", - "integrity": "sha512-T6amU5vtlyjohI065OBmLktNW4aNmVsh8UAxSOzKJBRyVDSk0cs1Ntb6FG6jNZLxe7AYh0YVmnYeLI9IVLs2VA==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-rose/-/legacy-plugin-chart-rose-0.17.1.tgz", + "integrity": "sha512-dQykgdgrtOTAq4ck/uZ98R8Vhv4UtercXUlNDMTsiJo5nz3/Ka/4Oz3zBf3Pey+3JrsIOQtJP1vKRKt/j37Zkg==", "requires": { - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "d3": "^3.5.17", "nvd3": "1.8.6", "prop-types": "^15.6.2" } }, "@superset-ui/legacy-plugin-chart-sankey": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sankey/-/legacy-plugin-chart-sankey-0.16.9.tgz", - "integrity": "sha512-Vkauo64wsuRMznDtpqHWPrP6vohnm119wUlWPYk2y9/0e1PgowlRyv6X0RUqWyk2z4/eUXIzGj5YUebqniYjtA==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sankey/-/legacy-plugin-chart-sankey-0.17.1.tgz", + "integrity": "sha512-gy5COpeeEHllYFuenXwdZr5OZ8bL1g+C16bGbjACG9/kIaF9ShoTN+Ad5cNTepb78Cox+WhmPHKIzbgPybN3Bw==", "requires": { - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "d3": "^3.5.17", "d3-sankey": "^0.4.2", "prop-types": "^15.6.2" } }, "@superset-ui/legacy-plugin-chart-sankey-loop": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sankey-loop/-/legacy-plugin-chart-sankey-loop-0.16.9.tgz", - "integrity": "sha512-LtnVsvnoNTrHrOoZcdCuzYX/akCsaiMvWG53KoQis9dsu9+vc4xfKySw0Dct/QpGU+904YbGfFX9qDzh+RCsyw==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sankey-loop/-/legacy-plugin-chart-sankey-loop-0.17.1.tgz", + "integrity": "sha512-r46LqceZi1hv6DDiZMinuU/hR+jyAgURcBvXy/5GvEAF+0eVHUzqbQ8SWivmFCNRwVTAEBMYkUa3IKZ6LBeUbw==", "requires": { - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "d3-sankey-diagram": "^0.7.3", "d3-selection": "^1.4.0", "prop-types": "^15.6.2" } }, "@superset-ui/legacy-plugin-chart-sunburst": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sunburst/-/legacy-plugin-chart-sunburst-0.16.9.tgz", - "integrity": "sha512-DTOuXy7fPUvwUM8WjiKGmbydcIynAbXlEGdmi/ObLlh4lACaTPCX8IOGc18eJP0G78pxeXpByK9JoEZuCbQyEA==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sunburst/-/legacy-plugin-chart-sunburst-0.17.1.tgz", + "integrity": "sha512-hyP36lNaLBWCKfRXTMTG/uvJZa1bMjyX0jYDRAY/tNVxPBAjhTrB76SW1eShfpugg6baaqys9XrbVkxiKRcFnA==", "requires": { - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "d3": "^3.5.17", "prop-types": "^15.6.2" } }, "@superset-ui/legacy-plugin-chart-treemap": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-treemap/-/legacy-plugin-chart-treemap-0.16.9.tgz", - "integrity": "sha512-P+IQYjaMrmN2RqmIEJ2V3w8UQG6jxLPGgmsK/KkGMCzl8PAJJtPcRZw2Z/9JGh8u/7B70JCAEL+sRcLQCA/aTQ==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-treemap/-/legacy-plugin-chart-treemap-0.17.1.tgz", + "integrity": "sha512-z6dQo1ZDb2/drZUJ3nYScBbDXHiIYchtpscYNszyB/jGxJ90ridUniLQicyLxMkjdK4mpuBW9MgidSg0y0I0sw==", "requires": { - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "d3-hierarchy": "^1.1.8", "d3-selection": "^1.4.0", "prop-types": "^15.6.2" } }, "@superset-ui/legacy-plugin-chart-world-map": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-world-map/-/legacy-plugin-chart-world-map-0.16.9.tgz", - "integrity": "sha512-PtYzSKgiaInMt5NYyAmG4b6acffYvN1W8hetpq8TpeMkhuLzA69D3rsAj72ky/dgy5/n14t8wgdfKyfrrt9c6A==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-world-map/-/legacy-plugin-chart-world-map-0.17.1.tgz", + "integrity": "sha512-S9XuCVUIbgfCH4sG0PWGMdEc14fzunZIiUZOC2hh2JtLV/vU452XO4a9viTzWFvAY75zHNetgkTtuoCsrQdLOw==", "requires": { - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "d3": "^3.5.17", "d3-array": "^2.4.0", "d3-color": "^1.4.1", @@ -18975,9 +18990,12 @@ }, "dependencies": { "d3-array": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", - "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.11.0.tgz", + "integrity": "sha512-26clcwmHQEdsLv34oNKq5Ia9tQ26Y/4HqS3dQzF42QBUqymZJ+9PORcN1G52bt37NsL2ABoX4lvyYZc+A9Y0zw==", + "requires": { + "internmap": "^1.0.0" + } }, "d3-color": { "version": "1.4.1", @@ -18987,13 +19005,13 @@ } }, "@superset-ui/legacy-preset-chart-big-number": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-big-number/-/legacy-preset-chart-big-number-0.16.9.tgz", - "integrity": "sha512-Iby3rNcdv+cVy1A/I/Lg/n76v71JdPxKJJc+A4YUil3SalmFFo4mxm0glrX9dex6IYxpZMWbDJKgnHEbhalXlw==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-big-number/-/legacy-preset-chart-big-number-0.17.1.tgz", + "integrity": "sha512-961i+DqTNcPNlvH3GAB2ofViEk4CcZFdcjZ/rMdCzlEichmLLrNzKUPfouvhMpMokb4ysEADOkQvE7POlKjDWw==", "requires": { "@data-ui/xy-chart": "^0.0.84", - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "@types/d3-color": "^1.2.2", "@types/shortid": "^0.0.29", "d3-color": "^1.2.3", @@ -19043,34 +19061,128 @@ "nvd3-fork": "^2.0.5", "prop-types": "^15.6.2", "urijs": "^1.18.10" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.16.9", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.16.9.tgz", + "integrity": "sha512-GTwnJx5AhiYqwed3F3FCz+8Yuc56jlLM/g872zoHYQUejnAXGs/Iomeznga6+281DKfsbCO6ptH6qiOZYDH8PA==", + "requires": { + "@superset-ui/core": "0.16.7", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.16.7.tgz", + "integrity": "sha512-9i/o9ZC+dJibhoWZnoKGvxMMFcz65LjHuYk+hRspuRWA4qwobcdu64piQpfwuFVhw1yh3cbdxq+OSsNmNX9A9g==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@types/seedrandom": "^2.4.28", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "seedrandom": "^3.0.5", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.11.0.tgz", + "integrity": "sha512-26clcwmHQEdsLv34oNKq5Ia9tQ26Y/4HqS3dQzF42QBUqymZJ+9PORcN1G52bt37NsL2ABoX4lvyYZc+A9Y0zw==", + "requires": { + "internmap": "^1.0.0" + } + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } } }, "@superset-ui/plugin-chart-echarts": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-echarts/-/plugin-chart-echarts-0.16.9.tgz", - "integrity": "sha512-MC1eEq3BLedRR+tL88BnisSUkqBxAi4t79JuGY2q9Lgg+7yh1wgD6bqKkR8JaH3SdqSK2XpG0dPkjHZXqFpkMQ==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-echarts/-/plugin-chart-echarts-0.17.1.tgz", + "integrity": "sha512-+PzvoEbeTfrZ+SkM/bldwGljTjy+VBSz/AoPsDEgKmaJ8UZSG7rXQM01X0so7XkD9WRvAMI5q6+uFjL2zfDJlw==", "requires": { - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", - "@types/echarts": "^4.6.3", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", + "@types/echarts": "^4.9.3", "@types/mathjs": "^6.0.7", "echarts": "^5.0.0", "mathjs": "^8.0.1" } }, "@superset-ui/plugin-chart-table": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-table/-/plugin-chart-table-0.16.9.tgz", - "integrity": "sha512-SA6ypwoJq1A9nG/xaI+xRhSRG34omNakgNv3vgeBqCYuZLHwSCf+a/c7liwAA9PhlSTNB7CuOZHC542SVSe6ZQ==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-table/-/plugin-chart-table-0.17.1.tgz", + "integrity": "sha512-Jkf4nAU2usJUQthRMO5dSdeatlUvI+3QYCWujneTXCbR4MMBXhnlePlD0LO5dTteLNBql56rg2SbD10NDIZGZA==", "requires": { "@emotion/core": "^10.0.28", - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "@types/d3-array": "^2.0.0", - "@types/match-sorter": "^4.0.0", "@types/react-table": "^7.0.19", "d3-array": "^2.4.0", - "match-sorter": "^4.1.0", + "match-sorter": "^6.1.0", "memoize-one": "^5.1.1", "react-icons": "^3.10.0", "react-table": "^7.2.1", @@ -19078,37 +19190,23 @@ "xss": "^1.0.6" }, "dependencies": { - "@babel/runtime": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", - "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, "d3-array": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", - "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" - }, - "match-sorter": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-4.2.1.tgz", - "integrity": "sha512-s+3h9TiZU9U1pWhIERHf8/f4LmBN6IXaRgo2CI17+XGByGS1GvG5VvXK9pcGyCjGe3WM3mSYRC3ipGrd5UEVgw==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.11.0.tgz", + "integrity": "sha512-26clcwmHQEdsLv34oNKq5Ia9tQ26Y/4HqS3dQzF42QBUqymZJ+9PORcN1G52bt37NsL2ABoX4lvyYZc+A9Y0zw==", "requires": { - "@babel/runtime": "^7.10.5", - "remove-accents": "0.4.2" + "internmap": "^1.0.0" } } } }, "@superset-ui/plugin-chart-word-cloud": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-word-cloud/-/plugin-chart-word-cloud-0.16.9.tgz", - "integrity": "sha512-NWbPAqxxF8V16xH3jxhhJu9HAoU9BWRZktvhKePQeAtFrGZQ79nQzU+tHYivWvVmjn2bcfhg3D5V5rJwysN9Uw==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-word-cloud/-/plugin-chart-word-cloud-0.17.1.tgz", + "integrity": "sha512-jPz/22L3IwIoQqsHEFqwQTGyYdatednazPB3zGUv1KMzkj4AyU/sd5AsnCCDDC7EL10ylhy9NB8EYk12x5Z7vw==", "requires": { - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "@types/d3-cloud": "^1.2.1", "@types/d3-scale": "^2.0.2", "d3-cloud": "^1.2.5", @@ -19118,9 +19216,12 @@ }, "dependencies": { "d3-array": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", - "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.11.0.tgz", + "integrity": "sha512-26clcwmHQEdsLv34oNKq5Ia9tQ26Y/4HqS3dQzF42QBUqymZJ+9PORcN1G52bt37NsL2ABoX4lvyYZc+A9Y0zw==", + "requires": { + "internmap": "^1.0.0" + } }, "d3-scale": { "version": "3.2.3", @@ -19137,14 +19238,14 @@ } }, "@superset-ui/preset-chart-xy": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@superset-ui/preset-chart-xy/-/preset-chart-xy-0.16.9.tgz", - "integrity": "sha512-hWIuTVcpVINXAlCSEDKKRWiAatx/d15pFmMaaZC7NzpQJ2XfZC6jpCmwgSxbvhbezbduQhwU7DViNINbg2XoZg==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@superset-ui/preset-chart-xy/-/preset-chart-xy-0.17.1.tgz", + "integrity": "sha512-N1mSF8OE04n+xM8Fh6ZsNLD1ARGnVlh3zzld1YmhasS7rRP8UZ3STGEjLm4IV8mTYeVc+i7+Xg/VI5Fl42Uhow==", "requires": { "@data-ui/theme": "^0.0.84", "@data-ui/xy-chart": "^0.0.84", - "@superset-ui/chart-controls": "0.16.9", - "@superset-ui/core": "0.16.7", + "@superset-ui/chart-controls": "0.17.1", + "@superset-ui/core": "0.17.1", "@vx/axis": "^0.0.198", "@vx/legend": "^0.0.198", "@vx/scale": "^0.0.197", @@ -21002,11 +21103,6 @@ "@types/react": "*" } }, - "@types/match-sorter": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/match-sorter/-/match-sorter-4.0.0.tgz", - "integrity": "sha512-JK7HNHXZA7i/nEp6fbNAxoX/1j1ysZXmv2/nlkt2UpX1LiUWKLtyt/dMmDTlMPR6t6PkwMmIr2W2AAyu6oELNw==" - }, "@types/mathjs": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/@types/mathjs/-/mathjs-6.0.11.tgz", @@ -21194,6 +21290,16 @@ "@types/react": "*" } }, + "@types/react-loadable": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/@types/react-loadable/-/react-loadable-5.5.4.tgz", + "integrity": "sha512-otKcjNCfVUzdBMdwOqFITTmBruIXw6GeoZitTBvJ6BMrif8Utu2JLy42GWukNnYI7ewJdncUCooz5Y/1dBz4+w==", + "dev": true, + "requires": { + "@types/react": "*", + "@types/webpack": "*" + } + }, "@types/react-redux": { "version": "7.1.10", "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.10.tgz", @@ -28683,9 +28789,12 @@ }, "dependencies": { "d3-array": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", - "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.11.0.tgz", + "integrity": "sha512-26clcwmHQEdsLv34oNKq5Ia9tQ26Y/4HqS3dQzF42QBUqymZJ+9PORcN1G52bt37NsL2ABoX4lvyYZc+A9Y0zw==", + "requires": { + "internmap": "^1.0.0" + } }, "d3-interpolate": { "version": "2.0.1", @@ -33267,6 +33376,11 @@ } } }, + "internmap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.0.tgz", + "integrity": "sha512-SdoDWwNOTE2n4JWUsLn4KXZGuZPjPF9yyOGc8bnfWnBQh7BD/l80rzSznKc/r4Y0aQ7z3RTk9X+tV4tHBpu+dA==" + }, "interpret": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index d95a87b83..6ebad9d3f 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -65,34 +65,34 @@ "@babel/runtime-corejs3": "^7.12.5", "@data-ui/sparkline": "^0.0.84", "@emotion/core": "^10.0.35", - "@superset-ui/chart-controls": "^0.16.9", - "@superset-ui/core": "^0.16.7", - "@superset-ui/legacy-plugin-chart-calendar": "^0.16.9", - "@superset-ui/legacy-plugin-chart-chord": "^0.16.9", - "@superset-ui/legacy-plugin-chart-country-map": "^0.16.9", - "@superset-ui/legacy-plugin-chart-event-flow": "^0.16.9", - "@superset-ui/legacy-plugin-chart-force-directed": "^0.16.9", - "@superset-ui/legacy-plugin-chart-heatmap": "^0.16.9", - "@superset-ui/legacy-plugin-chart-histogram": "^0.16.9", - "@superset-ui/legacy-plugin-chart-horizon": "^0.16.9", - "@superset-ui/legacy-plugin-chart-map-box": "^0.16.9", - "@superset-ui/legacy-plugin-chart-paired-t-test": "^0.16.9", - "@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.16.9", - "@superset-ui/legacy-plugin-chart-partition": "^0.16.9", - "@superset-ui/legacy-plugin-chart-pivot-table": "^0.16.9", - "@superset-ui/legacy-plugin-chart-rose": "^0.16.9", - "@superset-ui/legacy-plugin-chart-sankey": "^0.16.9", - "@superset-ui/legacy-plugin-chart-sankey-loop": "^0.16.9", - "@superset-ui/legacy-plugin-chart-sunburst": "^0.16.9", - "@superset-ui/legacy-plugin-chart-treemap": "^0.16.9", - "@superset-ui/legacy-plugin-chart-world-map": "^0.16.9", - "@superset-ui/legacy-preset-chart-big-number": "^0.16.9", + "@superset-ui/chart-controls": "^0.17.1", + "@superset-ui/core": "^0.17.1", + "@superset-ui/legacy-plugin-chart-calendar": "^0.17.1", + "@superset-ui/legacy-plugin-chart-chord": "^0.17.1", + "@superset-ui/legacy-plugin-chart-country-map": "^0.17.1", + "@superset-ui/legacy-plugin-chart-event-flow": "^0.17.1", + "@superset-ui/legacy-plugin-chart-force-directed": "^0.17.1", + "@superset-ui/legacy-plugin-chart-heatmap": "^0.17.1", + "@superset-ui/legacy-plugin-chart-histogram": "^0.17.1", + "@superset-ui/legacy-plugin-chart-horizon": "^0.17.1", + "@superset-ui/legacy-plugin-chart-map-box": "^0.17.1", + "@superset-ui/legacy-plugin-chart-paired-t-test": "^0.17.1", + "@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.17.1", + "@superset-ui/legacy-plugin-chart-partition": "^0.17.1", + "@superset-ui/legacy-plugin-chart-pivot-table": "^0.17.1", + "@superset-ui/legacy-plugin-chart-rose": "^0.17.1", + "@superset-ui/legacy-plugin-chart-sankey": "^0.17.1", + "@superset-ui/legacy-plugin-chart-sankey-loop": "^0.17.1", + "@superset-ui/legacy-plugin-chart-sunburst": "^0.17.1", + "@superset-ui/legacy-plugin-chart-treemap": "^0.17.1", + "@superset-ui/legacy-plugin-chart-world-map": "^0.17.1", + "@superset-ui/legacy-preset-chart-big-number": "^0.17.1", "@superset-ui/legacy-preset-chart-deckgl": "^0.4.1", "@superset-ui/legacy-preset-chart-nvd3": "^0.16.10", - "@superset-ui/plugin-chart-echarts": "^0.16.9", - "@superset-ui/plugin-chart-table": "^0.16.9", - "@superset-ui/plugin-chart-word-cloud": "^0.16.9", - "@superset-ui/preset-chart-xy": "^0.16.9", + "@superset-ui/plugin-chart-echarts": "^0.17.1", + "@superset-ui/plugin-chart-table": "^0.17.1", + "@superset-ui/plugin-chart-word-cloud": "^0.17.1", + "@superset-ui/preset-chart-xy": "^0.17.1", "@vx/responsive": "^0.0.195", "abortcontroller-polyfill": "^1.1.9", "antd": "^4.9.4", @@ -214,6 +214,7 @@ "@types/react-dom": "^16.9.8", "@types/react-gravatar": "^2.6.8", "@types/react-json-tree": "^0.6.11", + "@types/react-loadable": "^5.5.4", "@types/react-redux": "^7.1.10", "@types/react-router-dom": "^5.1.5", "@types/react-select": "^3.0.19", diff --git a/superset-frontend/tsconfig.json b/superset-frontend/tsconfig.json index ff434966a..09403431c 100644 --- a/superset-frontend/tsconfig.json +++ b/superset-frontend/tsconfig.json @@ -27,7 +27,7 @@ ], // for supressing errors caused by incompatible @types/react when `npm link` // Ref: https://github.com/Microsoft/typescript/issues/6496#issuecomment-384786222 - "react": ["./node_modules/@types/react"] + "react": ["./node_modules/@types/react", "react"] }, "skipLibCheck": true, "sourceMap": true, diff --git a/superset/charts/api.py b/superset/charts/api.py index 6f2d380a7..c84a84351 100644 --- a/superset/charts/api.py +++ b/superset/charts/api.py @@ -65,7 +65,7 @@ from superset.charts.schemas import ( from superset.commands.exceptions import CommandInvalidError from superset.commands.importers.v1.utils import get_contents_from_bundle from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod -from superset.exceptions import SupersetSecurityException +from superset.exceptions import QueryObjectValidationError, SupersetSecurityException from superset.extensions import event_logger from superset.models.slice import Slice from superset.tasks.thumbnails import cache_chart_thumbnail @@ -566,9 +566,13 @@ class ChartRestApi(BaseSupersetModelRestApi): command = ChartDataCommand() query_context = command.set_query_context(json_body) command.validate() + except QueryObjectValidationError as error: + return self.response_400(message=error.message) except ValidationError as error: return self.response_400( - message=_("Request is incorrect: %(error)s", error=error.messages) + message=_( + "Request is incorrect: %(error)s", error=error.normalized_messages() + ) ) except SupersetSecurityException: return self.response_401() diff --git a/superset/common/query_context.py b/superset/common/query_context.py index 3c2081364..6e3fc9f5c 100644 --- a/superset/common/query_context.py +++ b/superset/common/query_context.py @@ -22,7 +22,7 @@ from typing import Any, cast, ClassVar, Dict, List, Optional, Union import numpy as np import pandas as pd -from flask_babel import gettext as _ +from flask_babel import _ from superset import app, db, is_feature_enabled from superset.annotation_layers.dao import AnnotationLayerDAO @@ -142,7 +142,9 @@ class QueryContext: """Converting metrics to numeric when pandas.read_sql cannot""" for col, dtype in df.dtypes.items(): if dtype.type == np.object_ and col in query_object.metric_names: - df[col] = pd.to_numeric(df[col], errors="coerce") + # soft-convert a metric column to numeric + # will stay as strings if conversion fails + df[col] = df[col].infer_objects() def get_data(self, df: pd.DataFrame,) -> Union[str, List[Dict[str, Any]]]: if self.result_format == utils.ChartDataResultFormat.CSV: @@ -153,15 +155,15 @@ class QueryContext: return df.to_dict(orient="records") def get_single_payload( - self, query_obj: QueryObject, **kwargs: Any + self, query_obj: QueryObject, force_cached: Optional[bool] = False, ) -> Dict[str, Any]: - """Returns a payload of metadata and data""" - force_cached = kwargs.get("force_cached", False) + """Return results payload for a single quey""" if self.result_type == utils.ChartDataResultType.QUERY: return { "query": self.datasource.get_query_str(query_obj.to_dict()), "language": self.datasource.query_language, } + if self.result_type == utils.ChartDataResultType.SAMPLES: row_limit = query_obj.row_limit or math.inf query_obj = copy.copy(query_obj) @@ -173,10 +175,13 @@ class QueryContext: query_obj.row_limit = min(row_limit, config["SAMPLES_ROW_LIMIT"]) query_obj.row_offset = 0 query_obj.columns = [o.column_name for o in self.datasource.columns] + payload = self.get_df_payload(query_obj, force_cached=force_cached) df = payload["df"] status = payload["status"] if status != utils.QueryStatus.FAILED: + payload["colnames"] = list(df.columns) + payload["coltypes"] = utils.serialize_pandas_dtypes(df.dtypes) payload["data"] = self.get_data(df) del payload["df"] @@ -195,13 +200,19 @@ class QueryContext: if col not in columns ] + rejected_time_columns - if self.result_type == utils.ChartDataResultType.RESULTS: + if ( + self.result_type == utils.ChartDataResultType.RESULTS + and status != utils.QueryStatus.FAILED + ): return {"data": payload["data"]} return payload - def get_payload(self, **kwargs: Any) -> Dict[str, Any]: - cache_query_context = kwargs.get("cache_query_context", False) - force_cached = kwargs.get("force_cached", False) + def get_payload( + self, + cache_query_context: Optional[bool] = False, + force_cached: Optional[bool] = False, + ) -> Dict[str, Any]: + """Returns the query results with both metadata and data""" # Get all the payloads from the QueryObjects query_results = [ @@ -310,7 +321,7 @@ class QueryContext: chart = ChartDAO.find_by_id(annotation_layer["value"]) form_data = chart.form_data.copy() if not chart: - raise QueryObjectValidationError("The chart does not exist") + raise QueryObjectValidationError(_("The chart does not exist")) try: viz_obj = get_viz( datasource_type=chart.datasource.type, @@ -342,10 +353,9 @@ class QueryContext: return annotation_data def get_df_payload( # pylint: disable=too-many-statements,too-many-locals - self, query_obj: QueryObject, **kwargs: Any + self, query_obj: QueryObject, force_cached: Optional[bool] = False, ) -> Dict[str, Any]: """Handles caching around the df payload retrieval""" - force_cached = kwargs.get("force_cached", False) cache_key = self.query_cache_key(query_obj) logger.info("Cache key: %s", cache_key) is_loaded = False @@ -387,7 +397,7 @@ class QueryContext: for col in query_obj.columns + query_obj.groupby + utils.get_column_names_from_metrics(query_obj.metrics) - if col not in self.datasource.column_names + if col not in self.datasource.column_names and col != DTTM_ALIAS ] if invalid_columns: raise QueryObjectValidationError( @@ -446,5 +456,6 @@ class QueryContext: :raises SupersetSecurityException: If the user cannot access the resource """ - + for query in self.queries: + query.validate() security_manager.raise_for_access(query_context=self) diff --git a/superset/common/query_object.py b/superset/common/query_object.py index cabbba69a..249f6a9aa 100644 --- a/superset/common/query_object.py +++ b/superset/common/query_object.py @@ -28,7 +28,12 @@ from superset import app, is_feature_enabled from superset.exceptions import QueryObjectValidationError from superset.typing import Metric from superset.utils import pandas_postprocessing -from superset.utils.core import DTTM_ALIAS, get_metric_names, json_int_dttm_ser +from superset.utils.core import ( + DTTM_ALIAS, + find_duplicates, + get_metric_names, + json_int_dttm_ser, +) from superset.utils.date_parser import get_since_until, parse_human_timedelta from superset.views.utils import get_time_range_endpoints @@ -106,6 +111,8 @@ class QueryObject: ): annotation_layers = annotation_layers or [] metrics = metrics or [] + columns = columns or [] + groupby = groupby or [] extras = extras or {} is_sip_38 = is_feature_enabled("SIP_38_VIZ_REARCHITECTURE") self.annotation_layers = [ @@ -126,19 +133,18 @@ class QueryObject: time_range=time_range, time_shift=time_shift, ) - # is_timeseries is True if time column is in groupby + # is_timeseries is True if time column is in either columns or groupby + # (both are dimensions) self.is_timeseries = ( is_timeseries if is_timeseries is not None - else (DTTM_ALIAS in groupby if groupby else False) + else DTTM_ALIAS in columns + groupby ) self.time_range = time_range self.time_shift = parse_human_timedelta(time_shift) self.post_processing = [ post_proc for post_proc in post_processing or [] if post_proc ] - if not is_sip_38: - self.groupby = groupby or [] # Support metric reference/definition in the format of # 1. 'metric_name' - name of predefined metric @@ -162,13 +168,16 @@ class QueryObject: if config["SIP_15_ENABLED"] and "time_range_endpoints" not in self.extras: self.extras["time_range_endpoints"] = get_time_range_endpoints(form_data={}) - self.columns = columns or [] - if is_sip_38 and groupby: - self.columns += groupby - logger.warning( - "The field `groupby` is deprecated. Viz plugins should " - "pass all selectables via the `columns` field" - ) + self.columns = columns + if is_sip_38: + if groupby: + logger.warning( + "The field `groupby` is deprecated. Viz plugins should " + "pass all selectables via the `columns` field" + ) + self.columns += groupby + else: + self.groupby = groupby or [] self.orderby = orderby or [] @@ -214,8 +223,34 @@ class QueryObject: @property def metric_names(self) -> List[str]: + """Return metrics names (labels), coerce adhoc metrics to strings.""" return get_metric_names(self.metrics) + @property + def column_names(self) -> List[str]: + """Return column names (labels). Reserved for future adhoc calculated + columns.""" + return self.columns + + def validate( + self, raise_exceptions: Optional[bool] = True + ) -> Optional[QueryObjectValidationError]: + """Validate query object""" + error: Optional[QueryObjectValidationError] = None + all_labels = self.metric_names + self.column_names + if len(set(all_labels)) < len(all_labels): + dup_labels = find_duplicates(all_labels) + error = QueryObjectValidationError( + _( + "Duplicate column/metric labels: %(labels)s. Please make " + "sure all columns and metrics have a unique label.", + labels=", ".join(f'"{x}"' for x in dup_labels), + ) + ) + if error and raise_exceptions: + raise error + return error + def to_dict(self) -> Dict[str, Any]: query_object_dict = { "granularity": self.granularity, diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index 6969bf544..fdc00c1a1 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -939,6 +939,7 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at and (is_sip_38 or (not is_sip_38 and not groupby)) ): raise QueryObjectValidationError(_("Empty query?")) + metrics_exprs: List[ColumnElement] = [] for metric in metrics: if utils.is_adhoc_metric(metric): @@ -950,6 +951,7 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at raise QueryObjectValidationError( _("Metric '%(metric)s' does not exist", metric=metric) ) + if metrics_exprs: main_metric_expr = metrics_exprs[0] else: @@ -960,14 +962,16 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at groupby_exprs_sans_timestamp = OrderedDict() assert extras is not None - if (is_sip_38 and metrics and columns) or (not is_sip_38 and groupby): - # dedup columns while preserving order - columns_ = columns if is_sip_38 else groupby - assert columns_ - groupby = list(dict.fromkeys(columns_)) + # filter out the pseudo column __timestamp from columns + columns = columns or [] + columns = [col for col in columns if col != utils.DTTM_ALIAS] + + if (is_sip_38 and metrics and columns) or (not is_sip_38 and metrics): + # dedup columns while preserving order + columns = columns if is_sip_38 else (groupby or columns) select_exprs = [] - for selected in groupby: + for selected in columns: # if groupby field/expr equals granularity field/expr if selected == granularity: time_grain = extras.get("time_grain_sqla") @@ -979,7 +983,6 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at else: outer = literal_column(f"({selected})") outer = self.make_sqla_column_compatible(outer, selected) - groupby_exprs_sans_timestamp[outer.name] = outer select_exprs.append(outer) elif columns: @@ -1001,7 +1004,8 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at if is_timeseries: timestamp = dttm_col.get_timestamp_expression(time_grain) - select_exprs += [timestamp] + # always put timestamp as the first column + select_exprs.insert(0, timestamp) groupby_exprs_with_timestamp[timestamp.name] = timestamp # Use main dttm column to support index with secondary dttm columns. diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py index cae31ba80..fcf4b8fff 100644 --- a/superset/db_engine_specs/base.py +++ b/superset/db_engine_specs/base.py @@ -158,28 +158,22 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods try_remove_schema_from_table_name = True # pylint: disable=invalid-name run_multiple_statements_as_one = False - # default matching patterns for identifying column types - db_column_types: Dict[utils.GenericDataType, Tuple[Pattern[Any], ...]] = { + # default matching patterns to convert database specific column types to + # more generic types + db_column_types: Dict[utils.GenericDataType, Tuple[Pattern[str], ...]] = { utils.GenericDataType.NUMERIC: ( re.compile(r"BIT", re.IGNORECASE), - re.compile(r".*DOUBLE.*", re.IGNORECASE), - re.compile(r".*FLOAT.*", re.IGNORECASE), - re.compile(r".*INT.*", re.IGNORECASE), - re.compile(r".*NUMBER.*", re.IGNORECASE), + re.compile( + r".*(DOUBLE|FLOAT|INT|NUMBER|REAL|NUMERIC|DECIMAL|MONEY).*", + re.IGNORECASE, + ), re.compile(r".*LONG$", re.IGNORECASE), - re.compile(r".*REAL.*", re.IGNORECASE), - re.compile(r".*NUMERIC.*", re.IGNORECASE), - re.compile(r".*DECIMAL.*", re.IGNORECASE), - re.compile(r".*MONEY.*", re.IGNORECASE), ), utils.GenericDataType.STRING: ( - re.compile(r".*CHAR.*", re.IGNORECASE), - re.compile(r".*STRING.*", re.IGNORECASE), - re.compile(r".*TEXT.*", re.IGNORECASE), + re.compile(r".*(CHAR|STRING|TEXT).*", re.IGNORECASE), ), utils.GenericDataType.TEMPORAL: ( - re.compile(r".*DATE.*", re.IGNORECASE), - re.compile(r".*TIME.*", re.IGNORECASE), + re.compile(r".*(DATE|TIME).*", re.IGNORECASE), ), } diff --git a/superset/models/core.py b/superset/models/core.py index 8c15fc8aa..b603af67e 100755 --- a/superset/models/core.py +++ b/superset/models/core.py @@ -373,7 +373,11 @@ class Database( username = utils.get_username() def needs_conversion(df_series: pd.Series) -> bool: - return not df_series.empty and isinstance(df_series[0], (list, dict)) + return ( + not df_series.empty + and isinstance(df_series, pd.Series) + and isinstance(df_series[0], (list, dict)) + ) def _log_query(sql: str) -> None: if log_query: @@ -397,9 +401,9 @@ class Database( if mutator: mutator(df) - for k, v in df.dtypes.items(): - if v.type == numpy.object_ and needs_conversion(df[k]): - df[k] = df[k].apply(utils.json_dumps_w_dates) + for col, coltype in df.dtypes.to_dict().items(): + if coltype == numpy.object_ and needs_conversion(df[col]): + df[col] = df[col].apply(utils.json_dumps_w_dates) return df diff --git a/superset/utils/core.py b/superset/utils/core.py index 313a812d5..8c6da5c5e 100644 --- a/superset/utils/core.py +++ b/superset/utils/core.py @@ -15,6 +15,7 @@ # specific language governing permissions and limitations # under the License. """Utility functions used across Superset""" +import collections import decimal import errno import functools @@ -37,7 +38,7 @@ from email.mime.image import MIMEImage from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.utils import formatdate -from enum import Enum +from enum import Enum, IntEnum from timeit import default_timer from types import TracebackType from typing import ( @@ -55,6 +56,7 @@ from typing import ( Tuple, Type, TYPE_CHECKING, + TypeVar, Union, ) from urllib.parse import unquote_plus @@ -105,6 +107,8 @@ DTTM_ALIAS = "__timestamp" JS_MAX_INTEGER = 9007199254740991 # Largest int Java Script can handle 2^53-1 +InputType = TypeVar("InputType") + class LenientEnum(Enum): """Enums with a `get` method that convert a enum value to `Enum` if it is a @@ -130,14 +134,15 @@ class AnnotationType(str, Enum): TIME_SERIES = "TIME_SERIES" -class GenericDataType(Enum): +class GenericDataType(IntEnum): """ - Generic database column type + Generic database column type that fits both frontend and backend. """ NUMERIC = 0 STRING = 1 TEMPORAL = 2 + BOOLEAN = 3 class ChartDataResultFormat(str, Enum): @@ -1396,6 +1401,21 @@ def get_column_names_from_metrics(metrics: List[Metric]) -> List[str]: return columns +def serialize_pandas_dtypes(dtypes: List[np.dtype]) -> List[GenericDataType]: + """Serialize pandas/numpy dtypes to JavaScript types""" + mapping = { + "object": GenericDataType.STRING, + "category": GenericDataType.STRING, + "datetime64[ns]": GenericDataType.TEMPORAL, + "int64": GenericDataType.NUMERIC, + "in32": GenericDataType.NUMERIC, + "float64": GenericDataType.NUMERIC, + "float32": GenericDataType.NUMERIC, + "bool": GenericDataType.BOOLEAN, + } + return [mapping.get(str(x), GenericDataType.STRING) for x in dtypes] + + def indexed( items: List[Any], key: Union[str, Callable[[Any], Any]] ) -> Dict[Any, List[Any]]: @@ -1481,3 +1501,8 @@ def get_time_filter_status( # pylint: disable=too-many-branches def format_list(items: Sequence[str], sep: str = ", ", quote: str = '"') -> str: quote_escaped = "\\" + quote return sep.join(f"{quote}{x.replace(quote, quote_escaped)}{quote}" for x in items) + + +def find_duplicates(items: Iterable[InputType]) -> List[InputType]: + """Find duplicate items in an iterable.""" + return [item for item, count in collections.Counter(items).items() if count > 1] diff --git a/superset/utils/pandas_postprocessing.py b/superset/utils/pandas_postprocessing.py index 5d4c2013d..14d612f2d 100644 --- a/superset/utils/pandas_postprocessing.py +++ b/superset/utils/pandas_postprocessing.py @@ -16,7 +16,7 @@ # under the License. import logging from functools import partial -from typing import Any, Callable, cast, Dict, List, Optional, Set, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union import geohash as geohash_lib import numpy as np @@ -554,29 +554,53 @@ def geodetic_parse( raise QueryObjectValidationError(_("Invalid geodetic string")) +@validate_column_args("columns") def contribution( - df: DataFrame, orientation: PostProcessingContributionOrientation + df: DataFrame, + orientation: Optional[ + PostProcessingContributionOrientation + ] = PostProcessingContributionOrientation.COLUMN, + columns: Optional[List[str]] = None, + rename_columns: Optional[List[str]] = None, ) -> DataFrame: """ - Calculate cell contibution to row/column total. + Calculate cell contibution to row/column total for numeric columns. + Non-numeric columns will be kept untouched. + + If `columns` are specified, only calculate contributions on selected columns. :param df: DataFrame containing all-numeric data (temporal column ignored) + :param columns: Columns to calculate values from. + :param rename_columns: The new labels for the calculated contribution columns. + The original columns will not be removed. :param orientation: calculate by dividing cell with row/column total - :return: DataFrame with contributions, with temporal column at beginning if present + :return: DataFrame with contributions. """ - temporal_series: Optional[Series] = None contribution_df = df.copy() - if DTTM_ALIAS in df.columns: - temporal_series = cast(Series, contribution_df.pop(DTTM_ALIAS)) - - if orientation == PostProcessingContributionOrientation.ROW: - contribution_dft = contribution_df.T - contribution_df = (contribution_dft / contribution_dft.sum()).T - else: - contribution_df = contribution_df / contribution_df.sum() - - if temporal_series is not None: - contribution_df.insert(0, DTTM_ALIAS, temporal_series) + numeric_df = contribution_df.select_dtypes(include="number") + # verify column selections + if columns: + numeric_columns = numeric_df.columns.tolist() + for col in columns: + if col not in numeric_columns: + raise QueryObjectValidationError( + _( + 'Column "%(column)s" is not numeric or does not ' + "exists in the query results.", + column=col, + ) + ) + columns = columns or numeric_df.columns + rename_columns = rename_columns or columns + if len(rename_columns) != len(columns): + raise QueryObjectValidationError( + _("`rename_columns` must have the same length as `columns`.") + ) + # limit to selected columns + numeric_df = numeric_df[columns] + axis = 0 if orientation == PostProcessingContributionOrientation.COLUMN else 1 + numeric_df = numeric_df / numeric_df.values.sum(axis=axis, keepdims=True) + contribution_df[rename_columns] = numeric_df return contribution_df diff --git a/tests/fixtures/query_context.py b/tests/fixtures/query_context.py index 30384ec20..229549cfe 100644 --- a/tests/fixtures/query_context.py +++ b/tests/fixtures/query_context.py @@ -21,13 +21,17 @@ from superset.utils.core import AnnotationType, DTTM_ALIAS from tests.base_tests import get_table_by_name query_birth_names = { - "extras": {"where": "", "time_range_endpoints": ["inclusive", "exclusive"]}, - "granularity": "ds", + "extras": { + "where": "", + "time_range_endpoints": ["inclusive", "exclusive"], + "time_grain_sqla": "P1D", + }, "groupby": ["name"], "metrics": [{"label": "sum__num"}], "order_desc": True, "orderby": [["sum__num", False]], "row_limit": 100, + "granularity": "ds", "time_range": "100 years ago : now", "timeseries_limit": 0, "timeseries_limit_metric": None, diff --git a/tests/pandas_postprocessing_tests.py b/tests/pandas_postprocessing_tests.py index 4104cc106..030b17752 100644 --- a/tests/pandas_postprocessing_tests.py +++ b/tests/pandas_postprocessing_tests.py @@ -524,19 +524,40 @@ class TestPostProcessing(SupersetTestCase): "b": [1, 9], } ) + with pytest.raises(QueryObjectValidationError, match="not numeric"): + proc.contribution(df, columns=[DTTM_ALIAS]) + + with pytest.raises(QueryObjectValidationError, match="same length"): + proc.contribution(df, columns=["a"], rename_columns=["aa", "bb"]) # cell contribution across row - row_df = proc.contribution(df, PostProcessingContributionOrientation.ROW) - self.assertListEqual(df.columns.tolist(), [DTTM_ALIAS, "a", "b"]) - self.assertListEqual(series_to_list(row_df["a"]), [0.5, 0.25]) - self.assertListEqual(series_to_list(row_df["b"]), [0.5, 0.75]) + processed_df = proc.contribution( + df, orientation=PostProcessingContributionOrientation.ROW, + ) + self.assertListEqual(processed_df.columns.tolist(), [DTTM_ALIAS, "a", "b"]) + self.assertListEqual(processed_df["a"].tolist(), [0.5, 0.25]) + self.assertListEqual(processed_df["b"].tolist(), [0.5, 0.75]) # cell contribution across column without temporal column df.pop(DTTM_ALIAS) - column_df = proc.contribution(df, PostProcessingContributionOrientation.COLUMN) - self.assertListEqual(df.columns.tolist(), ["a", "b"]) - self.assertListEqual(series_to_list(column_df["a"]), [0.25, 0.75]) - self.assertListEqual(series_to_list(column_df["b"]), [0.1, 0.9]) + processed_df = proc.contribution( + df, orientation=PostProcessingContributionOrientation.COLUMN + ) + self.assertListEqual(processed_df.columns.tolist(), ["a", "b"]) + self.assertListEqual(processed_df["a"].tolist(), [0.25, 0.75]) + self.assertListEqual(processed_df["b"].tolist(), [0.1, 0.9]) + + # contribution only on selected columns + processed_df = proc.contribution( + df, + orientation=PostProcessingContributionOrientation.COLUMN, + columns=["a"], + rename_columns=["pct_a"], + ) + self.assertListEqual(processed_df.columns.tolist(), ["a", "b", "pct_a"]) + self.assertListEqual(processed_df["a"].tolist(), [1, 3]) + self.assertListEqual(processed_df["b"].tolist(), [1, 9]) + self.assertListEqual(processed_df["pct_a"].tolist(), [0.25, 0.75]) def test_prophet_valid(self): pytest.importorskip("fbprophet") diff --git a/tests/query_context_tests.py b/tests/query_context_tests.py index 99bc9421e..ec4eefa12 100644 --- a/tests/query_context_tests.py +++ b/tests/query_context_tests.py @@ -25,7 +25,6 @@ from superset.utils.core import ( AdhocMetricExpressionType, ChartDataResultFormat, ChartDataResultType, - FilterOperator, TimeRangeEndpoint, ) from tests.base_tests import SupersetTestCase diff --git a/tests/sqla_models_tests.py b/tests/sqla_models_tests.py index e049e512e..e03460980 100644 --- a/tests/sqla_models_tests.py +++ b/tests/sqla_models_tests.py @@ -20,7 +20,6 @@ from typing import Any, Dict, NamedTuple, List, Pattern, Tuple, Union from unittest.mock import patch import pytest -import tests.test_app from superset import db from superset.connectors.sqla.models import SqlaTable, TableColumn from superset.db_engine_specs.druid import DruidEngineSpec