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.wait('@getJson').its('response.statusCode').should('eq', 200);
|
||||||
cy.get('div.chart-container').contains(
|
cy.get('div.chart-container').contains(
|
||||||
'No results were returned for this query',
|
'No results were returned for this query',
|
||||||
|
|
|
@ -148,7 +148,7 @@ describe('Time range filter', () => {
|
||||||
metrics: [NUM_METRIC],
|
metrics: [NUM_METRIC],
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||||
|
|
||||||
cy.get('[data-test=time-range-trigger]')
|
cy.get('[data-test=time-range-trigger]')
|
||||||
|
@ -172,7 +172,7 @@ describe('Time range filter', () => {
|
||||||
time_range: 'Last year',
|
time_range: 'Last year',
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||||
|
|
||||||
cy.get('[data-test=time-range-trigger]')
|
cy.get('[data-test=time-range-trigger]')
|
||||||
|
@ -192,7 +192,7 @@ describe('Time range filter', () => {
|
||||||
time_range: 'previous calendar month',
|
time_range: 'previous calendar month',
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||||
|
|
||||||
cy.get('[data-test=time-range-trigger]')
|
cy.get('[data-test=time-range-trigger]')
|
||||||
|
@ -212,7 +212,7 @@ describe('Time range filter', () => {
|
||||||
time_range: 'DATEADD(DATETIME("today"), -7, day) : today',
|
time_range: 'DATEADD(DATETIME("today"), -7, day) : today',
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||||
|
|
||||||
cy.get('[data-test=time-range-trigger]')
|
cy.get('[data-test=time-range-trigger]')
|
||||||
|
@ -235,7 +235,7 @@ describe('Time range filter', () => {
|
||||||
time_range: 'No filter',
|
time_range: 'No filter',
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||||
|
|
||||||
cy.get('[data-test=time-range-trigger]')
|
cy.get('[data-test=time-range-trigger]')
|
||||||
|
|
|
@ -31,7 +31,7 @@ describe('explore view', () => {
|
||||||
it('should load Explore', () => {
|
it('should load Explore', () => {
|
||||||
const LINE_CHART_DEFAULTS = { ...FORM_DATA_DEFAULTS, viz_type: 'line' };
|
const LINE_CHART_DEFAULTS = { ...FORM_DATA_DEFAULTS, viz_type: 'line' };
|
||||||
const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] };
|
const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] };
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
cy.eyesOpen({
|
cy.eyesOpen({
|
||||||
testName: 'Explore page',
|
testName: 'Explore page',
|
||||||
|
|
|
@ -22,7 +22,7 @@ describe('Edit FilterBox Chart', () => {
|
||||||
const VIZ_DEFAULTS = { ...FORM_DATA_DEFAULTS, viz_type: 'filter_box' };
|
const VIZ_DEFAULTS = { ...FORM_DATA_DEFAULTS, viz_type: 'filter_box' };
|
||||||
|
|
||||||
function verify(formData) {
|
function verify(formData) {
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,7 @@ describe('Test explore links', () => {
|
||||||
};
|
};
|
||||||
const newChartName = `Test chart [${shortid.generate()}]`;
|
const newChartName = `Test chart [${shortid.generate()}]`;
|
||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@tableChartData' });
|
cy.verifySliceSuccess({ waitAlias: '@tableChartData' });
|
||||||
cy.url().then(() => {
|
cy.url().then(() => {
|
||||||
cy.get('[data-test="query-save-button"]').click();
|
cy.get('[data-test="query-save-button"]').click();
|
||||||
|
|
|
@ -51,7 +51,7 @@ describe('Visualization > Area', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function verify(formData) {
|
function verify(formData) {
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,23 +75,21 @@ describe('Visualization > Area', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with groupby and filter', () => {
|
it('should work with groupby and filter', () => {
|
||||||
cy.visitChartByParams(
|
cy.visitChartByParams({
|
||||||
JSON.stringify({
|
...AREA_FORM_DATA,
|
||||||
...AREA_FORM_DATA,
|
groupby: ['region'],
|
||||||
groupby: ['region'],
|
adhoc_filters: [
|
||||||
adhoc_filters: [
|
{
|
||||||
{
|
expressionType: 'SIMPLE',
|
||||||
expressionType: 'SIMPLE',
|
subject: 'region',
|
||||||
subject: 'region',
|
operator: 'IN',
|
||||||
operator: 'IN',
|
comparator: ['South Asia', 'North America'],
|
||||||
comparator: ['South Asia', 'North America'],
|
clause: 'WHERE',
|
||||||
clause: 'WHERE',
|
sqlExpression: null,
|
||||||
sqlExpression: null,
|
filterOptionName: 'filter_txje2ikiv6_wxmn0qwd1xo',
|
||||||
filterOptionName: 'filter_txje2ikiv6_wxmn0qwd1xo',
|
},
|
||||||
},
|
],
|
||||||
],
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
cy.wait('@getJson').then(async ({ response }) => {
|
cy.wait('@getJson').then(async ({ response }) => {
|
||||||
const responseBody = response?.body;
|
const responseBody = response?.body;
|
||||||
|
|
|
@ -25,11 +25,11 @@ describe('Visualization > Big Number with Trendline', () => {
|
||||||
slice_id: 42,
|
slice_id: 42,
|
||||||
granularity_sqla: 'year',
|
granularity_sqla: 'year',
|
||||||
time_grain_sqla: 'P1D',
|
time_grain_sqla: 'P1D',
|
||||||
time_range: '2000+:+2014-01-02',
|
time_range: '2000 : 2014-01-02',
|
||||||
metric: 'sum__SP_POP_TOTL',
|
metric: 'sum__SP_POP_TOTL',
|
||||||
adhoc_filters: [],
|
adhoc_filters: [],
|
||||||
compare_lag: '10',
|
compare_lag: '10',
|
||||||
compare_suffix: 'over+10Y',
|
compare_suffix: 'over 10Y',
|
||||||
y_axis_format: '.3s',
|
y_axis_format: '.3s',
|
||||||
show_trend_line: true,
|
show_trend_line: true,
|
||||||
start_y_axis_at_zero: true,
|
start_y_axis_at_zero: true,
|
||||||
|
@ -42,7 +42,7 @@ describe('Visualization > Big Number with Trendline', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function verify(formData) {
|
function verify(formData) {
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({
|
cy.verifySliceSuccess({
|
||||||
waitAlias: '@chartData',
|
waitAlias: '@chartData',
|
||||||
chartSelector: '.superset-legacy-chart-big-number',
|
chartSelector: '.superset-legacy-chart-big-number',
|
||||||
|
|
|
@ -33,7 +33,7 @@ describe('Visualization > Box Plot', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function verify(formData) {
|
function verify(formData) {
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ describe('Visualization > Bubble', () => {
|
||||||
slice_id: 46,
|
slice_id: 46,
|
||||||
granularity_sqla: 'year',
|
granularity_sqla: 'year',
|
||||||
time_grain_sqla: 'P1D',
|
time_grain_sqla: 'P1D',
|
||||||
time_range: '2011-01-01+:+2011-01-02',
|
time_range: '2011-01-01 : 2011-01-02',
|
||||||
series: 'region',
|
series: 'region',
|
||||||
entity: 'country_name',
|
entity: 'country_name',
|
||||||
x: 'sum__SP_RUR_TOTL_ZS',
|
x: 'sum__SP_RUR_TOTL_ZS',
|
||||||
|
@ -47,7 +47,7 @@ describe('Visualization > Bubble', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function verify(formData) {
|
function verify(formData) {
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ describe('Visualization > Bubble', () => {
|
||||||
// Since main functionality is already covered in filter test below,
|
// Since main functionality is already covered in filter test below,
|
||||||
// skip this test until we find a solution.
|
// skip this test until we find a solution.
|
||||||
it.skip('should work', () => {
|
it.skip('should work', () => {
|
||||||
cy.visitChartByParams(JSON.stringify(BUBBLE_FORM_DATA)).then(() => {
|
cy.visitChartByParams(BUBBLE_FORM_DATA).then(() => {
|
||||||
cy.wait('@getJson').then(xhr => {
|
cy.wait('@getJson').then(xhr => {
|
||||||
let expectedBubblesNumber = 0;
|
let expectedBubblesNumber = 0;
|
||||||
xhr.responseBody.data.forEach(element => {
|
xhr.responseBody.data.forEach(element => {
|
||||||
|
@ -86,7 +86,7 @@ describe('Visualization > Bubble', () => {
|
||||||
expressionType: 'SIMPLE',
|
expressionType: 'SIMPLE',
|
||||||
subject: 'region',
|
subject: 'region',
|
||||||
operator: '==',
|
operator: '==',
|
||||||
comparator: 'South+Asia',
|
comparator: 'South Asia',
|
||||||
clause: 'WHERE',
|
clause: 'WHERE',
|
||||||
sqlExpression: null,
|
sqlExpression: null,
|
||||||
filterOptionName: 'filter_b2tfg1rs8y_8kmrcyxvsqd',
|
filterOptionName: 'filter_b2tfg1rs8y_8kmrcyxvsqd',
|
||||||
|
|
|
@ -47,7 +47,7 @@ describe('Visualization > Compare', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function verify(formData) {
|
function verify(formData) {
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ describe('Visualization > Distribution bar chart', () => {
|
||||||
groupby: ['state'],
|
groupby: ['state'],
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({
|
cy.verifySliceSuccess({
|
||||||
waitAlias: '@getJson',
|
waitAlias: '@getJson',
|
||||||
querySubstring: NUM_METRIC.label,
|
querySubstring: NUM_METRIC.label,
|
||||||
|
@ -49,7 +49,7 @@ describe('Visualization > Distribution bar chart', () => {
|
||||||
columns: ['gender'],
|
columns: ['gender'],
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ describe('Visualization > Distribution bar chart', () => {
|
||||||
row_limit: 10,
|
row_limit: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ describe('Visualization > Distribution bar chart', () => {
|
||||||
contribution: true,
|
contribution: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -33,7 +33,7 @@ describe('Download Chart > Distribution bar chart', () => {
|
||||||
groupby: ['state'],
|
groupby: ['state'],
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.get('.header-with-actions .ant-dropdown-trigger').click();
|
cy.get('.header-with-actions .ant-dropdown-trigger').click();
|
||||||
cy.get(':nth-child(1) > .ant-dropdown-menu-submenu-title').click();
|
cy.get(':nth-child(1) > .ant-dropdown-menu-submenu-title').click();
|
||||||
cy.get(
|
cy.get(
|
||||||
|
|
|
@ -23,7 +23,7 @@ describe('Visualization > Dual Line', () => {
|
||||||
slice_id: 58,
|
slice_id: 58,
|
||||||
granularity_sqla: 'ds',
|
granularity_sqla: 'ds',
|
||||||
time_grain_sqla: 'P1D',
|
time_grain_sqla: 'P1D',
|
||||||
time_range: '100+years+ago+:+now',
|
time_range: '100 years ago : now',
|
||||||
color_scheme: 'bnbColors',
|
color_scheme: 'bnbColors',
|
||||||
x_axis_format: 'smart_date',
|
x_axis_format: 'smart_date',
|
||||||
metric: 'sum__num',
|
metric: 'sum__num',
|
||||||
|
@ -35,7 +35,7 @@ describe('Visualization > Dual Line', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function verify(formData) {
|
function verify(formData) {
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ describe('Visualization > Gauge', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function verify(formData) {
|
function verify(formData) {
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ describe('Visualization > Graph', () => {
|
||||||
function verify(formData: {
|
function verify(formData: {
|
||||||
[name: string]: string | boolean | number | Array<adhocFilter>;
|
[name: string]: string | boolean | number | Array<adhocFilter>;
|
||||||
}): void {
|
}): void {
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ describe('Visualization > Histogram', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function verify(formData: QueryFormData) {
|
function verify(formData: QueryFormData) {
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ describe('Visualization > Line', () => {
|
||||||
|
|
||||||
it('should show validator error when no metric', () => {
|
it('should show validator error when no metric', () => {
|
||||||
const formData = { ...LINE_CHART_DEFAULTS, metrics: [] };
|
const formData = { ...LINE_CHART_DEFAULTS, metrics: [] };
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.get('.panel-body').contains(
|
cy.get('.panel-body').contains(
|
||||||
`Add required control values to preview chart`,
|
`Add required control values to preview chart`,
|
||||||
);
|
);
|
||||||
|
@ -36,7 +36,7 @@ describe('Visualization > Line', () => {
|
||||||
|
|
||||||
it('should not show validator error when metric added', () => {
|
it('should not show validator error when metric added', () => {
|
||||||
const formData = { ...LINE_CHART_DEFAULTS, metrics: [] };
|
const formData = { ...LINE_CHART_DEFAULTS, metrics: [] };
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.get('.panel-body').contains(
|
cy.get('.panel-body').contains(
|
||||||
`Add required control values to preview chart`,
|
`Add required control values to preview chart`,
|
||||||
);
|
);
|
||||||
|
@ -61,7 +61,7 @@ describe('Visualization > Line', () => {
|
||||||
|
|
||||||
it('should allow negative values in Y bounds', () => {
|
it('should allow negative values in Y bounds', () => {
|
||||||
const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] };
|
const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] };
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.get('#controlSections-tab-display').click();
|
cy.get('#controlSections-tab-display').click();
|
||||||
cy.get('span').contains('Y Axis Bounds').scrollIntoView();
|
cy.get('span').contains('Y Axis Bounds').scrollIntoView();
|
||||||
cy.get('input[placeholder="Min"]').type('-0.1', { delay: 100 });
|
cy.get('input[placeholder="Min"]').type('-0.1', { delay: 100 });
|
||||||
|
@ -81,7 +81,7 @@ describe('Visualization > Line', () => {
|
||||||
|
|
||||||
it('should work with adhoc metric', () => {
|
it('should work with adhoc metric', () => {
|
||||||
const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] };
|
const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] };
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ describe('Visualization > Line', () => {
|
||||||
const metrics = ['count'];
|
const metrics = ['count'];
|
||||||
const groupby = ['gender'];
|
const groupby = ['gender'];
|
||||||
const formData = { ...LINE_CHART_DEFAULTS, metrics, groupby };
|
const formData = { ...LINE_CHART_DEFAULTS, metrics, groupby };
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ describe('Visualization > Line', () => {
|
||||||
metrics,
|
metrics,
|
||||||
adhoc_filters: filters,
|
adhoc_filters: filters,
|
||||||
};
|
};
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ describe('Visualization > Line', () => {
|
||||||
groupby: ['name'],
|
groupby: ['name'],
|
||||||
timeseries_limit_metric: NUM_METRIC,
|
timeseries_limit_metric: NUM_METRIC,
|
||||||
};
|
};
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -126,7 +126,7 @@ describe('Visualization > Line', () => {
|
||||||
timeseries_limit_metric: NUM_METRIC,
|
timeseries_limit_metric: NUM_METRIC,
|
||||||
order_desc: true,
|
order_desc: true,
|
||||||
};
|
};
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -138,7 +138,7 @@ describe('Visualization > Line', () => {
|
||||||
rolling_type: 'mean',
|
rolling_type: 'mean',
|
||||||
rolling_periods: 10,
|
rolling_periods: 10,
|
||||||
};
|
};
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -147,12 +147,12 @@ describe('Visualization > Line', () => {
|
||||||
const formData = {
|
const formData = {
|
||||||
...LINE_CHART_DEFAULTS,
|
...LINE_CHART_DEFAULTS,
|
||||||
metrics,
|
metrics,
|
||||||
time_compare: ['1+year'],
|
time_compare: ['1 year'],
|
||||||
comparison_type: 'values',
|
comparison_type: 'values',
|
||||||
groupby: ['gender'],
|
groupby: ['gender'],
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
|
|
||||||
// Offset color should match original line color
|
// Offset color should match original line color
|
||||||
|
@ -190,10 +190,10 @@ describe('Visualization > Line', () => {
|
||||||
const formData = {
|
const formData = {
|
||||||
...LINE_CHART_DEFAULTS,
|
...LINE_CHART_DEFAULTS,
|
||||||
metrics,
|
metrics,
|
||||||
time_compare: ['1+year'],
|
time_compare: ['1 year'],
|
||||||
comparison_type: 'ratio',
|
comparison_type: 'ratio',
|
||||||
};
|
};
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -202,10 +202,10 @@ describe('Visualization > Line', () => {
|
||||||
const formData = {
|
const formData = {
|
||||||
...LINE_CHART_DEFAULTS,
|
...LINE_CHART_DEFAULTS,
|
||||||
metrics,
|
metrics,
|
||||||
time_compare: ['1+year'],
|
time_compare: ['1 year'],
|
||||||
comparison_type: 'percentage',
|
comparison_type: 'percentage',
|
||||||
};
|
};
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -214,7 +214,7 @@ describe('Visualization > Line', () => {
|
||||||
...LINE_CHART_DEFAULTS,
|
...LINE_CHART_DEFAULTS,
|
||||||
metrics: ['count'],
|
metrics: ['count'],
|
||||||
};
|
};
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
cy.get('text.nv-legend-text').contains('COUNT(*)');
|
cy.get('text.nv-legend-text').contains('COUNT(*)');
|
||||||
});
|
});
|
||||||
|
@ -225,7 +225,7 @@ describe('Visualization > Line', () => {
|
||||||
metrics: ['count'],
|
metrics: ['count'],
|
||||||
annotation_layers: [
|
annotation_layers: [
|
||||||
{
|
{
|
||||||
name: 'Goal+line',
|
name: 'Goal line',
|
||||||
annotationType: 'FORMULA',
|
annotationType: 'FORMULA',
|
||||||
sourceType: '',
|
sourceType: '',
|
||||||
value: 'y=140000',
|
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.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
cy.get('.slice_container').within(() => {
|
cy.get('.slice_container').within(() => {
|
||||||
// Goal line annotation doesn't show up in legend
|
// 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' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
|
|
|
@ -37,7 +37,7 @@ describe('Visualization > Pie', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function verify(formData) {
|
function verify(formData) {
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ describe('Visualization > Pivot Table', () => {
|
||||||
slice_id: 61,
|
slice_id: 61,
|
||||||
granularity_sqla: 'ds',
|
granularity_sqla: 'ds',
|
||||||
time_grain_sqla: 'P1D',
|
time_grain_sqla: 'P1D',
|
||||||
time_range: '100+years+ago+:+now',
|
time_range: '100 years ago : now',
|
||||||
metrics: ['sum__num'],
|
metrics: ['sum__num'],
|
||||||
adhoc_filters: [],
|
adhoc_filters: [],
|
||||||
groupby: ['name'],
|
groupby: ['name'],
|
||||||
|
@ -54,7 +54,7 @@ describe('Visualization > Pivot Table', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function verify(formData) {
|
function verify(formData) {
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'table' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'table' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ describe('Visualization > Sankey', () => {
|
||||||
url_params: {},
|
url_params: {},
|
||||||
granularity_sqla: null,
|
granularity_sqla: null,
|
||||||
time_grain_sqla: 'P1D',
|
time_grain_sqla: 'P1D',
|
||||||
time_range: 'Last+week',
|
time_range: 'Last week',
|
||||||
groupby: ['source', 'target'],
|
groupby: ['source', 'target'],
|
||||||
metric: 'sum__value',
|
metric: 'sum__value',
|
||||||
adhoc_filters: [],
|
adhoc_filters: [],
|
||||||
|
@ -33,7 +33,7 @@ describe('Visualization > Sankey', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function verify(formData) {
|
function verify(formData) {
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ describe('Visualization > Sankey', () => {
|
||||||
adhoc_filters: [
|
adhoc_filters: [
|
||||||
{
|
{
|
||||||
expressionType: 'SQL',
|
expressionType: 'SQL',
|
||||||
sqlExpression: 'SUM(value)+>+0',
|
sqlExpression: 'SUM(value) > 0',
|
||||||
clause: 'HAVING',
|
clause: 'HAVING',
|
||||||
subject: null,
|
subject: null,
|
||||||
operator: null,
|
operator: null,
|
||||||
|
|
|
@ -24,7 +24,7 @@ export const FORM_DATA_DEFAULTS = {
|
||||||
datasource: '3__table',
|
datasource: '3__table',
|
||||||
granularity_sqla: 'ds',
|
granularity_sqla: 'ds',
|
||||||
time_grain_sqla: null,
|
time_grain_sqla: null,
|
||||||
time_range: '100+years+ago+:+now',
|
time_range: '100 years ago : now',
|
||||||
adhoc_filters: [],
|
adhoc_filters: [],
|
||||||
groupby: [],
|
groupby: [],
|
||||||
limit: null,
|
limit: null,
|
||||||
|
@ -37,7 +37,7 @@ export const HEALTH_POP_FORM_DATA_DEFAULTS = {
|
||||||
datasource: '2__table',
|
datasource: '2__table',
|
||||||
granularity_sqla: 'ds',
|
granularity_sqla: 'ds',
|
||||||
time_grain_sqla: 'P1D',
|
time_grain_sqla: 'P1D',
|
||||||
time_range: '1960-01-01+:+2014-01-02',
|
time_range: '1960-01-01 : 2014-01-02',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NUM_METRIC = {
|
export const NUM_METRIC = {
|
||||||
|
|
|
@ -32,7 +32,7 @@ describe('Visualization > Sunburst', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function verify(formData) {
|
function verify(formData) {
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -174,7 +174,7 @@ describe('Visualization > Table', () => {
|
||||||
groupby: ['name'],
|
groupby: ['name'],
|
||||||
row_limit: limit,
|
row_limit: limit,
|
||||||
};
|
};
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.wait('@chartData').then(({ response }) => {
|
cy.wait('@chartData').then(({ response }) => {
|
||||||
cy.verifySliceContainer('table');
|
cy.verifySliceContainer('table');
|
||||||
expect(response?.body.result[0].data.length).to.eq(limit);
|
expect(response?.body.result[0].data.length).to.eq(limit);
|
||||||
|
@ -219,7 +219,7 @@ describe('Visualization > Table', () => {
|
||||||
order_by_cols: ['["num", false]'],
|
order_by_cols: ['["num", false]'],
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.wait('@chartData').then(({ response }) => {
|
cy.wait('@chartData').then(({ response }) => {
|
||||||
cy.verifySliceContainer('table');
|
cy.verifySliceContainer('table');
|
||||||
const records = response?.body.result[0].data;
|
const records = response?.body.result[0].data;
|
||||||
|
@ -233,7 +233,7 @@ describe('Visualization > Table', () => {
|
||||||
|
|
||||||
const formData = { ...VIZ_DEFAULTS, metrics, adhoc_filters: filters };
|
const formData = { ...VIZ_DEFAULTS, metrics, adhoc_filters: filters };
|
||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@chartData', chartSelector: 'table' });
|
cy.verifySliceSuccess({ waitAlias: '@chartData', chartSelector: 'table' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -244,7 +244,7 @@ describe('Visualization > Table', () => {
|
||||||
groupby: ['state'],
|
groupby: ['state'],
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({
|
cy.verifySliceSuccess({
|
||||||
waitAlias: '@chartData',
|
waitAlias: '@chartData',
|
||||||
querySubstring: /group by.*state/i,
|
querySubstring: /group by.*state/i,
|
||||||
|
|
|
@ -33,7 +33,7 @@ describe('Visualization > Time TableViz', () => {
|
||||||
column_collection: [
|
column_collection: [
|
||||||
{
|
{
|
||||||
key: '9g4K-B-YL',
|
key: '9g4K-B-YL',
|
||||||
label: 'Last+Year',
|
label: 'Last Year',
|
||||||
colType: 'time',
|
colType: 'time',
|
||||||
timeLag: '1',
|
timeLag: '1',
|
||||||
comparisonType: 'value',
|
comparisonType: 'value',
|
||||||
|
@ -42,7 +42,7 @@ describe('Visualization > Time TableViz', () => {
|
||||||
url: '',
|
url: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({
|
cy.verifySliceSuccess({
|
||||||
waitAlias: '@getJson',
|
waitAlias: '@getJson',
|
||||||
querySubstring: NUM_METRIC.label,
|
querySubstring: NUM_METRIC.label,
|
||||||
|
@ -61,7 +61,7 @@ describe('Visualization > Time TableViz', () => {
|
||||||
column_collection: [
|
column_collection: [
|
||||||
{
|
{
|
||||||
key: '9g4K-B-YL',
|
key: '9g4K-B-YL',
|
||||||
label: 'Last+Year',
|
label: 'Last Year',
|
||||||
colType: 'time',
|
colType: 'time',
|
||||||
timeLag: '1',
|
timeLag: '1',
|
||||||
comparisonType: 'value',
|
comparisonType: 'value',
|
||||||
|
@ -70,7 +70,7 @@ describe('Visualization > Time TableViz', () => {
|
||||||
url: '',
|
url: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({
|
cy.verifySliceSuccess({
|
||||||
waitAlias: '@getJson',
|
waitAlias: '@getJson',
|
||||||
querySubstring: NUM_METRIC.label,
|
querySubstring: NUM_METRIC.label,
|
||||||
|
@ -107,7 +107,7 @@ describe('Visualization > Time TableViz', () => {
|
||||||
url: '',
|
url: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({
|
cy.verifySliceSuccess({
|
||||||
waitAlias: '@getJson',
|
waitAlias: '@getJson',
|
||||||
querySubstring: NUM_METRIC.label,
|
querySubstring: NUM_METRIC.label,
|
||||||
|
|
|
@ -38,7 +38,7 @@ describe('Visualization > Treemap', () => {
|
||||||
const level2 = '.chart-container rect[style="fill: rgb(0, 122, 135);"]';
|
const level2 = '.chart-container rect[style="fill: rgb(0, 122, 135);"]';
|
||||||
|
|
||||||
function verify(formData) {
|
function verify(formData) {
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ describe('Visualization > World Map', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function verify(formData) {
|
function verify(formData) {
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(formData);
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,12 +55,46 @@ Cypress.Commands.add('visitChartById', chartId =>
|
||||||
cy.visit(`${BASE_EXPLORE_URL}{"slice_id": ${chartId}}`),
|
cy.visit(`${BASE_EXPLORE_URL}{"slice_id": ${chartId}}`),
|
||||||
);
|
);
|
||||||
|
|
||||||
Cypress.Commands.add('visitChartByParams', params => {
|
Cypress.Commands.add(
|
||||||
const queryString =
|
'visitChartByParams',
|
||||||
typeof params === 'string' ? params : JSON.stringify(params);
|
(formData: {
|
||||||
const url = `${BASE_EXPLORE_URL}${queryString}`;
|
datasource?: string;
|
||||||
return cy.visit(url);
|
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 => {
|
Cypress.Commands.add('verifySliceContainer', chartSelector => {
|
||||||
// After a wait response check for valid slice container
|
// After a wait response check for valid slice container
|
||||||
|
|
|
@ -50,6 +50,14 @@ export type SharedControlComponents = typeof sharedControlComponents;
|
||||||
/** ----------------------------------------------
|
/** ----------------------------------------------
|
||||||
* Input data/props while rendering
|
* 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'> & {
|
export type ColumnMeta = Omit<Column, 'id'> & {
|
||||||
id?: number;
|
id?: number;
|
||||||
} & AnyDict;
|
} & AnyDict;
|
||||||
|
@ -67,8 +75,10 @@ export interface Dataset {
|
||||||
time_grain_sqla?: string;
|
time_grain_sqla?: string;
|
||||||
granularity_sqla?: string;
|
granularity_sqla?: string;
|
||||||
datasource_name: string | null;
|
datasource_name: string | null;
|
||||||
|
name?: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
uid?: string;
|
uid?: string;
|
||||||
|
owners?: Owner[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ControlPanelState {
|
export interface ControlPanelState {
|
||||||
|
|
|
@ -46,8 +46,11 @@ import {
|
||||||
SqlLabExploreRootState,
|
SqlLabExploreRootState,
|
||||||
getInitialState,
|
getInitialState,
|
||||||
ExploreDatasource,
|
ExploreDatasource,
|
||||||
|
SqlLabRootState,
|
||||||
} from 'src/SqlLab/types';
|
} 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 {
|
interface SaveDatasetModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -115,6 +118,9 @@ export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
|
||||||
modalDescription,
|
modalDescription,
|
||||||
datasource,
|
datasource,
|
||||||
}) => {
|
}) => {
|
||||||
|
const defaultVizType = useSelector<SqlLabRootState, string>(
|
||||||
|
state => state.common?.conf?.DEFAULT_VIZ_TYPE || 'table',
|
||||||
|
);
|
||||||
const query = datasource as QueryResponse;
|
const query = datasource as QueryResponse;
|
||||||
const getDefaultDatasetName = () =>
|
const getDefaultDatasetName = () =>
|
||||||
`${query.tab} ${moment().format('MM/DD/YYYY HH:mm:ss')}`;
|
`${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 dispatch = useDispatch<(dispatch: any) => Promise<JsonObject>>();
|
||||||
|
|
||||||
const handleOverwriteDataset = async () => {
|
const handleOverwriteDataset = async () => {
|
||||||
await updateDataset(
|
const [, key] = await Promise.all([
|
||||||
query.dbId,
|
updateDataset(
|
||||||
datasetToOverwrite.datasetId,
|
query.dbId,
|
||||||
query.sql,
|
datasetToOverwrite.datasetId,
|
||||||
query.results.selected_columns.map(
|
query.sql,
|
||||||
(d: { name: string; type: string; is_dttm: boolean }) => ({
|
query.results.selected_columns.map(
|
||||||
column_name: d.name,
|
(d: { name: string; type: string; is_dttm: boolean }) => ({
|
||||||
type: d.type,
|
column_name: d.name,
|
||||||
is_dttm: d.is_dttm,
|
type: d.type,
|
||||||
}),
|
is_dttm: d.is_dttm,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
datasetToOverwrite.owners.map((o: DatasetOwner) => o.id),
|
||||||
|
true,
|
||||||
),
|
),
|
||||||
datasetToOverwrite.owners.map((o: DatasetOwner) => o.id),
|
postFormData(datasetToOverwrite.datasetId, 'table', {
|
||||||
true,
|
...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);
|
setShouldOverwriteDataset(false);
|
||||||
setDatasetToOverwrite({});
|
setDatasetToOverwrite({});
|
||||||
setDatasetName(getDefaultDatasetName());
|
setDatasetName(getDefaultDatasetName());
|
||||||
|
|
||||||
exploreChart({
|
|
||||||
...EXPLORE_CHART_DEFAULT,
|
|
||||||
datasource: `${datasetToOverwrite.datasetId}__table`,
|
|
||||||
selected_columns: query.results.selected_columns,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserDatasets = async (searchText = '') => {
|
const getUserDatasets = async (searchText = '') => {
|
||||||
|
@ -235,15 +251,20 @@ export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
|
||||||
columns: selectedColumns,
|
columns: selectedColumns,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.then((data: { table_id: number }) => {
|
.then((data: { table_id: number }) =>
|
||||||
exploreChart({
|
postFormData(data.table_id, 'table', {
|
||||||
|
...EXPLORE_CHART_DEFAULT,
|
||||||
datasource: `${data.table_id}__table`,
|
datasource: `${data.table_id}__table`,
|
||||||
metrics: [],
|
...(defaultVizType === 'table' && {
|
||||||
groupby: [],
|
all_columns: selectedColumns.map(column => column.name),
|
||||||
time_range: 'No filter',
|
}),
|
||||||
selectedColumns,
|
}),
|
||||||
row_limit: 1000,
|
)
|
||||||
|
.then((key: string) => {
|
||||||
|
const url = mountExploreUrl(null, {
|
||||||
|
[URL_PARAMS.formDataKey.name]: key,
|
||||||
});
|
});
|
||||||
|
window.open(url, '_blank', 'noreferrer');
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
addDangerToast(t('An error occurred saving dataset'));
|
addDangerToast(t('An error occurred saving dataset'));
|
||||||
|
|
|
@ -16,11 +16,11 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* 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 { SupersetError } from 'src/components/ErrorMessage/types';
|
||||||
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
||||||
import { ToastType } from 'src/components/MessageToasts/types';
|
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';
|
import { ExploreRootState } from 'src/explore/types';
|
||||||
|
|
||||||
export type ExploreDatasource = Dataset | QueryResponse;
|
export type ExploreDatasource = Dataset | QueryResponse;
|
||||||
|
@ -68,7 +68,10 @@ export type SqlLabRootState = {
|
||||||
};
|
};
|
||||||
localStorageUsageInKilobytes: number;
|
localStorageUsageInKilobytes: number;
|
||||||
messageToasts: toastState[];
|
messageToasts: toastState[];
|
||||||
common: {};
|
common: {
|
||||||
|
flash_messages: string[];
|
||||||
|
conf: JsonObject;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SqlLabExploreRootState = SqlLabRootState | ExploreRootState;
|
export type SqlLabExploreRootState = SqlLabRootState | ExploreRootState;
|
||||||
|
@ -96,6 +99,7 @@ export const EXPLORE_CHART_DEFAULT = {
|
||||||
metrics: [],
|
metrics: [],
|
||||||
groupby: [],
|
groupby: [],
|
||||||
time_range: 'No filter',
|
time_range: 'No filter',
|
||||||
|
row_limit: 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface DatasetOwner {
|
export interface DatasetOwner {
|
||||||
|
|
|
@ -104,7 +104,7 @@ test('renders an enabled button if datasource and viz type are selected', async
|
||||||
const wrapper = await getWrapper();
|
const wrapper = await getWrapper();
|
||||||
wrapper.setState({
|
wrapper.setState({
|
||||||
datasource,
|
datasource,
|
||||||
visType: 'table',
|
vizType: 'table',
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
wrapper.find(Button).find({ disabled: true }).hostNodes(),
|
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.update();
|
||||||
wrapper.setState({
|
wrapper.setState({
|
||||||
datasource,
|
datasource,
|
||||||
visType: 'table',
|
vizType: 'table',
|
||||||
});
|
});
|
||||||
|
|
||||||
wrapper.instance().onVizTypeDoubleClick();
|
wrapper.instance().onVizTypeDoubleClick();
|
||||||
|
@ -136,9 +136,8 @@ test('formats Explore url', async () => {
|
||||||
const wrapper = await getWrapper();
|
const wrapper = await getWrapper();
|
||||||
wrapper.setState({
|
wrapper.setState({
|
||||||
datasource,
|
datasource,
|
||||||
visType: 'table',
|
vizType: 'table',
|
||||||
});
|
});
|
||||||
const formattedUrl =
|
const formattedUrl = '/superset/explore/?viz_type=table&datasource=1';
|
||||||
'/superset/explore/?form_data=%7B%22viz_type%22%3A%22table%22%2C%22datasource%22%3A%221%22%7D';
|
|
||||||
expect(wrapper.instance().exploreUrl()).toBe(formattedUrl);
|
expect(wrapper.instance().exploreUrl()).toBe(formattedUrl);
|
||||||
});
|
});
|
||||||
|
|
|
@ -45,7 +45,7 @@ export type AddSliceContainerProps = {
|
||||||
|
|
||||||
export type AddSliceContainerState = {
|
export type AddSliceContainerState = {
|
||||||
datasource?: { label: string; value: string };
|
datasource?: { label: string; value: string };
|
||||||
visType: string | null;
|
vizType: string | null;
|
||||||
canCreateDataset: boolean;
|
canCreateDataset: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -208,7 +208,7 @@ export default class AddSliceContainer extends React.PureComponent<
|
||||||
constructor(props: AddSliceContainerProps) {
|
constructor(props: AddSliceContainerProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
visType: null,
|
vizType: null,
|
||||||
canCreateDataset: findPermission(
|
canCreateDataset: findPermission(
|
||||||
'can_write',
|
'can_write',
|
||||||
'Dataset',
|
'Dataset',
|
||||||
|
@ -217,7 +217,7 @@ export default class AddSliceContainer extends React.PureComponent<
|
||||||
};
|
};
|
||||||
|
|
||||||
this.changeDatasource = this.changeDatasource.bind(this);
|
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.gotoSlice = this.gotoSlice.bind(this);
|
||||||
this.newLabel = this.newLabel.bind(this);
|
this.newLabel = this.newLabel.bind(this);
|
||||||
this.loadDatasources = this.loadDatasources.bind(this);
|
this.loadDatasources = this.loadDatasources.bind(this);
|
||||||
|
@ -226,14 +226,11 @@ export default class AddSliceContainer extends React.PureComponent<
|
||||||
|
|
||||||
exploreUrl() {
|
exploreUrl() {
|
||||||
const dashboardId = getUrlParam(URL_PARAMS.dashboardId);
|
const dashboardId = getUrlParam(URL_PARAMS.dashboardId);
|
||||||
const formData = encodeURIComponent(
|
let url = `/superset/explore/?viz_type=${this.state.vizType}&datasource=${this.state.datasource?.value}`;
|
||||||
JSON.stringify({
|
if (!isNullish(dashboardId)) {
|
||||||
viz_type: this.state.visType,
|
url += `&dashboard_id=${dashboardId}`;
|
||||||
datasource: this.state.datasource?.value,
|
}
|
||||||
...(!isNullish(dashboardId) && { dashboardId }),
|
return url;
|
||||||
}),
|
|
||||||
);
|
|
||||||
return `/superset/explore/?form_data=${formData}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gotoSlice() {
|
gotoSlice() {
|
||||||
|
@ -244,12 +241,12 @@ export default class AddSliceContainer extends React.PureComponent<
|
||||||
this.setState({ datasource });
|
this.setState({ datasource });
|
||||||
}
|
}
|
||||||
|
|
||||||
changeVisType(visType: string | null) {
|
changeVizType(vizType: string | null) {
|
||||||
this.setState({ visType });
|
this.setState({ vizType });
|
||||||
}
|
}
|
||||||
|
|
||||||
isBtnDisabled() {
|
isBtnDisabled() {
|
||||||
return !(this.state.datasource?.value && this.state.visType);
|
return !(this.state.datasource?.value && this.state.vizType);
|
||||||
}
|
}
|
||||||
|
|
||||||
onVizTypeDoubleClick() {
|
onVizTypeDoubleClick() {
|
||||||
|
@ -369,14 +366,14 @@ export default class AddSliceContainer extends React.PureComponent<
|
||||||
/>
|
/>
|
||||||
<Steps.Step
|
<Steps.Step
|
||||||
title={<StyledStepTitle>{t('Choose chart type')}</StyledStepTitle>}
|
title={<StyledStepTitle>{t('Choose chart type')}</StyledStepTitle>}
|
||||||
status={this.state.visType ? 'finish' : 'process'}
|
status={this.state.vizType ? 'finish' : 'process'}
|
||||||
description={
|
description={
|
||||||
<StyledStepDescription>
|
<StyledStepDescription>
|
||||||
<VizTypeGallery
|
<VizTypeGallery
|
||||||
className="viz-gallery"
|
className="viz-gallery"
|
||||||
onChange={this.changeVisType}
|
onChange={this.changeVizType}
|
||||||
onDoubleClick={this.onVizTypeDoubleClick}
|
onDoubleClick={this.onVizTypeDoubleClick}
|
||||||
selectedViz={this.state.visType}
|
selectedViz={this.state.vizType}
|
||||||
/>
|
/>
|
||||||
</StyledStepDescription>
|
</StyledStepDescription>
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { HYDRATE_DASHBOARD } from 'src/dashboard/actions/hydrate';
|
||||||
import { DatasourcesAction } from 'src/dashboard/actions/datasources';
|
import { DatasourcesAction } from 'src/dashboard/actions/datasources';
|
||||||
import { ChartState } from 'src/explore/types';
|
import { ChartState } from 'src/explore/types';
|
||||||
import { getFormDataFromControls } from 'src/explore/controlUtils';
|
import { getFormDataFromControls } from 'src/explore/controlUtils';
|
||||||
|
import { HYDRATE_EXPLORE } from 'src/explore/actions/hydrateExplore';
|
||||||
import { now } from 'src/utils/dates';
|
import { now } from 'src/utils/dates';
|
||||||
import * as actions from './chartAction';
|
import * as actions from './chartAction';
|
||||||
|
|
||||||
|
@ -194,7 +195,7 @@ export default function chartReducer(
|
||||||
delete charts[key];
|
delete charts[key];
|
||||||
return charts;
|
return charts;
|
||||||
}
|
}
|
||||||
if (action.type === HYDRATE_DASHBOARD) {
|
if (action.type === HYDRATE_DASHBOARD || action.type === HYDRATE_EXPLORE) {
|
||||||
return { ...action.data.charts };
|
return { ...action.data.charts };
|
||||||
}
|
}
|
||||||
if (action.type === DatasourcesAction.SET_DATASOURCES) {
|
if (action.type === DatasourcesAction.SET_DATASOURCES) {
|
||||||
|
|
|
@ -91,6 +91,10 @@ export const URL_PARAMS = {
|
||||||
name: 'permalink_key',
|
name: 'permalink_key',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
|
vizType: {
|
||||||
|
name: 'viz_type',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const RESERVED_CHART_URL_PARAMS: string[] = [
|
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 ToastContainer from 'src/components/MessageToasts/ToastContainer';
|
||||||
import setupApp from 'src/setup/setupApp';
|
import setupApp from 'src/setup/setupApp';
|
||||||
import setupPlugins from 'src/setup/setupPlugins';
|
import setupPlugins from 'src/setup/setupPlugins';
|
||||||
|
import { theme } from 'src/preamble';
|
||||||
|
import { ExplorePage } from './ExplorePage';
|
||||||
import './main.less';
|
import './main.less';
|
||||||
import '../assets/stylesheets/reactable-pagination.less';
|
import '../assets/stylesheets/reactable-pagination.less';
|
||||||
import { theme } from 'src/preamble';
|
|
||||||
import ExploreViewContainer from './components/ExploreViewContainer';
|
|
||||||
|
|
||||||
setupApp();
|
setupApp();
|
||||||
setupPlugins();
|
setupPlugins();
|
||||||
|
@ -41,7 +41,7 @@ const App = ({ store }) => (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<GlobalStyles />
|
<GlobalStyles />
|
||||||
<DynamicPluginProvider>
|
<DynamicPluginProvider>
|
||||||
<ExploreViewContainer />
|
<ExplorePage />
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
</DynamicPluginProvider>
|
</DynamicPluginProvider>
|
||||||
</ThemeProvider>
|
</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 { Dispatch } from 'redux';
|
||||||
import { Dataset } from '@superset-ui/chart-controls';
|
import { Dataset } from '@superset-ui/chart-controls';
|
||||||
import { updateFormDataByDatasource } from './exploreActions';
|
import { updateFormDataByDatasource } from './exploreActions';
|
||||||
import { ExplorePageState } from '../reducers/getInitialState';
|
import { ExplorePageState } from '../types';
|
||||||
|
|
||||||
export const SET_DATASOURCE = 'SET_DATASOURCE';
|
export const SET_DATASOURCE = 'SET_DATASOURCE';
|
||||||
export interface SetDatasource {
|
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 { usePrevious } from 'src/hooks/usePrevious';
|
||||||
import { getSectionsToRender } from 'src/explore/controlUtils';
|
import { getSectionsToRender } from 'src/explore/controlUtils';
|
||||||
import { ExploreActions } from 'src/explore/actions/exploreActions';
|
import { ExploreActions } from 'src/explore/actions/exploreActions';
|
||||||
import { ExplorePageState } from 'src/explore/reducers/getInitialState';
|
import { ChartState, ExplorePageState } from 'src/explore/types';
|
||||||
import { ChartState } from 'src/explore/types';
|
|
||||||
import { Tooltip } from 'src/components/Tooltip';
|
import { Tooltip } from 'src/components/Tooltip';
|
||||||
|
|
||||||
import { rgba } from 'emotion-rgba';
|
import { rgba } from 'emotion-rgba';
|
||||||
|
|
|
@ -47,7 +47,9 @@ const datasource = {
|
||||||
main_dttm_col: 'None',
|
main_dttm_col: 'None',
|
||||||
datasource_name: 'table1',
|
datasource_name: 'table1',
|
||||||
description: 'desc',
|
description: 'desc',
|
||||||
owners: [{ username: 'admin', userId: 1 }],
|
owners: [
|
||||||
|
{ first_name: 'admin', last_name: 'admin', username: 'admin', id: 1 },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockUser = {
|
const mockUser = {
|
||||||
|
|
|
@ -694,17 +694,9 @@ function ExploreViewContainer(props) {
|
||||||
ExploreViewContainer.propTypes = propTypes;
|
ExploreViewContainer.propTypes = propTypes;
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
const {
|
const { explore, charts, common, impressionId, dataMask, reports, user } =
|
||||||
explore,
|
state;
|
||||||
charts,
|
const { controls, slice, datasource } = explore;
|
||||||
common,
|
|
||||||
impressionId,
|
|
||||||
dataMask,
|
|
||||||
reports,
|
|
||||||
datasources,
|
|
||||||
user,
|
|
||||||
} = state;
|
|
||||||
const { controls, slice } = explore;
|
|
||||||
const form_data = getFormDataFromControls(controls);
|
const form_data = getFormDataFromControls(controls);
|
||||||
const slice_id = form_data.slice_id ?? slice?.slice_id ?? 0; // 0 - unsaved chart
|
const slice_id = form_data.slice_id ?? slice?.slice_id ?? 0; // 0 - unsaved chart
|
||||||
form_data.extra_form_data = mergeExtraFormData(
|
form_data.extra_form_data = mergeExtraFormData(
|
||||||
|
@ -720,8 +712,6 @@ function mapStateToProps(state) {
|
||||||
dashboardId = undefined;
|
dashboardId = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const datasource = datasources[form_data.datasource];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
|
isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
|
||||||
datasource,
|
datasource,
|
||||||
|
|
|
@ -237,7 +237,7 @@ class DatasourceControl extends React.PureComponent {
|
||||||
const isSqlSupported = datasource.type === 'table';
|
const isSqlSupported = datasource.type === 'table';
|
||||||
const { user } = this.props;
|
const { user } = this.props;
|
||||||
const allowEdit = datasource.owners
|
const allowEdit = datasource.owners
|
||||||
.map(o => o.id || o.value)
|
?.map(o => o.id || o.value)
|
||||||
.includes(user.userId);
|
.includes(user.userId);
|
||||||
isUserAdmin(user);
|
isUserAdmin(user);
|
||||||
|
|
||||||
|
|
|
@ -29,8 +29,8 @@ import { css, SupersetTheme, t, useTheme } from '@superset-ui/core';
|
||||||
import { usePluginContext } from 'src/components/DynamicPlugins';
|
import { usePluginContext } from 'src/components/DynamicPlugins';
|
||||||
import { Tooltip } from 'src/components/Tooltip';
|
import { Tooltip } from 'src/components/Tooltip';
|
||||||
import Icons from 'src/components/Icons';
|
import Icons from 'src/components/Icons';
|
||||||
import { ExplorePageState } from 'src/explore/reducers/getInitialState';
|
|
||||||
import { getChartKey } from 'src/explore/exploreUtils';
|
import { getChartKey } from 'src/explore/exploreUtils';
|
||||||
|
import { ExplorePageState } from 'src/explore/types';
|
||||||
|
|
||||||
export interface VizMeta {
|
export interface VizMeta {
|
||||||
icon: ReactElement;
|
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) });
|
SupersetClient.postForm(url, { form_data: safeStringify(payload) });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const exploreChart = formData => {
|
export const exploreChart = (formData, requestParams) => {
|
||||||
const url = getExploreUrl({
|
const url = getExploreUrl({
|
||||||
formData,
|
formData,
|
||||||
endpointType: 'base',
|
endpointType: 'base',
|
||||||
allowDomainSharding: false,
|
allowDomainSharding: false,
|
||||||
|
requestParams,
|
||||||
});
|
});
|
||||||
SupersetClient.postForm(url, { form_data: safeStringify(formData) });
|
SupersetClient.postForm(url, { form_data: safeStringify(formData) });
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,13 +18,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { t } from '@superset-ui/core';
|
import { DatasourceType, t } from '@superset-ui/core';
|
||||||
import {
|
import {
|
||||||
ColumnMeta,
|
ColumnMeta,
|
||||||
ColumnOption,
|
ColumnOption,
|
||||||
ControlConfig,
|
ControlConfig,
|
||||||
ControlPanelSectionConfig,
|
ControlPanelSectionConfig,
|
||||||
} from '@superset-ui/chart-controls';
|
} from '@superset-ui/chart-controls';
|
||||||
|
import { ExplorePageInitialData } from './types';
|
||||||
|
|
||||||
export const controlPanelSectionsChartOptions: (ControlPanelSectionConfig | null)[] =
|
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 ReactDOM from 'react-dom';
|
||||||
import { createStore, applyMiddleware, compose } from 'redux';
|
import { createStore, applyMiddleware, compose } from 'redux';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import logger from '../middleware/loggerMiddleware';
|
import shortid from 'shortid';
|
||||||
import { initFeatureFlags } from '../featureFlags';
|
import getToastsFromPyFlashMessages from 'src/components/MessageToasts/getToastsFromPyFlashMessages';
|
||||||
import { initEnhancer } from '../reduxUtils';
|
import logger from 'src/middleware/loggerMiddleware';
|
||||||
import getInitialState from './reducers/getInitialState';
|
import { initFeatureFlags } from 'src/featureFlags';
|
||||||
|
import { initEnhancer } from 'src/reduxUtils';
|
||||||
import rootReducer from './reducers/index';
|
import rootReducer from './reducers/index';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
|
@ -31,11 +32,18 @@ const exploreViewContainer = document.getElementById('app');
|
||||||
const bootstrapData = JSON.parse(
|
const bootstrapData = JSON.parse(
|
||||||
exploreViewContainer.getAttribute('data-bootstrap'),
|
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(
|
const store = createStore(
|
||||||
rootReducer,
|
rootReducer,
|
||||||
initState,
|
{
|
||||||
|
user,
|
||||||
|
common,
|
||||||
|
impressionId: shortid.generate(),
|
||||||
|
messageToasts: getToastsFromPyFlashMessages(common?.flash_messages || []),
|
||||||
|
},
|
||||||
compose(applyMiddleware(thunk, logger), initEnhancer(false)),
|
compose(applyMiddleware(thunk, logger), initEnhancer(false)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -22,11 +22,12 @@ import {
|
||||||
AnyDatasourcesAction,
|
AnyDatasourcesAction,
|
||||||
SET_DATASOURCE,
|
SET_DATASOURCE,
|
||||||
} from '../actions/datasourcesActions';
|
} from '../actions/datasourcesActions';
|
||||||
|
import { HYDRATE_EXPLORE, HydrateExplore } from '../actions/hydrateExplore';
|
||||||
|
|
||||||
export default function datasourcesReducer(
|
export default function datasourcesReducer(
|
||||||
// TODO: change type to include other datasource types
|
// TODO: change type to include other datasource types
|
||||||
datasources: { [key: string]: Dataset },
|
datasources: { [key: string]: Dataset },
|
||||||
action: AnyDatasourcesAction,
|
action: AnyDatasourcesAction | HydrateExplore,
|
||||||
) {
|
) {
|
||||||
if (action.type === SET_DATASOURCE) {
|
if (action.type === SET_DATASOURCE) {
|
||||||
return {
|
return {
|
||||||
|
@ -34,5 +35,8 @@ export default function datasourcesReducer(
|
||||||
[getDatasourceUid(action.datasource)]: action.datasource,
|
[getDatasourceUid(action.datasource)]: action.datasource,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (action.type === HYDRATE_EXPLORE) {
|
||||||
|
return { ...(action as HydrateExplore).data.datasources };
|
||||||
|
}
|
||||||
return datasources || {};
|
return datasources || {};
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ import {
|
||||||
StandardizedFormData,
|
StandardizedFormData,
|
||||||
} from 'src/explore/controlUtils';
|
} from 'src/explore/controlUtils';
|
||||||
import * as actions from 'src/explore/actions/exploreActions';
|
import * as actions from 'src/explore/actions/exploreActions';
|
||||||
|
import { HYDRATE_EXPLORE } from '../actions/hydrateExplore';
|
||||||
|
|
||||||
export default function exploreReducer(state = {}, action) {
|
export default function exploreReducer(state = {}, action) {
|
||||||
const actionHandlers = {
|
const actionHandlers = {
|
||||||
|
@ -247,8 +248,12 @@ export default function exploreReducer(state = {}, action) {
|
||||||
force: action.force,
|
force: action.force,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
[HYDRATE_EXPLORE]() {
|
||||||
|
return {
|
||||||
|
...action.data.explore,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (action.type in actionHandlers) {
|
if (action.type in actionHandlers) {
|
||||||
return actionHandlers[action.type]();
|
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 */
|
/* eslint camelcase: 0 */
|
||||||
import * as actions from '../actions/saveModalActions';
|
import * as actions from '../actions/saveModalActions';
|
||||||
|
import { HYDRATE_EXPLORE } from '../actions/hydrateExplore';
|
||||||
|
|
||||||
export default function saveModalReducer(state = {}, action) {
|
export default function saveModalReducer(state = {}, action) {
|
||||||
const actionHandlers = {
|
const actionHandlers = {
|
||||||
|
@ -39,6 +40,9 @@ export default function saveModalReducer(state = {}, action) {
|
||||||
[actions.REMOVE_SAVE_MODAL_ALERT]() {
|
[actions.REMOVE_SAVE_MODAL_ALERT]() {
|
||||||
return { ...state, saveModalAlert: null };
|
return { ...state, saveModalAlert: null };
|
||||||
},
|
},
|
||||||
|
[HYDRATE_EXPLORE]() {
|
||||||
|
return { ...action.data.saveModal };
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (action.type in actionHandlers) {
|
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
|
// Getting a list of active control names for the current viz
|
||||||
const formData = { ...inputFormData };
|
const formData = { ...inputFormData };
|
||||||
const vizType =
|
const vizType =
|
||||||
formData.viz_type || state.common.conf.DEFAULT_VIZ_TYPE || 'table';
|
formData.viz_type || state.common?.conf.DEFAULT_VIZ_TYPE || 'table';
|
||||||
|
|
||||||
handleDeprecatedControls(formData);
|
handleDeprecatedControls(formData);
|
||||||
|
|
||||||
|
|
|
@ -21,13 +21,17 @@ import {
|
||||||
QueryFormData,
|
QueryFormData,
|
||||||
AnnotationData,
|
AnnotationData,
|
||||||
AdhocMetric,
|
AdhocMetric,
|
||||||
|
JsonObject,
|
||||||
} from '@superset-ui/core';
|
} 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 { DatabaseObject } from 'src/views/CRUD/types';
|
||||||
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
||||||
import { toastState } from 'src/SqlLab/types';
|
import { toastState } from 'src/SqlLab/types';
|
||||||
|
import { Slice } from 'src/types/Chart';
|
||||||
export { Slice, Chart } from 'src/types/Chart';
|
|
||||||
|
|
||||||
export type ChartStatus =
|
export type ChartStatus =
|
||||||
| 'loading'
|
| 'loading'
|
||||||
|
@ -90,3 +94,40 @@ export type ExploreRootState = {
|
||||||
messageToasts: toastState[];
|
messageToasts: toastState[];
|
||||||
common: {};
|
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