mirror of https://github.com/apache/superset.git
chore(explore): Get Explore data from endpoint instead of bootstrap_data (#20519)
* feat(explore): Use v1/explore endpoint data instead of bootstrapData * Add tests * Fix ci * Remove redundant dependency * Use form_data_key in cypress tests * Add auth headers to for data request * Address comments * Remove displaying danger toast * Conditionally add auth headers * Address comments * Fix typing bug * fix * Fix opening dataset * Fix sqllab chart create * Run queries in parallel * Fix dashboard id autofill * Fix lint * Fix test
This commit is contained in:
parent
f2af81b1c7
commit
b30f6a5db1
|
@ -41,7 +41,7 @@ describe('No Results', () => {
|
|||
],
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.wait('@getJson').its('response.statusCode').should('eq', 200);
|
||||
cy.get('div.chart-container').contains(
|
||||
'No results were returned for this query',
|
||||
|
|
|
@ -148,7 +148,7 @@ describe('Time range filter', () => {
|
|||
metrics: [NUM_METRIC],
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||
|
||||
cy.get('[data-test=time-range-trigger]')
|
||||
|
@ -172,7 +172,7 @@ describe('Time range filter', () => {
|
|||
time_range: 'Last year',
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||
|
||||
cy.get('[data-test=time-range-trigger]')
|
||||
|
@ -192,7 +192,7 @@ describe('Time range filter', () => {
|
|||
time_range: 'previous calendar month',
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||
|
||||
cy.get('[data-test=time-range-trigger]')
|
||||
|
@ -212,7 +212,7 @@ describe('Time range filter', () => {
|
|||
time_range: 'DATEADD(DATETIME("today"), -7, day) : today',
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||
|
||||
cy.get('[data-test=time-range-trigger]')
|
||||
|
@ -235,7 +235,7 @@ describe('Time range filter', () => {
|
|||
time_range: 'No filter',
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||
|
||||
cy.get('[data-test=time-range-trigger]')
|
||||
|
|
|
@ -31,7 +31,7 @@ describe('explore view', () => {
|
|||
it('should load Explore', () => {
|
||||
const LINE_CHART_DEFAULTS = { ...FORM_DATA_DEFAULTS, viz_type: 'line' };
|
||||
const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] };
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
cy.eyesOpen({
|
||||
testName: 'Explore page',
|
||||
|
|
|
@ -22,7 +22,7 @@ describe('Edit FilterBox Chart', () => {
|
|||
const VIZ_DEFAULTS = { ...FORM_DATA_DEFAULTS, viz_type: 'filter_box' };
|
||||
|
||||
function verify(formData) {
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson' });
|
||||
}
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ describe('Test explore links', () => {
|
|||
};
|
||||
const newChartName = `Test chart [${shortid.generate()}]`;
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@tableChartData' });
|
||||
cy.url().then(() => {
|
||||
cy.get('[data-test="query-save-button"]').click();
|
||||
|
|
|
@ -51,7 +51,7 @@ describe('Visualization > Area', () => {
|
|||
};
|
||||
|
||||
function verify(formData) {
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
}
|
||||
|
||||
|
@ -75,23 +75,21 @@ describe('Visualization > Area', () => {
|
|||
});
|
||||
|
||||
it('should work with groupby and filter', () => {
|
||||
cy.visitChartByParams(
|
||||
JSON.stringify({
|
||||
...AREA_FORM_DATA,
|
||||
groupby: ['region'],
|
||||
adhoc_filters: [
|
||||
{
|
||||
expressionType: 'SIMPLE',
|
||||
subject: 'region',
|
||||
operator: 'IN',
|
||||
comparator: ['South Asia', 'North America'],
|
||||
clause: 'WHERE',
|
||||
sqlExpression: null,
|
||||
filterOptionName: 'filter_txje2ikiv6_wxmn0qwd1xo',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
cy.visitChartByParams({
|
||||
...AREA_FORM_DATA,
|
||||
groupby: ['region'],
|
||||
adhoc_filters: [
|
||||
{
|
||||
expressionType: 'SIMPLE',
|
||||
subject: 'region',
|
||||
operator: 'IN',
|
||||
comparator: ['South Asia', 'North America'],
|
||||
clause: 'WHERE',
|
||||
sqlExpression: null,
|
||||
filterOptionName: 'filter_txje2ikiv6_wxmn0qwd1xo',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
cy.wait('@getJson').then(async ({ response }) => {
|
||||
const responseBody = response?.body;
|
||||
|
|
|
@ -25,11 +25,11 @@ describe('Visualization > Big Number with Trendline', () => {
|
|||
slice_id: 42,
|
||||
granularity_sqla: 'year',
|
||||
time_grain_sqla: 'P1D',
|
||||
time_range: '2000+:+2014-01-02',
|
||||
time_range: '2000 : 2014-01-02',
|
||||
metric: 'sum__SP_POP_TOTL',
|
||||
adhoc_filters: [],
|
||||
compare_lag: '10',
|
||||
compare_suffix: 'over+10Y',
|
||||
compare_suffix: 'over 10Y',
|
||||
y_axis_format: '.3s',
|
||||
show_trend_line: true,
|
||||
start_y_axis_at_zero: true,
|
||||
|
@ -42,7 +42,7 @@ describe('Visualization > Big Number with Trendline', () => {
|
|||
};
|
||||
|
||||
function verify(formData) {
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@chartData',
|
||||
chartSelector: '.superset-legacy-chart-big-number',
|
||||
|
|
|
@ -33,7 +33,7 @@ describe('Visualization > Box Plot', () => {
|
|||
};
|
||||
|
||||
function verify(formData) {
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson' });
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ describe('Visualization > Bubble', () => {
|
|||
slice_id: 46,
|
||||
granularity_sqla: 'year',
|
||||
time_grain_sqla: 'P1D',
|
||||
time_range: '2011-01-01+:+2011-01-02',
|
||||
time_range: '2011-01-01 : 2011-01-02',
|
||||
series: 'region',
|
||||
entity: 'country_name',
|
||||
x: 'sum__SP_RUR_TOTL_ZS',
|
||||
|
@ -47,7 +47,7 @@ describe('Visualization > Bubble', () => {
|
|||
};
|
||||
|
||||
function verify(formData) {
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,7 @@ describe('Visualization > Bubble', () => {
|
|||
// Since main functionality is already covered in filter test below,
|
||||
// skip this test until we find a solution.
|
||||
it.skip('should work', () => {
|
||||
cy.visitChartByParams(JSON.stringify(BUBBLE_FORM_DATA)).then(() => {
|
||||
cy.visitChartByParams(BUBBLE_FORM_DATA).then(() => {
|
||||
cy.wait('@getJson').then(xhr => {
|
||||
let expectedBubblesNumber = 0;
|
||||
xhr.responseBody.data.forEach(element => {
|
||||
|
@ -86,7 +86,7 @@ describe('Visualization > Bubble', () => {
|
|||
expressionType: 'SIMPLE',
|
||||
subject: 'region',
|
||||
operator: '==',
|
||||
comparator: 'South+Asia',
|
||||
comparator: 'South Asia',
|
||||
clause: 'WHERE',
|
||||
sqlExpression: null,
|
||||
filterOptionName: 'filter_b2tfg1rs8y_8kmrcyxvsqd',
|
||||
|
|
|
@ -47,7 +47,7 @@ describe('Visualization > Compare', () => {
|
|||
};
|
||||
|
||||
function verify(formData) {
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ describe('Visualization > Distribution bar chart', () => {
|
|||
groupby: ['state'],
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@getJson',
|
||||
querySubstring: NUM_METRIC.label,
|
||||
|
@ -49,7 +49,7 @@ describe('Visualization > Distribution bar chart', () => {
|
|||
columns: ['gender'],
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
});
|
||||
|
||||
|
@ -61,7 +61,7 @@ describe('Visualization > Distribution bar chart', () => {
|
|||
row_limit: 10,
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
});
|
||||
|
||||
|
@ -74,7 +74,7 @@ describe('Visualization > Distribution bar chart', () => {
|
|||
contribution: true,
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -33,7 +33,7 @@ describe('Download Chart > Distribution bar chart', () => {
|
|||
groupby: ['state'],
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.get('.header-with-actions .ant-dropdown-trigger').click();
|
||||
cy.get(':nth-child(1) > .ant-dropdown-menu-submenu-title').click();
|
||||
cy.get(
|
||||
|
|
|
@ -23,7 +23,7 @@ describe('Visualization > Dual Line', () => {
|
|||
slice_id: 58,
|
||||
granularity_sqla: 'ds',
|
||||
time_grain_sqla: 'P1D',
|
||||
time_range: '100+years+ago+:+now',
|
||||
time_range: '100 years ago : now',
|
||||
color_scheme: 'bnbColors',
|
||||
x_axis_format: 'smart_date',
|
||||
metric: 'sum__num',
|
||||
|
@ -35,7 +35,7 @@ describe('Visualization > Dual Line', () => {
|
|||
};
|
||||
|
||||
function verify(formData) {
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ describe('Visualization > Gauge', () => {
|
|||
};
|
||||
|
||||
function verify(formData) {
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson' });
|
||||
}
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ describe('Visualization > Graph', () => {
|
|||
function verify(formData: {
|
||||
[name: string]: string | boolean | number | Array<adhocFilter>;
|
||||
}): void {
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson' });
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ describe('Visualization > Histogram', () => {
|
|||
};
|
||||
|
||||
function verify(formData: QueryFormData) {
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ describe('Visualization > Line', () => {
|
|||
|
||||
it('should show validator error when no metric', () => {
|
||||
const formData = { ...LINE_CHART_DEFAULTS, metrics: [] };
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.get('.panel-body').contains(
|
||||
`Add required control values to preview chart`,
|
||||
);
|
||||
|
@ -36,7 +36,7 @@ describe('Visualization > Line', () => {
|
|||
|
||||
it('should not show validator error when metric added', () => {
|
||||
const formData = { ...LINE_CHART_DEFAULTS, metrics: [] };
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.get('.panel-body').contains(
|
||||
`Add required control values to preview chart`,
|
||||
);
|
||||
|
@ -61,7 +61,7 @@ describe('Visualization > Line', () => {
|
|||
|
||||
it('should allow negative values in Y bounds', () => {
|
||||
const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] };
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.get('#controlSections-tab-display').click();
|
||||
cy.get('span').contains('Y Axis Bounds').scrollIntoView();
|
||||
cy.get('input[placeholder="Min"]').type('-0.1', { delay: 100 });
|
||||
|
@ -81,7 +81,7 @@ describe('Visualization > Line', () => {
|
|||
|
||||
it('should work with adhoc metric', () => {
|
||||
const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] };
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
});
|
||||
|
||||
|
@ -89,7 +89,7 @@ describe('Visualization > Line', () => {
|
|||
const metrics = ['count'];
|
||||
const groupby = ['gender'];
|
||||
const formData = { ...LINE_CHART_DEFAULTS, metrics, groupby };
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
});
|
||||
|
||||
|
@ -101,7 +101,7 @@ describe('Visualization > Line', () => {
|
|||
metrics,
|
||||
adhoc_filters: filters,
|
||||
};
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
});
|
||||
|
||||
|
@ -113,7 +113,7 @@ describe('Visualization > Line', () => {
|
|||
groupby: ['name'],
|
||||
timeseries_limit_metric: NUM_METRIC,
|
||||
};
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
});
|
||||
|
||||
|
@ -126,7 +126,7 @@ describe('Visualization > Line', () => {
|
|||
timeseries_limit_metric: NUM_METRIC,
|
||||
order_desc: true,
|
||||
};
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
});
|
||||
|
||||
|
@ -138,7 +138,7 @@ describe('Visualization > Line', () => {
|
|||
rolling_type: 'mean',
|
||||
rolling_periods: 10,
|
||||
};
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
});
|
||||
|
||||
|
@ -147,12 +147,12 @@ describe('Visualization > Line', () => {
|
|||
const formData = {
|
||||
...LINE_CHART_DEFAULTS,
|
||||
metrics,
|
||||
time_compare: ['1+year'],
|
||||
time_compare: ['1 year'],
|
||||
comparison_type: 'values',
|
||||
groupby: ['gender'],
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
|
||||
// Offset color should match original line color
|
||||
|
@ -190,10 +190,10 @@ describe('Visualization > Line', () => {
|
|||
const formData = {
|
||||
...LINE_CHART_DEFAULTS,
|
||||
metrics,
|
||||
time_compare: ['1+year'],
|
||||
time_compare: ['1 year'],
|
||||
comparison_type: 'ratio',
|
||||
};
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
});
|
||||
|
||||
|
@ -202,10 +202,10 @@ describe('Visualization > Line', () => {
|
|||
const formData = {
|
||||
...LINE_CHART_DEFAULTS,
|
||||
metrics,
|
||||
time_compare: ['1+year'],
|
||||
time_compare: ['1 year'],
|
||||
comparison_type: 'percentage',
|
||||
};
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
});
|
||||
|
||||
|
@ -214,7 +214,7 @@ describe('Visualization > Line', () => {
|
|||
...LINE_CHART_DEFAULTS,
|
||||
metrics: ['count'],
|
||||
};
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
cy.get('text.nv-legend-text').contains('COUNT(*)');
|
||||
});
|
||||
|
@ -225,7 +225,7 @@ describe('Visualization > Line', () => {
|
|||
metrics: ['count'],
|
||||
annotation_layers: [
|
||||
{
|
||||
name: 'Goal+line',
|
||||
name: 'Goal line',
|
||||
annotationType: 'FORMULA',
|
||||
sourceType: '',
|
||||
value: 'y=140000',
|
||||
|
@ -245,7 +245,7 @@ describe('Visualization > Line', () => {
|
|||
},
|
||||
],
|
||||
};
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
cy.get('.slice_container').within(() => {
|
||||
// Goal line annotation doesn't show up in legend
|
||||
|
@ -281,7 +281,7 @@ describe('Visualization > Line', () => {
|
|||
},
|
||||
],
|
||||
};
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
},
|
||||
);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
|
|
|
@ -37,7 +37,7 @@ describe('Visualization > Pie', () => {
|
|||
};
|
||||
|
||||
function verify(formData) {
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson' });
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ describe('Visualization > Pivot Table', () => {
|
|||
slice_id: 61,
|
||||
granularity_sqla: 'ds',
|
||||
time_grain_sqla: 'P1D',
|
||||
time_range: '100+years+ago+:+now',
|
||||
time_range: '100 years ago : now',
|
||||
metrics: ['sum__num'],
|
||||
adhoc_filters: [],
|
||||
groupby: ['name'],
|
||||
|
@ -54,7 +54,7 @@ describe('Visualization > Pivot Table', () => {
|
|||
};
|
||||
|
||||
function verify(formData) {
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'table' });
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ describe('Visualization > Sankey', () => {
|
|||
url_params: {},
|
||||
granularity_sqla: null,
|
||||
time_grain_sqla: 'P1D',
|
||||
time_range: 'Last+week',
|
||||
time_range: 'Last week',
|
||||
groupby: ['source', 'target'],
|
||||
metric: 'sum__value',
|
||||
adhoc_filters: [],
|
||||
|
@ -33,7 +33,7 @@ describe('Visualization > Sankey', () => {
|
|||
};
|
||||
|
||||
function verify(formData) {
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,7 @@ describe('Visualization > Sankey', () => {
|
|||
adhoc_filters: [
|
||||
{
|
||||
expressionType: 'SQL',
|
||||
sqlExpression: 'SUM(value)+>+0',
|
||||
sqlExpression: 'SUM(value) > 0',
|
||||
clause: 'HAVING',
|
||||
subject: null,
|
||||
operator: null,
|
||||
|
|
|
@ -24,7 +24,7 @@ export const FORM_DATA_DEFAULTS = {
|
|||
datasource: '3__table',
|
||||
granularity_sqla: 'ds',
|
||||
time_grain_sqla: null,
|
||||
time_range: '100+years+ago+:+now',
|
||||
time_range: '100 years ago : now',
|
||||
adhoc_filters: [],
|
||||
groupby: [],
|
||||
limit: null,
|
||||
|
@ -37,7 +37,7 @@ export const HEALTH_POP_FORM_DATA_DEFAULTS = {
|
|||
datasource: '2__table',
|
||||
granularity_sqla: 'ds',
|
||||
time_grain_sqla: 'P1D',
|
||||
time_range: '1960-01-01+:+2014-01-02',
|
||||
time_range: '1960-01-01 : 2014-01-02',
|
||||
};
|
||||
|
||||
export const NUM_METRIC = {
|
||||
|
|
|
@ -32,7 +32,7 @@ describe('Visualization > Sunburst', () => {
|
|||
};
|
||||
|
||||
function verify(formData) {
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
}
|
||||
|
||||
|
|
|
@ -174,7 +174,7 @@ describe('Visualization > Table', () => {
|
|||
groupby: ['name'],
|
||||
row_limit: limit,
|
||||
};
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.wait('@chartData').then(({ response }) => {
|
||||
cy.verifySliceContainer('table');
|
||||
expect(response?.body.result[0].data.length).to.eq(limit);
|
||||
|
@ -219,7 +219,7 @@ describe('Visualization > Table', () => {
|
|||
order_by_cols: ['["num", false]'],
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.wait('@chartData').then(({ response }) => {
|
||||
cy.verifySliceContainer('table');
|
||||
const records = response?.body.result[0].data;
|
||||
|
@ -233,7 +233,7 @@ describe('Visualization > Table', () => {
|
|||
|
||||
const formData = { ...VIZ_DEFAULTS, metrics, adhoc_filters: filters };
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@chartData', chartSelector: 'table' });
|
||||
});
|
||||
|
||||
|
@ -244,7 +244,7 @@ describe('Visualization > Table', () => {
|
|||
groupby: ['state'],
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@chartData',
|
||||
querySubstring: /group by.*state/i,
|
||||
|
|
|
@ -33,7 +33,7 @@ describe('Visualization > Time TableViz', () => {
|
|||
column_collection: [
|
||||
{
|
||||
key: '9g4K-B-YL',
|
||||
label: 'Last+Year',
|
||||
label: 'Last Year',
|
||||
colType: 'time',
|
||||
timeLag: '1',
|
||||
comparisonType: 'value',
|
||||
|
@ -42,7 +42,7 @@ describe('Visualization > Time TableViz', () => {
|
|||
url: '',
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@getJson',
|
||||
querySubstring: NUM_METRIC.label,
|
||||
|
@ -61,7 +61,7 @@ describe('Visualization > Time TableViz', () => {
|
|||
column_collection: [
|
||||
{
|
||||
key: '9g4K-B-YL',
|
||||
label: 'Last+Year',
|
||||
label: 'Last Year',
|
||||
colType: 'time',
|
||||
timeLag: '1',
|
||||
comparisonType: 'value',
|
||||
|
@ -70,7 +70,7 @@ describe('Visualization > Time TableViz', () => {
|
|||
url: '',
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@getJson',
|
||||
querySubstring: NUM_METRIC.label,
|
||||
|
@ -107,7 +107,7 @@ describe('Visualization > Time TableViz', () => {
|
|||
url: '',
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@getJson',
|
||||
querySubstring: NUM_METRIC.label,
|
||||
|
|
|
@ -38,7 +38,7 @@ describe('Visualization > Treemap', () => {
|
|||
const level2 = '.chart-container rect[style="fill: rgb(0, 122, 135);"]';
|
||||
|
||||
function verify(formData) {
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ describe('Visualization > World Map', () => {
|
|||
};
|
||||
|
||||
function verify(formData) {
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
}
|
||||
|
||||
|
|
|
@ -55,12 +55,46 @@ Cypress.Commands.add('visitChartById', chartId =>
|
|||
cy.visit(`${BASE_EXPLORE_URL}{"slice_id": ${chartId}}`),
|
||||
);
|
||||
|
||||
Cypress.Commands.add('visitChartByParams', params => {
|
||||
const queryString =
|
||||
typeof params === 'string' ? params : JSON.stringify(params);
|
||||
const url = `${BASE_EXPLORE_URL}${queryString}`;
|
||||
return cy.visit(url);
|
||||
});
|
||||
Cypress.Commands.add(
|
||||
'visitChartByParams',
|
||||
(formData: {
|
||||
datasource?: string;
|
||||
datasource_id?: number;
|
||||
datasource_type?: string;
|
||||
[key: string]: unknown;
|
||||
}) => {
|
||||
let datasource_id;
|
||||
let datasource_type;
|
||||
if (formData.datasource_id && formData.datasource_type) {
|
||||
({ datasource_id, datasource_type } = formData);
|
||||
} else {
|
||||
[datasource_id, datasource_type] = formData.datasource?.split('__') || [];
|
||||
}
|
||||
const accessToken = window.localStorage.getItem('access_token');
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: 'api/v1/explore/form_data',
|
||||
body: {
|
||||
datasource_id,
|
||||
datasource_type,
|
||||
form_data: JSON.stringify(formData),
|
||||
},
|
||||
headers: {
|
||||
...(accessToken && {
|
||||
Cookie: `csrf_access_token=${accessToken}`,
|
||||
'X-CSRFToken': accessToken,
|
||||
}),
|
||||
...(TokenName && { Authorization: `Bearer ${TokenName}` }),
|
||||
'Content-Type': 'application/json',
|
||||
Referer: `${Cypress.config().baseUrl}/`,
|
||||
},
|
||||
}).then(response => {
|
||||
const formDataKey = response.body.key;
|
||||
const url = `/superset/explore/?form_data_key=${formDataKey}`;
|
||||
cy.visit(url);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Cypress.Commands.add('verifySliceContainer', chartSelector => {
|
||||
// After a wait response check for valid slice container
|
||||
|
|
|
@ -50,6 +50,14 @@ export type SharedControlComponents = typeof sharedControlComponents;
|
|||
/** ----------------------------------------------
|
||||
* Input data/props while rendering
|
||||
* ---------------------------------------------*/
|
||||
export interface Owner {
|
||||
first_name: string;
|
||||
id: number;
|
||||
last_name: string;
|
||||
username: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export type ColumnMeta = Omit<Column, 'id'> & {
|
||||
id?: number;
|
||||
} & AnyDict;
|
||||
|
@ -67,8 +75,10 @@ export interface Dataset {
|
|||
time_grain_sqla?: string;
|
||||
granularity_sqla?: string;
|
||||
datasource_name: string | null;
|
||||
name?: string;
|
||||
description: string | null;
|
||||
uid?: string;
|
||||
owners?: Owner[];
|
||||
}
|
||||
|
||||
export interface ControlPanelState {
|
||||
|
|
|
@ -46,8 +46,11 @@ import {
|
|||
SqlLabExploreRootState,
|
||||
getInitialState,
|
||||
ExploreDatasource,
|
||||
SqlLabRootState,
|
||||
} from 'src/SqlLab/types';
|
||||
import { exploreChart } from 'src/explore/exploreUtils';
|
||||
import { mountExploreUrl } from 'src/explore/exploreUtils';
|
||||
import { postFormData } from 'src/explore/exploreUtils/formData';
|
||||
import { URL_PARAMS } from 'src/constants';
|
||||
|
||||
interface SaveDatasetModalProps {
|
||||
visible: boolean;
|
||||
|
@ -115,6 +118,9 @@ export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
|
|||
modalDescription,
|
||||
datasource,
|
||||
}) => {
|
||||
const defaultVizType = useSelector<SqlLabRootState, string>(
|
||||
state => state.common?.conf?.DEFAULT_VIZ_TYPE || 'table',
|
||||
);
|
||||
const query = datasource as QueryResponse;
|
||||
const getDefaultDatasetName = () =>
|
||||
`${query.tab} ${moment().format('MM/DD/YYYY HH:mm:ss')}`;
|
||||
|
@ -137,30 +143,40 @@ export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
|
|||
const dispatch = useDispatch<(dispatch: any) => Promise<JsonObject>>();
|
||||
|
||||
const handleOverwriteDataset = async () => {
|
||||
await updateDataset(
|
||||
query.dbId,
|
||||
datasetToOverwrite.datasetId,
|
||||
query.sql,
|
||||
query.results.selected_columns.map(
|
||||
(d: { name: string; type: string; is_dttm: boolean }) => ({
|
||||
column_name: d.name,
|
||||
type: d.type,
|
||||
is_dttm: d.is_dttm,
|
||||
}),
|
||||
const [, key] = await Promise.all([
|
||||
updateDataset(
|
||||
query.dbId,
|
||||
datasetToOverwrite.datasetId,
|
||||
query.sql,
|
||||
query.results.selected_columns.map(
|
||||
(d: { name: string; type: string; is_dttm: boolean }) => ({
|
||||
column_name: d.name,
|
||||
type: d.type,
|
||||
is_dttm: d.is_dttm,
|
||||
}),
|
||||
),
|
||||
datasetToOverwrite.owners.map((o: DatasetOwner) => o.id),
|
||||
true,
|
||||
),
|
||||
datasetToOverwrite.owners.map((o: DatasetOwner) => o.id),
|
||||
true,
|
||||
);
|
||||
postFormData(datasetToOverwrite.datasetId, 'table', {
|
||||
...EXPLORE_CHART_DEFAULT,
|
||||
datasource: `${datasetToOverwrite.datasetId}__table`,
|
||||
...(defaultVizType === 'table' && {
|
||||
all_columns: query.results.selected_columns.map(
|
||||
column => column.name,
|
||||
),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
const url = mountExploreUrl(null, {
|
||||
[URL_PARAMS.formDataKey.name]: key,
|
||||
});
|
||||
window.open(url, '_blank', 'noreferrer');
|
||||
|
||||
setShouldOverwriteDataset(false);
|
||||
setDatasetToOverwrite({});
|
||||
setDatasetName(getDefaultDatasetName());
|
||||
|
||||
exploreChart({
|
||||
...EXPLORE_CHART_DEFAULT,
|
||||
datasource: `${datasetToOverwrite.datasetId}__table`,
|
||||
selected_columns: query.results.selected_columns,
|
||||
});
|
||||
};
|
||||
|
||||
const getUserDatasets = async (searchText = '') => {
|
||||
|
@ -235,15 +251,20 @@ export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
|
|||
columns: selectedColumns,
|
||||
}),
|
||||
)
|
||||
.then((data: { table_id: number }) => {
|
||||
exploreChart({
|
||||
.then((data: { table_id: number }) =>
|
||||
postFormData(data.table_id, 'table', {
|
||||
...EXPLORE_CHART_DEFAULT,
|
||||
datasource: `${data.table_id}__table`,
|
||||
metrics: [],
|
||||
groupby: [],
|
||||
time_range: 'No filter',
|
||||
selectedColumns,
|
||||
row_limit: 1000,
|
||||
...(defaultVizType === 'table' && {
|
||||
all_columns: selectedColumns.map(column => column.name),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.then((key: string) => {
|
||||
const url = mountExploreUrl(null, {
|
||||
[URL_PARAMS.formDataKey.name]: key,
|
||||
});
|
||||
window.open(url, '_blank', 'noreferrer');
|
||||
})
|
||||
.catch(() => {
|
||||
addDangerToast(t('An error occurred saving dataset'));
|
||||
|
|
|
@ -16,11 +16,11 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Dataset } from '@superset-ui/chart-controls';
|
||||
import { JsonObject, Query, QueryResponse } from '@superset-ui/core';
|
||||
import { SupersetError } from 'src/components/ErrorMessage/types';
|
||||
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
||||
import { ToastType } from 'src/components/MessageToasts/types';
|
||||
import { Dataset } from '@superset-ui/chart-controls';
|
||||
import { Query, QueryResponse } from '@superset-ui/core';
|
||||
import { ExploreRootState } from 'src/explore/types';
|
||||
|
||||
export type ExploreDatasource = Dataset | QueryResponse;
|
||||
|
@ -68,7 +68,10 @@ export type SqlLabRootState = {
|
|||
};
|
||||
localStorageUsageInKilobytes: number;
|
||||
messageToasts: toastState[];
|
||||
common: {};
|
||||
common: {
|
||||
flash_messages: string[];
|
||||
conf: JsonObject;
|
||||
};
|
||||
};
|
||||
|
||||
export type SqlLabExploreRootState = SqlLabRootState | ExploreRootState;
|
||||
|
@ -96,6 +99,7 @@ export const EXPLORE_CHART_DEFAULT = {
|
|||
metrics: [],
|
||||
groupby: [],
|
||||
time_range: 'No filter',
|
||||
row_limit: 1000,
|
||||
};
|
||||
|
||||
export interface DatasetOwner {
|
||||
|
|
|
@ -104,7 +104,7 @@ test('renders an enabled button if datasource and viz type are selected', async
|
|||
const wrapper = await getWrapper();
|
||||
wrapper.setState({
|
||||
datasource,
|
||||
visType: 'table',
|
||||
vizType: 'table',
|
||||
});
|
||||
expect(
|
||||
wrapper.find(Button).find({ disabled: true }).hostNodes(),
|
||||
|
@ -125,7 +125,7 @@ test('double-click viz type submits if datasource is selected', async () => {
|
|||
wrapper.update();
|
||||
wrapper.setState({
|
||||
datasource,
|
||||
visType: 'table',
|
||||
vizType: 'table',
|
||||
});
|
||||
|
||||
wrapper.instance().onVizTypeDoubleClick();
|
||||
|
@ -136,9 +136,8 @@ test('formats Explore url', async () => {
|
|||
const wrapper = await getWrapper();
|
||||
wrapper.setState({
|
||||
datasource,
|
||||
visType: 'table',
|
||||
vizType: 'table',
|
||||
});
|
||||
const formattedUrl =
|
||||
'/superset/explore/?form_data=%7B%22viz_type%22%3A%22table%22%2C%22datasource%22%3A%221%22%7D';
|
||||
const formattedUrl = '/superset/explore/?viz_type=table&datasource=1';
|
||||
expect(wrapper.instance().exploreUrl()).toBe(formattedUrl);
|
||||
});
|
||||
|
|
|
@ -45,7 +45,7 @@ export type AddSliceContainerProps = {
|
|||
|
||||
export type AddSliceContainerState = {
|
||||
datasource?: { label: string; value: string };
|
||||
visType: string | null;
|
||||
vizType: string | null;
|
||||
canCreateDataset: boolean;
|
||||
};
|
||||
|
||||
|
@ -208,7 +208,7 @@ export default class AddSliceContainer extends React.PureComponent<
|
|||
constructor(props: AddSliceContainerProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
visType: null,
|
||||
vizType: null,
|
||||
canCreateDataset: findPermission(
|
||||
'can_write',
|
||||
'Dataset',
|
||||
|
@ -217,7 +217,7 @@ export default class AddSliceContainer extends React.PureComponent<
|
|||
};
|
||||
|
||||
this.changeDatasource = this.changeDatasource.bind(this);
|
||||
this.changeVisType = this.changeVisType.bind(this);
|
||||
this.changeVizType = this.changeVizType.bind(this);
|
||||
this.gotoSlice = this.gotoSlice.bind(this);
|
||||
this.newLabel = this.newLabel.bind(this);
|
||||
this.loadDatasources = this.loadDatasources.bind(this);
|
||||
|
@ -226,14 +226,11 @@ export default class AddSliceContainer extends React.PureComponent<
|
|||
|
||||
exploreUrl() {
|
||||
const dashboardId = getUrlParam(URL_PARAMS.dashboardId);
|
||||
const formData = encodeURIComponent(
|
||||
JSON.stringify({
|
||||
viz_type: this.state.visType,
|
||||
datasource: this.state.datasource?.value,
|
||||
...(!isNullish(dashboardId) && { dashboardId }),
|
||||
}),
|
||||
);
|
||||
return `/superset/explore/?form_data=${formData}`;
|
||||
let url = `/superset/explore/?viz_type=${this.state.vizType}&datasource=${this.state.datasource?.value}`;
|
||||
if (!isNullish(dashboardId)) {
|
||||
url += `&dashboard_id=${dashboardId}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
gotoSlice() {
|
||||
|
@ -244,12 +241,12 @@ export default class AddSliceContainer extends React.PureComponent<
|
|||
this.setState({ datasource });
|
||||
}
|
||||
|
||||
changeVisType(visType: string | null) {
|
||||
this.setState({ visType });
|
||||
changeVizType(vizType: string | null) {
|
||||
this.setState({ vizType });
|
||||
}
|
||||
|
||||
isBtnDisabled() {
|
||||
return !(this.state.datasource?.value && this.state.visType);
|
||||
return !(this.state.datasource?.value && this.state.vizType);
|
||||
}
|
||||
|
||||
onVizTypeDoubleClick() {
|
||||
|
@ -369,14 +366,14 @@ export default class AddSliceContainer extends React.PureComponent<
|
|||
/>
|
||||
<Steps.Step
|
||||
title={<StyledStepTitle>{t('Choose chart type')}</StyledStepTitle>}
|
||||
status={this.state.visType ? 'finish' : 'process'}
|
||||
status={this.state.vizType ? 'finish' : 'process'}
|
||||
description={
|
||||
<StyledStepDescription>
|
||||
<VizTypeGallery
|
||||
className="viz-gallery"
|
||||
onChange={this.changeVisType}
|
||||
onChange={this.changeVizType}
|
||||
onDoubleClick={this.onVizTypeDoubleClick}
|
||||
selectedViz={this.state.visType}
|
||||
selectedViz={this.state.vizType}
|
||||
/>
|
||||
</StyledStepDescription>
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import { HYDRATE_DASHBOARD } from 'src/dashboard/actions/hydrate';
|
|||
import { DatasourcesAction } from 'src/dashboard/actions/datasources';
|
||||
import { ChartState } from 'src/explore/types';
|
||||
import { getFormDataFromControls } from 'src/explore/controlUtils';
|
||||
import { HYDRATE_EXPLORE } from 'src/explore/actions/hydrateExplore';
|
||||
import { now } from 'src/utils/dates';
|
||||
import * as actions from './chartAction';
|
||||
|
||||
|
@ -194,7 +195,7 @@ export default function chartReducer(
|
|||
delete charts[key];
|
||||
return charts;
|
||||
}
|
||||
if (action.type === HYDRATE_DASHBOARD) {
|
||||
if (action.type === HYDRATE_DASHBOARD || action.type === HYDRATE_EXPLORE) {
|
||||
return { ...action.data.charts };
|
||||
}
|
||||
if (action.type === DatasourcesAction.SET_DATASOURCES) {
|
||||
|
|
|
@ -91,6 +91,10 @@ export const URL_PARAMS = {
|
|||
name: 'permalink_key',
|
||||
type: 'string',
|
||||
},
|
||||
vizType: {
|
||||
name: 'viz_type',
|
||||
type: 'string',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const RESERVED_CHART_URL_PARAMS: string[] = [
|
||||
|
|
|
@ -27,10 +27,10 @@ import { DynamicPluginProvider } from 'src/components/DynamicPlugins';
|
|||
import ToastContainer from 'src/components/MessageToasts/ToastContainer';
|
||||
import setupApp from 'src/setup/setupApp';
|
||||
import setupPlugins from 'src/setup/setupPlugins';
|
||||
import { theme } from 'src/preamble';
|
||||
import { ExplorePage } from './ExplorePage';
|
||||
import './main.less';
|
||||
import '../assets/stylesheets/reactable-pagination.less';
|
||||
import { theme } from 'src/preamble';
|
||||
import ExploreViewContainer from './components/ExploreViewContainer';
|
||||
|
||||
setupApp();
|
||||
setupPlugins();
|
||||
|
@ -41,7 +41,7 @@ const App = ({ store }) => (
|
|||
<ThemeProvider theme={theme}>
|
||||
<GlobalStyles />
|
||||
<DynamicPluginProvider>
|
||||
<ExploreViewContainer />
|
||||
<ExplorePage />
|
||||
<ToastContainer />
|
||||
</DynamicPluginProvider>
|
||||
</ThemeProvider>
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { makeApi, t } from '@superset-ui/core';
|
||||
import Loading from 'src/components/Loading';
|
||||
import { getParsedExploreURLParams } from './exploreUtils/getParsedExploreURLParams';
|
||||
import { hydrateExplore } from './actions/hydrateExplore';
|
||||
import ExploreViewContainer from './components/ExploreViewContainer';
|
||||
import { ExploreResponsePayload } from './types';
|
||||
import { fallbackExploreInitialData } from './fixtures';
|
||||
import { addDangerToast } from '../components/MessageToasts/actions';
|
||||
import { isNullish } from '../utils/common';
|
||||
|
||||
const loadErrorMessage = t('Failed to load chart data.');
|
||||
|
||||
const fetchExploreData = () => {
|
||||
const exploreUrlParams = getParsedExploreURLParams();
|
||||
return makeApi<{}, ExploreResponsePayload>({
|
||||
method: 'GET',
|
||||
endpoint: 'api/v1/explore/',
|
||||
})(exploreUrlParams);
|
||||
};
|
||||
|
||||
export const ExplorePage = () => {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
fetchExploreData()
|
||||
.then(({ result }) => {
|
||||
if (isNullish(result.dataset?.id) && isNullish(result.dataset?.uid)) {
|
||||
dispatch(hydrateExplore(fallbackExploreInitialData));
|
||||
dispatch(addDangerToast(loadErrorMessage));
|
||||
} else {
|
||||
dispatch(hydrateExplore(result));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(hydrateExplore(fallbackExploreInitialData));
|
||||
dispatch(addDangerToast(loadErrorMessage));
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoaded(true);
|
||||
});
|
||||
}, [dispatch]);
|
||||
|
||||
if (!isLoaded) {
|
||||
return <Loading />;
|
||||
}
|
||||
return <ExploreViewContainer />;
|
||||
};
|
|
@ -20,7 +20,7 @@
|
|||
import { Dispatch } from 'redux';
|
||||
import { Dataset } from '@superset-ui/chart-controls';
|
||||
import { updateFormDataByDatasource } from './exploreActions';
|
||||
import { ExplorePageState } from '../reducers/getInitialState';
|
||||
import { ExplorePageState } from '../types';
|
||||
|
||||
export const SET_DATASOURCE = 'SET_DATASOURCE';
|
||||
export interface SetDatasource {
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* 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 { hydrateExplore, HYDRATE_EXPLORE } from './hydrateExplore';
|
||||
import { exploreInitialData } from '../fixtures';
|
||||
|
||||
test('creates hydrate action from initial data', () => {
|
||||
const dispatch = jest.fn();
|
||||
const getState = jest.fn(() => ({
|
||||
user: {},
|
||||
charts: {},
|
||||
datasources: {},
|
||||
common: {},
|
||||
explore: {},
|
||||
}));
|
||||
// ignore type check - we dont need exact explore state for this test
|
||||
// @ts-ignore
|
||||
hydrateExplore(exploreInitialData)(dispatch, getState);
|
||||
expect(dispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: HYDRATE_EXPLORE,
|
||||
data: {
|
||||
charts: {
|
||||
371: {
|
||||
id: 371,
|
||||
chartAlert: null,
|
||||
chartStatus: null,
|
||||
chartStackTrace: null,
|
||||
chartUpdateEndTime: null,
|
||||
chartUpdateStartTime: 0,
|
||||
latestQueryFormData: {
|
||||
cache_timeout: undefined,
|
||||
datasource: '8__table',
|
||||
slice_id: 371,
|
||||
url_params: undefined,
|
||||
viz_type: 'table',
|
||||
},
|
||||
sliceFormData: {
|
||||
cache_timeout: undefined,
|
||||
datasource: '8__table',
|
||||
slice_id: 371,
|
||||
url_params: undefined,
|
||||
viz_type: 'table',
|
||||
},
|
||||
queryController: null,
|
||||
queriesResponse: null,
|
||||
triggerQuery: false,
|
||||
lastRendered: 0,
|
||||
},
|
||||
},
|
||||
datasources: {
|
||||
'8__table': exploreInitialData.dataset,
|
||||
},
|
||||
saveModal: {
|
||||
dashboards: [],
|
||||
saveModalAlert: null,
|
||||
},
|
||||
explore: {
|
||||
can_add: false,
|
||||
can_download: false,
|
||||
can_overwrite: false,
|
||||
isDatasourceMetaLoading: false,
|
||||
isStarred: false,
|
||||
triggerRender: false,
|
||||
datasource: exploreInitialData.dataset,
|
||||
controls: expect.any(Object),
|
||||
form_data: exploreInitialData.form_data,
|
||||
slice: exploreInitialData.slice,
|
||||
controlsTransferred: [],
|
||||
standalone: null,
|
||||
force: null,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
|
@ -0,0 +1,146 @@
|
|||
/**
|
||||
* 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 { ControlStateMapping } from '@superset-ui/chart-controls';
|
||||
|
||||
import {
|
||||
ChartState,
|
||||
ExplorePageInitialData,
|
||||
ExplorePageState,
|
||||
} from 'src/explore/types';
|
||||
import { getChartKey } from 'src/explore/exploreUtils';
|
||||
import { getControlsState } from 'src/explore/store';
|
||||
import { Dispatch } from 'redux';
|
||||
import { ensureIsArray } from '@superset-ui/core';
|
||||
import {
|
||||
getFormDataFromControls,
|
||||
applyMapStateToPropsToControl,
|
||||
} from 'src/explore/controlUtils';
|
||||
import { getDatasourceUid } from 'src/utils/getDatasourceUid';
|
||||
import { getUrlParam } from 'src/utils/urlUtils';
|
||||
import { URL_PARAMS } from 'src/constants';
|
||||
import { findPermission } from 'src/utils/findPermission';
|
||||
|
||||
export const HYDRATE_EXPLORE = 'HYDRATE_EXPLORE';
|
||||
export const hydrateExplore =
|
||||
({ form_data, slice, dataset }: ExplorePageInitialData) =>
|
||||
(dispatch: Dispatch, getState: () => ExplorePageState) => {
|
||||
const { user, datasources, charts, sliceEntities, common } = getState();
|
||||
|
||||
const sliceId = getUrlParam(URL_PARAMS.sliceId);
|
||||
const dashboardId = getUrlParam(URL_PARAMS.dashboardId);
|
||||
const fallbackSlice = sliceId ? sliceEntities?.slices?.[sliceId] : null;
|
||||
const initialSlice = slice ?? fallbackSlice;
|
||||
const initialFormData = form_data ?? initialSlice?.form_data;
|
||||
if (!initialFormData.viz_type) {
|
||||
const defaultVizType = common?.conf.DEFAULT_VIZ_TYPE || 'table';
|
||||
initialFormData.viz_type =
|
||||
getUrlParam(URL_PARAMS.vizType) || defaultVizType;
|
||||
}
|
||||
if (dashboardId) {
|
||||
initialFormData.dashboardId = dashboardId;
|
||||
}
|
||||
const initialDatasource =
|
||||
datasources?.[initialFormData.datasource] ?? dataset;
|
||||
|
||||
const initialExploreState = {
|
||||
form_data: initialFormData,
|
||||
slice: initialSlice,
|
||||
datasource: initialDatasource,
|
||||
};
|
||||
const initialControls = getControlsState(
|
||||
initialExploreState,
|
||||
initialFormData,
|
||||
) as ControlStateMapping;
|
||||
|
||||
const exploreState = {
|
||||
// note this will add `form_data` to state,
|
||||
// which will be manipulable by future reducers.
|
||||
can_add: findPermission('can_write', 'Chart', user?.roles),
|
||||
can_download: findPermission('can_csv', 'Superset', user?.roles),
|
||||
can_overwrite: ensureIsArray(slice?.owners).includes(
|
||||
user?.userId as number,
|
||||
),
|
||||
isDatasourceMetaLoading: false,
|
||||
isStarred: false,
|
||||
triggerRender: false,
|
||||
// duplicate datasource in exploreState - it's needed by getControlsState
|
||||
datasource: initialDatasource,
|
||||
// Initial control state will skip `control.mapStateToProps`
|
||||
// because `bootstrapData.controls` is undefined.
|
||||
controls: initialControls,
|
||||
form_data: initialFormData,
|
||||
slice: initialSlice,
|
||||
controlsTransferred: [],
|
||||
standalone: getUrlParam(URL_PARAMS.standalone),
|
||||
force: getUrlParam(URL_PARAMS.force),
|
||||
};
|
||||
|
||||
// apply initial mapStateToProps for all controls, must execute AFTER
|
||||
// bootstrapState has initialized `controls`. Order of execution is not
|
||||
// guaranteed, so controls shouldn't rely on each other's mapped state.
|
||||
Object.entries(exploreState.controls).forEach(([key, controlState]) => {
|
||||
exploreState.controls[key] = applyMapStateToPropsToControl(
|
||||
controlState,
|
||||
exploreState,
|
||||
);
|
||||
});
|
||||
const sliceFormData = initialSlice
|
||||
? getFormDataFromControls(initialControls)
|
||||
: null;
|
||||
|
||||
const chartKey: number = getChartKey(initialExploreState);
|
||||
const chart: ChartState = {
|
||||
id: chartKey,
|
||||
chartAlert: null,
|
||||
chartStatus: null,
|
||||
chartStackTrace: null,
|
||||
chartUpdateEndTime: null,
|
||||
chartUpdateStartTime: 0,
|
||||
latestQueryFormData: getFormDataFromControls(exploreState.controls),
|
||||
sliceFormData,
|
||||
queryController: null,
|
||||
queriesResponse: null,
|
||||
triggerQuery: false,
|
||||
lastRendered: 0,
|
||||
};
|
||||
|
||||
return dispatch({
|
||||
type: HYDRATE_EXPLORE,
|
||||
data: {
|
||||
charts: {
|
||||
...charts,
|
||||
[chartKey]: chart,
|
||||
},
|
||||
datasources: {
|
||||
...datasources,
|
||||
[getDatasourceUid(initialDatasource)]: initialDatasource,
|
||||
},
|
||||
saveModal: {
|
||||
dashboards: [],
|
||||
saveModalAlert: null,
|
||||
},
|
||||
explore: exploreState,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export type HydrateExplore = {
|
||||
type: typeof HYDRATE_EXPLORE;
|
||||
data: ExplorePageState;
|
||||
};
|
|
@ -54,8 +54,7 @@ import Loading from 'src/components/Loading';
|
|||
import { usePrevious } from 'src/hooks/usePrevious';
|
||||
import { getSectionsToRender } from 'src/explore/controlUtils';
|
||||
import { ExploreActions } from 'src/explore/actions/exploreActions';
|
||||
import { ExplorePageState } from 'src/explore/reducers/getInitialState';
|
||||
import { ChartState } from 'src/explore/types';
|
||||
import { ChartState, ExplorePageState } from 'src/explore/types';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
|
||||
import { rgba } from 'emotion-rgba';
|
||||
|
|
|
@ -47,7 +47,9 @@ const datasource = {
|
|||
main_dttm_col: 'None',
|
||||
datasource_name: 'table1',
|
||||
description: 'desc',
|
||||
owners: [{ username: 'admin', userId: 1 }],
|
||||
owners: [
|
||||
{ first_name: 'admin', last_name: 'admin', username: 'admin', id: 1 },
|
||||
],
|
||||
};
|
||||
|
||||
const mockUser = {
|
||||
|
|
|
@ -694,17 +694,9 @@ function ExploreViewContainer(props) {
|
|||
ExploreViewContainer.propTypes = propTypes;
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const {
|
||||
explore,
|
||||
charts,
|
||||
common,
|
||||
impressionId,
|
||||
dataMask,
|
||||
reports,
|
||||
datasources,
|
||||
user,
|
||||
} = state;
|
||||
const { controls, slice } = explore;
|
||||
const { explore, charts, common, impressionId, dataMask, reports, user } =
|
||||
state;
|
||||
const { controls, slice, datasource } = explore;
|
||||
const form_data = getFormDataFromControls(controls);
|
||||
const slice_id = form_data.slice_id ?? slice?.slice_id ?? 0; // 0 - unsaved chart
|
||||
form_data.extra_form_data = mergeExtraFormData(
|
||||
|
@ -720,8 +712,6 @@ function mapStateToProps(state) {
|
|||
dashboardId = undefined;
|
||||
}
|
||||
|
||||
const datasource = datasources[form_data.datasource];
|
||||
|
||||
return {
|
||||
isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
|
||||
datasource,
|
||||
|
|
|
@ -237,7 +237,7 @@ class DatasourceControl extends React.PureComponent {
|
|||
const isSqlSupported = datasource.type === 'table';
|
||||
const { user } = this.props;
|
||||
const allowEdit = datasource.owners
|
||||
.map(o => o.id || o.value)
|
||||
?.map(o => o.id || o.value)
|
||||
.includes(user.userId);
|
||||
isUserAdmin(user);
|
||||
|
||||
|
|
|
@ -29,8 +29,8 @@ import { css, SupersetTheme, t, useTheme } from '@superset-ui/core';
|
|||
import { usePluginContext } from 'src/components/DynamicPlugins';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import Icons from 'src/components/Icons';
|
||||
import { ExplorePageState } from 'src/explore/reducers/getInitialState';
|
||||
import { getChartKey } from 'src/explore/exploreUtils';
|
||||
import { ExplorePageState } from 'src/explore/types';
|
||||
|
||||
export interface VizMeta {
|
||||
icon: ReactElement;
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* 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 { getParsedExploreURLParams } from './getParsedExploreURLParams';
|
||||
|
||||
const EXPLORE_BASE_URL = 'http://localhost:9000/superset/explore/';
|
||||
const setupLocation = (newUrl: string) => {
|
||||
delete (window as any).location;
|
||||
// @ts-ignore
|
||||
window.location = new URL(newUrl);
|
||||
};
|
||||
|
||||
test('get form_data_key and slice_id from search params - url when moving from dashboard to explore', () => {
|
||||
setupLocation(
|
||||
`${EXPLORE_BASE_URL}?form_data_key=yrLXmyE9fmhQ11lM1KgaD1PoPSBpuLZIJfqdyIdw9GoBwhPFRZHeIgeFiNZljbpd&slice_id=56`,
|
||||
);
|
||||
expect(getParsedExploreURLParams().toString()).toEqual(
|
||||
'slice_id=56&form_data_key=yrLXmyE9fmhQ11lM1KgaD1PoPSBpuLZIJfqdyIdw9GoBwhPFRZHeIgeFiNZljbpd',
|
||||
);
|
||||
});
|
||||
|
||||
test('get slice_id from form_data search param - url on Chart List', () => {
|
||||
setupLocation(`${EXPLORE_BASE_URL}?form_data=%7B%22slice_id%22%3A%2056%7D`);
|
||||
expect(getParsedExploreURLParams().toString()).toEqual('slice_id=56');
|
||||
});
|
||||
|
||||
test('get datasource and viz type from form_data search param - url when creating new chart', () => {
|
||||
setupLocation(
|
||||
`${EXPLORE_BASE_URL}?form_data=%7B%22viz_type%22%3A%22big_number%22%2C%22datasource%22%3A%222__table%22%7D`,
|
||||
);
|
||||
expect(getParsedExploreURLParams().toString()).toEqual(
|
||||
'viz_type=big_number&dataset_id=2&dataset_type=table',
|
||||
);
|
||||
});
|
||||
|
||||
test('get permalink key from path params', () => {
|
||||
setupLocation(`${EXPLORE_BASE_URL}p/kpOqweaMY9R/`);
|
||||
expect(getParsedExploreURLParams().toString()).toEqual(
|
||||
'permalink_key=kpOqweaMY9R',
|
||||
);
|
||||
});
|
||||
|
||||
test('get dataset id from path params', () => {
|
||||
setupLocation(`${EXPLORE_BASE_URL}table/42/`);
|
||||
expect(getParsedExploreURLParams().toString()).toEqual('dataset_id=42');
|
||||
});
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// mapping { url_param: v1_explore_request_param }
|
||||
const EXPLORE_URL_SEARCH_PARAMS = {
|
||||
form_data: {
|
||||
name: 'form_data',
|
||||
parser: (formData: string) => {
|
||||
const formDataObject = JSON.parse(formData);
|
||||
if (formDataObject.datasource) {
|
||||
const [dataset_id, dataset_type] =
|
||||
formDataObject.datasource.split('__');
|
||||
formDataObject.dataset_id = dataset_id;
|
||||
formDataObject.dataset_type = dataset_type;
|
||||
delete formDataObject.datasource;
|
||||
}
|
||||
return formDataObject;
|
||||
},
|
||||
},
|
||||
slice_id: {
|
||||
name: 'slice_id',
|
||||
},
|
||||
dataset_id: {
|
||||
name: 'dataset_id',
|
||||
},
|
||||
dataset_type: {
|
||||
name: 'dataset_type',
|
||||
},
|
||||
datasource: {
|
||||
name: 'datasource',
|
||||
parser: (datasource: string) => {
|
||||
const [dataset_id, dataset_type] = datasource.split('__');
|
||||
return { dataset_id, dataset_type };
|
||||
},
|
||||
},
|
||||
form_data_key: {
|
||||
name: 'form_data_key',
|
||||
},
|
||||
permalink_key: {
|
||||
name: 'permalink_key',
|
||||
},
|
||||
viz_type: {
|
||||
name: 'viz_type',
|
||||
},
|
||||
dashboard_id: {
|
||||
name: 'dashboard_id',
|
||||
},
|
||||
};
|
||||
|
||||
const EXPLORE_URL_PATH_PARAMS = {
|
||||
p: 'permalink_key', // permalink
|
||||
table: 'dataset_id',
|
||||
};
|
||||
|
||||
// search params can be placed in form_data object
|
||||
// we need to "flatten" the search params to use them with /v1/explore endpoint
|
||||
const getParsedExploreURLSearchParams = () => {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
return Object.keys(EXPLORE_URL_SEARCH_PARAMS).reduce((acc, currentParam) => {
|
||||
const paramValue = urlSearchParams.get(currentParam);
|
||||
if (paramValue === null) {
|
||||
return acc;
|
||||
}
|
||||
let parsedParamValue;
|
||||
try {
|
||||
parsedParamValue =
|
||||
EXPLORE_URL_SEARCH_PARAMS[currentParam].parser?.(paramValue) ??
|
||||
paramValue;
|
||||
} catch {
|
||||
parsedParamValue = paramValue;
|
||||
}
|
||||
if (typeof parsedParamValue === 'object') {
|
||||
return { ...acc, ...parsedParamValue };
|
||||
}
|
||||
return {
|
||||
...acc,
|
||||
[EXPLORE_URL_SEARCH_PARAMS[currentParam].name]: parsedParamValue,
|
||||
};
|
||||
}, {});
|
||||
};
|
||||
|
||||
// path params need to be transformed to search params to use them with /v1/explore endpoint
|
||||
const getParsedExploreURLPathParams = () =>
|
||||
Object.keys(EXPLORE_URL_PATH_PARAMS).reduce((acc, currentParam) => {
|
||||
const re = new RegExp(`/(${currentParam})/(\\w+)`);
|
||||
const pathGroups = window.location.pathname.match(re);
|
||||
if (pathGroups && pathGroups[2]) {
|
||||
return { ...acc, [EXPLORE_URL_PATH_PARAMS[currentParam]]: pathGroups[2] };
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
export const getParsedExploreURLParams = () =>
|
||||
new URLSearchParams(
|
||||
Object.entries({
|
||||
...getParsedExploreURLSearchParams(),
|
||||
...getParsedExploreURLPathParams(),
|
||||
})
|
||||
.map(entry => entry.join('='))
|
||||
.join('&'),
|
||||
);
|
|
@ -267,11 +267,12 @@ export const exportChart = ({
|
|||
SupersetClient.postForm(url, { form_data: safeStringify(payload) });
|
||||
};
|
||||
|
||||
export const exploreChart = formData => {
|
||||
export const exploreChart = (formData, requestParams) => {
|
||||
const url = getExploreUrl({
|
||||
formData,
|
||||
endpointType: 'base',
|
||||
allowDomainSharding: false,
|
||||
requestParams,
|
||||
});
|
||||
SupersetClient.postForm(url, { form_data: safeStringify(formData) });
|
||||
};
|
||||
|
|
|
@ -18,13 +18,14 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { DatasourceType, t } from '@superset-ui/core';
|
||||
import {
|
||||
ColumnMeta,
|
||||
ColumnOption,
|
||||
ControlConfig,
|
||||
ControlPanelSectionConfig,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { ExplorePageInitialData } from './types';
|
||||
|
||||
export const controlPanelSectionsChartOptions: (ControlPanelSectionConfig | null)[] =
|
||||
[
|
||||
|
@ -108,3 +109,59 @@ export const controlPanelSectionsChartOptionsTable: ControlPanelSectionConfig[]
|
|||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const exploreInitialData: ExplorePageInitialData = {
|
||||
form_data: {
|
||||
datasource: '8__table',
|
||||
metric: 'count',
|
||||
slice_id: 371,
|
||||
time_range: 'No filter',
|
||||
viz_type: 'table',
|
||||
},
|
||||
slice: {
|
||||
cache_timeout: null,
|
||||
description: null,
|
||||
slice_id: 371,
|
||||
slice_name: 'Age distribution of respondents',
|
||||
is_managed_externally: false,
|
||||
form_data: {
|
||||
datasource: '8__table',
|
||||
metric: 'count',
|
||||
slice_id: 371,
|
||||
time_range: 'No filter',
|
||||
viz_type: 'table',
|
||||
},
|
||||
},
|
||||
dataset: {
|
||||
id: 8,
|
||||
type: DatasourceType.Table,
|
||||
columns: [{ column_name: 'a' }],
|
||||
metrics: [{ metric_name: 'first' }, { metric_name: 'second' }],
|
||||
column_format: {},
|
||||
verbose_map: {},
|
||||
main_dttm_col: '',
|
||||
datasource_name: '8__table',
|
||||
description: null,
|
||||
},
|
||||
};
|
||||
|
||||
export const fallbackExploreInitialData: ExplorePageInitialData = {
|
||||
form_data: {
|
||||
datasource: '0__table',
|
||||
viz_type: 'table',
|
||||
},
|
||||
dataset: {
|
||||
id: 0,
|
||||
type: DatasourceType.Table,
|
||||
columns: [],
|
||||
metrics: [],
|
||||
column_format: {},
|
||||
verbose_map: {},
|
||||
main_dttm_col: '',
|
||||
owners: [],
|
||||
datasource_name: 'missing_datasource',
|
||||
name: 'missing_datasource',
|
||||
description: null,
|
||||
},
|
||||
slice: null,
|
||||
};
|
||||
|
|
|
@ -20,10 +20,11 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
import { createStore, applyMiddleware, compose } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import logger from '../middleware/loggerMiddleware';
|
||||
import { initFeatureFlags } from '../featureFlags';
|
||||
import { initEnhancer } from '../reduxUtils';
|
||||
import getInitialState from './reducers/getInitialState';
|
||||
import shortid from 'shortid';
|
||||
import getToastsFromPyFlashMessages from 'src/components/MessageToasts/getToastsFromPyFlashMessages';
|
||||
import logger from 'src/middleware/loggerMiddleware';
|
||||
import { initFeatureFlags } from 'src/featureFlags';
|
||||
import { initEnhancer } from 'src/reduxUtils';
|
||||
import rootReducer from './reducers/index';
|
||||
import App from './App';
|
||||
|
||||
|
@ -31,11 +32,18 @@ const exploreViewContainer = document.getElementById('app');
|
|||
const bootstrapData = JSON.parse(
|
||||
exploreViewContainer.getAttribute('data-bootstrap'),
|
||||
);
|
||||
initFeatureFlags(bootstrapData.common.feature_flags);
|
||||
const initState = getInitialState(bootstrapData);
|
||||
|
||||
const user = { ...bootstrapData.user };
|
||||
const common = { ...bootstrapData.common };
|
||||
initFeatureFlags(common.feature_flags);
|
||||
const store = createStore(
|
||||
rootReducer,
|
||||
initState,
|
||||
{
|
||||
user,
|
||||
common,
|
||||
impressionId: shortid.generate(),
|
||||
messageToasts: getToastsFromPyFlashMessages(common?.flash_messages || []),
|
||||
},
|
||||
compose(applyMiddleware(thunk, logger), initEnhancer(false)),
|
||||
);
|
||||
|
||||
|
|
|
@ -22,11 +22,12 @@ import {
|
|||
AnyDatasourcesAction,
|
||||
SET_DATASOURCE,
|
||||
} from '../actions/datasourcesActions';
|
||||
import { HYDRATE_EXPLORE, HydrateExplore } from '../actions/hydrateExplore';
|
||||
|
||||
export default function datasourcesReducer(
|
||||
// TODO: change type to include other datasource types
|
||||
datasources: { [key: string]: Dataset },
|
||||
action: AnyDatasourcesAction,
|
||||
action: AnyDatasourcesAction | HydrateExplore,
|
||||
) {
|
||||
if (action.type === SET_DATASOURCE) {
|
||||
return {
|
||||
|
@ -34,5 +35,8 @@ export default function datasourcesReducer(
|
|||
[getDatasourceUid(action.datasource)]: action.datasource,
|
||||
};
|
||||
}
|
||||
if (action.type === HYDRATE_EXPLORE) {
|
||||
return { ...(action as HydrateExplore).data.datasources };
|
||||
}
|
||||
return datasources || {};
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
StandardizedFormData,
|
||||
} from 'src/explore/controlUtils';
|
||||
import * as actions from 'src/explore/actions/exploreActions';
|
||||
import { HYDRATE_EXPLORE } from '../actions/hydrateExplore';
|
||||
|
||||
export default function exploreReducer(state = {}, action) {
|
||||
const actionHandlers = {
|
||||
|
@ -247,8 +248,12 @@ export default function exploreReducer(state = {}, action) {
|
|||
force: action.force,
|
||||
};
|
||||
},
|
||||
[HYDRATE_EXPLORE]() {
|
||||
return {
|
||||
...action.data.explore,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
if (action.type in actionHandlers) {
|
||||
return actionHandlers[action.type]();
|
||||
}
|
||||
|
|
|
@ -1,146 +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 shortid from 'shortid';
|
||||
import {
|
||||
DatasourceType,
|
||||
ensureIsArray,
|
||||
JsonObject,
|
||||
QueryFormData,
|
||||
} from '@superset-ui/core';
|
||||
import { ControlStateMapping, Dataset } from '@superset-ui/chart-controls';
|
||||
import {
|
||||
CommonBootstrapData,
|
||||
UserWithPermissionsAndRoles,
|
||||
} from 'src/types/bootstrapTypes';
|
||||
import getToastsFromPyFlashMessages from 'src/components/MessageToasts/getToastsFromPyFlashMessages';
|
||||
|
||||
import { ChartState, Slice } from 'src/explore/types';
|
||||
import { getChartKey } from 'src/explore/exploreUtils';
|
||||
import { getControlsState } from 'src/explore/store';
|
||||
import {
|
||||
getFormDataFromControls,
|
||||
applyMapStateToPropsToControl,
|
||||
} from 'src/explore/controlUtils';
|
||||
import { findPermission } from 'src/utils/findPermission';
|
||||
import { getDatasourceUid } from 'src/utils/getDatasourceUid';
|
||||
import { getUrlParam } from 'src/utils/urlUtils';
|
||||
import { URL_PARAMS } from 'src/constants';
|
||||
|
||||
export interface ExplorePageBootstrapData extends JsonObject {
|
||||
can_add: boolean;
|
||||
can_download: boolean;
|
||||
can_overwrite: boolean;
|
||||
common: CommonBootstrapData;
|
||||
datasource: Dataset;
|
||||
datasource_id: number;
|
||||
datasource_type: DatasourceType;
|
||||
forced_height: string | null;
|
||||
form_data: QueryFormData;
|
||||
slice: Slice | null;
|
||||
standalone: boolean;
|
||||
force: boolean;
|
||||
user: UserWithPermissionsAndRoles;
|
||||
}
|
||||
|
||||
export default function getInitialState(
|
||||
bootstrapData: ExplorePageBootstrapData,
|
||||
) {
|
||||
const {
|
||||
form_data: initialFormData,
|
||||
common,
|
||||
user,
|
||||
datasource,
|
||||
slice,
|
||||
} = bootstrapData;
|
||||
|
||||
const exploreState = {
|
||||
// note this will add `form_data` to state,
|
||||
// which will be manipulatable by future reducers.
|
||||
can_add: findPermission('can_write', 'Chart', user?.roles),
|
||||
can_download: findPermission('can_csv', 'Superset', user?.roles),
|
||||
can_overwrite: ensureIsArray(slice?.owners).includes(
|
||||
user?.userId as number,
|
||||
),
|
||||
isDatasourceMetaLoading: false,
|
||||
isStarred: false,
|
||||
triggerRender: false,
|
||||
// duplicate datasource in exploreState - it's needed by getControlsState
|
||||
datasource,
|
||||
// Initial control state will skip `control.mapStateToProps`
|
||||
// because `bootstrapData.controls` is undefined.
|
||||
controls: getControlsState(
|
||||
bootstrapData,
|
||||
initialFormData,
|
||||
) as ControlStateMapping,
|
||||
form_data: initialFormData,
|
||||
slice,
|
||||
controlsTransferred: [],
|
||||
standalone: getUrlParam(URL_PARAMS.standalone),
|
||||
force: getUrlParam(URL_PARAMS.force),
|
||||
};
|
||||
|
||||
// apply initial mapStateToProps for all controls, must execute AFTER
|
||||
// bootstrapState has initialized `controls`. Order of execution is not
|
||||
// guaranteed, so controls shouldn't rely on each other's mapped state.
|
||||
Object.entries(exploreState.controls).forEach(([key, controlState]) => {
|
||||
exploreState.controls[key] = applyMapStateToPropsToControl(
|
||||
controlState,
|
||||
exploreState,
|
||||
);
|
||||
});
|
||||
const sliceFormData = slice
|
||||
? getFormDataFromControls(getControlsState(bootstrapData, slice.form_data))
|
||||
: null;
|
||||
|
||||
const chartKey: number = getChartKey(bootstrapData);
|
||||
const chart: ChartState = {
|
||||
id: chartKey,
|
||||
chartAlert: null,
|
||||
chartStatus: null,
|
||||
chartStackTrace: null,
|
||||
chartUpdateEndTime: null,
|
||||
chartUpdateStartTime: 0,
|
||||
latestQueryFormData: getFormDataFromControls(exploreState.controls),
|
||||
sliceFormData,
|
||||
queryController: null,
|
||||
queriesResponse: null,
|
||||
triggerQuery: false,
|
||||
lastRendered: 0,
|
||||
};
|
||||
|
||||
return {
|
||||
common: common || {},
|
||||
user: user || {},
|
||||
charts: {
|
||||
[chartKey]: chart,
|
||||
},
|
||||
datasources: { [getDatasourceUid(datasource)]: datasource },
|
||||
saveModal: {
|
||||
dashboards: [],
|
||||
saveModalAlert: null,
|
||||
},
|
||||
explore: exploreState,
|
||||
impressionId: shortid.generate(),
|
||||
messageToasts: getToastsFromPyFlashMessages(
|
||||
(bootstrapData.common || {}).flash_messages || [],
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export type ExplorePageState = ReturnType<typeof getInitialState>;
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
/* eslint camelcase: 0 */
|
||||
import * as actions from '../actions/saveModalActions';
|
||||
import { HYDRATE_EXPLORE } from '../actions/hydrateExplore';
|
||||
|
||||
export default function saveModalReducer(state = {}, action) {
|
||||
const actionHandlers = {
|
||||
|
@ -39,6 +40,9 @@ export default function saveModalReducer(state = {}, action) {
|
|||
[actions.REMOVE_SAVE_MODAL_ALERT]() {
|
||||
return { ...state, saveModalAlert: null };
|
||||
},
|
||||
[HYDRATE_EXPLORE]() {
|
||||
return { ...action.data.saveModal };
|
||||
},
|
||||
};
|
||||
|
||||
if (action.type in actionHandlers) {
|
||||
|
|
|
@ -42,7 +42,7 @@ export function getControlsState(state, inputFormData) {
|
|||
// Getting a list of active control names for the current viz
|
||||
const formData = { ...inputFormData };
|
||||
const vizType =
|
||||
formData.viz_type || state.common.conf.DEFAULT_VIZ_TYPE || 'table';
|
||||
formData.viz_type || state.common?.conf.DEFAULT_VIZ_TYPE || 'table';
|
||||
|
||||
handleDeprecatedControls(formData);
|
||||
|
||||
|
|
|
@ -21,13 +21,17 @@ import {
|
|||
QueryFormData,
|
||||
AnnotationData,
|
||||
AdhocMetric,
|
||||
JsonObject,
|
||||
} from '@superset-ui/core';
|
||||
import { ColumnMeta, Dataset } from '@superset-ui/chart-controls';
|
||||
import {
|
||||
ColumnMeta,
|
||||
ControlStateMapping,
|
||||
Dataset,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { DatabaseObject } from 'src/views/CRUD/types';
|
||||
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
||||
import { toastState } from 'src/SqlLab/types';
|
||||
|
||||
export { Slice, Chart } from 'src/types/Chart';
|
||||
import { Slice } from 'src/types/Chart';
|
||||
|
||||
export type ChartStatus =
|
||||
| 'loading'
|
||||
|
@ -90,3 +94,40 @@ export type ExploreRootState = {
|
|||
messageToasts: toastState[];
|
||||
common: {};
|
||||
};
|
||||
|
||||
export interface ExplorePageInitialData {
|
||||
dataset: Dataset;
|
||||
form_data: QueryFormData;
|
||||
slice: Slice | null;
|
||||
}
|
||||
|
||||
export interface ExploreResponsePayload {
|
||||
result: ExplorePageInitialData & { message: string };
|
||||
}
|
||||
|
||||
export interface ExplorePageState {
|
||||
user: UserWithPermissionsAndRoles;
|
||||
common: {
|
||||
flash_messages: string[];
|
||||
conf: JsonObject;
|
||||
};
|
||||
charts: { [key: number]: ChartState };
|
||||
datasources: { [key: string]: Dataset };
|
||||
explore: {
|
||||
can_add: boolean;
|
||||
can_download: boolean;
|
||||
can_overwrite: boolean;
|
||||
isDatasourceMetaLoading: boolean;
|
||||
isStarred: boolean;
|
||||
triggerRender: boolean;
|
||||
// duplicate datasource in exploreState - it's needed by getControlsState
|
||||
datasource: Dataset;
|
||||
controls: ControlStateMapping;
|
||||
form_data: QueryFormData;
|
||||
slice: Slice;
|
||||
controlsTransferred: string[];
|
||||
standalone: boolean;
|
||||
force: boolean;
|
||||
};
|
||||
sliceEntities?: JsonObject; // propagated from Dashboard view
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue