mirror of https://github.com/apache/superset.git
feat: upgrade react-select and make multi-select sortable (#9628)
* feat: upgrade react-select v1.3.0 to v3.1.0 Upgrade `react-select`, replace `react-virtualized-select` with a custom solution implemented with `react-window`. Future plans include deprecate `react-virtualized` used in other places, too. Migrate all react-select related components to `src/Components/Select`. * Fix new list view * Fix tests * Address PR comments * Fix a flacky Cypress test * Adjust styles for Select in CRUD ListView * Fix loadOptions for owners select in chart PropertiesModal TODO: add typing support for AsyncSelect props. * Address PR comments; allow isMulti in SelectControl, too * Clean up NaN in table filter values * Fix flacky test
This commit is contained in:
parent
68832d2fa5
commit
81ab8dd8b4
|
@ -53,13 +53,11 @@ export default () =>
|
|||
});
|
||||
|
||||
it('should apply filter', () => {
|
||||
cy.wait(10);
|
||||
|
||||
cy.get('.Select-placeholder')
|
||||
cy.get('.Select__control')
|
||||
.contains('Select [region]')
|
||||
.click()
|
||||
.next()
|
||||
.find('input')
|
||||
.click({ force: true });
|
||||
cy.get('.Select__control input[type=text]')
|
||||
.first()
|
||||
.type('South Asia{enter}', { force: true });
|
||||
|
||||
// wait again after applied filters
|
||||
|
|
|
@ -128,10 +128,11 @@ export default () =>
|
|||
cy.wait('@treemapRequest');
|
||||
|
||||
// apply filter
|
||||
cy.get('.Select-control')
|
||||
.first()
|
||||
.find('input')
|
||||
cy.get('.Select__control').first().should('be.visible');
|
||||
cy.get('.Select__control').first().click({ force: true });
|
||||
cy.get('.Select__control input[type=text]')
|
||||
.first()
|
||||
.should('be.visible')
|
||||
.type('South Asia{enter}', { force: true });
|
||||
|
||||
// send new query from same tab
|
||||
|
@ -182,8 +183,11 @@ export default () =>
|
|||
.find('ul.nav.nav-tabs li')
|
||||
.first()
|
||||
.click();
|
||||
cy.get('.tab-content ul.nav.nav-tabs li').first().click();
|
||||
cy.get('span.Select-clear').click();
|
||||
cy.get('.tab-content ul.nav.nav-tabs li')
|
||||
.first()
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.get('.Select__clear-indicator').click();
|
||||
|
||||
// trigger 1 new query
|
||||
cy.wait('@treemapRequest');
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { FORM_DATA_DEFAULTS, NUM_METRIC } from './visualizations/shared.helper';
|
||||
import readResponseBlob from '../../utils/readResponseBlob';
|
||||
|
||||
describe('No Results', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -39,7 +38,6 @@ describe('No Results', () => {
|
|||
comparator: ['Fake State'],
|
||||
clause: 'WHERE',
|
||||
sqlExpression: null,
|
||||
fromFormData: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -32,9 +32,8 @@ describe('Groupby', () => {
|
|||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||
|
||||
cy.get('[data-test=groupby]').within(() => {
|
||||
cy.get('.Select-control').click();
|
||||
cy.get('input.select-input').type('state', { force: true });
|
||||
cy.get('.VirtualizedSelectFocusedOption').click();
|
||||
cy.get('.Select__control').click();
|
||||
cy.get('input[type=text]').type('state{enter}');
|
||||
});
|
||||
cy.get('button.query').click();
|
||||
cy.verifySliceSuccess({ waitAlias: '@postJson', chartSelector: 'svg' });
|
||||
|
@ -50,16 +49,16 @@ describe('AdhocMetrics', () => {
|
|||
});
|
||||
|
||||
it('Clear metric and set simple adhoc metric', () => {
|
||||
const metric = 'sum(sum_girls)';
|
||||
const metricName = 'Girl Births';
|
||||
|
||||
cy.visitChartByName('Num Births Trend');
|
||||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||
|
||||
cy.get('[data-test=metrics]').within(() => {
|
||||
cy.get('.select-clear').click();
|
||||
cy.get('.Select-control').click({ force: true });
|
||||
cy.get('input').type('sum_girls', { force: true });
|
||||
cy.get('.VirtualizedSelectFocusedOption').trigger('mousedown').click();
|
||||
cy.get('.Select__clear-indicator').click();
|
||||
cy.get('.Select__control input').type('sum_girls');
|
||||
cy.get('.Select__option--is-focused').trigger('mousedown').click();
|
||||
});
|
||||
|
||||
cy.get('#metrics-edit-popover').within(() => {
|
||||
|
@ -69,30 +68,29 @@ describe('AdhocMetrics', () => {
|
|||
});
|
||||
cy.get('button').contains('Save').click();
|
||||
});
|
||||
cy.get('.Select__multi-value__label').contains(metricName);
|
||||
|
||||
cy.get('button.query').click();
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@postJson',
|
||||
querySubstring: metricName,
|
||||
querySubstring: `${metric} AS "${metricName}"`, // SQL statement
|
||||
chartSelector: 'svg',
|
||||
});
|
||||
});
|
||||
|
||||
it('Clear metric and set custom sql adhoc metric', () => {
|
||||
const metric = 'SUM(num)/COUNT(DISTINCT name)';
|
||||
|
||||
it('Switch from simple to custom sql', () => {
|
||||
cy.visitChartByName('Num Births Trend');
|
||||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||
|
||||
// select column "num"
|
||||
cy.get('[data-test=metrics]').within(() => {
|
||||
cy.get('.select-clear').click();
|
||||
cy.get('.Select-control').click({ force: true });
|
||||
cy.get('input').type('num', { force: true });
|
||||
cy.get('.VirtualizedSelectOption[data-test=_col_num]')
|
||||
.trigger('mousedown')
|
||||
.click();
|
||||
cy.get('.Select__clear-indicator').click();
|
||||
cy.get('.Select__control').click();
|
||||
cy.get('.Select__control input').type('num');
|
||||
cy.get('.option-label').contains(/^num$/).click();
|
||||
});
|
||||
|
||||
// add custom SQL
|
||||
cy.get('#metrics-edit-popover').within(() => {
|
||||
cy.get('#adhoc-metric-edit-tabs-tab-SQL').click();
|
||||
cy.get('.ace_content').click();
|
||||
|
@ -101,39 +99,56 @@ describe('AdhocMetrics', () => {
|
|||
});
|
||||
|
||||
cy.get('button.query').click();
|
||||
|
||||
const metric = 'SUM(num)/COUNT(DISTINCT name)';
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@postJson',
|
||||
querySubstring: metric,
|
||||
querySubstring: `${metric} AS "${metric}"`,
|
||||
chartSelector: 'svg',
|
||||
});
|
||||
});
|
||||
|
||||
it('Switch between simple and custom sql tabs', () => {
|
||||
cy.visitChartByName('Num Births Trend');
|
||||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||
|
||||
it('Switch from custom sql tabs to simple', () => {
|
||||
cy.get('[data-test=metrics]').within(() => {
|
||||
cy.get('.select-clear').click();
|
||||
cy.get('.Select-control').click({ force: true });
|
||||
cy.get('input').type('sum_girls', { force: true });
|
||||
cy.get('.VirtualizedSelectFocusedOption').trigger('mousedown').click();
|
||||
cy.get('.Select__dropdown-indicator').click();
|
||||
cy.get('input[type=text]').type('sum_girls{enter}');
|
||||
});
|
||||
|
||||
cy.get('#metrics-edit-popover').within(() => {
|
||||
cy.get('#adhoc-metric-edit-tabs-tab-SQL').click();
|
||||
cy.get('.ace_identifier').contains('sum_girls');
|
||||
cy.get('.ace_content').click();
|
||||
cy.get('.ace_text-input').type('{selectall}{backspace}SUM(num)', {
|
||||
force: true,
|
||||
});
|
||||
cy.get('.ace_text-input').type('{selectall}{backspace}SUM(num)');
|
||||
cy.get('#adhoc-metric-edit-tabs-tab-SIMPLE').click();
|
||||
cy.get('.select-value-label').contains('num');
|
||||
cy.get('.Select__single-value').contains(/^num$/);
|
||||
cy.get('button').contains('Save').click();
|
||||
});
|
||||
|
||||
cy.get('button.query').click();
|
||||
|
||||
const metric = 'SUM(num)';
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@postJson',
|
||||
querySubstring: `${metric} AS "${metric}"`,
|
||||
chartSelector: 'svg',
|
||||
});
|
||||
});
|
||||
|
||||
it('Typing starts with aggregate function name', () => {
|
||||
// select column "num"
|
||||
cy.get('[data-test=metrics]').within(() => {
|
||||
cy.get('.Select__dropdown-indicator').click();
|
||||
cy.get('.Select__control input[type=text]').type('avg(');
|
||||
cy.get('.Select__option').contains('ds');
|
||||
cy.get('.Select__option').contains('name');
|
||||
cy.get('.Select__option').contains('sum_boys').click();
|
||||
});
|
||||
|
||||
const metric = 'AVG(sum_boys)';
|
||||
cy.get('button.query').click();
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@postJson',
|
||||
querySubstring: `${metric} AS "${metric}"`,
|
||||
chartSelector: 'svg',
|
||||
});
|
||||
});
|
||||
|
@ -152,16 +167,13 @@ describe('AdhocFilters', () => {
|
|||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||
|
||||
cy.get('[data-test=adhoc_filters]').within(() => {
|
||||
cy.get('.Select-control').click({ force: true });
|
||||
cy.get('input').type('name', { force: true });
|
||||
cy.get('.VirtualizedSelectFocusedOption').trigger('mousedown').click();
|
||||
cy.get('.Select__control').click();
|
||||
cy.get('input[type=text]').type('name{enter}');
|
||||
});
|
||||
cy.get('.adhoc-filter-option').click({ force: true });
|
||||
cy.get('#filter-edit-popover').within(() => {
|
||||
cy.get('[data-test=adhoc-filter-simple-value]').within(() => {
|
||||
cy.get('div.select-input').click({ force: true });
|
||||
cy.get('input.select-input').type('Amy', { force: true });
|
||||
cy.get('.VirtualizedSelectFocusedOption').trigger('mousedown').click();
|
||||
cy.get('.Select__control').click();
|
||||
cy.get('input[type=text]').type('Any{enter}');
|
||||
});
|
||||
cy.get('button').contains('Save').click();
|
||||
});
|
||||
|
@ -178,16 +190,14 @@ describe('AdhocFilters', () => {
|
|||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||
|
||||
cy.get('[data-test=adhoc_filters]').within(() => {
|
||||
cy.get('.Select-control').click({ force: true });
|
||||
cy.get('input').type('name', { force: true });
|
||||
cy.get('.VirtualizedSelectFocusedOption').trigger('mousedown').click();
|
||||
cy.get('.Select__control').click();
|
||||
cy.get('input[type=text]').type('name{enter}');
|
||||
});
|
||||
|
||||
cy.get('.adhoc-filter-option').click({ force: true });
|
||||
cy.get('#filter-edit-popover').within(() => {
|
||||
cy.get('#adhoc-filter-edit-tabs-tab-SQL').click();
|
||||
cy.get('.ace_content').click();
|
||||
cy.get('.ace_text-input').type("'Amy' OR name = 'Bob'", { force: true });
|
||||
cy.get('.ace_text-input').type("'Amy' OR name = 'Bob'");
|
||||
cy.get('button').contains('Save').click();
|
||||
});
|
||||
|
||||
|
@ -211,17 +221,15 @@ describe('Advanced analytics', () => {
|
|||
cy.visitChartByName('Num Births Trend');
|
||||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||
|
||||
cy.get('span')
|
||||
.contains('Advanced Analytics')
|
||||
.parent()
|
||||
.siblings()
|
||||
.first()
|
||||
.click();
|
||||
cy.get('.panel-title').contains('Advanced Analytics').click();
|
||||
|
||||
cy.get('[data-test=time_compare]').within(() => {
|
||||
cy.get('.Select-control').click({ force: true });
|
||||
cy.get('input').type('364 days', { force: true });
|
||||
cy.get('.VirtualizedSelectOption').trigger('mousedown').click();
|
||||
cy.get('.Select__control').click();
|
||||
cy.get('input[type=text]').type('28 days{enter}');
|
||||
|
||||
cy.get('.Select__control').click();
|
||||
cy.get('input[type=text]').type('364 days{enter}');
|
||||
cy.get('.Select__multi-value__label').contains('364 days');
|
||||
});
|
||||
|
||||
cy.get('button.query').click();
|
||||
|
@ -233,7 +241,8 @@ describe('Advanced analytics', () => {
|
|||
});
|
||||
|
||||
cy.get('[data-test=time_compare]').within(() => {
|
||||
cy.get('.select-value-label').contains('364 days');
|
||||
cy.get('.Select__multi-value__label').contains('364 days');
|
||||
cy.get('.Select__multi-value__label').contains('28 days');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -105,7 +105,7 @@ describe('Test explore links', () => {
|
|||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||
|
||||
cy.get('[data-test=groupby]').within(() => {
|
||||
cy.get('span.select-clear-zone').click();
|
||||
cy.get('.Select__clear-indicator').click();
|
||||
});
|
||||
cy.get('button[data-target="#save_modal"]').click();
|
||||
cy.get('.modal-content').within(() => {
|
||||
|
@ -149,11 +149,11 @@ describe('Test explore links', () => {
|
|||
cy.get('.modal-content').within(() => {
|
||||
cy.get('input[name=new_slice_name]').type(chartName);
|
||||
cy.get('input[data-test=add-to-existing-dashboard]').check();
|
||||
cy.get('.select.save-modal-selector')
|
||||
cy.get('.save-modal-selector')
|
||||
.click()
|
||||
.within(() => {
|
||||
cy.get('input').type(dashboardTitle, { force: true });
|
||||
cy.get('.select-option.is-focused').trigger('mousedown');
|
||||
cy.get('input').type(dashboardTitle);
|
||||
cy.get('.Select__option--is-focused').trigger('mousedown');
|
||||
});
|
||||
cy.get('button#btn_modal_save').click();
|
||||
});
|
||||
|
|
|
@ -91,7 +91,6 @@ export default () =>
|
|||
comparator: ['South Asia', 'North America'],
|
||||
clause: 'WHERE',
|
||||
sqlExpression: null,
|
||||
fromFormData: true,
|
||||
filterOptionName: 'filter_txje2ikiv6_wxmn0qwd1xo',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -53,7 +53,6 @@ export default () =>
|
|||
comparator: ['Aaron', 'Amy', 'Andrea'],
|
||||
clause: 'WHERE',
|
||||
sqlExpression: null,
|
||||
fromFormData: true,
|
||||
filterOptionName: 'filter_4y6teao56zs_ebjsvwy48c',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -63,7 +63,6 @@ export default () =>
|
|||
comparator: 'South Asia',
|
||||
clause: 'WHERE',
|
||||
sqlExpression: null,
|
||||
fromFormData: true,
|
||||
filterOptionName: 'filter_8aqxcf5co1a_x7lm2d1fq0l',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -74,7 +74,6 @@ export default () =>
|
|||
comparator: 'South+Asia',
|
||||
clause: 'WHERE',
|
||||
sqlExpression: null,
|
||||
fromFormData: true,
|
||||
filterOptionName: 'filter_b2tfg1rs8y_8kmrcyxvsqd',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -82,7 +82,6 @@ export default () =>
|
|||
comparator: 'boy',
|
||||
clause: 'WHERE',
|
||||
sqlExpression: null,
|
||||
fromFormData: true,
|
||||
filterOptionName: 'filter_tqx1en70hh_7nksse7nqic',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -62,7 +62,6 @@ export default () =>
|
|||
comparator: 'girl',
|
||||
clause: 'WHERE',
|
||||
sqlExpression: null,
|
||||
fromFormData: true,
|
||||
filterOptionName: 'filter_1ep6q50g8vk_48jj6qxdems',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -72,7 +72,6 @@ export default () =>
|
|||
comparator: 'boy',
|
||||
clause: 'WHERE',
|
||||
sqlExpression: null,
|
||||
fromFormData: true,
|
||||
filterOptionName: 'filter_tqx1en70hh_7nksse7nqic',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -67,7 +67,6 @@ export default () =>
|
|||
comparator: 'boy',
|
||||
clause: 'WHERE',
|
||||
sqlExpression: null,
|
||||
fromFormData: true,
|
||||
filterOptionName: 'filter_tqx1en70hh_7nksse7nqic',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -50,7 +50,6 @@ export default () =>
|
|||
},
|
||||
aggregate: 'SUM',
|
||||
hasCustomLabel: false,
|
||||
fromFormData: false,
|
||||
label: 'SUM(sum_boys)',
|
||||
optionName: 'metric_gvpdjt0v2qf_6hkf56o012',
|
||||
};
|
||||
|
|
|
@ -60,7 +60,6 @@ export default () =>
|
|||
subject: null,
|
||||
operator: null,
|
||||
comparator: null,
|
||||
fromFormData: false,
|
||||
filterOptionName: 'filter_jbdwe0hayaj_h9jfer8fy58',
|
||||
},
|
||||
{
|
||||
|
@ -70,7 +69,6 @@ export default () =>
|
|||
comparator: 'Energy',
|
||||
clause: 'WHERE',
|
||||
sqlExpression: null,
|
||||
fromFormData: true,
|
||||
filterOptionName: 'filter_8e0otka9uif_vmqri4gmbqc',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -59,7 +59,6 @@ export const NUM_METRIC = {
|
|||
aggregate: 'SUM',
|
||||
sqlExpression: null,
|
||||
hasCustomLabel: false,
|
||||
fromFormData: false,
|
||||
label: 'Sum(num)',
|
||||
optionName: 'metric_1de0s4viy5d_ly7y8k6ghvk',
|
||||
};
|
||||
|
@ -71,6 +70,5 @@ export const SIMPLE_FILTER = {
|
|||
comparator: ['Aaron', 'Amy', 'Andrea'],
|
||||
clause: 'WHERE',
|
||||
sqlExpression: null,
|
||||
fromFormData: true,
|
||||
filterOptionName: 'filter_4y6teao56zs_ebjsvwy48c',
|
||||
};
|
||||
|
|
|
@ -76,7 +76,6 @@ export default () =>
|
|||
comparator: ['South Asia', 'North America'],
|
||||
clause: 'WHERE',
|
||||
sqlExpression: null,
|
||||
fromFormData: true,
|
||||
filterOptionName: 'filter_txje2ikiv6_wxmn0qwd1xo',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -152,7 +152,6 @@ export default () =>
|
|||
column: null,
|
||||
aggregate: null,
|
||||
hasCustomLabel: true,
|
||||
fromFormData: true,
|
||||
label: '%+Girls',
|
||||
optionName: 'metric_6qwzgc8bh2v_zox7hil1mzs',
|
||||
};
|
||||
|
|
|
@ -70,7 +70,6 @@ export default () =>
|
|||
comparator: 'South Asia',
|
||||
clause: 'WHERE',
|
||||
sqlExpression: null,
|
||||
fromFormData: true,
|
||||
filterOptionName: 'filter_8aqxcf5co1a_x7lm2d1fq0l',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -63,7 +63,6 @@ export default () =>
|
|||
comparator: 'South Asia',
|
||||
clause: 'WHERE',
|
||||
sqlExpression: null,
|
||||
fromFormData: true,
|
||||
filterOptionName: 'filter_8aqxcf5co1a_x7lm2d1fq0l',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -4465,6 +4465,18 @@
|
|||
"requires": {
|
||||
"prop-types": "^15.5.10",
|
||||
"react-select": "^1.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-select": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-select/-/react-select-1.3.0.tgz",
|
||||
"integrity": "sha512-g/QAU1HZrzSfxkwMAo/wzi6/ezdWye302RGZevsATec07hI/iSxcpB1hejFIp7V63DJ8mwuign6KmB3VjdlinQ==",
|
||||
"requires": {
|
||||
"classnames": "^2.2.4",
|
||||
"prop-types": "^15.5.8",
|
||||
"react-input-autosize": "^2.1.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@data-ui/histogram": {
|
||||
|
@ -7508,9 +7520,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "16.9.23",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.23.tgz",
|
||||
"integrity": "sha512-SsGVT4E7L2wLN3tPYLiF20hmZTPGuzaayVunfgXzUn1x4uHVsKH6QDJQ/TdpHqwsTLd4CwrmQ2vOgxN7gE24gw==",
|
||||
"version": "16.9.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.34.tgz",
|
||||
"integrity": "sha512-8AJlYMOfPe1KGLKyHpflCg5z46n0b5DbRfqDksxBLBTUpB75ypDBAO9eCUcjNwE6LCUslwTz00yyG/X9gaVtow==",
|
||||
"requires": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^2.2.0"
|
||||
|
@ -7525,10 +7537,9 @@
|
|||
}
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"version": "16.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.5.tgz",
|
||||
"integrity": "sha512-BX6RQ8s9D+2/gDhxrj8OW+YD4R+8hj7FEM/OJHGNR0KipE1h1mSsf39YeyC81qafkq+N3rU3h3RFbLSwE5VqUg==",
|
||||
"dev": true,
|
||||
"version": "16.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.7.tgz",
|
||||
"integrity": "sha512-GHTYhM8/OwUCf254WO5xqR/aqD3gC9kSTLpopWGpQLpnw23jk44RvMHsyUSEplvRJZdHxhJGMMLF0kCPYHPhQA==",
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
|
@ -7590,11 +7601,13 @@
|
|||
}
|
||||
},
|
||||
"@types/react-select": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-1.3.4.tgz",
|
||||
"integrity": "sha512-0BwjswNzKBszG5O4xq72W54NrrbmOZvJfaM/Dwru3F6DhvFO9nihMP1IRzXSOJ1qGRCS3VCu9FnBYJ+25lSldw==",
|
||||
"version": "3.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-3.0.12.tgz",
|
||||
"integrity": "sha512-3NVEc1sbaNtI1b06smzr9dlNKTkYWttL27CdEsorMvd2EgTOM/PJmrzkClaVQmBDg52MzQO05xVwNZruEUKpHw==",
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"@types/react-transition-group": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-table": {
|
||||
|
@ -7606,6 +7619,14 @@
|
|||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-transition-group": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.2.4.tgz",
|
||||
"integrity": "sha512-8DMUaDqh0S70TjkqU0DxOu80tFUiiaS9rxkWip/nb7gtvAsbqOXm02UCmR8zdcjWujgeYPiPNTVpVpKzUDotwA==",
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-ultimate-pagination": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-ultimate-pagination/-/react-ultimate-pagination-1.2.0.tgz",
|
||||
|
@ -7615,6 +7636,14 @@
|
|||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-window": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.2.tgz",
|
||||
"integrity": "sha512-gP1xam68Wc4ZTAee++zx6pTdDAH08rAkQrWm4B4F/y6hhmlT9Mgx2q8lTCXnrPHXsr15XjRN9+K2DLKcz44qEQ==",
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/rison": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/rison/-/rison-0.0.6.tgz",
|
||||
|
@ -8897,6 +8926,11 @@
|
|||
"es-abstract": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"array-move": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/array-move/-/array-move-2.2.1.tgz",
|
||||
"integrity": "sha512-qQpEHBnVT6HAFgEVUwRdHVd8TYJThrZIT5wSXpEUTPwBaYhPLclw12mEpyUvRWVdl1VwPOqnIy6LqTFN3cSeUQ=="
|
||||
},
|
||||
"array-union": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
|
||||
|
@ -20582,6 +20616,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"memoize-one": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz",
|
||||
"integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA=="
|
||||
},
|
||||
"memory-fs": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz",
|
||||
|
@ -24206,12 +24245,20 @@
|
|||
}
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
|
||||
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
|
||||
"version": "15.7.2",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
|
||||
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.3.1",
|
||||
"object-assign": "^4.1.1"
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.8.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"prop-types-exact": {
|
||||
|
@ -24686,9 +24733,9 @@
|
|||
}
|
||||
},
|
||||
"react-input-autosize": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.1.tgz",
|
||||
"integrity": "sha512-3+K4CD13iE4lQQ2WlF8PuV5htfmTRLH6MDnfndHM6LuBRszuXnuyIfE7nhSKt8AzRBZ50bu0sAhkNMeS5pxQQA==",
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.2.tgz",
|
||||
"integrity": "sha512-jQJgYCA3S0j+cuOwzuCd1OjmBmnZLdqQdiLKRYrsMMzbjUrVDS5RvJUDwJqA7sKuksDuzFtm6hZGKFu7Mjk5aw==",
|
||||
"requires": {
|
||||
"prop-types": "^15.5.8"
|
||||
}
|
||||
|
@ -25005,13 +25052,53 @@
|
|||
}
|
||||
},
|
||||
"react-select": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-select/-/react-select-1.2.1.tgz",
|
||||
"integrity": "sha512-vaCgT2bEl+uTyE/uKOEgzE5Dc/wLtzhnBvoHCeuLoJWc4WuadN6WQDhoL42DW+TziniZK2Gaqe/wUXydI3NSaQ==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-select/-/react-select-3.1.0.tgz",
|
||||
"integrity": "sha512-wBFVblBH1iuCBprtpyGtd1dGMadsG36W5/t2Aj8OE6WbByDg5jIFyT7X5gT+l0qmT5TqWhxX+VsKJvCEl2uL9g==",
|
||||
"requires": {
|
||||
"classnames": "^2.2.4",
|
||||
"prop-types": "^15.5.8",
|
||||
"react-input-autosize": "^2.1.2"
|
||||
"@babel/runtime": "^7.4.4",
|
||||
"@emotion/cache": "^10.0.9",
|
||||
"@emotion/core": "^10.0.9",
|
||||
"@emotion/css": "^10.0.9",
|
||||
"memoize-one": "^5.0.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"react-input-autosize": "^2.2.2",
|
||||
"react-transition-group": "^4.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.6.tgz",
|
||||
"integrity": "sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==",
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
},
|
||||
"dom-helpers": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz",
|
||||
"integrity": "sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^2.6.7"
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
|
||||
"integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.5",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
|
||||
"integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-select-fast-filter-options": {
|
||||
|
@ -25023,12 +25110,12 @@
|
|||
}
|
||||
},
|
||||
"react-sortable-hoc": {
|
||||
"version": "0.8.4",
|
||||
"resolved": "https://registry.npmjs.org/react-sortable-hoc/-/react-sortable-hoc-0.8.4.tgz",
|
||||
"integrity": "sha512-J9AFEQAJ7u2YWdVzkU5E3ewrG82xQ4xF1ZPrZYKliDwlVBDkmjri+iKFAEt6NCDIRiBZ4hiN5vzI8pwy/dGPHw==",
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-sortable-hoc/-/react-sortable-hoc-1.11.0.tgz",
|
||||
"integrity": "sha512-v1CDCvdfoR3zLGNp6qsBa4J1BWMEVH25+UKxF/RvQRh+mrB+emqtVHMgZ+WreUiKJoEaiwYoScaueIKhMVBHUg==",
|
||||
"requires": {
|
||||
"babel-runtime": "^6.11.6",
|
||||
"invariant": "^2.2.1",
|
||||
"@babel/runtime": "^7.2.0",
|
||||
"invariant": "^2.2.4",
|
||||
"prop-types": "^15.5.7"
|
||||
}
|
||||
},
|
||||
|
@ -25155,6 +25242,27 @@
|
|||
"prop-types": "^15.5.8",
|
||||
"react-select": "^1.0.0-rc.2",
|
||||
"react-virtualized": "^9.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-select": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-select/-/react-select-1.3.0.tgz",
|
||||
"integrity": "sha512-g/QAU1HZrzSfxkwMAo/wzi6/ezdWye302RGZevsATec07hI/iSxcpB1hejFIp7V63DJ8mwuign6KmB3VjdlinQ==",
|
||||
"requires": {
|
||||
"classnames": "^2.2.4",
|
||||
"prop-types": "^15.5.8",
|
||||
"react-input-autosize": "^2.1.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-window": {
|
||||
"version": "1.8.5",
|
||||
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.5.tgz",
|
||||
"integrity": "sha512-HeTwlNa37AFa8MDZFZOKcNEkuF2YflA0hpGPiTT9vR7OawEt+GZbfM6wqkBahD3D3pUjIabQYzsnY/BSJbgq6Q==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"memoize-one": ">=3.1.1 <6"
|
||||
}
|
||||
},
|
||||
"react-with-styles": {
|
||||
|
|
|
@ -99,12 +99,15 @@
|
|||
"@superset-ui/translation": "^0.13.3",
|
||||
"@superset-ui/validator": "^0.13.3",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/react-bootstrap": "^0.32.21",
|
||||
"@types/react-json-tree": "^0.6.11",
|
||||
"@types/react-select": "^1.2.1",
|
||||
"@types/react-select": "^3.0.12",
|
||||
"@types/react-window": "^1.8.2",
|
||||
"@types/rison": "0.0.6",
|
||||
"@vx/responsive": "^0.0.195",
|
||||
"abortcontroller-polyfill": "^1.1.9",
|
||||
"aphrodite": "^2.3.1",
|
||||
"array-move": "^2.2.1",
|
||||
"bootstrap": "^3.4.1",
|
||||
"bootstrap-slider": "^10.0.0",
|
||||
"brace": "^0.11.1",
|
||||
|
@ -130,7 +133,7 @@
|
|||
"mousetrap": "^1.6.1",
|
||||
"mustache": "^2.2.1",
|
||||
"omnibar": "^2.1.1",
|
||||
"prop-types": "^15.6.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"re-resizable": "^4.3.1",
|
||||
"react": "^16.13.0",
|
||||
"react-ace": "^5.10.0",
|
||||
|
@ -151,9 +154,9 @@
|
|||
"react-redux": "^5.0.2",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-search-input": "^0.11.3",
|
||||
"react-select": "1.2.1",
|
||||
"react-select": "^3.1.0",
|
||||
"react-select-fast-filter-options": "^0.2.1",
|
||||
"react-sortable-hoc": "^0.8.3",
|
||||
"react-sortable-hoc": "^1.11.0",
|
||||
"react-split": "^2.0.4",
|
||||
"react-sticky": "^6.0.2",
|
||||
"react-syntax-highlighter": "^7.0.4",
|
||||
|
@ -161,7 +164,9 @@
|
|||
"react-transition-group": "^2.5.3",
|
||||
"react-ultimate-pagination": "^1.2.0",
|
||||
"react-virtualized": "9.19.1",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"react-virtualized-select": "^3.1.3",
|
||||
"react-window": "^1.8.5",
|
||||
"reactable-arc": "0.14.42",
|
||||
"redux": "^3.5.2",
|
||||
"redux-localstorage": "^0.4.1",
|
||||
|
@ -192,8 +197,8 @@
|
|||
"@types/dom-to-image": "^2.6.0",
|
||||
"@types/jest": "^25.1.4",
|
||||
"@types/jquery": "^3.3.32",
|
||||
"@types/react": "^16.9.23",
|
||||
"@types/react-dom": "^16.9.5",
|
||||
"@types/react": "^16.9.34",
|
||||
"@types/react-dom": "^16.9.6",
|
||||
"@types/react-json-tree": "^0.6.11",
|
||||
"@types/react-redux": "^7.1.7",
|
||||
"@types/react-table": "^7.0.2",
|
||||
|
|
|
@ -19,8 +19,7 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import Select from 'react-virtualized-select';
|
||||
|
||||
import Select from 'src/components/Select';
|
||||
import AddSliceContainer from 'src/addSlice/AddSliceContainer';
|
||||
import VizTypeControl from 'src/explore/components/controls/VizTypeControl';
|
||||
|
||||
|
|
|
@ -17,10 +17,9 @@
|
|||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { shallow } from 'enzyme';
|
||||
import fetchMock from 'fetch-mock';
|
||||
|
||||
import Select from 'src/components/Select';
|
||||
import AsyncSelect from 'src/components/AsyncSelect';
|
||||
|
||||
describe('AsyncSelect', () => {
|
||||
|
|
|
@ -20,7 +20,7 @@ import React from 'react';
|
|||
import { mount, shallow } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { MenuItem, Pagination } from 'react-bootstrap';
|
||||
import Select from 'react-select';
|
||||
import Select from 'src/components/Select';
|
||||
import { QueryParamProvider } from 'use-query-params';
|
||||
|
||||
import ListView from 'src/components/ListView/ListView';
|
||||
|
|
|
@ -20,14 +20,16 @@
|
|||
import React from 'react';
|
||||
import sinon from 'sinon';
|
||||
import { shallow } from 'enzyme';
|
||||
import VirtualizedSelect from 'react-virtualized-select';
|
||||
import Select, { Creatable } from 'react-select';
|
||||
|
||||
import OnPasteSelect from 'src/components/OnPasteSelect';
|
||||
import {
|
||||
Select,
|
||||
AsyncSelect,
|
||||
OnPasteSelect,
|
||||
CreatableSelect,
|
||||
} from 'src/components/Select';
|
||||
|
||||
const defaultProps = {
|
||||
onChange: sinon.spy(),
|
||||
multi: true,
|
||||
isMulti: true,
|
||||
isValidNewOption: sinon.spy(s => !!s.label),
|
||||
value: [],
|
||||
options: [
|
||||
|
@ -60,17 +62,16 @@ describe('OnPasteSelect', () => {
|
|||
});
|
||||
|
||||
it('renders the supplied selectWrap component', () => {
|
||||
const select = wrapper.find(Select);
|
||||
const select = wrapper.findWhere(x => x.type() === Select);
|
||||
expect(select).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders custom selectWrap components', () => {
|
||||
props.selectWrap = Creatable;
|
||||
props.selectWrap = CreatableSelect;
|
||||
wrapper = shallow(<OnPasteSelect {...props} />);
|
||||
expect(wrapper.find(Creatable)).toHaveLength(1);
|
||||
props.selectWrap = VirtualizedSelect;
|
||||
wrapper = shallow(<OnPasteSelect {...props} />);
|
||||
expect(wrapper.find(VirtualizedSelect)).toHaveLength(1);
|
||||
expect(wrapper.findWhere(x => x.type() === CreatableSelect)).toHaveLength(
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
describe('onPaste', () => {
|
||||
|
|
|
@ -1,120 +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.
|
||||
*/
|
||||
/* eslint-disable no-unused-expressions */
|
||||
import React from 'react';
|
||||
import sinon from 'sinon';
|
||||
import PropTypes from 'prop-types';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import VirtualizedRendererWrap from 'src/components/VirtualizedRendererWrap';
|
||||
|
||||
const defaultProps = {
|
||||
focusedOption: { label: 'focusedOn', value: 'focusedOn' },
|
||||
focusOption: sinon.spy(),
|
||||
key: 'key1',
|
||||
option: { label: 'option1', value: 'option1' },
|
||||
selectValue: sinon.spy(),
|
||||
valueArray: [],
|
||||
};
|
||||
|
||||
function TestOption({ option }) {
|
||||
return <span>{option.label}</span>;
|
||||
}
|
||||
TestOption.propTypes = {
|
||||
option: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const defaultRenderer = opt => <TestOption option={opt} />;
|
||||
const RendererWrap = VirtualizedRendererWrap(defaultRenderer);
|
||||
|
||||
describe('VirtualizedRendererWrap', () => {
|
||||
let wrapper;
|
||||
let props;
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<RendererWrap {...defaultProps} />);
|
||||
props = { ...defaultProps };
|
||||
});
|
||||
|
||||
it('uses the provided renderer', () => {
|
||||
const option = wrapper.find(TestOption);
|
||||
expect(option).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders nothing when no option is provided', () => {
|
||||
props.option = null;
|
||||
wrapper = shallow(<RendererWrap {...props} />);
|
||||
const option = wrapper.find(TestOption);
|
||||
expect(option).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders unfocused, unselected options with the default class', () => {
|
||||
const optionDiv = wrapper.find('div');
|
||||
expect(optionDiv).toHaveLength(1);
|
||||
expect(optionDiv.props().className).toBe('VirtualizedSelectOption');
|
||||
});
|
||||
|
||||
it('renders focused option with the correct class', () => {
|
||||
props.option = props.focusedOption;
|
||||
wrapper = shallow(<RendererWrap {...props} />);
|
||||
const optionDiv = wrapper.find('div');
|
||||
expect(optionDiv.props().className).toBe(
|
||||
'VirtualizedSelectOption VirtualizedSelectFocusedOption',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders disabled option with the correct class', () => {
|
||||
props.option.disabled = true;
|
||||
wrapper = shallow(<RendererWrap {...props} />);
|
||||
const optionDiv = wrapper.find('div');
|
||||
expect(optionDiv.props().className).toBe(
|
||||
'VirtualizedSelectOption VirtualizedSelectDisabledOption',
|
||||
);
|
||||
props.option.disabled = false;
|
||||
});
|
||||
|
||||
it('renders selected option with the correct class', () => {
|
||||
props.valueArray = [props.option, props.focusedOption];
|
||||
wrapper = shallow(<RendererWrap {...props} />);
|
||||
const optionDiv = wrapper.find('div');
|
||||
expect(optionDiv.props().className).toBe(
|
||||
'VirtualizedSelectOption VirtualizedSelectSelectedOption',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders options with custom classes', () => {
|
||||
props.option.className = 'CustomClass';
|
||||
wrapper = shallow(<RendererWrap {...props} />);
|
||||
const optionDiv = wrapper.find('div');
|
||||
expect(optionDiv.props().className).toBe(
|
||||
'VirtualizedSelectOption CustomClass',
|
||||
);
|
||||
});
|
||||
|
||||
it('calls focusedOption on its own option onMouseEnter', () => {
|
||||
const optionDiv = wrapper.find('div');
|
||||
optionDiv.simulate('mouseEnter');
|
||||
expect(props.focusOption.calledWith(props.option)).toBe(true);
|
||||
});
|
||||
|
||||
it('calls selectValue on its own option onClick', () => {
|
||||
const optionDiv = wrapper.find('div');
|
||||
optionDiv.simulate('click');
|
||||
expect(props.selectValue.calledWith(props.option)).toBe(true);
|
||||
});
|
||||
});
|
|
@ -39,13 +39,14 @@ describe('AdhocFilter', () => {
|
|||
clause: CLAUSES.WHERE,
|
||||
filterOptionName: adhocFilter.filterOptionName,
|
||||
sqlExpression: null,
|
||||
fromFormData: false,
|
||||
isExtra: false,
|
||||
isNew: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('can create altered duplicates', () => {
|
||||
const adhocFilter1 = new AdhocFilter({
|
||||
isNew: true,
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: 'value',
|
||||
operator: '>',
|
||||
|
@ -61,6 +62,10 @@ describe('AdhocFilter', () => {
|
|||
|
||||
expect(adhocFilter1.operator).toBe('>');
|
||||
expect(adhocFilter2.operator).toBe('<');
|
||||
|
||||
// duplicated clone should not be new
|
||||
expect(adhocFilter1.isNew).toBe(true);
|
||||
expect(adhocFilter2.isNew).toStrictEqual(false);
|
||||
});
|
||||
|
||||
it('can verify equality', () => {
|
||||
|
|
|
@ -32,11 +32,11 @@ describe('AdhocMetric', () => {
|
|||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
column: valueColumn,
|
||||
aggregate: AGGREGATES.SUM,
|
||||
fromFormData: false,
|
||||
label: 'SUM(value)',
|
||||
hasCustomLabel: false,
|
||||
optionName: adhocMetric.optionName,
|
||||
sqlExpression: null,
|
||||
isNew: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -44,6 +44,7 @@ describe('AdhocMetric', () => {
|
|||
const adhocMetric1 = new AdhocMetric({
|
||||
column: valueColumn,
|
||||
aggregate: AGGREGATES.SUM,
|
||||
isNew: true,
|
||||
});
|
||||
const adhocMetric2 = adhocMetric1.duplicateWith({
|
||||
aggregate: AGGREGATES.AVG,
|
||||
|
@ -54,6 +55,10 @@ describe('AdhocMetric', () => {
|
|||
|
||||
expect(adhocMetric1.aggregate).toBe(AGGREGATES.SUM);
|
||||
expect(adhocMetric2.aggregate).toBe(AGGREGATES.AVG);
|
||||
|
||||
// duplicated clone should not be new
|
||||
expect(adhocMetric1.isNew).toBe(true);
|
||||
expect(adhocMetric2.isNew).toStrictEqual(false);
|
||||
});
|
||||
|
||||
it('can verify equality', () => {
|
||||
|
|
|
@ -21,6 +21,7 @@ import React from 'react';
|
|||
import sinon from 'sinon';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import OnPasteSelect from 'src/components/Select/OnPasteSelect';
|
||||
import AdhocFilter, {
|
||||
EXPRESSION_TYPES,
|
||||
CLAUSES,
|
||||
|
@ -28,7 +29,6 @@ import AdhocFilter, {
|
|||
import AdhocFilterControl from 'src/explore/components/controls/AdhocFilterControl';
|
||||
import AdhocMetric from 'src/explore/AdhocMetric';
|
||||
import { AGGREGATES, OPERATORS } from 'src/explore/constants';
|
||||
import OnPasteSelect from 'src/components/OnPasteSelect';
|
||||
|
||||
const simpleAdhocFilter = new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
|
|
|
@ -95,58 +95,50 @@ describe('AdhocFilterEditPopoverSimpleTabContent', () => {
|
|||
.instance()
|
||||
.onSubjectChange({ type: 'VARCHAR(255)', column_name: 'source' });
|
||||
expect(onChange.calledOnce).toBe(true);
|
||||
expect(
|
||||
onChange.lastCall.args[0].equals(
|
||||
simpleAdhocFilter.duplicateWith({ subject: 'source' }),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(onChange.lastCall.args[0]).toEqual(
|
||||
simpleAdhocFilter.duplicateWith({ subject: 'source' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('may alter the clause in onSubjectChange if the old clause is not appropriate', () => {
|
||||
const { wrapper, onChange } = setup();
|
||||
wrapper.instance().onSubjectChange(sumValueAdhocMetric);
|
||||
expect(onChange.calledOnce).toBe(true);
|
||||
expect(
|
||||
onChange.lastCall.args[0].equals(
|
||||
simpleAdhocFilter.duplicateWith({
|
||||
subject: sumValueAdhocMetric.label,
|
||||
clause: CLAUSES.HAVING,
|
||||
}),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(onChange.lastCall.args[0]).toEqual(
|
||||
simpleAdhocFilter.duplicateWith({
|
||||
subject: sumValueAdhocMetric.label,
|
||||
clause: CLAUSES.HAVING,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('will convert from individual comparator to array if the operator changes to multi', () => {
|
||||
const { wrapper, onChange } = setup();
|
||||
wrapper.instance().onOperatorChange({ operator: 'in' });
|
||||
wrapper.instance().onOperatorChange('in');
|
||||
expect(onChange.calledOnce).toBe(true);
|
||||
expect(onChange.lastCall.args[0].comparator).toHaveLength(1);
|
||||
expect(onChange.lastCall.args[0].comparator[0]).toBe('10');
|
||||
expect(onChange.lastCall.args[0].operator).toBe('in');
|
||||
expect(onChange.lastCall.args[0]).toEqual(
|
||||
simpleAdhocFilter.duplicateWith({ operator: 'in', comparator: ['10'] }),
|
||||
);
|
||||
});
|
||||
|
||||
it('will convert from array to individual comparators if the operator changes from multi', () => {
|
||||
const { wrapper, onChange } = setup({
|
||||
adhocFilter: simpleMultiAdhocFilter,
|
||||
});
|
||||
wrapper.instance().onOperatorChange({ operator: '<' });
|
||||
wrapper.instance().onOperatorChange('<');
|
||||
expect(onChange.calledOnce).toBe(true);
|
||||
expect(
|
||||
onChange.lastCall.args[0].equals(
|
||||
simpleAdhocFilter.duplicateWith({ operator: '<', comparator: '10' }),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(onChange.lastCall.args[0]).toEqual(
|
||||
simpleMultiAdhocFilter.duplicateWith({ operator: '<', comparator: '10' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('passes the new adhocFilter to onChange after onComparatorChange', () => {
|
||||
const { wrapper, onChange } = setup();
|
||||
wrapper.instance().onComparatorChange('20');
|
||||
expect(onChange.calledOnce).toBe(true);
|
||||
expect(
|
||||
onChange.lastCall.args[0].equals(
|
||||
simpleAdhocFilter.duplicateWith({ comparator: '20' }),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(onChange.lastCall.args[0]).toEqual(
|
||||
simpleAdhocFilter.duplicateWith({ comparator: '20' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('will filter operators for table datasources', () => {
|
||||
|
@ -195,20 +187,17 @@ describe('AdhocFilterEditPopoverSimpleTabContent', () => {
|
|||
partitionColumn: 'ds',
|
||||
});
|
||||
|
||||
wrapper.instance().onOperatorChange({ operator: 'LATEST PARTITION' });
|
||||
expect(
|
||||
onChange.lastCall.args[0].equals(
|
||||
testAdhocFilter.duplicateWith({
|
||||
subject: 'ds',
|
||||
operator: 'LATEST PARTITION',
|
||||
comparator: null,
|
||||
clause: 'WHERE',
|
||||
expressionType: 'SQL',
|
||||
sqlExpression:
|
||||
"ds = '{{ presto.latest_partition('schema.table1') }}' ",
|
||||
}),
|
||||
),
|
||||
).toBe(true);
|
||||
wrapper.instance().onOperatorChange('LATEST PARTITION');
|
||||
expect(onChange.lastCall.args[0]).toEqual(
|
||||
testAdhocFilter.duplicateWith({
|
||||
subject: 'ds',
|
||||
operator: 'LATEST PARTITION',
|
||||
comparator: null,
|
||||
clause: 'WHERE',
|
||||
expressionType: 'SQL',
|
||||
sqlExpression: "ds = '{{ presto.latest_partition('schema.table1') }}' ",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('expands when its multi comparator input field expands', () => {
|
||||
|
|
|
@ -52,7 +52,15 @@ function setup(overrides) {
|
|||
describe('AdhocFilterOption', () => {
|
||||
it('renders an overlay trigger wrapper for the label', () => {
|
||||
const { wrapper } = setup();
|
||||
expect(wrapper.find(OverlayTrigger)).toHaveLength(1);
|
||||
const overlay = wrapper.find(OverlayTrigger);
|
||||
expect(overlay).toHaveLength(1);
|
||||
expect(overlay.props().defaultOverlayShown).toBe(false);
|
||||
expect(wrapper.find(Label)).toHaveLength(1);
|
||||
});
|
||||
it('should open new filter popup by default', () => {
|
||||
const { wrapper } = setup({
|
||||
adhocFilter: simpleAdhocFilter.duplicateWith({ isNew: true }),
|
||||
});
|
||||
expect(wrapper.find(OverlayTrigger).props().defaultOverlayShown).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -75,7 +75,7 @@ describe('AdhocMetricEditPopover', () => {
|
|||
|
||||
it('overwrites the adhocMetric in state with onAggregateChange', () => {
|
||||
const { wrapper } = setup();
|
||||
wrapper.instance().onAggregateChange({ aggregate: AGGREGATES.AVG });
|
||||
wrapper.instance().onAggregateChange(AGGREGATES.AVG);
|
||||
expect(wrapper.state('adhocMetric')).toEqual(
|
||||
sumValueAdhocMetric.duplicateWith({ aggregate: AGGREGATES.AVG }),
|
||||
);
|
||||
|
|
|
@ -55,4 +55,11 @@ describe('AdhocMetricOption', () => {
|
|||
expect(wrapper.find(OverlayTrigger)).toHaveLength(1);
|
||||
expect(wrapper.find(Label)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('overlay should open if metric is new', () => {
|
||||
const { wrapper } = setup({
|
||||
adhocMetric: sumValueAdhocMetric.duplicateWith({ isNew: true }),
|
||||
});
|
||||
expect(wrapper.find(OverlayTrigger).props().defaultOverlayShown).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
/* eslint-disable no-unused-expressions */
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { Creatable } from 'react-select';
|
||||
import Creatable from 'react-select/creatable';
|
||||
import { getCategoricalSchemeRegistry } from '@superset-ui/color';
|
||||
|
||||
import ColorSchemeControl from 'src/explore/components/controls/ColorSchemeControl';
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { DisplayQueryButton } from 'src/explore/components/DisplayQueryButton';
|
||||
import ModalTrigger from 'src/components/ModalTrigger';
|
||||
import { DisplayQueryButton } from 'src/explore/components/DisplayQueryButton';
|
||||
|
||||
describe('DisplayQueryButton', () => {
|
||||
const defaultProps = {
|
||||
|
|
|
@ -23,7 +23,7 @@ import { shallow } from 'enzyme';
|
|||
|
||||
import MetricsControl from 'src/explore/components/controls/MetricsControl';
|
||||
import { AGGREGATES } from 'src/explore/constants';
|
||||
import OnPasteSelect from 'src/components/OnPasteSelect';
|
||||
import OnPasteSelect from 'src/components/Select/OnPasteSelect';
|
||||
import AdhocMetric, { EXPRESSION_TYPES } from 'src/explore/AdhocMetric';
|
||||
|
||||
const defaultProps = {
|
||||
|
@ -138,11 +138,11 @@ describe('MetricsControl', () => {
|
|||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
column: { type: 'double', column_name: 'value' },
|
||||
aggregate: AGGREGATES.SUM,
|
||||
fromFormData: true,
|
||||
label: 'SUM(value)',
|
||||
hasCustomLabel: false,
|
||||
optionName: 'blahblahblah',
|
||||
sqlExpression: null,
|
||||
isNew: false,
|
||||
},
|
||||
'avg__value',
|
||||
]);
|
||||
|
@ -163,7 +163,8 @@ describe('MetricsControl', () => {
|
|||
select.simulate('change', [valueColumn]);
|
||||
|
||||
const adhocMetric = onChange.lastCall.args[0][0];
|
||||
expect(adhocMetric instanceof AdhocMetric).toBe(true);
|
||||
expect(adhocMetric).toBeInstanceOf(AdhocMetric);
|
||||
expect(adhocMetric.isNew).toBe(true);
|
||||
expect(onChange.lastCall.args).toEqual([
|
||||
[
|
||||
{
|
||||
|
@ -171,35 +172,46 @@ describe('MetricsControl', () => {
|
|||
column: valueColumn,
|
||||
aggregate: AGGREGATES.SUM,
|
||||
label: 'SUM(value)',
|
||||
fromFormData: false,
|
||||
hasCustomLabel: false,
|
||||
optionName: adhocMetric.optionName,
|
||||
sqlExpression: null,
|
||||
isNew: true,
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles aggregates being selected', () => {
|
||||
it('handles aggregates being selected', done => {
|
||||
const { wrapper, onChange } = setup();
|
||||
const select = wrapper.find(OnPasteSelect);
|
||||
|
||||
// mock out the Select ref
|
||||
const setInputSpy = sinon.spy();
|
||||
const handleInputSpy = sinon.spy();
|
||||
wrapper.instance().select = {
|
||||
setInputValue: setInputSpy,
|
||||
handleInputChange: handleInputSpy,
|
||||
input: { input: {} },
|
||||
};
|
||||
const instance = wrapper.instance();
|
||||
const handleInputChangeSpy = jest.fn();
|
||||
const focusInputSpy = jest.fn();
|
||||
// simulate react-select StateManager
|
||||
instance.selectRef({
|
||||
select: {
|
||||
handleInputChange: handleInputChangeSpy,
|
||||
inputRef: { value: '' },
|
||||
focusInput: focusInputSpy,
|
||||
},
|
||||
});
|
||||
|
||||
select.simulate('change', [{ aggregate_name: 'SUM', optionName: 'SUM' }]);
|
||||
|
||||
expect(setInputSpy.calledWith('SUM()')).toBe(true);
|
||||
expect(handleInputSpy.calledWith({ target: { value: 'SUM()' } })).toBe(
|
||||
true,
|
||||
);
|
||||
expect(onChange.lastCall.args).toEqual([[]]);
|
||||
expect(instance.select.inputRef.value).toBe('SUM()');
|
||||
expect(handleInputChangeSpy).toHaveBeenCalledWith({
|
||||
currentTarget: { value: 'SUM()' },
|
||||
});
|
||||
expect(onChange.calledOnceWith([])).toBe(true);
|
||||
expect(focusInputSpy).toHaveBeenCalledTimes(0);
|
||||
setTimeout(() => {
|
||||
expect(focusInputSpy).toHaveBeenCalledTimes(1);
|
||||
expect(instance.select.inputRef.selectionStart).toBe(4);
|
||||
expect(instance.select.inputRef.selectionEnd).toBe(4);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves existing selected AdhocMetrics', () => {
|
||||
|
@ -255,9 +267,11 @@ describe('MetricsControl', () => {
|
|||
expect(
|
||||
!!wrapper.instance().selectFilterOption(
|
||||
{
|
||||
metric_name: 'a_metric',
|
||||
optionName: 'a_metric',
|
||||
expression: 'SUM(FANCY(metric))',
|
||||
data: {
|
||||
metric_name: 'a_metric',
|
||||
optionName: 'a_metric',
|
||||
expression: 'SUM(FANCY(metric))',
|
||||
},
|
||||
},
|
||||
'a',
|
||||
),
|
||||
|
@ -270,9 +284,11 @@ describe('MetricsControl', () => {
|
|||
expect(
|
||||
!!wrapper.instance().selectFilterOption(
|
||||
{
|
||||
metric_name: 'avg__metric',
|
||||
optionName: 'avg__metric',
|
||||
expression: 'AVG(metric)',
|
||||
data: {
|
||||
metric_name: 'avg__metric',
|
||||
optionName: 'avg__metric',
|
||||
expression: 'AVG(metric)',
|
||||
},
|
||||
},
|
||||
'a',
|
||||
),
|
||||
|
@ -285,9 +301,11 @@ describe('MetricsControl', () => {
|
|||
expect(
|
||||
!!wrapper.instance().selectFilterOption(
|
||||
{
|
||||
type: 'VARCHAR(255)',
|
||||
column_name: 'source',
|
||||
optionName: '_col_source',
|
||||
data: {
|
||||
type: 'VARCHAR(255)',
|
||||
column_name: 'source',
|
||||
optionName: '_col_source',
|
||||
},
|
||||
},
|
||||
'sou',
|
||||
),
|
||||
|
@ -297,7 +315,7 @@ describe('MetricsControl', () => {
|
|||
!!wrapper
|
||||
.instance()
|
||||
.selectFilterOption(
|
||||
{ aggregate_name: 'AVG', optionName: '_aggregate_AVG' },
|
||||
{ data: { aggregate_name: 'AVG', optionName: '_aggregate_AVG' } },
|
||||
'av',
|
||||
),
|
||||
).toBe(true);
|
||||
|
@ -309,9 +327,11 @@ describe('MetricsControl', () => {
|
|||
expect(
|
||||
!!wrapper.instance().selectFilterOption(
|
||||
{
|
||||
metric_name: 'sum__num',
|
||||
verbose_name: 'babies',
|
||||
optionName: '_col_sum_num',
|
||||
data: {
|
||||
metric_name: 'sum__num',
|
||||
verbose_name: 'babies',
|
||||
optionName: '_col_sum_num',
|
||||
},
|
||||
},
|
||||
'bab',
|
||||
),
|
||||
|
@ -324,9 +344,11 @@ describe('MetricsControl', () => {
|
|||
expect(
|
||||
!!wrapper.instance().selectFilterOption(
|
||||
{
|
||||
metric_name: 'avg__metric',
|
||||
optionName: 'avg__metric',
|
||||
expression: 'AVG(metric)',
|
||||
data: {
|
||||
metric_name: 'avg__metric',
|
||||
optionName: 'avg__metric',
|
||||
expression: 'AVG(metric)',
|
||||
},
|
||||
},
|
||||
'a',
|
||||
),
|
||||
|
@ -339,9 +361,11 @@ describe('MetricsControl', () => {
|
|||
expect(
|
||||
!!wrapper.instance().selectFilterOption(
|
||||
{
|
||||
metric_name: 'my_fancy_sum_metric',
|
||||
optionName: 'my_fancy_sum_metric',
|
||||
expression: 'SUM(value)',
|
||||
data: {
|
||||
metric_name: 'my_fancy_sum_metric',
|
||||
optionName: 'my_fancy_sum_metric',
|
||||
expression: 'SUM(value)',
|
||||
},
|
||||
},
|
||||
'sum',
|
||||
),
|
||||
|
@ -354,9 +378,11 @@ describe('MetricsControl', () => {
|
|||
expect(
|
||||
!!wrapper.instance().selectFilterOption(
|
||||
{
|
||||
metric_name: 'sum__value',
|
||||
optionName: 'sum__value',
|
||||
expression: 'SUM(value)',
|
||||
data: {
|
||||
metric_name: 'sum__value',
|
||||
optionName: 'sum__value',
|
||||
expression: 'SUM(value)',
|
||||
},
|
||||
},
|
||||
'sum',
|
||||
),
|
||||
|
@ -365,9 +391,11 @@ describe('MetricsControl', () => {
|
|||
expect(
|
||||
!!wrapper.instance().selectFilterOption(
|
||||
{
|
||||
metric_name: 'sum__value',
|
||||
optionName: 'sum__value',
|
||||
expression: 'SUM("table"."value")',
|
||||
data: {
|
||||
metric_name: 'sum__value',
|
||||
optionName: 'sum__value',
|
||||
expression: 'SUM("table"."value")',
|
||||
},
|
||||
},
|
||||
'sum',
|
||||
),
|
||||
|
@ -379,12 +407,12 @@ describe('MetricsControl', () => {
|
|||
wrapper.setState({ aggregateInInput: true });
|
||||
|
||||
expect(
|
||||
!!wrapper
|
||||
.instance()
|
||||
.selectFilterOption(
|
||||
{ metric_name: 'metric', expression: 'SUM(FANCY(metric))' },
|
||||
'SUM(',
|
||||
),
|
||||
!!wrapper.instance().selectFilterOption(
|
||||
{
|
||||
data: { metric_name: 'metric', expression: 'SUM(FANCY(metric))' },
|
||||
},
|
||||
'SUM(',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
|
@ -395,7 +423,10 @@ describe('MetricsControl', () => {
|
|||
expect(
|
||||
!!wrapper
|
||||
.instance()
|
||||
.selectFilterOption({ type: 'DOUBLE', column_name: 'value' }, 'SUM('),
|
||||
.selectFilterOption(
|
||||
{ data: { type: 'DOUBLE', column_name: 'value' } },
|
||||
'SUM(',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
|
|
|
@ -18,12 +18,10 @@
|
|||
*/
|
||||
/* eslint-disable no-unused-expressions */
|
||||
import React from 'react';
|
||||
import Select, { Creatable } from 'react-select';
|
||||
import VirtualizedSelect from 'react-virtualized-select';
|
||||
import sinon from 'sinon';
|
||||
import { shallow } from 'enzyme';
|
||||
import OnPasteSelect from 'src/components/OnPasteSelect';
|
||||
import VirtualizedRendererWrap from 'src/components/VirtualizedRendererWrap';
|
||||
import { Select, CreatableSelect } from 'src/components/Select';
|
||||
import OnPasteSelect from 'src/components/Select/OnPasteSelect';
|
||||
import SelectControl from 'src/explore/components/controls/SelectControl';
|
||||
|
||||
const defaultProps = {
|
||||
|
@ -47,16 +45,42 @@ describe('SelectControl', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<SelectControl {...defaultProps} />);
|
||||
wrapper.setProps(defaultProps);
|
||||
});
|
||||
|
||||
it('renders an OnPasteSelect', () => {
|
||||
it('renders with Select by default', () => {
|
||||
expect(wrapper.find(OnPasteSelect)).toHaveLength(0);
|
||||
expect(wrapper.findWhere(x => x.type() === Select)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders with OnPasteSelect when multi', () => {
|
||||
wrapper.setProps({ multi: true });
|
||||
expect(wrapper.find(OnPasteSelect)).toHaveLength(1);
|
||||
expect(wrapper.findWhere(x => x.type() === Select)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('calls onChange when toggled', () => {
|
||||
it('renders with Creatable when freeForm', () => {
|
||||
wrapper.setProps({ freeForm: true });
|
||||
expect(wrapper.find(OnPasteSelect)).toHaveLength(0);
|
||||
expect(wrapper.findWhere(x => x.type() === CreatableSelect)).toHaveLength(
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it('uses Select in onPasteSelect when freeForm=false', () => {
|
||||
wrapper = shallow(<SelectControl {...defaultProps} multi />);
|
||||
const select = wrapper.find(OnPasteSelect);
|
||||
select.simulate('change', { value: 50 });
|
||||
expect(select.props().selectWrap).toBe(Select);
|
||||
});
|
||||
|
||||
it('uses Creatable in onPasteSelect when freeForm=true', () => {
|
||||
wrapper = shallow(<SelectControl {...defaultProps} multi freeForm />);
|
||||
const select = wrapper.find(OnPasteSelect);
|
||||
expect(select.props().selectWrap).toBe(CreatableSelect);
|
||||
});
|
||||
|
||||
it('calls props.onChange when select', () => {
|
||||
const select = wrapper.instance();
|
||||
select.onChange({ value: 50 });
|
||||
expect(defaultProps.onChange.calledWith(50)).toBe(true);
|
||||
});
|
||||
|
||||
|
@ -72,36 +96,10 @@ describe('SelectControl', () => {
|
|||
onChange: sinon.spy(),
|
||||
};
|
||||
wrapper.setProps(selectAllProps);
|
||||
const select = wrapper.find(OnPasteSelect);
|
||||
select.simulate('change', [{ meta: true, value: 'Select All' }]);
|
||||
wrapper.instance().onChange([{ meta: true, value: 'Select All' }]);
|
||||
expect(selectAllProps.onChange.calledWith(expectedValues)).toBe(true);
|
||||
});
|
||||
|
||||
it('passes VirtualizedSelect as selectWrap', () => {
|
||||
const select = wrapper.find(OnPasteSelect);
|
||||
expect(select.props().selectWrap).toBe(VirtualizedSelect);
|
||||
});
|
||||
|
||||
it('passes Creatable as selectComponent when freeForm=true', () => {
|
||||
wrapper = shallow(<SelectControl {...defaultProps} freeForm />);
|
||||
const select = wrapper.find(OnPasteSelect);
|
||||
expect(select.props().selectComponent).toBe(Creatable);
|
||||
});
|
||||
|
||||
it('passes Select as selectComponent when freeForm=false', () => {
|
||||
const select = wrapper.find(OnPasteSelect);
|
||||
expect(select.props().selectComponent).toBe(Select);
|
||||
});
|
||||
|
||||
it('wraps optionRenderer in a VirtualizedRendererWrap', () => {
|
||||
const select = wrapper.find(OnPasteSelect);
|
||||
const defaultOptionRenderer = SelectControl.defaultProps.optionRenderer;
|
||||
const wrappedRenderer = VirtualizedRendererWrap(defaultOptionRenderer);
|
||||
expect(typeof select.props().optionRenderer).toBe('function');
|
||||
// different instances of wrapper with same inner renderer are unequal
|
||||
expect(select.props().optionRenderer.name).toBe(wrappedRenderer.name);
|
||||
});
|
||||
|
||||
describe('getOptions', () => {
|
||||
it('returns the correct options', () => {
|
||||
wrapper.setProps(defaultProps);
|
||||
|
@ -132,15 +130,18 @@ describe('SelectControl', () => {
|
|||
value: ['one', 'two'],
|
||||
name: 'row_limit',
|
||||
label: 'Row Limit',
|
||||
valueKey: 'value',
|
||||
valueKey: 'custom_value_key',
|
||||
onChange: sinon.spy(),
|
||||
};
|
||||
const newOptions = [
|
||||
{ value: 'one', label: 'one' },
|
||||
{ value: 'two', label: 'two' },
|
||||
// the last added option is at the top
|
||||
const expectedNewOptions = [
|
||||
{ custom_value_key: 'two', label: 'two' },
|
||||
{ custom_value_key: 'one', label: 'one' },
|
||||
];
|
||||
wrapper.setProps(freeFormProps);
|
||||
expect(wrapper.instance().getOptions(freeFormProps)).toEqual(newOptions);
|
||||
expect(wrapper.instance().getOptions(freeFormProps)).toEqual(
|
||||
expectedNewOptions,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -17,11 +17,11 @@
|
|||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { shallow } from 'enzyme';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import Select from 'src/components/Select';
|
||||
import QuerySearch from 'src/SqlLab/components/QuerySearch';
|
||||
|
||||
describe('QuerySearch', () => {
|
||||
|
@ -39,7 +39,7 @@ describe('QuerySearch', () => {
|
|||
});
|
||||
|
||||
it('should have three Select', () => {
|
||||
expect(wrapper.find(Select)).toHaveLength(3);
|
||||
expect(wrapper.findWhere(x => x.type() === Select)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('updates fromTime on user selects from time', () => {
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import Select from 'react-select';
|
||||
import Select from 'src/components/Select';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { SupersetClient } from '@superset-ui/connection';
|
||||
|
||||
|
|
|
@ -383,17 +383,17 @@ div.tablePopover {
|
|||
font-family: @font-family-monospace;
|
||||
}
|
||||
|
||||
.Select-menu-outer {
|
||||
.Select__menu-outer {
|
||||
min-width: 100%;
|
||||
width: inherit;
|
||||
z-index: @z-index-dropdown;
|
||||
}
|
||||
|
||||
.Select-clear {
|
||||
.Select__clear-indicator {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.Select-arrow {
|
||||
.Select__arrow {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Panel } from 'react-bootstrap';
|
||||
import Select from 'react-virtualized-select';
|
||||
import Select from 'src/components/Select';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import VizTypeControl from '../explore/components/controls/VizTypeControl';
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Select from 'react-select';
|
||||
// TODO: refactor this with `import { AsyncSelect } from src/components/Select`
|
||||
import { Select } from 'src/components/Select';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { SupersetClient } from '@superset-ui/connection';
|
||||
import getClientErrorObject from '../utils/getClientErrorObject';
|
||||
|
|
|
@ -42,14 +42,14 @@ export default function ColumnOption({ column, showType }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<div className="column-option">
|
||||
{showType && columnType && <ColumnTypeLabel type={columnType} />}
|
||||
<span className="m-r-5 option-label">
|
||||
<span className="option-label column-option-label">
|
||||
{column.verbose_name || column.column_name}
|
||||
</span>
|
||||
{column.description && (
|
||||
<InfoTooltipWithTrigger
|
||||
className="m-r-5 text-muted"
|
||||
className="text-muted"
|
||||
icon="info"
|
||||
tooltip={column.description}
|
||||
label={`descr-${column.column_name}`}
|
||||
|
@ -57,13 +57,13 @@ export default function ColumnOption({ column, showType }) {
|
|||
)}
|
||||
{hasExpression && (
|
||||
<InfoTooltipWithTrigger
|
||||
className="m-r-5 text-muted"
|
||||
className="text-muted"
|
||||
icon="question-circle-o"
|
||||
tooltip={column.expression}
|
||||
label={`expr-${column.column_name}`}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ColumnOption.propTypes = propTypes;
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
// @ts-ignore
|
||||
import { css } from '@emotion/core';
|
||||
import Button from 'src/components/Button';
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -16,19 +16,31 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import styled from '@superset-ui/style';
|
||||
import { withTheme } from 'emotion-theming';
|
||||
|
||||
import StyledSelect, { AsyncStyledSelect } from 'src/components/StyledSelect';
|
||||
import {
|
||||
Select,
|
||||
AsyncSelect,
|
||||
PartialThemeConfig,
|
||||
PartialStylesConfig,
|
||||
} from 'src/components/Select';
|
||||
import SearchInput from 'src/components/SearchInput';
|
||||
import { Filter, Filters, FilterValue, InternalFilter } from './types';
|
||||
import {
|
||||
Filter,
|
||||
Filters,
|
||||
FilterValue,
|
||||
InternalFilter,
|
||||
SelectOption,
|
||||
} from './types';
|
||||
|
||||
interface BaseFilter {
|
||||
Header: string;
|
||||
initialValue: any;
|
||||
}
|
||||
interface SelectFilterProps extends BaseFilter {
|
||||
name?: string;
|
||||
onSelect: (selected: any) => any;
|
||||
selects: Filter['selects'];
|
||||
emptyLabel?: string;
|
||||
|
@ -37,13 +49,38 @@ interface SelectFilterProps extends BaseFilter {
|
|||
|
||||
const FilterContainer = styled.div`
|
||||
display: inline-flex;
|
||||
margin-right: 8px;
|
||||
margin-right: 2em;
|
||||
`;
|
||||
|
||||
const Title = styled.span`
|
||||
const FilterTitle = styled.label`
|
||||
font-weight: bold;
|
||||
line-height: 27px;
|
||||
margin: 0 0.4em 0 0;
|
||||
`;
|
||||
|
||||
const filterSelectTheme: PartialThemeConfig = {
|
||||
spacing: {
|
||||
baseUnit: 2,
|
||||
minWidth: '5em',
|
||||
},
|
||||
};
|
||||
|
||||
const filterSelectStyles: PartialStylesConfig = {
|
||||
container: (provider, { getValue }) => ({
|
||||
...provider,
|
||||
// dynamic width based on label string length
|
||||
minWidth: `${Math.min(
|
||||
12,
|
||||
Math.max(5, 3 + getValue()[0].label.length / 2),
|
||||
)}em`,
|
||||
}),
|
||||
control: provider => ({
|
||||
...provider,
|
||||
borderWidth: 0,
|
||||
boxShadow: 'none',
|
||||
}),
|
||||
};
|
||||
|
||||
const CLEAR_SELECT_FILTER_VALUE = 'CLEAR_SELECT_FILTER_VALUE';
|
||||
|
||||
function SelectFilter({
|
||||
|
@ -59,46 +96,60 @@ function SelectFilter({
|
|||
value: CLEAR_SELECT_FILTER_VALUE,
|
||||
};
|
||||
|
||||
const options = React.useMemo(() => [clearFilterSelect, ...selects], [
|
||||
emptyLabel,
|
||||
selects,
|
||||
]);
|
||||
|
||||
const [value, setValue] = useState(
|
||||
typeof initialValue === 'undefined'
|
||||
? clearFilterSelect.value
|
||||
: initialValue,
|
||||
const options = [clearFilterSelect, ...selects];
|
||||
const optionsCache: React.MutableRefObject<SelectOption[] | null> = useRef(
|
||||
null,
|
||||
);
|
||||
const onChange = (selected: { label: string; value: any } | null) => {
|
||||
|
||||
const [selectedOption, setSelectedOption] = useState(clearFilterSelect);
|
||||
const onChange = (selected: SelectOption | null) => {
|
||||
if (selected === null) return;
|
||||
setValue(selected.value);
|
||||
onSelect(
|
||||
selected.value === CLEAR_SELECT_FILTER_VALUE ? undefined : selected.value,
|
||||
);
|
||||
setSelectedOption(selected);
|
||||
};
|
||||
const fetchAndFormatSelects = async () => {
|
||||
if (!fetchSelects) return { options: [clearFilterSelect] };
|
||||
const selectValues = await fetchSelects();
|
||||
return { options: [clearFilterSelect, ...selectValues] };
|
||||
const fetchAndFormatSelects = async (inputValue: string) => {
|
||||
// only include clear filter when filter value exists
|
||||
let result = inputValue ? [] : [clearFilterSelect];
|
||||
// only call fetch once
|
||||
// TODO: allow real async search with `inputValue`
|
||||
if (optionsCache.current) return optionsCache.current;
|
||||
if (fetchSelects) {
|
||||
const selectValues = await fetchSelects();
|
||||
// update matching option at initial load
|
||||
const matchingOption = result.find(x => x.value === initialValue);
|
||||
if (matchingOption) {
|
||||
setSelectedOption(matchingOption);
|
||||
}
|
||||
result = [...result, ...selectValues];
|
||||
}
|
||||
optionsCache.current = result;
|
||||
return result;
|
||||
};
|
||||
|
||||
return (
|
||||
<FilterContainer>
|
||||
<Title>{Header}:</Title>
|
||||
<FilterTitle>{Header}</FilterTitle>
|
||||
{fetchSelects ? (
|
||||
<AsyncStyledSelect
|
||||
<AsyncSelect
|
||||
data-test="filters-select"
|
||||
value={value}
|
||||
themeConfig={filterSelectTheme}
|
||||
stylesConfig={filterSelectStyles}
|
||||
value={selectedOption}
|
||||
onChange={onChange}
|
||||
loadOptions={fetchAndFormatSelects}
|
||||
placeholder={initialValue || emptyLabel}
|
||||
loadingPlaceholder="Loading..."
|
||||
defaultOptions
|
||||
placeholder={emptyLabel}
|
||||
loadingMessage={() => 'Loading...'}
|
||||
clearable={false}
|
||||
/>
|
||||
) : (
|
||||
<StyledSelect
|
||||
<Select
|
||||
data-test="filters-select"
|
||||
value={value}
|
||||
themeConfig={filterSelectTheme}
|
||||
stylesConfig={filterSelectStyles}
|
||||
value={selectedOption}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
clearable={false}
|
||||
|
@ -155,13 +206,17 @@ function UIFilters({
|
|||
return (
|
||||
<FilterWrapper>
|
||||
{filters.map(
|
||||
({ Header, input, selects, unfilteredLabel, fetchSelects }, index) => {
|
||||
(
|
||||
{ Header, id, input, selects, unfilteredLabel, fetchSelects },
|
||||
index,
|
||||
) => {
|
||||
const initialValue =
|
||||
internalFilters[index] && internalFilters[index].value;
|
||||
if (input === 'select') {
|
||||
return (
|
||||
<SelectFilter
|
||||
key={Header}
|
||||
key={id}
|
||||
name={id}
|
||||
Header={Header}
|
||||
selects={selects}
|
||||
emptyLabel={unfilteredLabel}
|
||||
|
@ -174,7 +229,7 @@ function UIFilters({
|
|||
if (input === 'search') {
|
||||
return (
|
||||
<SearchFilter
|
||||
key={Header}
|
||||
key={id}
|
||||
Header={Header}
|
||||
initialValue={initialValue}
|
||||
onSubmit={(value: string) => updateFilterValue(index, value)}
|
||||
|
|
|
@ -25,13 +25,9 @@ import {
|
|||
FormControl,
|
||||
MenuItem,
|
||||
Row,
|
||||
// @ts-ignore
|
||||
} from 'react-bootstrap';
|
||||
// @ts-ignore
|
||||
import SelectComponent from 'react-select';
|
||||
// @ts-ignore
|
||||
import VirtualizedSelect from 'react-virtualized-select';
|
||||
import { Filters, InternalFilter, Select } from './types';
|
||||
import { Select } from 'src/components/Select';
|
||||
import { Filters, InternalFilter, SelectOption } from './types';
|
||||
import { extractInputValue, getDefaultFilterOperator } from './utils';
|
||||
|
||||
export const FilterMenu = ({
|
||||
|
@ -68,9 +64,9 @@ export const FilterMenu = ({
|
|||
key={ft.id}
|
||||
eventKey={ft}
|
||||
// @ts-ignore
|
||||
onSelect={(fltr: typeof ft) =>
|
||||
setInternalFilters([...internalFilters, fltr])
|
||||
}
|
||||
onSelect={(fltr: typeof ft) => {
|
||||
setInternalFilters([...internalFilters, fltr]);
|
||||
}}
|
||||
>
|
||||
{ft.Header}
|
||||
</MenuItem>
|
||||
|
@ -98,6 +94,7 @@ export const FilterInputs = ({
|
|||
{internalFilters.map((ft, i) => {
|
||||
const filter = filters.find(f => f.id === ft.id);
|
||||
if (!filter) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`could not find filter for ${ft.id}`);
|
||||
return null;
|
||||
}
|
||||
|
@ -120,26 +117,27 @@ export const FilterInputs = ({
|
|||
});
|
||||
}}
|
||||
>
|
||||
{(filter.operators || []).map(({ label, value }: Select) => (
|
||||
<option key={label} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
{(filter.operators || []).map(
|
||||
({ label, value }: SelectOption) => (
|
||||
<option key={label} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
),
|
||||
)}
|
||||
</FormControl>
|
||||
</Col>
|
||||
<Col md={1} />
|
||||
<Col md={4}>
|
||||
{filter.input === 'select' && (
|
||||
<VirtualizedSelect
|
||||
<Select
|
||||
autoFocus
|
||||
multi
|
||||
searchable
|
||||
name={`filter-${filter.id}-select`}
|
||||
options={filter.selects}
|
||||
placeholder="Select Value"
|
||||
value={ft.value}
|
||||
selectComponent={SelectComponent}
|
||||
onChange={(e: Select[] | null) => {
|
||||
value={ft.value as SelectOption['value'][] | undefined}
|
||||
onChange={(e: SelectOption[] | null) => {
|
||||
updateInternalFilter(i, {
|
||||
operator: ft.operator || getDefaultFilterOperator(filter),
|
||||
value: e ? e.map(s => s.value) : e,
|
||||
|
@ -152,8 +150,9 @@ export const FilterInputs = ({
|
|||
<FormControl
|
||||
type={filter.input ? filter.input : 'text'}
|
||||
bsSize="small"
|
||||
value={ft.value || ''}
|
||||
value={String(ft.value || '')}
|
||||
checked={Boolean(ft.value)}
|
||||
// @ts-ignore
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
e.persist();
|
||||
updateInternalFilter(i, {
|
||||
|
|
|
@ -18,17 +18,7 @@
|
|||
*/
|
||||
import { t } from '@superset-ui/translation';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import {
|
||||
Col,
|
||||
DropdownButton,
|
||||
MenuItem,
|
||||
Row,
|
||||
// @ts-ignore
|
||||
} from 'react-bootstrap';
|
||||
// @ts-ignore
|
||||
import SelectComponent from 'react-select';
|
||||
// @ts-ignore
|
||||
import VirtualizedSelect from 'react-virtualized-select';
|
||||
import { Col, DropdownButton, MenuItem, Row } from 'react-bootstrap';
|
||||
import IndeterminateCheckbox from '../IndeterminateCheckbox';
|
||||
import TableCollection from './TableCollection';
|
||||
import Pagination from './Pagination';
|
||||
|
@ -208,9 +198,9 @@ const ListView: FunctionComponent<Props> = ({
|
|||
{bulkActions.map(action => (
|
||||
// @ts-ignore
|
||||
<MenuItem
|
||||
id={action.name}
|
||||
key={action.key || action.name}
|
||||
key={action.key}
|
||||
eventKey={selectedFlatRows}
|
||||
// @ts-ignore
|
||||
onSelect={(selectedRows: typeof selectedFlatRows) => {
|
||||
action.onSelect(
|
||||
selectedRows.map((r: any) => r.original),
|
||||
|
|
|
@ -23,7 +23,7 @@ export interface SortColumn {
|
|||
|
||||
export type SortColumns = SortColumn[];
|
||||
|
||||
export interface Select {
|
||||
export interface SelectOption {
|
||||
label: string;
|
||||
value: any;
|
||||
}
|
||||
|
@ -31,13 +31,13 @@ export interface Select {
|
|||
export interface Filter {
|
||||
Header: string;
|
||||
id: string;
|
||||
operators?: Select[];
|
||||
operators?: SelectOption[];
|
||||
operator?: string;
|
||||
input?: 'text' | 'textarea' | 'select' | 'checkbox' | 'search';
|
||||
unfilteredLabel?: string;
|
||||
selects?: Select[];
|
||||
selects?: SelectOption[];
|
||||
onFilterOpen?: () => void;
|
||||
fetchSelects?: () => Promise<Select[]>;
|
||||
fetchSelects?: () => Promise<SelectOption[]>;
|
||||
}
|
||||
|
||||
export type Filters = Filter[];
|
||||
|
|
|
@ -50,12 +50,12 @@ export default function MetricOption({
|
|||
verbose
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<div className="metric-option">
|
||||
{showType && <ColumnTypeLabel type="expression" />}
|
||||
<span className="m-r-5 option-label">{link}</span>
|
||||
<span className="option-label">{link}</span>
|
||||
{metric.description && (
|
||||
<InfoTooltipWithTrigger
|
||||
className="m-r-5 text-muted"
|
||||
className="text-muted"
|
||||
icon="info"
|
||||
tooltip={metric.description}
|
||||
label={`descr-${metric.metric_name}`}
|
||||
|
@ -63,7 +63,7 @@ export default function MetricOption({
|
|||
)}
|
||||
{showFormula && (
|
||||
<InfoTooltipWithTrigger
|
||||
className="m-r-5 text-muted"
|
||||
className="text-muted"
|
||||
icon="question-circle-o"
|
||||
tooltip={metric.expression}
|
||||
label={`expr-${metric.metric_name}`}
|
||||
|
@ -71,7 +71,7 @@ export default function MetricOption({
|
|||
)}
|
||||
{metric.warning_text && (
|
||||
<InfoTooltipWithTrigger
|
||||
className="m-r-5 text-danger"
|
||||
className="text-danger"
|
||||
icon="warning"
|
||||
tooltip={metric.warning_text}
|
||||
label={`warn-${metric.metric_name}`}
|
||||
|
|
|
@ -18,11 +18,16 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Select from 'react-select';
|
||||
import { Select } from 'src/components/Select';
|
||||
|
||||
export default class OnPasteSelect extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onPaste = this.onPaste.bind(this);
|
||||
}
|
||||
|
||||
onPaste(evt) {
|
||||
if (!this.props.multi) {
|
||||
if (!this.props.isMulti) {
|
||||
return;
|
||||
}
|
||||
evt.preventDefault();
|
||||
|
@ -68,39 +73,31 @@ export default class OnPasteSelect extends React.Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const SelectComponent = this.props.selectWrap;
|
||||
const refFunc = ref => {
|
||||
if (this.props.refFunc) {
|
||||
this.props.refFunc(ref);
|
||||
}
|
||||
this.pasteInput = ref;
|
||||
};
|
||||
const inputProps = { onPaste: this.onPaste.bind(this) };
|
||||
return (
|
||||
<SelectComponent {...this.props} ref={refFunc} inputProps={inputProps} />
|
||||
);
|
||||
const { selectWrap: SelectComponent, ...restProps } = this.props;
|
||||
return <SelectComponent {...restProps} onPaste={this.onPaste} />;
|
||||
}
|
||||
}
|
||||
|
||||
OnPasteSelect.propTypes = {
|
||||
separator: PropTypes.array.isRequired,
|
||||
selectWrap: PropTypes.func.isRequired,
|
||||
refFunc: PropTypes.func,
|
||||
selectWrap: PropTypes.elementType,
|
||||
selectRef: PropTypes.func,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
valueKey: PropTypes.string.isRequired,
|
||||
labelKey: PropTypes.string.isRequired,
|
||||
options: PropTypes.array,
|
||||
multi: PropTypes.bool.isRequired,
|
||||
isMulti: PropTypes.bool,
|
||||
value: PropTypes.any,
|
||||
isValidNewOption: PropTypes.func,
|
||||
noResultsText: PropTypes.string,
|
||||
};
|
||||
OnPasteSelect.defaultProps = {
|
||||
separator: [',', '\n', '\t'],
|
||||
separator: [',', '\n', '\t', ';'],
|
||||
selectWrap: Select,
|
||||
valueKey: 'value',
|
||||
labelKey: 'label',
|
||||
options: [],
|
||||
multi: false,
|
||||
isMulti: false,
|
||||
};
|
|
@ -0,0 +1,289 @@
|
|||
/**
|
||||
* 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, { SyntheticEvent, MutableRefObject } from 'react';
|
||||
import { merge } from 'lodash';
|
||||
import BasicSelect, {
|
||||
OptionTypeBase,
|
||||
MultiValueProps,
|
||||
FormatOptionLabelMeta,
|
||||
ValueType,
|
||||
SelectComponentsConfig,
|
||||
components as defaultComponents,
|
||||
createFilter,
|
||||
} from 'react-select';
|
||||
import Async from 'react-select/async';
|
||||
import Creatable from 'react-select/creatable';
|
||||
import AsyncCreatable from 'react-select/async-creatable';
|
||||
|
||||
import { SelectComponents } from 'react-select/src/components';
|
||||
import {
|
||||
SortableContainer,
|
||||
SortableElement,
|
||||
SortableContainerProps,
|
||||
} from 'react-sortable-hoc';
|
||||
import arrayMove from 'array-move';
|
||||
import {
|
||||
WindowedSelectComponentType,
|
||||
WindowedSelectProps,
|
||||
WindowedSelect,
|
||||
WindowedAsyncSelect,
|
||||
WindowedCreatableSelect,
|
||||
WindowedAsyncCreatableSelect,
|
||||
} from './WindowedSelect';
|
||||
import {
|
||||
DEFAULT_CLASS_NAME,
|
||||
DEFAULT_CLASS_NAME_PREFIX,
|
||||
DEFAULT_STYLES,
|
||||
DEFAULT_THEME,
|
||||
DEFAULT_COMPONENTS,
|
||||
VALUE_LABELED_STYLES,
|
||||
PartialThemeConfig,
|
||||
PartialStylesConfig,
|
||||
} from './styles';
|
||||
import { findValue } from './utils';
|
||||
|
||||
type AnyReactSelect<OptionType extends OptionTypeBase> =
|
||||
| BasicSelect<OptionType>
|
||||
| Async<OptionType>
|
||||
| Creatable<OptionType>
|
||||
| AsyncCreatable<OptionType>;
|
||||
|
||||
export type SupersetStyledSelectProps<
|
||||
OptionType extends OptionTypeBase,
|
||||
T extends WindowedSelectProps<OptionType> = WindowedSelectProps<OptionType>
|
||||
> = T & {
|
||||
// additional props for easier usage or backward compatibility
|
||||
labelKey?: string;
|
||||
valueKey?: string;
|
||||
multi?: boolean;
|
||||
clearable?: boolean;
|
||||
sortable?: boolean;
|
||||
ignoreAccents?: boolean;
|
||||
creatable?: boolean;
|
||||
selectRef?:
|
||||
| React.RefCallback<AnyReactSelect<OptionType>>
|
||||
| MutableRefObject<AnyReactSelect<OptionType>>;
|
||||
getInputValue?: (selectBalue: ValueType<OptionType>) => string | undefined;
|
||||
optionRenderer?: (option: OptionType) => React.ReactNode;
|
||||
valueRenderer?: (option: OptionType) => React.ReactNode;
|
||||
valueRenderedAsLabel?: boolean;
|
||||
// callback for paste event
|
||||
onPaste?: (e: SyntheticEvent) => void;
|
||||
// for simplier theme overrides
|
||||
themeConfig?: PartialThemeConfig;
|
||||
stylesConfig?: PartialStylesConfig;
|
||||
};
|
||||
|
||||
function styled<
|
||||
OptionType extends OptionTypeBase,
|
||||
SelectComponentType extends WindowedSelectComponentType<
|
||||
OptionType
|
||||
> = WindowedSelectComponentType<OptionType>
|
||||
>(SelectComponent: SelectComponentType) {
|
||||
type SelectProps = SupersetStyledSelectProps<OptionType>;
|
||||
type Components = SelectComponents<OptionType>;
|
||||
|
||||
const SortableSelectComponent = SortableContainer(SelectComponent, {
|
||||
withRef: true,
|
||||
});
|
||||
|
||||
// default components for the given OptionType
|
||||
const supersetDefaultComponents: SelectComponentsConfig<OptionType> = DEFAULT_COMPONENTS;
|
||||
|
||||
const getSortableMultiValue = (MultiValue: Components['MultiValue']) => {
|
||||
return SortableElement((props: MultiValueProps<OptionType>) => {
|
||||
const onMouseDown = (e: SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
const innerProps = { onMouseDown };
|
||||
return <MultiValue {...props} innerProps={innerProps} />;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Superset styled `Select` component. Apply Superset themed stylesheets and
|
||||
* consolidate props API for backward compatibility with react-select v1.
|
||||
*/
|
||||
function StyledSelect(selectProps: SelectProps) {
|
||||
let stateManager: AnyReactSelect<OptionType>; // reference to react-select StateManager
|
||||
const {
|
||||
// additional props for Superset Select
|
||||
selectRef,
|
||||
labelKey = 'label',
|
||||
valueKey = 'value',
|
||||
themeConfig,
|
||||
stylesConfig = {},
|
||||
optionRenderer,
|
||||
valueRenderer,
|
||||
// whether value is rendered as `option-label` in input,
|
||||
// useful for AdhocMetric and AdhocFilter
|
||||
valueRenderedAsLabel: valueRenderedAsLabel_,
|
||||
onPaste,
|
||||
multi = false, // same as `isMulti`, used for backward compatibility
|
||||
clearable, // same as `isClearable`
|
||||
sortable = true, // whether to enable drag & drop sorting
|
||||
|
||||
// react-select props
|
||||
className = DEFAULT_CLASS_NAME,
|
||||
classNamePrefix = DEFAULT_CLASS_NAME_PREFIX,
|
||||
options,
|
||||
value: value_,
|
||||
components: components_,
|
||||
isMulti: isMulti_,
|
||||
isClearable: isClearable_,
|
||||
minMenuHeight = 100, // apply different defaults
|
||||
maxMenuHeight = 220,
|
||||
filterOption,
|
||||
ignoreAccents = false, // default is `true`, but it is slow
|
||||
|
||||
getOptionValue = option =>
|
||||
typeof option === 'string' ? option : option[valueKey],
|
||||
|
||||
getOptionLabel = option =>
|
||||
typeof option === 'string'
|
||||
? option
|
||||
: option[labelKey] || option[valueKey],
|
||||
|
||||
formatOptionLabel = (
|
||||
option: OptionType,
|
||||
{ context }: FormatOptionLabelMeta<OptionType>,
|
||||
) => {
|
||||
if (context === 'value') {
|
||||
return valueRenderer ? valueRenderer(option) : getOptionLabel(option);
|
||||
}
|
||||
return optionRenderer ? optionRenderer(option) : getOptionLabel(option);
|
||||
},
|
||||
|
||||
...restProps
|
||||
} = selectProps;
|
||||
|
||||
// `value` may be rendered values (strings), we want option objects
|
||||
const value: OptionType[] = findValue(value_, options || [], valueKey);
|
||||
|
||||
// Add backward compability to v1 API
|
||||
const isMulti = isMulti_ === undefined ? multi : isMulti_;
|
||||
const isClearable = isClearable_ === undefined ? clearable : isClearable_;
|
||||
|
||||
// Sort is only applied when there are multiple selected values
|
||||
const shouldAllowSort =
|
||||
isMulti && sortable && Array.isArray(value) && value.length > 1;
|
||||
|
||||
const MaybeSortableSelect = shouldAllowSort
|
||||
? SortableSelectComponent
|
||||
: SelectComponent;
|
||||
const components = { ...supersetDefaultComponents, ...components_ };
|
||||
|
||||
// Make multi-select sortable as per https://react-select.netlify.app/advanced
|
||||
if (shouldAllowSort) {
|
||||
components.MultiValue = getSortableMultiValue(
|
||||
components.MultiValue || defaultComponents.MultiValue,
|
||||
);
|
||||
|
||||
const sortableContainerProps: Partial<SortableContainerProps> = {
|
||||
getHelperDimensions: ({ node }) => node.getBoundingClientRect(),
|
||||
axis: 'xy',
|
||||
onSortEnd: ({ oldIndex, newIndex }) => {
|
||||
const newValue = arrayMove(value, oldIndex, newIndex);
|
||||
if (restProps.onChange) {
|
||||
restProps.onChange(newValue, { action: 'set-value' });
|
||||
}
|
||||
},
|
||||
distance: 4,
|
||||
};
|
||||
Object.assign(restProps, sortableContainerProps);
|
||||
}
|
||||
|
||||
// When values are rendered as labels, adjust valueContainer padding
|
||||
const valueRenderedAsLabel =
|
||||
valueRenderedAsLabel_ === undefined ? isMulti : valueRenderedAsLabel_;
|
||||
if (valueRenderedAsLabel && !stylesConfig.valueContainer) {
|
||||
Object.assign(stylesConfig, VALUE_LABELED_STYLES);
|
||||
}
|
||||
|
||||
// Handle onPaste event
|
||||
if (onPaste) {
|
||||
const Input = components.Input || defaultComponents.Input;
|
||||
// @ts-ignore (needed for passing `onPaste`)
|
||||
components.Input = props => <Input {...props} onPaste={onPaste} />;
|
||||
}
|
||||
// for CreaTable
|
||||
if (SelectComponent === WindowedCreatableSelect) {
|
||||
restProps.getNewOptionData = (inputValue: string, label: string) => ({
|
||||
label: label || inputValue,
|
||||
[valueKey]: inputValue,
|
||||
isNew: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Make sure always return StateManager for the refs.
|
||||
// To get the real `Select` component, keep tap into `obj.select`:
|
||||
// - for normal <Select /> component: StateManager -> Select,
|
||||
// - for <Creatable />: StateManager -> Creatable -> Select
|
||||
const setRef = (instance: any) => {
|
||||
stateManager =
|
||||
shouldAllowSort && instance && 'refs' in instance
|
||||
? instance.refs.wrappedInstance // obtain StateManger from SortableContainer
|
||||
: instance;
|
||||
if (typeof selectRef === 'function') {
|
||||
selectRef(stateManager);
|
||||
} else if (selectRef && 'current' in selectRef) {
|
||||
selectRef.current = stateManager;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<MaybeSortableSelect
|
||||
ref={setRef}
|
||||
className={className}
|
||||
classNamePrefix={classNamePrefix}
|
||||
isMulti={isMulti}
|
||||
isClearable={isClearable}
|
||||
options={options}
|
||||
value={value}
|
||||
minMenuHeight={minMenuHeight}
|
||||
maxMenuHeight={maxMenuHeight}
|
||||
filterOption={
|
||||
// filterOption may be NULL
|
||||
filterOption !== undefined
|
||||
? filterOption
|
||||
: createFilter({ ignoreAccents })
|
||||
}
|
||||
styles={{ ...DEFAULT_STYLES, ...stylesConfig } as SelectProps['styles']}
|
||||
// merge default theme from `react-select`, default theme for Superset,
|
||||
// and the theme from props.
|
||||
theme={defaultTheme => merge(defaultTheme, DEFAULT_THEME, themeConfig)}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
getOptionLabel={getOptionLabel}
|
||||
getOptionValue={getOptionValue}
|
||||
components={components}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// React.memo makes sure the component does no rerender given the same props
|
||||
return React.memo(StyledSelect);
|
||||
}
|
||||
|
||||
export const Select = styled(WindowedSelect);
|
||||
export const AsyncSelect = styled(WindowedAsyncSelect);
|
||||
export const CreatableSelect = styled(WindowedCreatableSelect);
|
||||
export const AsyncCreatableSelect = styled(WindowedAsyncCreatableSelect);
|
||||
export default Select;
|
|
@ -0,0 +1,155 @@
|
|||
/**
|
||||
* 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, {
|
||||
useRef,
|
||||
useEffect,
|
||||
Component,
|
||||
FunctionComponent,
|
||||
RefObject,
|
||||
} from 'react';
|
||||
import {
|
||||
ListChildComponentProps,
|
||||
FixedSizeList as WindowedList,
|
||||
} from 'react-window';
|
||||
import {
|
||||
OptionTypeBase,
|
||||
OptionProps,
|
||||
MenuListComponentProps,
|
||||
} from 'react-select';
|
||||
import { ThemeConfig } from '../styles';
|
||||
|
||||
export type WindowedMenuListProps = {
|
||||
selectProps: {
|
||||
windowListRef?: RefObject<WindowedList>;
|
||||
optionHeight?: number;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* MenuListComponentProps should always have `children` elements, as guaranteed
|
||||
* by https://github.com/JedWatson/react-select/blob/32ad5c040bdd96cd1ca71010c2558842d684629c/packages/react-select/src/Select.js#L1686-L1719
|
||||
*
|
||||
* `children` may also be `Component<NoticeProps<OptionType>>` if options are not
|
||||
* provided (e.g., when async list is still loading, or no results), but that's
|
||||
* not possible because this MenuList will only be rendered when
|
||||
* optionsLength > windowThreshold.
|
||||
*
|
||||
* If may also be `Component<GroupProps<OptionType>>[]` but we are not supporting
|
||||
* grouped options just yet.
|
||||
*/
|
||||
export type MenuListProps<
|
||||
OptionType extends OptionTypeBase
|
||||
> = MenuListComponentProps<OptionType> & {
|
||||
children: Component<OptionProps<OptionType>>[];
|
||||
// theme is not present with built-in @types/react-select, but is actually
|
||||
// available via CommonProps.
|
||||
theme?: ThemeConfig;
|
||||
className?: string;
|
||||
} & WindowedMenuListProps;
|
||||
|
||||
const DEFAULT_OPTION_HEIGHT = 30;
|
||||
|
||||
/**
|
||||
* Get the index of the last selected option.
|
||||
*/
|
||||
function getLastSelected(children: Component<any>[]) {
|
||||
return Array.isArray(children)
|
||||
? children.findIndex(
|
||||
({ props: { isFocused = false } = {} }) => isFocused,
|
||||
) || 0
|
||||
: -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate probable option height as set in theme configs
|
||||
*/
|
||||
function detectHeight({ spacing: { baseUnit, lineHeight } }: ThemeConfig) {
|
||||
// Option item expects 2 * baseUnit for each of top and bottom padding.
|
||||
return baseUnit * 4 + lineHeight;
|
||||
}
|
||||
|
||||
export default function WindowedMenuList<OptionType extends OptionTypeBase>({
|
||||
children,
|
||||
...props
|
||||
}: MenuListProps<OptionType>) {
|
||||
const {
|
||||
maxHeight,
|
||||
selectProps,
|
||||
theme,
|
||||
getStyles,
|
||||
cx,
|
||||
innerRef,
|
||||
isMulti,
|
||||
className,
|
||||
} = props;
|
||||
const {
|
||||
// Expose react-window VariableSizeList instance and HTML elements
|
||||
windowListRef = useRef(null),
|
||||
windowListInnerRef,
|
||||
} = selectProps;
|
||||
|
||||
// try get default option height from theme configs
|
||||
let optionHeight = selectProps.optionHeight;
|
||||
if (!optionHeight) {
|
||||
optionHeight = theme ? detectHeight(theme) : DEFAULT_OPTION_HEIGHT;
|
||||
}
|
||||
|
||||
const itemCount = children.length;
|
||||
const totalHeight = optionHeight * itemCount;
|
||||
const listRef: RefObject<WindowedList> = windowListRef || useRef(null);
|
||||
|
||||
const Row: FunctionComponent<ListChildComponentProps> = ({
|
||||
data,
|
||||
index,
|
||||
style,
|
||||
}) => {
|
||||
return <div style={style}>{data[index]}</div>;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const lastSelected = getLastSelected(children);
|
||||
if (listRef.current && lastSelected) {
|
||||
listRef.current.scrollToItem(lastSelected);
|
||||
}
|
||||
}, [children]);
|
||||
|
||||
return (
|
||||
<WindowedList
|
||||
css={getStyles('menuList', props)}
|
||||
// @ts-ignore
|
||||
className={cx(
|
||||
{
|
||||
'menu-list': true,
|
||||
'menu-list--is-multi': isMulti,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
ref={listRef}
|
||||
outerRef={innerRef}
|
||||
innerRef={windowListInnerRef}
|
||||
height={Math.min(totalHeight, maxHeight)}
|
||||
width="100%"
|
||||
itemData={children}
|
||||
itemCount={children.length}
|
||||
itemSize={optionHeight}
|
||||
>
|
||||
{Row}
|
||||
</WindowedList>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* 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 Select from 'react-select';
|
||||
import Async from 'react-select/async';
|
||||
import Creatable from 'react-select/creatable';
|
||||
import AsyncCreatable from 'react-select/async-creatable';
|
||||
import windowed from './windowed';
|
||||
|
||||
export * from './windowed';
|
||||
|
||||
export const WindowedSelect = windowed(Select);
|
||||
export const WindowedAsyncSelect = windowed(Async);
|
||||
export const WindowedCreatableSelect = windowed(Creatable);
|
||||
export const WindowedAsyncCreatableSelect = windowed(AsyncCreatable);
|
||||
export default WindowedSelect;
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* 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, { ComponentType, FunctionComponent } from 'react';
|
||||
import Select, {
|
||||
Props as SelectProps,
|
||||
OptionTypeBase,
|
||||
MenuListComponentProps,
|
||||
components as defaultComponents,
|
||||
} from 'react-select';
|
||||
import WindowedMenuList, { WindowedMenuListProps } from './WindowedMenuList';
|
||||
|
||||
const { MenuList: DefaultMenuList } = defaultComponents;
|
||||
|
||||
export const DEFAULT_WINDOW_THRESHOLD = 100;
|
||||
|
||||
export type WindowedSelectProps<
|
||||
OptionType extends OptionTypeBase
|
||||
> = SelectProps<OptionType> & {
|
||||
windowThreshold?: number;
|
||||
} & WindowedMenuListProps['selectProps'];
|
||||
|
||||
export type WindowedSelectComponentType<
|
||||
OptionType extends OptionTypeBase
|
||||
> = FunctionComponent<WindowedSelectProps<OptionType>>;
|
||||
|
||||
export function MenuList<OptionType extends OptionTypeBase>({
|
||||
children,
|
||||
...props
|
||||
}: MenuListComponentProps<OptionType> & {
|
||||
selectProps: WindowedSelectProps<OptionType>;
|
||||
}) {
|
||||
const { windowThreshold = DEFAULT_WINDOW_THRESHOLD } = props.selectProps;
|
||||
if (Array.isArray(children) && children.length > windowThreshold) {
|
||||
// @ts-ignore
|
||||
return <WindowedMenuList {...props}>{children}</WindowedMenuList>;
|
||||
}
|
||||
return <DefaultMenuList {...props}>{children}</DefaultMenuList>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add "windowThreshold" option to a react-select component, turn the options
|
||||
* list into a virtualized list when appropriate.
|
||||
*
|
||||
* @param SelectComponent the React component to render Select
|
||||
*/
|
||||
export default function windowed<OptionType extends OptionTypeBase>(
|
||||
SelectComponent: ComponentType<SelectProps<OptionType>>,
|
||||
): WindowedSelectComponentType<OptionType> {
|
||||
function WindowedSelect(
|
||||
props: WindowedSelectProps<OptionType>,
|
||||
ref: React.RefObject<Select<OptionType>>,
|
||||
) {
|
||||
const { components: components_ = {}, ...restProps } = props;
|
||||
const components = { ...components_, MenuList };
|
||||
return <SelectComponent components={components} ref={ref} {...restProps} />;
|
||||
}
|
||||
return React.forwardRef(WindowedSelect);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export * from './SupersetStyledSelect';
|
||||
export * from './styles';
|
||||
export { default } from './SupersetStyledSelect';
|
||||
export { default as OnPasteSelect } from './OnPasteSelect';
|
|
@ -0,0 +1,298 @@
|
|||
/**
|
||||
* 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, { CSSProperties } from 'react';
|
||||
import { css, SerializedStyles } from '@emotion/core';
|
||||
import { supersetTheme } from '@superset-ui/style';
|
||||
import {
|
||||
Styles,
|
||||
Theme,
|
||||
SelectComponentsConfig,
|
||||
components as defaultComponents,
|
||||
} from 'react-select';
|
||||
import { colors as reactSelectColros } from 'react-select/src/theme';
|
||||
import { supersetColors } from 'src/components/styles';
|
||||
|
||||
export const DEFAULT_CLASS_NAME = 'Select';
|
||||
export const DEFAULT_CLASS_NAME_PREFIX = 'Select';
|
||||
|
||||
type RecursivePartial<T> = {
|
||||
[P in keyof T]?: RecursivePartial<T[P]>;
|
||||
};
|
||||
|
||||
export type ThemeConfig = {
|
||||
borderRadius: number;
|
||||
// z-index for menu dropdown
|
||||
// (the same as `@z-index-above-dashboard-charts + 1` in variables.less)
|
||||
zIndex: number;
|
||||
colors: {
|
||||
// add known colors
|
||||
[key in keyof typeof reactSelectColros]: string;
|
||||
} &
|
||||
{
|
||||
[key in keyof typeof supersetColors]: string;
|
||||
} & {
|
||||
[key: string]: string; // any other colors
|
||||
};
|
||||
spacing: Theme['spacing'] & {
|
||||
// line height and font size must be pixels for easier computation
|
||||
// of option item height in WindowedMenuList
|
||||
lineHeight: number;
|
||||
fontSize: number;
|
||||
// other relative size must be string
|
||||
minWidth: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type PartialThemeConfig = RecursivePartial<ThemeConfig>;
|
||||
|
||||
export const DEFAULT_THEME: PartialThemeConfig = {
|
||||
borderRadius: parseInt(supersetTheme.borderRadius, 10),
|
||||
zIndex: 11,
|
||||
colors: {
|
||||
...supersetColors,
|
||||
dangerLight: supersetColors.warning,
|
||||
},
|
||||
spacing: {
|
||||
baseUnit: 3,
|
||||
menuGutter: 0,
|
||||
controlHeight: 28,
|
||||
lineHeight: 19,
|
||||
fontSize: 14,
|
||||
minWidth: '7.5em', // just enough to display 'No options'
|
||||
},
|
||||
};
|
||||
|
||||
// let styles accept serialized CSS, too
|
||||
type CSSStyles = CSSProperties | SerializedStyles;
|
||||
type styleFnWithSerializedStyles = (
|
||||
base: CSSProperties,
|
||||
state: any,
|
||||
) => CSSStyles | CSSStyles[];
|
||||
|
||||
export type StylesConfig = {
|
||||
[key in keyof Styles]: styleFnWithSerializedStyles;
|
||||
};
|
||||
export type PartialStylesConfig = Partial<StylesConfig>;
|
||||
|
||||
export const DEFAULT_STYLES: PartialStylesConfig = {
|
||||
container: (
|
||||
provider,
|
||||
{
|
||||
theme: {
|
||||
spacing: { minWidth },
|
||||
},
|
||||
},
|
||||
) => [
|
||||
provider,
|
||||
css`
|
||||
min-width: ${minWidth};
|
||||
`,
|
||||
],
|
||||
placeholder: provider => [
|
||||
provider,
|
||||
css`
|
||||
white-space: nowrap;
|
||||
`,
|
||||
],
|
||||
indicatorSeparator: () => css`
|
||||
display: none;
|
||||
`,
|
||||
indicatorsContainer: provider => [
|
||||
provider,
|
||||
css`
|
||||
i {
|
||||
width: 1em;
|
||||
display: inline-block;
|
||||
}
|
||||
`,
|
||||
],
|
||||
clearIndicator: provider => [
|
||||
provider,
|
||||
css`
|
||||
padding-right: 0;
|
||||
`,
|
||||
],
|
||||
control: (
|
||||
provider,
|
||||
{ isFocused, menuIsOpen, theme: { borderRadius, colors } },
|
||||
) => {
|
||||
const isPseudoFocused = isFocused && !menuIsOpen;
|
||||
let borderColor = '#ccc';
|
||||
if (isPseudoFocused) {
|
||||
borderColor = '#000';
|
||||
} else if (menuIsOpen) {
|
||||
borderColor = `${colors.grayBorderDark} ${colors.grayBorder} ${colors.grayBorderLight}`;
|
||||
}
|
||||
return [
|
||||
provider,
|
||||
css`
|
||||
border-color: ${borderColor};
|
||||
box-shadow: ${isPseudoFocused
|
||||
? 'inset 0 1px 1px rgba(0,0,0,.075), 0 0 0 3px rgba(0,0,0,.1)'
|
||||
: 'none'};
|
||||
border-radius: ${menuIsOpen
|
||||
? `${borderRadius}px ${borderRadius}px 0 0`
|
||||
: `${borderRadius}px`};
|
||||
&:hover {
|
||||
border-color: ${borderColor};
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
`,
|
||||
];
|
||||
},
|
||||
menu: (provider, { theme: { borderRadius, zIndex, colors } }) => [
|
||||
provider,
|
||||
css`
|
||||
border-radius: 0 0 ${borderRadius}px ${borderRadius}px;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
|
||||
margin-top: -1px;
|
||||
border-top-color: ${colors.grayBorderLight};
|
||||
min-width: 100%;
|
||||
width: auto;
|
||||
z-index: ${zIndex}; /* override at least multi-page pagination */
|
||||
`,
|
||||
],
|
||||
menuList: (provider, { theme: { borderRadius } }) => [
|
||||
provider,
|
||||
css`
|
||||
border-radius: 0 0 ${borderRadius}px ${borderRadius}px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
`,
|
||||
],
|
||||
option: (
|
||||
provider,
|
||||
{
|
||||
isDisabled,
|
||||
isFocused,
|
||||
isSelected,
|
||||
theme: {
|
||||
colors,
|
||||
spacing: { lineHeight, fontSize },
|
||||
},
|
||||
},
|
||||
) => {
|
||||
let color = colors.textDefault;
|
||||
let backgroundColor = colors.lightest;
|
||||
if (isFocused) {
|
||||
backgroundColor = colors.grayBgDarker;
|
||||
} else if (isDisabled) {
|
||||
color = '#ccc';
|
||||
}
|
||||
return [
|
||||
provider,
|
||||
css`
|
||||
cursor: pointer;
|
||||
line-height: ${lineHeight}px;
|
||||
font-size: ${fontSize}px;
|
||||
background-color: ${backgroundColor};
|
||||
color: ${color};
|
||||
font-weight: ${isSelected ? 600 : 400};
|
||||
white-space: nowrap;
|
||||
&:hover:active {
|
||||
background-color: ${colors.grayBg};
|
||||
}
|
||||
`,
|
||||
];
|
||||
},
|
||||
valueContainer: (
|
||||
provider,
|
||||
{
|
||||
isMulti,
|
||||
hasValue,
|
||||
theme: {
|
||||
spacing: { baseUnit },
|
||||
},
|
||||
},
|
||||
) => [
|
||||
provider,
|
||||
css`
|
||||
padding-left: ${isMulti && hasValue ? 1 : baseUnit * 3}px;
|
||||
`,
|
||||
],
|
||||
multiValueLabel: (
|
||||
provider,
|
||||
{
|
||||
theme: {
|
||||
spacing: { baseUnit },
|
||||
},
|
||||
},
|
||||
) => ({
|
||||
...provider,
|
||||
paddingLeft: baseUnit * 1.2,
|
||||
paddingRight: baseUnit * 1.2,
|
||||
}),
|
||||
};
|
||||
|
||||
const { ClearIndicator, DropdownIndicator, Option } = defaultComponents;
|
||||
|
||||
export const DEFAULT_COMPONENTS: SelectComponentsConfig<any> = {
|
||||
Option: ({ children, innerProps, data, ...props }) => (
|
||||
<Option
|
||||
{...props}
|
||||
data={data}
|
||||
innerProps={{
|
||||
...innerProps,
|
||||
// `@types/react-select` didn't define `style` for `innerProps`
|
||||
// @ts-ignore
|
||||
style: data && data.style ? data.style : null,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Option>
|
||||
),
|
||||
ClearIndicator: props => (
|
||||
<ClearIndicator {...props}>
|
||||
<i className="fa">×</i>
|
||||
</ClearIndicator>
|
||||
),
|
||||
DropdownIndicator: props => (
|
||||
<DropdownIndicator {...props}>
|
||||
<i
|
||||
className={`fa fa-caret-${
|
||||
props.selectProps.menuIsOpen ? 'up' : 'down'
|
||||
}`}
|
||||
/>
|
||||
</DropdownIndicator>
|
||||
),
|
||||
};
|
||||
|
||||
export const VALUE_LABELED_STYLES: PartialStylesConfig = {
|
||||
valueContainer: (
|
||||
provider,
|
||||
{
|
||||
getValue,
|
||||
theme: {
|
||||
spacing: { baseUnit },
|
||||
},
|
||||
},
|
||||
) => ({
|
||||
...provider,
|
||||
paddingLeft: getValue().length > 0 ? 1 : baseUnit * 3,
|
||||
}),
|
||||
// render single value as is they are multi-value
|
||||
singleValue: (provider, props) => {
|
||||
const { getStyles } = props;
|
||||
return {
|
||||
...getStyles('multiValue', props),
|
||||
'.metric-option': getStyles('multiValueLabel', props),
|
||||
};
|
||||
},
|
||||
};
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* 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 {
|
||||
OptionTypeBase,
|
||||
ValueType,
|
||||
OptionsType,
|
||||
GroupedOptionsType,
|
||||
} from 'react-select';
|
||||
|
||||
/**
|
||||
* Find Option value that matches a possibly string value.
|
||||
*
|
||||
* Translate possible string values to `OptionType` objects, fallback to value
|
||||
* itself if cannot be found in the options list.
|
||||
*
|
||||
* Always returns an array.
|
||||
*/
|
||||
export function findValue<OptionType extends OptionTypeBase>(
|
||||
value: ValueType<OptionType> | string,
|
||||
options: GroupedOptionsType<OptionType> | OptionsType<OptionType> = [],
|
||||
valueKey = 'value',
|
||||
): OptionType[] {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return [];
|
||||
}
|
||||
const isGroup = Array.isArray((options[0] || {}).options);
|
||||
const flatOptions = isGroup
|
||||
? (options as GroupedOptionsType<OptionType>).flatMap(x => x.options || [])
|
||||
: (options as OptionsType<OptionType>);
|
||||
|
||||
const find = (val: OptionType) => {
|
||||
const realVal = (value || {}).hasOwnProperty(valueKey)
|
||||
? val[valueKey]
|
||||
: val;
|
||||
return (
|
||||
flatOptions.find(x => x === realVal || x[valueKey] === realVal) || val
|
||||
);
|
||||
};
|
||||
|
||||
// If value is a single string, must return an Array so `cleanValue` won't be
|
||||
// empty: https://github.com/JedWatson/react-select/blob/32ad5c040bdd96cd1ca71010c2558842d684629c/packages/react-select/src/utils.js#L64
|
||||
return (Array.isArray(value) ? value : [value]).map(find);
|
||||
}
|
|
@ -1,80 +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 styled from '@superset-ui/style';
|
||||
import { css } from '@emotion/core';
|
||||
// @ts-ignore
|
||||
import Select, { Async } from 'react-select';
|
||||
|
||||
const styles = css`
|
||||
display: block;
|
||||
&.is-focused:not(.is-open) > .Select-control {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.is-open > .Select-control .Select-arrow {
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.Select-control {
|
||||
display: inline-flex;
|
||||
border: none;
|
||||
width: 128px;
|
||||
top: -5px;
|
||||
&:focus,
|
||||
&:hover {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.Select-multi-value-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
.Select-value {
|
||||
position: relative;
|
||||
padding-right: 2px;
|
||||
max-width: 104px;
|
||||
}
|
||||
.Select-input {
|
||||
padding-left: 0;
|
||||
padding-right: 8px;
|
||||
}
|
||||
.Select-arrow-zone {
|
||||
width: auto;
|
||||
padding: 0;
|
||||
.Select-arrow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
.Select-menu-outer {
|
||||
margin-top: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default styled(Select)`
|
||||
${styles}
|
||||
`;
|
||||
|
||||
export const AsyncStyledSelect = styled(Async)`
|
||||
${styles}
|
||||
`;
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Select from 'react-virtualized-select';
|
||||
import Select from 'src/components/Select';
|
||||
import { ControlLabel, Label } from 'react-bootstrap';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { SupersetClient } from '@superset-ui/connection';
|
||||
|
@ -221,41 +221,18 @@ export default class TableSelector extends React.PureComponent {
|
|||
</span>
|
||||
);
|
||||
}
|
||||
renderTableOption({
|
||||
focusOption,
|
||||
focusedOption,
|
||||
key,
|
||||
option,
|
||||
selectValue,
|
||||
style,
|
||||
valueArray,
|
||||
}) {
|
||||
const classNames = ['Select-option'];
|
||||
if (option === focusedOption) {
|
||||
classNames.push('is-focused');
|
||||
}
|
||||
if (valueArray.indexOf(option) >= 0) {
|
||||
classNames.push('is-selected');
|
||||
}
|
||||
renderTableOption(option) {
|
||||
return (
|
||||
<div
|
||||
className={classNames.join(' ')}
|
||||
key={key}
|
||||
onClick={() => selectValue(option)}
|
||||
onMouseEnter={() => focusOption(option)}
|
||||
style={style}
|
||||
>
|
||||
<span className="TableLabel">
|
||||
<span className="m-r-5">
|
||||
<small className="text-muted">
|
||||
<i
|
||||
className={`fa fa-${option.type === 'view' ? 'eye' : 'table'}`}
|
||||
/>
|
||||
</small>
|
||||
</span>
|
||||
{option.label}
|
||||
<span className="TableLabel">
|
||||
<span className="m-r-5">
|
||||
<small className="text-muted">
|
||||
<i
|
||||
className={`fa fa-${option.type === 'view' ? 'eye' : 'table'}`}
|
||||
/>
|
||||
</small>
|
||||
</span>
|
||||
</div>
|
||||
{option.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
renderSelectRow(select, refreshBtn) {
|
||||
|
|
|
@ -1,77 +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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function VirtualizedRendererWrap(renderer) {
|
||||
function WrapperRenderer({
|
||||
focusedOption,
|
||||
focusOption,
|
||||
key,
|
||||
option,
|
||||
selectValue,
|
||||
style,
|
||||
valueArray,
|
||||
}) {
|
||||
if (!option) {
|
||||
return null;
|
||||
}
|
||||
const className = ['VirtualizedSelectOption'];
|
||||
if (option === focusedOption) {
|
||||
className.push('VirtualizedSelectFocusedOption');
|
||||
}
|
||||
if (option.disabled) {
|
||||
className.push('VirtualizedSelectDisabledOption');
|
||||
}
|
||||
if (valueArray && valueArray.indexOf(option) >= 0) {
|
||||
className.push('VirtualizedSelectSelectedOption');
|
||||
}
|
||||
if (option.className) {
|
||||
className.push(option.className);
|
||||
}
|
||||
const events = option.disabled
|
||||
? {}
|
||||
: {
|
||||
onClick: () => selectValue(option),
|
||||
onMouseEnter: () => focusOption(option),
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={className.join(' ')}
|
||||
key={key}
|
||||
style={{ ...(option.style || {}), ...style }}
|
||||
title={option.title}
|
||||
data-test={option.optionName}
|
||||
{...events}
|
||||
>
|
||||
{renderer(option)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
WrapperRenderer.propTypes = {
|
||||
focusedOption: PropTypes.object.isRequired,
|
||||
focusOption: PropTypes.func.isRequired,
|
||||
key: PropTypes.string,
|
||||
option: PropTypes.object,
|
||||
selectValue: PropTypes.func.isRequired,
|
||||
style: PropTypes.object,
|
||||
valueArray: PropTypes.array,
|
||||
};
|
||||
return WrapperRenderer;
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Variables ported from "src/stylesheets/less/variables.less"
|
||||
// TODO: move to `@superset-ui/style`
|
||||
// Keep it here to make PRs easier for review.
|
||||
export const supersetColors = {
|
||||
primary: '#00a699',
|
||||
danger: '#fe4a49',
|
||||
warning: '#ffab00',
|
||||
indicator: '#44c0ff',
|
||||
almostBlack: '#263238',
|
||||
grayDark: '#484848',
|
||||
grayLight: '#cfd8dc',
|
||||
gray: '#879399',
|
||||
grayBg: '#f5f5f5',
|
||||
grayBgDarker: '#e8e8e8', // select option menu hover
|
||||
grayBgDarkest: '#d2d2d2', // table cell bar chart
|
||||
grayHeading: '#a3a3a3',
|
||||
menuHover: '#f2f3f5',
|
||||
lightest: '#fff',
|
||||
darkest: '#000',
|
||||
|
||||
// addition most common colors
|
||||
grayBorder: '#ccc',
|
||||
grayBorderLight: '#d9d9d9',
|
||||
grayBorderDark: '#b3b3b3',
|
||||
textDefault: '#333',
|
||||
textDarkest: '#111',
|
||||
};
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Select from 'react-select';
|
||||
import Select from 'src/components/Select';
|
||||
import AceEditor from 'react-ace';
|
||||
import 'brace/mode/css';
|
||||
import 'brace/theme/github';
|
||||
|
|
|
@ -20,7 +20,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { Row, Col, Button, Modal, FormControl } from 'react-bootstrap';
|
||||
import Dialog from 'react-bootstrap-dialog';
|
||||
import { Async as SelectAsync } from 'react-select';
|
||||
import { AsyncSelect } from 'src/components/Select';
|
||||
import AceEditor from 'react-ace';
|
||||
import rison from 'rison';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
@ -116,15 +116,14 @@ class PropertiesModal extends React.PureComponent {
|
|||
endpoint: `/api/v1/dashboard/related/owners?q=${query}`,
|
||||
}).then(
|
||||
response => {
|
||||
const options = response.json.result.map(item => ({
|
||||
return response.json.result.map(item => ({
|
||||
value: item.value,
|
||||
label: item.text,
|
||||
}));
|
||||
return { options };
|
||||
},
|
||||
badResponse => {
|
||||
this.handleErrorResponse(badResponse);
|
||||
return { options: [] };
|
||||
return [];
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -238,14 +237,16 @@ class PropertiesModal extends React.PureComponent {
|
|||
<label className="control-label" htmlFor="owners">
|
||||
{t('Owners')}
|
||||
</label>
|
||||
<SelectAsync
|
||||
<AsyncSelect
|
||||
name="owners"
|
||||
multi
|
||||
isMulti
|
||||
value={values.owners}
|
||||
loadOptions={this.loadOwnerOptions}
|
||||
defaultOptions // load options on render
|
||||
cacheOptions
|
||||
onChange={this.onOwnersChange}
|
||||
disabled={!isDashboardLoaded}
|
||||
filterOption={() => true} // options are filtered at the api
|
||||
filterOption={null} // options are filtered at the api
|
||||
/>
|
||||
<p className="help-block">
|
||||
{t(
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Select from 'react-select';
|
||||
import Select from 'src/components/Select';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import ModalTrigger from '../../components/ModalTrigger';
|
||||
|
|
|
@ -73,7 +73,7 @@ class ChartHolder extends React.Component {
|
|||
static renderInFocusCSS(columnName) {
|
||||
return (
|
||||
<style>
|
||||
{`label[for=${columnName}] + .Select .Select-control {
|
||||
{`label[for=${columnName}] + .Select .Select__control {
|
||||
border-color: #00736a;
|
||||
transition: border-color 1s ease-in-out;
|
||||
}`}
|
||||
|
|
|
@ -48,10 +48,10 @@ const OPERATORS_TO_SQL = {
|
|||
|
||||
function translateToSql(adhocMetric, { useSimple } = {}) {
|
||||
if (adhocMetric.expressionType === EXPRESSION_TYPES.SIMPLE || useSimple) {
|
||||
const isMulti = MULTI_OPERATORS.indexOf(adhocMetric.operator) >= 0;
|
||||
const isMulti = MULTI_OPERATORS.has(adhocMetric.operator);
|
||||
const subject = adhocMetric.subject;
|
||||
const operator =
|
||||
adhocMetric.operator && CUSTOM_OPERATORS.includes(adhocMetric.operator)
|
||||
adhocMetric.operator && CUSTOM_OPERATORS.has(adhocMetric.operator)
|
||||
? OPERATORS_TO_SQL[adhocMetric.operator](adhocMetric)
|
||||
: OPERATORS_TO_SQL[adhocMetric.operator];
|
||||
const comparator = Array.isArray(adhocMetric.comparator)
|
||||
|
@ -81,10 +81,7 @@ export default class AdhocFilter {
|
|||
? adhocFilter.sqlExpression
|
||||
: translateToSql(adhocFilter, { useSimple: true });
|
||||
this.clause = adhocFilter.clause;
|
||||
if (
|
||||
adhocFilter.operator &&
|
||||
CUSTOM_OPERATORS.includes(adhocFilter.operator)
|
||||
) {
|
||||
if (adhocFilter.operator && CUSTOM_OPERATORS.has(adhocFilter.operator)) {
|
||||
this.subject = adhocFilter.subject;
|
||||
this.operator = adhocFilter.operator;
|
||||
} else {
|
||||
|
@ -94,7 +91,7 @@ export default class AdhocFilter {
|
|||
this.comparator = null;
|
||||
}
|
||||
this.isExtra = !!adhocFilter.isExtra;
|
||||
this.fromFormData = !!adhocFilter.filterOptionName;
|
||||
this.isNew = !!adhocFilter.isNew;
|
||||
|
||||
this.filterOptionName =
|
||||
adhocFilter.filterOptionName ||
|
||||
|
@ -106,13 +103,8 @@ export default class AdhocFilter {
|
|||
duplicateWith(nextFields) {
|
||||
return new AdhocFilter({
|
||||
...this,
|
||||
expressionType: this.expressionType,
|
||||
subject: this.subject,
|
||||
operator: this.operator,
|
||||
clause: this.clause,
|
||||
sqlExpression: this.sqlExpression,
|
||||
fromFormData: this.fromFormData,
|
||||
filterOptionName: this.filterOptionName,
|
||||
// all duplicated fields are not new (i.e. will not open popup automatically)
|
||||
isNew: false,
|
||||
...nextFields,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -72,8 +72,8 @@ export default class AdhocMetric {
|
|||
this.column = null;
|
||||
this.aggregate = null;
|
||||
}
|
||||
this.isNew = !!adhocMetric.isNew;
|
||||
this.hasCustomLabel = !!(adhocMetric.hasCustomLabel && adhocMetric.label);
|
||||
this.fromFormData = !!adhocMetric.optionName;
|
||||
this.label = this.hasCustomLabel
|
||||
? adhocMetric.label
|
||||
: this.getDefaultLabel();
|
||||
|
@ -104,6 +104,9 @@ export default class AdhocMetric {
|
|||
duplicateWith(nextFields) {
|
||||
return new AdhocMetric({
|
||||
...this,
|
||||
// all duplicate metrics are not considered new by default
|
||||
isNew: false,
|
||||
// but still overriddable by nextFields
|
||||
...nextFields,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormGroup } from 'react-bootstrap';
|
||||
import VirtualizedSelect from 'react-virtualized-select';
|
||||
import { Select, Creatable } from 'src/components/Select';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { SupersetClient } from '@superset-ui/connection';
|
||||
|
||||
|
@ -28,6 +28,7 @@ import adhocMetricType from '../propTypes/adhocMetricType';
|
|||
import columnType from '../propTypes/columnType';
|
||||
import {
|
||||
OPERATORS,
|
||||
OPERATORS_OPTIONS,
|
||||
TABLE_ONLY_OPERATORS,
|
||||
DRUID_ONLY_OPERATORS,
|
||||
HAVING_OPERATORS,
|
||||
|
@ -36,9 +37,7 @@ import {
|
|||
DISABLE_INPUT_OPERATORS,
|
||||
} from '../constants';
|
||||
import FilterDefinitionOption from './FilterDefinitionOption';
|
||||
import OnPasteSelect from '../../components/OnPasteSelect';
|
||||
import SelectControl from './controls/SelectControl';
|
||||
import VirtualizedRendererWrap from '../../components/VirtualizedRendererWrap';
|
||||
|
||||
const propTypes = {
|
||||
adhocFilter: PropTypes.instanceOf(AdhocFilter).isRequired,
|
||||
|
@ -94,12 +93,11 @@ export default class AdhocFilterEditPopoverSimpleTabContent extends React.Compon
|
|||
};
|
||||
|
||||
this.selectProps = {
|
||||
multi: false,
|
||||
isMulti: false,
|
||||
name: 'select-column',
|
||||
labelKey: 'label',
|
||||
autosize: false,
|
||||
clearable: false,
|
||||
selectWrap: VirtualizedSelect,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -148,7 +146,7 @@ export default class AdhocFilterEditPopoverSimpleTabContent extends React.Compon
|
|||
let newComparator;
|
||||
// convert between list of comparators and individual comparators
|
||||
// (e.g. `in ('North America', 'Africa')` to `== 'North America'`)
|
||||
if (MULTI_OPERATORS.indexOf(operator.operator) >= 0) {
|
||||
if (MULTI_OPERATORS.has(operator)) {
|
||||
newComparator = Array.isArray(currentComparator)
|
||||
? currentComparator
|
||||
: [currentComparator].filter(element => element);
|
||||
|
@ -158,12 +156,12 @@ export default class AdhocFilterEditPopoverSimpleTabContent extends React.Compon
|
|||
: currentComparator;
|
||||
}
|
||||
|
||||
if (operator && CUSTOM_OPERATORS.includes(operator.operator)) {
|
||||
if (operator && CUSTOM_OPERATORS.has(operator)) {
|
||||
this.props.onChange(
|
||||
this.props.adhocFilter.duplicateWith({
|
||||
subject: this.props.adhocFilter.subject,
|
||||
clause: CLAUSES.WHERE,
|
||||
operator: operator && operator.operator,
|
||||
operator,
|
||||
expressionType: EXPRESSION_TYPES.SQL,
|
||||
datasource: this.props.datasource,
|
||||
}),
|
||||
|
@ -171,7 +169,7 @@ export default class AdhocFilterEditPopoverSimpleTabContent extends React.Compon
|
|||
} else {
|
||||
this.props.onChange(
|
||||
this.props.adhocFilter.duplicateWith({
|
||||
operator: operator && operator.operator,
|
||||
operator,
|
||||
comparator: newComparator,
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
}),
|
||||
|
@ -233,18 +231,26 @@ export default class AdhocFilterEditPopoverSimpleTabContent extends React.Compon
|
|||
SupersetClient.get({
|
||||
signal,
|
||||
endpoint: `/superset/filter/${datasource.type}/${datasource.id}/${col}/`,
|
||||
}).then(({ json }) => {
|
||||
this.setState(() => ({
|
||||
suggestions: json,
|
||||
abortActiveRequest: null,
|
||||
loading: false,
|
||||
}));
|
||||
});
|
||||
})
|
||||
.then(({ json }) => {
|
||||
this.setState(() => ({
|
||||
suggestions: json,
|
||||
abortActiveRequest: null,
|
||||
loading: false,
|
||||
}));
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState(() => ({
|
||||
suggestions: [],
|
||||
abortActiveRequest: null,
|
||||
loading: false,
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isOperatorRelevant(operator, subject) {
|
||||
if (operator && CUSTOM_OPERATORS.includes(operator)) {
|
||||
if (operator && CUSTOM_OPERATORS.has(operator)) {
|
||||
const { partitionColumn } = this.props;
|
||||
return partitionColumn && subject && subject === partitionColumn;
|
||||
}
|
||||
|
@ -271,16 +277,23 @@ export default class AdhocFilterEditPopoverSimpleTabContent extends React.Compon
|
|||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { adhocFilter, options, datasource } = this.props;
|
||||
renderSubjectOptionLabel(option) {
|
||||
return <FilterDefinitionOption option={option} />;
|
||||
}
|
||||
|
||||
let subjectSelectProps = {
|
||||
value: adhocFilter.subject ? { value: adhocFilter.subject } : undefined,
|
||||
renderSubjectOptionValue({ value }) {
|
||||
return <span>{value}</span>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { adhocFilter, options: columns, datasource } = this.props;
|
||||
const { subject, operator, comparator } = adhocFilter;
|
||||
const subjectSelectProps = {
|
||||
options: columns,
|
||||
value: subject ? { value: subject } : undefined,
|
||||
onChange: this.onSubjectChange,
|
||||
optionRenderer: VirtualizedRendererWrap(option => (
|
||||
<FilterDefinitionOption option={option} />
|
||||
)),
|
||||
valueRenderer: option => <span>{option.value}</span>,
|
||||
optionRenderer: this.renderSubjectOptionLabel,
|
||||
valueRenderer: this.renderSubjectOptionValue,
|
||||
valueKey: 'filterOptionName',
|
||||
noResultsText: t(
|
||||
'No such column found. To filter on a metric, try the Custom SQL tab.',
|
||||
|
@ -288,80 +301,76 @@ export default class AdhocFilterEditPopoverSimpleTabContent extends React.Compon
|
|||
};
|
||||
|
||||
if (datasource.type === 'druid') {
|
||||
subjectSelectProps = {
|
||||
...subjectSelectProps,
|
||||
placeholder: t('%s column(s) and metric(s)', options.length),
|
||||
options,
|
||||
};
|
||||
subjectSelectProps.placeholder = t(
|
||||
'%s column(s) and metric(s)',
|
||||
columns.length,
|
||||
);
|
||||
} else {
|
||||
// we cannot support simple ad-hoc filters for metrics because we don't know what type
|
||||
// the value should be cast to (without knowing the output type of the aggregate, which
|
||||
// becomes a rather complicated problem)
|
||||
subjectSelectProps = {
|
||||
...subjectSelectProps,
|
||||
placeholder:
|
||||
adhocFilter.clause === CLAUSES.WHERE
|
||||
? t('%s column(s)', options.length)
|
||||
: t('To filter on a metric, use Custom SQL tab.'),
|
||||
options: options.filter(option => option.column_name),
|
||||
};
|
||||
subjectSelectProps.placeholder =
|
||||
adhocFilter.clause === CLAUSES.WHERE
|
||||
? t('%s column(s)', columns.length)
|
||||
: t('To filter on a metric, use Custom SQL tab.');
|
||||
// make sure options have `column_name`
|
||||
subjectSelectProps.options = columns.filter(option => option.column_name);
|
||||
}
|
||||
|
||||
const operatorSelectProps = {
|
||||
placeholder: t('%s operators(s)', Object.keys(OPERATORS).length),
|
||||
options: Object.keys(OPERATORS)
|
||||
.filter(operator =>
|
||||
this.isOperatorRelevant(operator, adhocFilter.subject),
|
||||
)
|
||||
.map(operator => ({
|
||||
operator,
|
||||
label: translateOperator(operator),
|
||||
value: operator,
|
||||
})),
|
||||
value: adhocFilter.operator,
|
||||
placeholder: t('%s operators(s)', OPERATORS_OPTIONS.length),
|
||||
// like AGGREGTES_OPTIONS, operator options are string
|
||||
options: OPERATORS_OPTIONS.filter(op =>
|
||||
this.isOperatorRelevant(op, subject),
|
||||
),
|
||||
value: operator,
|
||||
onChange: this.onOperatorChange,
|
||||
optionRenderer: VirtualizedRendererWrap(operator =>
|
||||
translateOperator(operator.operator),
|
||||
),
|
||||
valueRenderer: operator => (
|
||||
<span>{translateOperator(operator.operator)}</span>
|
||||
),
|
||||
valueKey: 'operator',
|
||||
getOptionLabel: translateOperator,
|
||||
};
|
||||
|
||||
return (
|
||||
<span>
|
||||
<FormGroup className="adhoc-filter-simple-column-dropdown">
|
||||
<OnPasteSelect {...this.selectProps} {...subjectSelectProps} />
|
||||
<Select
|
||||
{...this.selectProps}
|
||||
{...subjectSelectProps}
|
||||
name="filter-column"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<OnPasteSelect {...this.selectProps} {...operatorSelectProps} />
|
||||
<Select
|
||||
{...this.selectProps}
|
||||
{...operatorSelectProps}
|
||||
name="filter-operator"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup data-test="adhoc-filter-simple-value">
|
||||
{MULTI_OPERATORS.indexOf(adhocFilter.operator) >= 0 ||
|
||||
{MULTI_OPERATORS.has(operator) ||
|
||||
this.state.suggestions.length > 0 ? (
|
||||
<SelectControl
|
||||
multi={MULTI_OPERATORS.indexOf(adhocFilter.operator) >= 0}
|
||||
name="filter-value"
|
||||
autoFocus
|
||||
freeForm
|
||||
name="filter-comparator-value"
|
||||
value={adhocFilter.comparator}
|
||||
multi={MULTI_OPERATORS.has(operator)}
|
||||
value={comparator}
|
||||
isLoading={this.state.loading}
|
||||
choices={this.state.suggestions}
|
||||
onChange={this.onComparatorChange}
|
||||
showHeader={false}
|
||||
noResultsText={t('type a value here')}
|
||||
refFunc={this.multiComparatorRef}
|
||||
disabled={DISABLE_INPUT_OPERATORS.includes(adhocFilter.operator)}
|
||||
selectRef={this.multiComparatorRef}
|
||||
disabled={DISABLE_INPUT_OPERATORS.includes(operator)}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
name="filter-value"
|
||||
ref={this.focusComparator}
|
||||
type="text"
|
||||
onChange={this.onInputComparatorChange}
|
||||
value={adhocFilter.comparator || ''}
|
||||
value={comparator}
|
||||
className="form-control input-sm"
|
||||
placeholder={t('Filter value')}
|
||||
disabled={DISABLE_INPUT_OPERATORS.includes(adhocFilter.operator)}
|
||||
disabled={DISABLE_INPUT_OPERATORS.includes(operator)}
|
||||
/>
|
||||
)}
|
||||
</FormGroup>
|
||||
|
|
|
@ -24,15 +24,13 @@ import 'brace/mode/sql';
|
|||
import 'brace/theme/github';
|
||||
import 'brace/ext/language_tools';
|
||||
import { FormGroup } from 'react-bootstrap';
|
||||
import VirtualizedSelect from 'react-virtualized-select';
|
||||
import Select from 'src/components/Select';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import sqlKeywords from '../../SqlLab/utils/sqlKeywords';
|
||||
import AdhocFilter, { EXPRESSION_TYPES, CLAUSES } from '../AdhocFilter';
|
||||
import adhocMetricType from '../propTypes/adhocMetricType';
|
||||
import columnType from '../propTypes/columnType';
|
||||
import OnPasteSelect from '../../components/OnPasteSelect';
|
||||
import VirtualizedRendererWrap from '../../components/VirtualizedRendererWrap';
|
||||
|
||||
const propTypes = {
|
||||
adhocFilter: PropTypes.instanceOf(AdhocFilter).isRequired,
|
||||
|
@ -59,12 +57,11 @@ export default class AdhocFilterEditPopoverSqlTabContent extends React.Component
|
|||
this.handleAceEditorRef = this.handleAceEditorRef.bind(this);
|
||||
|
||||
this.selectProps = {
|
||||
multi: false,
|
||||
isMulti: false,
|
||||
name: 'select-column',
|
||||
labelKey: 'label',
|
||||
autosize: false,
|
||||
clearable: false,
|
||||
selectWrap: VirtualizedSelect,
|
||||
};
|
||||
|
||||
if (langTools) {
|
||||
|
@ -123,18 +120,15 @@ export default class AdhocFilterEditPopoverSqlTabContent extends React.Component
|
|||
|
||||
const clauseSelectProps = {
|
||||
placeholder: t('choose WHERE or HAVING...'),
|
||||
options: Object.keys(CLAUSES).map(clause => ({ clause })),
|
||||
options: Object.keys(CLAUSES),
|
||||
value: adhocFilter.clause,
|
||||
onChange: this.onSqlExpressionClauseChange,
|
||||
optionRenderer: VirtualizedRendererWrap(clause => clause.clause),
|
||||
valueRenderer: clause => <span>{clause.clause}</span>,
|
||||
valueKey: 'clause',
|
||||
};
|
||||
|
||||
return (
|
||||
<span>
|
||||
<FormGroup className="filter-edit-clause-section">
|
||||
<OnPasteSelect
|
||||
<Select
|
||||
{...this.selectProps}
|
||||
{...clauseSelectProps}
|
||||
className="filter-edit-clause-dropdown"
|
||||
|
|
|
@ -56,6 +56,10 @@ export default class AdhocFilterOption extends React.PureComponent {
|
|||
}
|
||||
|
||||
onOverlayEntered() {
|
||||
// isNew is used to indicate whether to automatically open the overlay
|
||||
// once the overlay has been opened, the metric/filter will never be
|
||||
// considered new again.
|
||||
this.props.adhocFilter.isNew = false;
|
||||
this.setState({ overlayShown: true });
|
||||
}
|
||||
|
||||
|
@ -63,10 +67,6 @@ export default class AdhocFilterOption extends React.PureComponent {
|
|||
this.setState({ overlayShown: false });
|
||||
}
|
||||
|
||||
onMouseDown(e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
closeFilterEditOverlay() {
|
||||
this.refs.overlay.hide();
|
||||
}
|
||||
|
@ -85,43 +85,38 @@ export default class AdhocFilterOption extends React.PureComponent {
|
|||
/>
|
||||
);
|
||||
return (
|
||||
<OverlayTrigger
|
||||
ref="overlay"
|
||||
placement="right"
|
||||
trigger="click"
|
||||
disabled
|
||||
overlay={overlay}
|
||||
rootClose
|
||||
shouldUpdatePosition
|
||||
onEntered={this.onOverlayEntered}
|
||||
onExited={this.onOverlayExited}
|
||||
>
|
||||
<div>
|
||||
{adhocFilter.isExtra && (
|
||||
<InfoTooltipWithTrigger
|
||||
icon="exclamation-triangle"
|
||||
placement="top"
|
||||
className="m-r-5 text-muted"
|
||||
tooltip={t(`
|
||||
<div onMouseDownCapture={e => e.stopPropagation()}>
|
||||
{adhocFilter.isExtra && (
|
||||
<InfoTooltipWithTrigger
|
||||
icon="exclamation-triangle"
|
||||
placement="top"
|
||||
className="m-r-5 text-muted"
|
||||
tooltip={t(`
|
||||
This filter was inherited from the dashboard's context.
|
||||
It won't be saved when saving the chart.
|
||||
`)}
|
||||
/>
|
||||
)}
|
||||
<OverlayTrigger
|
||||
ref="overlay"
|
||||
placement="right"
|
||||
trigger="click"
|
||||
disabled
|
||||
overlay={overlay}
|
||||
rootClose
|
||||
shouldUpdatePosition
|
||||
defaultOverlayShown={adhocFilter.isNew}
|
||||
onEntered={this.onOverlayEntered}
|
||||
onExited={this.onOverlayExited}
|
||||
>
|
||||
<Label className="option-label adhoc-option adhoc-filter-option">
|
||||
{adhocFilter.getDefaultLabel()}
|
||||
<i
|
||||
className={`glyphicon glyphicon-triangle-right adhoc-label-arrow`}
|
||||
/>
|
||||
)}
|
||||
<Label className="adhoc-filter-option">
|
||||
<div onMouseDownCapture={this.onMouseDown}>
|
||||
<span className="m-r-5 option-label">
|
||||
{adhocFilter.getDefaultLabel()}
|
||||
<i
|
||||
className={`glyphicon glyphicon-triangle-${
|
||||
this.state.overlayShown ? 'left' : 'right'
|
||||
} adhoc-label-arrow`}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
Tab,
|
||||
Tabs,
|
||||
} from 'react-bootstrap';
|
||||
import VirtualizedSelect from 'react-virtualized-select';
|
||||
import Select from 'src/components/Select';
|
||||
import ace from 'brace';
|
||||
import AceEditor from 'react-ace';
|
||||
import 'brace/mode/sql';
|
||||
|
@ -34,9 +34,7 @@ import 'brace/theme/github';
|
|||
import 'brace/ext/language_tools';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import { AGGREGATES } from '../constants';
|
||||
import VirtualizedRendererWrap from '../../components/VirtualizedRendererWrap';
|
||||
import OnPasteSelect from '../../components/OnPasteSelect';
|
||||
import { AGGREGATES, AGGREGATES_OPTIONS } from '../constants';
|
||||
import AdhocMetricEditPopoverTitle from './AdhocMetricEditPopoverTitle';
|
||||
import columnType from '../propTypes/columnType';
|
||||
import AdhocMetric, { EXPRESSION_TYPES } from '../AdhocMetric';
|
||||
|
@ -80,12 +78,10 @@ export default class AdhocMetricEditPopover extends React.Component {
|
|||
height: startingHeight,
|
||||
};
|
||||
this.selectProps = {
|
||||
multi: false,
|
||||
name: 'select-column',
|
||||
labelKey: 'label',
|
||||
isMulti: false,
|
||||
autosize: false,
|
||||
clearable: true,
|
||||
selectWrap: VirtualizedSelect,
|
||||
};
|
||||
if (langTools) {
|
||||
const words = sqlKeywords.concat(
|
||||
|
@ -129,7 +125,7 @@ export default class AdhocMetricEditPopover extends React.Component {
|
|||
// we construct this object explicitly to overwrite the value in the case aggregate is null
|
||||
this.setState({
|
||||
adhocMetric: this.state.adhocMetric.duplicateWith({
|
||||
aggregate: aggregate && aggregate.aggregate,
|
||||
aggregate,
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
}),
|
||||
});
|
||||
|
@ -189,6 +185,10 @@ export default class AdhocMetricEditPopover extends React.Component {
|
|||
setTimeout(() => this.aceEditorRef.editor.resize(), 0);
|
||||
}
|
||||
|
||||
renderColumnOption(option) {
|
||||
return <ColumnOption column={option} showType />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
adhocMetric: propsAdhocMetric,
|
||||
|
@ -209,26 +209,20 @@ export default class AdhocMetricEditPopover extends React.Component {
|
|||
(adhocMetric.column && adhocMetric.column.column_name) ||
|
||||
adhocMetric.inferSqlExpressionColumn(),
|
||||
onChange: this.onColumnChange,
|
||||
optionRenderer: VirtualizedRendererWrap(option => (
|
||||
<ColumnOption column={option} showType />
|
||||
)),
|
||||
valueRenderer: column => column.column_name,
|
||||
optionRenderer: this.renderColumnOption,
|
||||
valueKey: 'column_name',
|
||||
};
|
||||
|
||||
const aggregateSelectProps = {
|
||||
placeholder: t('%s aggregates(s)', Object.keys(AGGREGATES).length),
|
||||
options: Object.keys(AGGREGATES).map(aggregate => ({ aggregate })),
|
||||
placeholder: t('%s aggregates(s)', AGGREGATES_OPTIONS.length),
|
||||
options: AGGREGATES_OPTIONS,
|
||||
value: adhocMetric.aggregate || adhocMetric.inferSqlExpressionAggregate(),
|
||||
onChange: this.onAggregateChange,
|
||||
optionRenderer: VirtualizedRendererWrap(aggregate => aggregate.aggregate),
|
||||
valueRenderer: aggregate => aggregate.aggregate,
|
||||
valueKey: 'aggregate',
|
||||
};
|
||||
|
||||
if (this.props.datasourceType === 'druid') {
|
||||
aggregateSelectProps.options = aggregateSelectProps.options.filter(
|
||||
option => option.aggregate !== 'AVG',
|
||||
aggregate => aggregate !== 'AVG',
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -260,16 +254,21 @@ export default class AdhocMetricEditPopover extends React.Component {
|
|||
<ControlLabel>
|
||||
<strong>column</strong>
|
||||
</ControlLabel>
|
||||
<OnPasteSelect {...this.selectProps} {...columnSelectProps} />
|
||||
<Select
|
||||
name="select-column"
|
||||
{...this.selectProps}
|
||||
{...columnSelectProps}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<ControlLabel>
|
||||
<strong>aggregate</strong>
|
||||
</ControlLabel>
|
||||
<OnPasteSelect
|
||||
autoFocus
|
||||
<Select
|
||||
name="select-aggregate"
|
||||
{...this.selectProps}
|
||||
{...aggregateSelectProps}
|
||||
autoFocus
|
||||
/>
|
||||
</FormGroup>
|
||||
</Tab>
|
||||
|
|
|
@ -55,12 +55,6 @@ export default class AdhocMetricEditPopoverTitle extends React.Component {
|
|||
this.setState({ isEditable: false });
|
||||
}
|
||||
|
||||
refFunc(ref) {
|
||||
if (ref) {
|
||||
ref.focus();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { adhocMetric, onChange } = this.props;
|
||||
|
||||
|
@ -68,7 +62,16 @@ export default class AdhocMetricEditPopoverTitle extends React.Component {
|
|||
<Tooltip id="edit-metric-label-tooltip">Click to edit label</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
return this.state.isEditable ? (
|
||||
<FormControl
|
||||
className="metric-edit-popover-label-input"
|
||||
type="text"
|
||||
placeholder={adhocMetric.label}
|
||||
value={adhocMetric.hasCustomLabel ? adhocMetric.label : ''}
|
||||
autoFocus
|
||||
onChange={onChange}
|
||||
/>
|
||||
) : (
|
||||
<OverlayTrigger
|
||||
placement="top"
|
||||
overlay={editPrompt}
|
||||
|
@ -78,25 +81,14 @@ export default class AdhocMetricEditPopoverTitle extends React.Component {
|
|||
onBlur={this.onBlur}
|
||||
className="AdhocMetricEditPopoverTitle"
|
||||
>
|
||||
{this.state.isEditable ? (
|
||||
<FormControl
|
||||
className="metric-edit-popover-label-input"
|
||||
type="text"
|
||||
placeholder={adhocMetric.label}
|
||||
value={adhocMetric.hasCustomLabel ? adhocMetric.label : ''}
|
||||
onChange={onChange}
|
||||
inputRef={this.refFunc}
|
||||
<span className="inline-editable">
|
||||
{adhocMetric.hasCustomLabel ? adhocMetric.label : 'My Metric'}
|
||||
|
||||
<i
|
||||
className="fa fa-pencil"
|
||||
style={{ color: this.state.isHovered ? 'black' : 'grey' }}
|
||||
/>
|
||||
) : (
|
||||
<span className="inline-editable">
|
||||
{adhocMetric.hasCustomLabel ? adhocMetric.label : 'My Metric'}
|
||||
|
||||
<i
|
||||
className="fa fa-pencil"
|
||||
style={{ color: this.state.isHovered ? 'black' : 'grey' }}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Label, OverlayTrigger } from 'react-bootstrap';
|
||||
import { thresholdScott } from 'd3-array';
|
||||
|
||||
import AdhocMetricEditPopover from './AdhocMetricEditPopover';
|
||||
import AdhocMetric from '../AdhocMetric';
|
||||
|
@ -40,6 +41,7 @@ export default class AdhocMetricOption extends React.PureComponent {
|
|||
this.onOverlayExited = this.onOverlayExited.bind(this);
|
||||
this.onPopoverResize = this.onPopoverResize.bind(this);
|
||||
this.state = { overlayShown: false };
|
||||
this.overlay = null;
|
||||
}
|
||||
|
||||
onPopoverResize() {
|
||||
|
@ -47,6 +49,10 @@ export default class AdhocMetricOption extends React.PureComponent {
|
|||
}
|
||||
|
||||
onOverlayEntered() {
|
||||
// isNew is used to indicate whether to automatically open the overlay
|
||||
// once the overlay has been opened, the metric/filter will never be
|
||||
// considered new again.
|
||||
this.props.adhocMetric.isNew = false;
|
||||
this.setState({ overlayShown: false });
|
||||
}
|
||||
|
||||
|
@ -55,12 +61,12 @@ export default class AdhocMetricOption extends React.PureComponent {
|
|||
}
|
||||
|
||||
closeMetricEditOverlay() {
|
||||
this.refs.overlay.hide();
|
||||
this.overlay.hide();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { adhocMetric } = this.props;
|
||||
const overlay = (
|
||||
const overlayContent = (
|
||||
<AdhocMetricEditPopover
|
||||
onResize={this.onPopoverResize}
|
||||
adhocMetric={adhocMetric}
|
||||
|
@ -72,35 +78,32 @@ export default class AdhocMetricOption extends React.PureComponent {
|
|||
);
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
ref="overlay"
|
||||
placement="right"
|
||||
trigger="click"
|
||||
disabled
|
||||
overlay={overlay}
|
||||
rootClose
|
||||
shouldUpdatePosition
|
||||
defaultOverlayShown={!adhocMetric.fromFormData}
|
||||
onEntered={this.onOverlayEntered}
|
||||
onExited={this.onOverlayExited}
|
||||
<div
|
||||
className="metric-option"
|
||||
onMouseDownCapture={e => e.stopPropagation()}
|
||||
>
|
||||
<Label style={{ margin: this.props.multi ? 0 : 3, cursor: 'pointer' }}>
|
||||
<div
|
||||
onMouseDownCapture={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<span className="m-r-5 option-label">
|
||||
{adhocMetric.label}
|
||||
<i
|
||||
className={`glyphicon glyphicon-triangle-${
|
||||
this.state.overlayShown ? 'left' : 'right'
|
||||
} adhoc-label-arrow`}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
</OverlayTrigger>
|
||||
<OverlayTrigger
|
||||
ref={ref => {
|
||||
this.overlay = ref;
|
||||
}}
|
||||
placement="right"
|
||||
trigger="click"
|
||||
disabled
|
||||
overlay={overlayContent}
|
||||
rootClose
|
||||
shouldUpdatePosition
|
||||
defaultOverlayShown={adhocMetric.isNew}
|
||||
onEntered={this.onOverlayEntered}
|
||||
onExited={this.onOverlayExited}
|
||||
>
|
||||
<Label className="option-label adhoc-option">
|
||||
{adhocMetric.label}
|
||||
<i
|
||||
className={`glyphicon glyphicon-triangle-right adhoc-label-arrow`}
|
||||
/>
|
||||
</Label>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,11 +27,14 @@ const propTypes = {
|
|||
showType: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default function AdhocMetricStaticOption({ adhocMetric, showType }) {
|
||||
export default function AdhocMetricStaticOption({
|
||||
adhocMetric,
|
||||
showType = false,
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
{showType && <ColumnTypeLabel type="expression" />}
|
||||
<span className="m-r-5 option-label">{adhocMetric.label}</span>
|
||||
<span className="option-label">{adhocMetric.label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -147,8 +147,8 @@ export class ExploreChartHeader extends React.PureComponent {
|
|||
<div className="pull-right">
|
||||
{chartFinished && queryResponse && (
|
||||
<RowCountLabel
|
||||
rowcount={queryResponse.rowcount}
|
||||
limit={formData.row_limit}
|
||||
rowcount={Number(queryResponse.rowcount) || 0}
|
||||
limit={Number(formData.row_limit) || 0}
|
||||
/>
|
||||
)}
|
||||
{chartFinished && queryResponse && queryResponse.is_cached && (
|
||||
|
|
|
@ -38,7 +38,7 @@ export default function FilterDefinitionOption({ option }) {
|
|||
return (
|
||||
<div>
|
||||
<ColumnTypeLabel type="expression" />
|
||||
<span className="m-r-5 option-label">{option.saved_metric_name}</span>
|
||||
<span className="option-label">{option.saved_metric_name}</span>
|
||||
</div>
|
||||
);
|
||||
} else if (option.column_name) {
|
||||
|
|
|
@ -43,7 +43,8 @@ export default function MetricDefinitionValue({
|
|||
}) {
|
||||
if (option.metric_name) {
|
||||
return <MetricOption metric={option} />;
|
||||
} else if (option instanceof AdhocMetric) {
|
||||
}
|
||||
if (option instanceof AdhocMetric) {
|
||||
return (
|
||||
<AdhocMetricOption
|
||||
adhocMetric={option}
|
||||
|
|
|
@ -28,7 +28,8 @@ import {
|
|||
} from 'react-bootstrap';
|
||||
// @ts-ignore
|
||||
import Dialog from 'react-bootstrap-dialog';
|
||||
import { Async as SelectAsync, Option } from 'react-select';
|
||||
import { OptionsType } from 'react-select/src/types';
|
||||
import { AsyncSelect } from 'src/components/Select';
|
||||
import rison from 'rison';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { SupersetClient, Json } from '@superset-ui/connection';
|
||||
|
@ -48,6 +49,11 @@ type InternalProps = {
|
|||
onSave: (chart: Chart) => void;
|
||||
};
|
||||
|
||||
type OwnerOption = {
|
||||
label: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
export type WrapperProps = InternalProps & {
|
||||
show: boolean;
|
||||
animation?: boolean; // for the modal
|
||||
|
@ -78,7 +84,7 @@ function PropertiesModal({ slice, onHide, onSave }: InternalProps) {
|
|||
const [cacheTimeout, setCacheTimeout] = useState(
|
||||
slice.cache_timeout != null ? slice.cache_timeout : '',
|
||||
);
|
||||
const [owners, setOwners] = useState<Option[] | null>(null);
|
||||
const [owners, setOwners] = useState<OptionsType<OwnerOption> | null>(null);
|
||||
|
||||
function showError({ error, statusText }: any) {
|
||||
errorDialog.current.show({
|
||||
|
@ -120,15 +126,14 @@ function PropertiesModal({ slice, onHide, onSave }: InternalProps) {
|
|||
}).then(
|
||||
response => {
|
||||
const { result } = response.json as Json;
|
||||
const options = result.map((item: any) => ({
|
||||
return result.map((item: any) => ({
|
||||
value: item.value,
|
||||
label: item.text,
|
||||
}));
|
||||
return { options };
|
||||
},
|
||||
badResponse => {
|
||||
getClientErrorObject(badResponse).then(showError);
|
||||
return { options: [] };
|
||||
return [];
|
||||
},
|
||||
);
|
||||
};
|
||||
|
@ -239,14 +244,16 @@ function PropertiesModal({ slice, onHide, onSave }: InternalProps) {
|
|||
<label className="control-label" htmlFor="owners">
|
||||
{t('Owners')}
|
||||
</label>
|
||||
<SelectAsync
|
||||
multi
|
||||
<AsyncSelect
|
||||
isMulti
|
||||
name="owners"
|
||||
value={owners || []}
|
||||
loadOptions={loadOptions}
|
||||
defaultOptions // load options on render
|
||||
cacheOptions
|
||||
onChange={setOwners}
|
||||
disabled={!owners}
|
||||
filterOption={() => true} // options are filtered at the api
|
||||
filterOption={null} // options are filtered at the api
|
||||
/>
|
||||
<p className="help-block">
|
||||
{t(
|
||||
|
|
|
@ -21,7 +21,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { Modal, Alert, Button, Radio } from 'react-bootstrap';
|
||||
import Select from 'react-select';
|
||||
import Select from 'src/components/Select';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import { supersetURL } from '../../utils/common';
|
||||
|
|
|
@ -18,11 +18,12 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import VirtualizedSelect from 'react-virtualized-select';
|
||||
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { SupersetClient } from '@superset-ui/connection';
|
||||
|
||||
import OnPasteSelect from 'src/components/Select/OnPasteSelect';
|
||||
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import adhocFilterType from '../../propTypes/adhocFilterType';
|
||||
import adhocMetricType from '../../propTypes/adhocMetricType';
|
||||
|
@ -31,8 +32,6 @@ import columnType from '../../propTypes/columnType';
|
|||
import AdhocFilter, { CLAUSES, EXPRESSION_TYPES } from '../../AdhocFilter';
|
||||
import AdhocMetric from '../../AdhocMetric';
|
||||
import { OPERATORS } from '../../constants';
|
||||
import VirtualizedRendererWrap from '../../../components/VirtualizedRendererWrap';
|
||||
import OnPasteSelect from '../../../components/OnPasteSelect';
|
||||
import AdhocFilterOption from '../AdhocFilterOption';
|
||||
import FilterDefinitionOption from '../FilterDefinitionOption';
|
||||
|
||||
|
@ -75,9 +74,7 @@ export default class AdhocFilterControl extends React.Component {
|
|||
isDictionaryForAdhocFilter(filter) ? new AdhocFilter(filter) : filter,
|
||||
);
|
||||
|
||||
this.optionRenderer = VirtualizedRendererWrap(option => (
|
||||
<FilterDefinitionOption option={option} />
|
||||
));
|
||||
this.optionRenderer = option => <FilterDefinitionOption option={option} />;
|
||||
this.valueRenderer = adhocFilter => (
|
||||
<AdhocFilterOption
|
||||
adhocFilter={adhocFilter}
|
||||
|
@ -165,52 +162,59 @@ export default class AdhocFilterControl extends React.Component {
|
|||
}
|
||||
|
||||
onChange(opts) {
|
||||
this.props.onChange(
|
||||
opts
|
||||
.map(option => {
|
||||
if (option.saved_metric_name) {
|
||||
return new AdhocFilter({
|
||||
expressionType:
|
||||
this.props.datasource.type === 'druid'
|
||||
? EXPRESSION_TYPES.SIMPLE
|
||||
: EXPRESSION_TYPES.SQL,
|
||||
subject:
|
||||
this.props.datasource.type === 'druid'
|
||||
? option.saved_metric_name
|
||||
: this.getMetricExpression(option.saved_metric_name),
|
||||
operator: OPERATORS['>'],
|
||||
comparator: 0,
|
||||
clause: CLAUSES.HAVING,
|
||||
});
|
||||
} else if (option.label) {
|
||||
return new AdhocFilter({
|
||||
expressionType:
|
||||
this.props.datasource.type === 'druid'
|
||||
? EXPRESSION_TYPES.SIMPLE
|
||||
: EXPRESSION_TYPES.SQL,
|
||||
subject:
|
||||
this.props.datasource.type === 'druid'
|
||||
? option.label
|
||||
: new AdhocMetric(option).translateToSql(),
|
||||
operator: OPERATORS['>'],
|
||||
comparator: 0,
|
||||
clause: CLAUSES.HAVING,
|
||||
});
|
||||
} else if (option.column_name) {
|
||||
return new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: option.column_name,
|
||||
operator: OPERATORS['=='],
|
||||
comparator: '',
|
||||
clause: CLAUSES.WHERE,
|
||||
});
|
||||
} else if (option instanceof AdhocFilter) {
|
||||
return option;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(option => option),
|
||||
);
|
||||
const options = (opts || [])
|
||||
.map(option => {
|
||||
// already a AdhocFilter, skip
|
||||
if (option instanceof AdhocFilter) {
|
||||
return option;
|
||||
}
|
||||
// via datasource saved metric
|
||||
if (option.saved_metric_name) {
|
||||
return new AdhocFilter({
|
||||
expressionType:
|
||||
this.props.datasource.type === 'druid'
|
||||
? EXPRESSION_TYPES.SIMPLE
|
||||
: EXPRESSION_TYPES.SQL,
|
||||
subject:
|
||||
this.props.datasource.type === 'druid'
|
||||
? option.saved_metric_name
|
||||
: this.getMetricExpression(option.saved_metric_name),
|
||||
operator: OPERATORS['>'],
|
||||
comparator: 0,
|
||||
clause: CLAUSES.HAVING,
|
||||
});
|
||||
}
|
||||
// has a custom label
|
||||
if (option.label) {
|
||||
return new AdhocFilter({
|
||||
expressionType:
|
||||
this.props.datasource.type === 'druid'
|
||||
? EXPRESSION_TYPES.SIMPLE
|
||||
: EXPRESSION_TYPES.SQL,
|
||||
subject:
|
||||
this.props.datasource.type === 'druid'
|
||||
? option.label
|
||||
: new AdhocMetric(option).translateToSql(),
|
||||
operator: OPERATORS['>'],
|
||||
comparator: 0,
|
||||
clause: CLAUSES.HAVING,
|
||||
});
|
||||
}
|
||||
// add a new filter item
|
||||
if (option.column_name) {
|
||||
return new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: option.column_name,
|
||||
operator: OPERATORS['=='],
|
||||
comparator: '',
|
||||
clause: CLAUSES.WHERE,
|
||||
isNew: true,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(option => option);
|
||||
this.props.onChange(options);
|
||||
}
|
||||
|
||||
getMetricExpression(savedMetricName) {
|
||||
|
@ -263,7 +267,7 @@ export default class AdhocFilterControl extends React.Component {
|
|||
<div className="metrics-select">
|
||||
<ControlHeader {...this.props} />
|
||||
<OnPasteSelect
|
||||
multi
|
||||
isMulti
|
||||
name={`select-${this.props.name}`}
|
||||
placeholder={t('choose a column or metric')}
|
||||
options={this.state.options}
|
||||
|
@ -275,7 +279,6 @@ export default class AdhocFilterControl extends React.Component {
|
|||
onChange={this.onChange}
|
||||
optionRenderer={this.optionRenderer}
|
||||
valueRenderer={this.valueRenderer}
|
||||
selectWrap={VirtualizedSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isFunction } from 'lodash';
|
||||
import { Creatable } from 'react-select';
|
||||
import { CreatableSelect } from 'src/components/Select';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import TooltipWrapper from '../../../components/TooltipWrapper';
|
||||
|
||||
|
@ -119,7 +119,7 @@ export default class ColorSchemeControl extends React.PureComponent {
|
|||
return (
|
||||
<div>
|
||||
<ControlHeader {...this.props} />
|
||||
<Creatable {...selectProps} />
|
||||
<CreatableSelect {...selectProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,13 +18,12 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import VirtualizedSelect from 'react-virtualized-select';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import OnPasteSelect from 'src/components/Select/OnPasteSelect';
|
||||
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import VirtualizedRendererWrap from '../../../components/VirtualizedRendererWrap';
|
||||
import OnPasteSelect from '../../../components/OnPasteSelect';
|
||||
import MetricDefinitionOption from '../MetricDefinitionOption';
|
||||
import MetricDefinitionValue from '../MetricDefinitionValue';
|
||||
import AdhocMetric from '../../AdhocMetric';
|
||||
|
@ -33,6 +32,7 @@ import savedMetricType from '../../propTypes/savedMetricType';
|
|||
import adhocMetricType from '../../propTypes/adhocMetricType';
|
||||
import {
|
||||
AGGREGATES,
|
||||
AGGREGATES_OPTIONS,
|
||||
sqlaAutoGeneratedMetricNameRegex,
|
||||
druidAutoGeneratedMetricRegex,
|
||||
} from '../../constants';
|
||||
|
@ -141,10 +141,7 @@ export default class MetricsControl extends React.PureComponent {
|
|||
this.optionsForSelect = this.optionsForSelect.bind(this);
|
||||
this.selectFilterOption = this.selectFilterOption.bind(this);
|
||||
this.isAutoGeneratedMetric = this.isAutoGeneratedMetric.bind(this);
|
||||
this.optionRenderer = VirtualizedRendererWrap(
|
||||
option => <MetricDefinitionOption option={option} />,
|
||||
{ ignoreAutogeneratedMetrics: true },
|
||||
);
|
||||
this.optionRenderer = option => <MetricDefinitionOption option={option} />;
|
||||
this.valueRenderer = option => (
|
||||
<MetricDefinitionValue
|
||||
option={option}
|
||||
|
@ -154,10 +151,12 @@ export default class MetricsControl extends React.PureComponent {
|
|||
datasourceType={this.props.datasourceType}
|
||||
/>
|
||||
);
|
||||
this.refFunc = ref => {
|
||||
this.select = null;
|
||||
this.selectRef = ref => {
|
||||
if (ref) {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
this.select = ref._selectRef;
|
||||
this.select = ref.select;
|
||||
} else {
|
||||
this.select = null;
|
||||
}
|
||||
};
|
||||
this.state = {
|
||||
|
@ -207,60 +206,66 @@ export default class MetricsControl extends React.PureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
let transformedOpts = opts;
|
||||
if (!this.props.multi) {
|
||||
transformedOpts = [opts].filter(option => option);
|
||||
let transformedOpts;
|
||||
if (Array.isArray(opts)) {
|
||||
transformedOpts = opts;
|
||||
} else {
|
||||
transformedOpts = opts ? [opts] : [];
|
||||
}
|
||||
let optionValues = transformedOpts
|
||||
const optionValues = transformedOpts
|
||||
.map(option => {
|
||||
// pre-defined metric
|
||||
if (option.metric_name) {
|
||||
return option.metric_name;
|
||||
} else if (option.column_name) {
|
||||
}
|
||||
// adding a new adhoc metric
|
||||
if (option.column_name) {
|
||||
const clearedAggregate = this.clearedAggregateInInput;
|
||||
this.clearedAggregateInInput = null;
|
||||
return new AdhocMetric({
|
||||
isNew: true,
|
||||
column: option,
|
||||
aggregate: clearedAggregate || getDefaultAggregateForColumn(option),
|
||||
});
|
||||
} else if (option instanceof AdhocMetric) {
|
||||
}
|
||||
// existing adhoc metric or custom SQL metric
|
||||
if (option instanceof AdhocMetric) {
|
||||
return option;
|
||||
} else if (option.aggregate_name) {
|
||||
}
|
||||
// start with selecting an aggregate function
|
||||
if (option.aggregate_name && this.select) {
|
||||
const newValue = `${option.aggregate_name}()`;
|
||||
this.select.setInputValue(newValue);
|
||||
this.select.handleInputChange({ target: { value: newValue } });
|
||||
this.select.inputRef.value = newValue;
|
||||
this.select.handleInputChange({ currentTarget: { value: newValue } });
|
||||
// we need to set a timeout here or the selectionWill be overwritten
|
||||
// by some browsers (e.g. Chrome)
|
||||
setTimeout(() => {
|
||||
this.select.input.input.selectionStart = newValue.length - 1;
|
||||
this.select.input.input.selectionEnd = newValue.length - 1;
|
||||
}, 0);
|
||||
return null;
|
||||
this.select.focusInput();
|
||||
this.select.inputRef.selectionStart = newValue.length - 1;
|
||||
this.select.inputRef.selectionEnd = newValue.length - 1;
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(option => option);
|
||||
if (!this.props.multi) {
|
||||
optionValues = optionValues[0];
|
||||
}
|
||||
this.props.onChange(optionValues);
|
||||
this.props.onChange(this.props.multi ? optionValues : optionValues[0]);
|
||||
}
|
||||
|
||||
checkIfAggregateInInput(input) {
|
||||
let nextState = { aggregateInInput: null };
|
||||
Object.keys(AGGREGATES).forEach(aggregate => {
|
||||
if (input.toLowerCase().startsWith(aggregate.toLowerCase() + '(')) {
|
||||
nextState = { aggregateInInput: aggregate };
|
||||
}
|
||||
});
|
||||
const lowercaseInput = input.toLowerCase();
|
||||
const aggregateInInput =
|
||||
AGGREGATES_OPTIONS.find(x =>
|
||||
lowercaseInput.startsWith(`${x.toLowerCase()}(`),
|
||||
) || null;
|
||||
this.clearedAggregateInInput = this.state.aggregateInInput;
|
||||
this.setState(nextState);
|
||||
this.setState({ aggregateInInput });
|
||||
}
|
||||
|
||||
optionsForSelect(props) {
|
||||
const { columns, savedMetrics } = props;
|
||||
const aggregates =
|
||||
columns && columns.length
|
||||
? Object.keys(AGGREGATES).map(aggregate => ({
|
||||
? AGGREGATES_OPTIONS.map(aggregate => ({
|
||||
aggregate_name: aggregate,
|
||||
}))
|
||||
: [];
|
||||
|
@ -292,7 +297,7 @@ export default class MetricsControl extends React.PureComponent {
|
|||
return sqlaAutoGeneratedMetricNameRegex.test(savedMetric.metric_name);
|
||||
}
|
||||
|
||||
selectFilterOption(option, filterValue) {
|
||||
selectFilterOption({ data: option }, filterValue) {
|
||||
if (this.state.aggregateInInput) {
|
||||
let endIndex = filterValue.length;
|
||||
if (filterValue.endsWith(')')) {
|
||||
|
@ -324,11 +329,11 @@ export default class MetricsControl extends React.PureComponent {
|
|||
<div className="metrics-select">
|
||||
<ControlHeader {...this.props} />
|
||||
<OnPasteSelect
|
||||
multi={this.props.multi}
|
||||
isMulti={this.props.multi}
|
||||
name={`select-${this.props.name}`}
|
||||
placeholder={t('choose a column or aggregate function')}
|
||||
options={this.state.options}
|
||||
value={this.props.multi ? this.state.value : this.state.value[0]}
|
||||
value={this.state.value}
|
||||
labelKey="label"
|
||||
valueKey="optionName"
|
||||
clearable={this.props.clearable}
|
||||
|
@ -336,10 +341,10 @@ export default class MetricsControl extends React.PureComponent {
|
|||
onChange={this.onChange}
|
||||
optionRenderer={this.optionRenderer}
|
||||
valueRenderer={this.valueRenderer}
|
||||
valueRenderedAsLabel
|
||||
onInputChange={this.checkIfAggregateInInput}
|
||||
filterOption={this.selectFilterOption}
|
||||
refFunc={this.refFunc}
|
||||
selectWrap={VirtualizedSelect}
|
||||
selectRef={this.selectRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -18,15 +18,12 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import VirtualizedSelect from 'react-virtualized-select';
|
||||
import Select, { Creatable } from 'react-select';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import VirtualizedRendererWrap from '../../../components/VirtualizedRendererWrap';
|
||||
import OnPasteSelect from '../../../components/OnPasteSelect';
|
||||
import { Select, CreatableSelect, OnPasteSelect } from 'src/components/Select';
|
||||
import ControlHeader from 'src/explore/components/ControlHeader';
|
||||
|
||||
const propTypes = {
|
||||
autoFocus: PropTypes.bool,
|
||||
choices: PropTypes.array,
|
||||
clearable: PropTypes.bool,
|
||||
description: PropTypes.string,
|
||||
|
@ -35,6 +32,7 @@ const propTypes = {
|
|||
isLoading: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
multi: PropTypes.bool,
|
||||
isMulti: PropTypes.bool,
|
||||
allowAll: PropTypes.bool,
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
|
@ -51,13 +49,14 @@ const propTypes = {
|
|||
options: PropTypes.array,
|
||||
placeholder: PropTypes.string,
|
||||
noResultsText: PropTypes.string,
|
||||
refFunc: PropTypes.func,
|
||||
selectRef: PropTypes.func,
|
||||
filterOption: PropTypes.func,
|
||||
promptTextCreator: PropTypes.func,
|
||||
commaChoosesOption: PropTypes.bool,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
autoFocus: false,
|
||||
choices: [],
|
||||
clearable: true,
|
||||
description: null,
|
||||
|
@ -69,8 +68,6 @@ const defaultProps = {
|
|||
onChange: () => {},
|
||||
onFocus: () => {},
|
||||
showHeader: true,
|
||||
optionRenderer: opt => opt.label,
|
||||
valueRenderer: opt => opt.label,
|
||||
valueKey: 'value',
|
||||
noResultsText: t('No results found'),
|
||||
promptTextCreator: label => `Create Option ${label}`,
|
||||
|
@ -84,6 +81,9 @@ export default class SelectControl extends React.PureComponent {
|
|||
this.state = { options: this.getOptions(props) };
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.createMetaSelectAllOption = this.createMetaSelectAllOption.bind(this);
|
||||
this.select = null; // pointer to the react-select instance
|
||||
this.getSelectRef = this.getSelectRef.bind(this);
|
||||
this.handleKeyDownForCreate = this.handleKeyDownForCreate.bind(this);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
|
@ -102,14 +102,16 @@ export default class SelectControl extends React.PureComponent {
|
|||
if (this.props.multi) {
|
||||
optionValue = [];
|
||||
for (const o of opt) {
|
||||
// select all options
|
||||
if (o.meta === true) {
|
||||
optionValue = this.getOptions(this.props)
|
||||
.filter(x => !x.meta)
|
||||
.map(x => x[this.props.valueKey]);
|
||||
break;
|
||||
} else {
|
||||
optionValue.push(o[this.props.valueKey]);
|
||||
this.props.onChange(
|
||||
this.getOptions(this.props)
|
||||
.filter(x => !x.meta)
|
||||
.map(x => x[this.props.valueKey]),
|
||||
);
|
||||
return;
|
||||
}
|
||||
optionValue.push(o[this.props.valueKey] || o);
|
||||
}
|
||||
} else if (opt.meta === true) {
|
||||
return;
|
||||
|
@ -117,46 +119,47 @@ export default class SelectControl extends React.PureComponent {
|
|||
optionValue = opt[this.props.valueKey];
|
||||
}
|
||||
}
|
||||
// will eventually call `exploreReducer`: SET_FIELD_VALUE
|
||||
this.props.onChange(optionValue);
|
||||
}
|
||||
|
||||
getSelectRef(instance) {
|
||||
this.select = instance;
|
||||
if (this.props.selectRef) {
|
||||
this.props.selectRef(instance);
|
||||
}
|
||||
}
|
||||
|
||||
getOptions(props) {
|
||||
let options = [];
|
||||
if (props.options) {
|
||||
options = props.options.map(x => x);
|
||||
} else {
|
||||
} else if (props.choices) {
|
||||
// Accepts different formats of input
|
||||
options = props.choices.map(c => {
|
||||
let option;
|
||||
if (Array.isArray(c)) {
|
||||
const label = c.length > 1 ? c[1] : c[0];
|
||||
option = { label };
|
||||
option[props.valueKey] = c[0];
|
||||
} else if (Object.is(c)) {
|
||||
option = c;
|
||||
} else {
|
||||
option = { label: c };
|
||||
option[props.valueKey] = c;
|
||||
const [value, label] = c.length > 1 ? c : [c[0], c[0]];
|
||||
return { label, [props.valueKey]: value };
|
||||
}
|
||||
return option;
|
||||
if (Object.is(c)) {
|
||||
return c;
|
||||
}
|
||||
return { label: c, [props.valueKey]: c };
|
||||
});
|
||||
}
|
||||
if (props.freeForm) {
|
||||
// For FreeFormSelect, insert value into options if not exist
|
||||
const values = options.map(c => c[props.valueKey]);
|
||||
if (props.value) {
|
||||
let valuesToAdd = props.value;
|
||||
if (!Array.isArray(valuesToAdd)) {
|
||||
valuesToAdd = [valuesToAdd];
|
||||
// For FreeFormSelect, insert newly created values into options
|
||||
if (props.freeForm && props.value) {
|
||||
const existingOptionValues = new Set(options.map(c => c[props.valueKey]));
|
||||
const selectedValues = Array.isArray(props.value)
|
||||
? props.value
|
||||
: [props.value];
|
||||
selectedValues.forEach(v => {
|
||||
if (!existingOptionValues.has(v)) {
|
||||
// place the newly created options at the top
|
||||
options.unshift({ label: v, [props.valueKey]: v });
|
||||
}
|
||||
valuesToAdd.forEach(v => {
|
||||
if (values.indexOf(v) < 0) {
|
||||
const toAdd = { label: v };
|
||||
toAdd[props.valueKey] = v;
|
||||
options.push(toAdd);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (props.allowAll === true && props.multi === true) {
|
||||
if (options.findIndex(o => this.isMetaSelectAllOption(o)) < 0) {
|
||||
|
@ -168,6 +171,16 @@ export default class SelectControl extends React.PureComponent {
|
|||
return options;
|
||||
}
|
||||
|
||||
handleKeyDownForCreate(event) {
|
||||
const key = event.key;
|
||||
if (key === 'Tab' || (this.props.commaChoosesOption && key === ',')) {
|
||||
// simulate an Enter event
|
||||
if (this.select) {
|
||||
this.select.onKeyDown({ ...event, key: 'Enter' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isMetaSelectAllOption(o) {
|
||||
return o.meta && o.meta === true && o.label === 'Select All';
|
||||
}
|
||||
|
@ -182,44 +195,49 @@ export default class SelectControl extends React.PureComponent {
|
|||
// Tab, comma or Enter will trigger a new option created for FreeFormSelect
|
||||
const placeholder =
|
||||
this.props.placeholder || t('%s option(s)', this.state.options.length);
|
||||
const isMulti = this.props.isMulti || this.props.multi;
|
||||
|
||||
const selectProps = {
|
||||
multi: this.props.multi,
|
||||
autoFocus: this.props.autoFocus,
|
||||
isMulti,
|
||||
selectRef: this.getSelectRef,
|
||||
name: `select-${this.props.name}`,
|
||||
placeholder,
|
||||
options: this.state.options,
|
||||
value: this.props.value,
|
||||
labelKey: 'label',
|
||||
valueKey: this.props.valueKey,
|
||||
autosize: false,
|
||||
clearable: this.props.clearable,
|
||||
isLoading: this.props.isLoading,
|
||||
onChange: this.onChange,
|
||||
onFocus: this.props.onFocus,
|
||||
optionRenderer: VirtualizedRendererWrap(this.props.optionRenderer),
|
||||
optionRenderer: this.props.optionRenderer,
|
||||
valueRenderer: this.props.valueRenderer,
|
||||
noResultsText: this.props.noResultsText,
|
||||
disabled: this.props.disabled,
|
||||
refFunc: this.props.refFunc,
|
||||
filterOption: this.props.filterOption,
|
||||
promptTextCreator: this.props.promptTextCreator,
|
||||
ignoreAccents: false,
|
||||
};
|
||||
|
||||
let SelectComponent;
|
||||
if (this.props.freeForm) {
|
||||
selectProps.selectComponent = Creatable;
|
||||
selectProps.shouldKeyDownEventCreateNewOption = key => {
|
||||
const keyCode = key.keyCode;
|
||||
if (this.props.commaChoosesOption && keyCode === 188) {
|
||||
return true;
|
||||
}
|
||||
return keyCode === 9 || keyCode === 13;
|
||||
};
|
||||
SelectComponent = CreatableSelect;
|
||||
// Don't create functions in `render` because React relies on shallow
|
||||
// compare to decide weathere to rerender child components.
|
||||
selectProps.onKeyDown = this.handleKeyDownForCreate;
|
||||
} else {
|
||||
selectProps.selectComponent = Select;
|
||||
SelectComponent = Select;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.props.showHeader && <ControlHeader {...this.props} />}
|
||||
<OnPasteSelect {...selectProps} selectWrap={VirtualizedSelect} />
|
||||
{isMulti ? (
|
||||
<OnPasteSelect {...selectProps} selectWrap={SelectComponent} />
|
||||
) : (
|
||||
<SelectComponent {...selectProps} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
OverlayTrigger,
|
||||
Popover,
|
||||
} from 'react-bootstrap';
|
||||
import Select from 'react-select';
|
||||
import Select from 'src/components/Select';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import { InfoTooltipWithTrigger } from '@superset-ui/control-utils';
|
||||
|
|
|
@ -20,12 +20,13 @@ import { t } from '@superset-ui/translation';
|
|||
|
||||
export const AGGREGATES = {
|
||||
AVG: 'AVG',
|
||||
COUNT: 'COUNT ',
|
||||
COUNT: 'COUNT',
|
||||
COUNT_DISTINCT: 'COUNT_DISTINCT',
|
||||
MAX: 'MAX',
|
||||
MIN: 'MIN',
|
||||
SUM: 'SUM',
|
||||
};
|
||||
export const AGGREGATES_OPTIONS = Object.values(AGGREGATES);
|
||||
|
||||
export const OPERATORS = {
|
||||
'==': '==',
|
||||
|
@ -42,6 +43,7 @@ export const OPERATORS = {
|
|||
'IS NULL': 'IS NULL',
|
||||
'LATEST PARTITION': 'LATEST PARTITION',
|
||||
};
|
||||
export const OPERATORS_OPTIONS = Object.values(OPERATORS);
|
||||
|
||||
export const TABLE_ONLY_OPERATORS = [OPERATORS.LIKE];
|
||||
export const DRUID_ONLY_OPERATORS = [OPERATORS.regex];
|
||||
|
@ -53,10 +55,10 @@ export const HAVING_OPERATORS = [
|
|||
OPERATORS['>='],
|
||||
OPERATORS['<='],
|
||||
];
|
||||
export const MULTI_OPERATORS = [OPERATORS.in, OPERATORS['not in']];
|
||||
export const MULTI_OPERATORS = new Set([OPERATORS.in, OPERATORS['not in']]);
|
||||
// CUSTOM_OPERATORS will show operator in simple mode,
|
||||
// but will generate customized sqlExpression
|
||||
export const CUSTOM_OPERATORS = [OPERATORS['LATEST PARTITION']];
|
||||
export const CUSTOM_OPERATORS = new Set([OPERATORS['LATEST PARTITION']]);
|
||||
// DISABLE_INPUT_OPERATORS will disable filter value input
|
||||
// in adhocFilter control
|
||||
export const DISABLE_INPUT_OPERATORS = [
|
||||
|
|
|
@ -136,7 +136,7 @@ const groupByControl = {
|
|||
valueRenderer: c => <ColumnOption column={c} />,
|
||||
valueKey: 'column_name',
|
||||
allowAll: true,
|
||||
filterOption: (opt, text) =>
|
||||
filterOption: ({ label, value, data: opt }, text) =>
|
||||
(opt.column_name &&
|
||||
opt.column_name.toLowerCase().indexOf(text.toLowerCase()) >= 0) ||
|
||||
(opt.verbose_name &&
|
||||
|
@ -397,6 +397,7 @@ export const controls = {
|
|||
type: 'MetricsControl',
|
||||
label: t('Sort By'),
|
||||
default: null,
|
||||
clearable: true,
|
||||
description: t('Metric used to define the top series'),
|
||||
mapStateToProps: state => ({
|
||||
columns: state.datasource ? state.datasource.columns : [],
|
||||
|
|
|
@ -55,14 +55,6 @@
|
|||
&:last-child {
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.Select-multi-value-wrapper .Select-input > input {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.metrics-select .Select-multi-value-wrapper .Select-input > input {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.background-transparent {
|
||||
|
@ -106,14 +98,16 @@
|
|||
float: left;
|
||||
padding-right: 3px;
|
||||
|
||||
.Select-control {
|
||||
.dropdown {
|
||||
display: flex;
|
||||
|
||||
.Select-arrow-zone {
|
||||
width: auto;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 12px;
|
||||
button {
|
||||
padding-right: 20px;
|
||||
.caret {
|
||||
width: auto;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -136,6 +130,17 @@
|
|||
background-color: transparent;
|
||||
}
|
||||
|
||||
.column-option {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
display: inline-block;
|
||||
& ~ i {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.type-label {
|
||||
margin-right: 8px;
|
||||
width: 30px;
|
||||
|
@ -201,7 +206,7 @@
|
|||
display: inline-flex;
|
||||
}
|
||||
|
||||
.adhoc-filter-option {
|
||||
.adhoc-option {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,5 +16,4 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import '../stylesheets/react-select/select.less';
|
||||
import '../stylesheets/superset.less';
|
||||
|
|
|
@ -363,10 +363,13 @@ class ChartList extends React.PureComponent<Props, State> {
|
|||
});
|
||||
};
|
||||
|
||||
createFetchResource = (
|
||||
resource: string,
|
||||
postProcess?: (value: []) => any[],
|
||||
) => async () => {
|
||||
createFetchResource = ({
|
||||
resource,
|
||||
postProcess,
|
||||
}: {
|
||||
resource: string;
|
||||
postProcess?: (value: []) => any[];
|
||||
}) => async () => {
|
||||
try {
|
||||
const { json = {} } = await SupersetClient.get({
|
||||
endpoint: resource,
|
||||
|
@ -389,10 +392,10 @@ class ChartList extends React.PureComponent<Props, State> {
|
|||
|
||||
updateFilters = async () => {
|
||||
const { filterOperators } = this.state;
|
||||
const fetchOwners = this.createFetchResource(
|
||||
'/api/v1/chart/related/owners',
|
||||
this.convertOwners,
|
||||
);
|
||||
const fetchOwners = this.createFetchResource({
|
||||
resource: '/api/v1/chart/related/owners',
|
||||
postProcess: this.convertOwners,
|
||||
});
|
||||
|
||||
if (this.isNewUIEnabled) {
|
||||
this.setState({
|
||||
|
@ -421,10 +424,10 @@ class ChartList extends React.PureComponent<Props, State> {
|
|||
input: 'select',
|
||||
operator: 'eq',
|
||||
unfilteredLabel: 'All',
|
||||
fetchSelects: this.createFetchResource(
|
||||
'/api/v1/chart/datasources',
|
||||
this.stringifyValues,
|
||||
),
|
||||
fetchSelects: this.createFetchResource({
|
||||
resource: '/api/v1/chart/datasources',
|
||||
postProcess: this.stringifyValues,
|
||||
}),
|
||||
},
|
||||
{
|
||||
Header: 'Search',
|
||||
|
|
|
@ -18,8 +18,7 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import VirtualizedSelect from 'react-virtualized-select';
|
||||
import { Creatable } from 'react-select';
|
||||
import { CreatableSelect } from 'src/components/Select';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
|
@ -27,8 +26,7 @@ import DateFilterControl from '../../explore/components/controls/DateFilterContr
|
|||
import ControlRow from '../../explore/components/ControlRow';
|
||||
import Control from '../../explore/components/Control';
|
||||
import controls from '../../explore/controls';
|
||||
import OnPasteSelect from '../../components/OnPasteSelect';
|
||||
import VirtualizedRendererWrap from '../../components/VirtualizedRendererWrap';
|
||||
import OnPasteSelect from '../../components/Select/OnPasteSelect';
|
||||
import { getDashboardFilterKey } from '../../dashboard/util/getDashboardFilterKey';
|
||||
import { getFilterColorMap } from '../../dashboard/util/dashboardFiltersColorMap';
|
||||
import {
|
||||
|
@ -102,23 +100,18 @@ class FilterBox extends React.Component {
|
|||
// this flag is used by non-instant filter, to make the apply button enabled/disabled
|
||||
hasChanged: false,
|
||||
};
|
||||
this.changeFilter = this.changeFilter.bind(this);
|
||||
this.onFilterMenuOpen = this.onFilterMenuOpen.bind(this, props.chartId);
|
||||
this.onFilterMenuClose = this.onFilterMenuClose.bind(this);
|
||||
this.onFilterMenuClose = () => {
|
||||
this.props.onFilterMenuClose();
|
||||
};
|
||||
this.onFilterMenuOpen = (...args) => {
|
||||
return this.props.onFilterMenuOpen(this.props.chartId, ...args);
|
||||
};
|
||||
this.onOpenDateFilterControl = (...args) => {
|
||||
return this.onFilterMenuOpen(TIME_RANGE, ...args);
|
||||
};
|
||||
this.onFocus = this.onFilterMenuOpen;
|
||||
this.onBlur = this.onFilterMenuClose;
|
||||
this.onOpenDateFilterControl = this.onFilterMenuOpen.bind(
|
||||
props.chartId,
|
||||
TIME_RANGE,
|
||||
);
|
||||
}
|
||||
|
||||
onFilterMenuOpen(chartId, column) {
|
||||
this.props.onFilterMenuOpen(chartId, column);
|
||||
}
|
||||
|
||||
onFilterMenuClose() {
|
||||
this.props.onFilterMenuClose();
|
||||
this.changeFilter = this.changeFilter.bind(this);
|
||||
}
|
||||
|
||||
getControlData(controlName) {
|
||||
|
@ -275,22 +268,20 @@ class FilterBox extends React.Component {
|
|||
}
|
||||
return (
|
||||
<OnPasteSelect
|
||||
placeholder={t('Select [%s]', label)}
|
||||
key={key}
|
||||
multi={filterConfig[FILTER_CONFIG_ATTRIBUTES.MULTIPLE]}
|
||||
placeholder={t('Select [%s]', label)}
|
||||
isMulti={filterConfig[FILTER_CONFIG_ATTRIBUTES.MULTIPLE]}
|
||||
clearable={filterConfig.clearable}
|
||||
value={value}
|
||||
options={data.map(opt => {
|
||||
const perc = Math.round((opt.metric / max) * 100);
|
||||
const backgroundImage =
|
||||
'linear-gradient(to right, lightgrey, ' +
|
||||
`lightgrey ${perc}%, rgba(0,0,0,0) ${perc}%`;
|
||||
const style = {
|
||||
backgroundImage,
|
||||
padding: '2px 5px',
|
||||
};
|
||||
return { value: opt.id, label: opt.id, style };
|
||||
})}
|
||||
options={data
|
||||
.filter(opt => opt.id !== null)
|
||||
.map(opt => {
|
||||
const perc = Math.round((opt.metric / max) * 100);
|
||||
const color = 'lightgrey';
|
||||
const backgroundImage = `linear-gradient(to right, ${color}, ${color} ${perc}%, rgba(0,0,0,0) ${perc}%`;
|
||||
const style = { backgroundImage };
|
||||
return { value: opt.id, label: opt.id, style };
|
||||
})}
|
||||
onChange={(...args) => {
|
||||
this.changeFilter(key, ...args);
|
||||
}}
|
||||
|
@ -300,9 +291,7 @@ class FilterBox extends React.Component {
|
|||
this.onFilterMenuOpen(key, ...args);
|
||||
}}
|
||||
onClose={this.onFilterMenuClose}
|
||||
selectComponent={Creatable}
|
||||
selectWrap={VirtualizedSelect}
|
||||
optionRenderer={VirtualizedRendererWrap(opt => opt.label)}
|
||||
selectComponent={CreatableSelect}
|
||||
noResultsText={t('No results found')}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -18,35 +18,10 @@
|
|||
*/
|
||||
@import '../../../stylesheets/less/variables.less';
|
||||
|
||||
.select2-highlighted > .filter_box {
|
||||
background-color: transparent;
|
||||
border: 1px superset @darkest;
|
||||
}
|
||||
|
||||
.dashboard .filter_box .slice_container > div:not(.alert) {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.input-inline {
|
||||
float: left;
|
||||
// display: inline-block; //Ignored when float is applied. Leaving it here in case we lose the floats later.
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
ul.select2-results li.select2-highlighted div.filter_box {
|
||||
color: @darkest;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: @gray;
|
||||
}
|
||||
|
||||
ul.select2-results div.filter_box {
|
||||
color: @darkest;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.filter_box {
|
||||
padding: 10px 0;
|
||||
overflow: visible !important;
|
||||
|
|
|
@ -1,64 +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 '../../stylesheets/less/variables.less';
|
||||
|
||||
@import '~react-select/less/select.less';
|
||||
@select-primary-color: @darkest;
|
||||
@select-input-height: 30px;
|
||||
|
||||
// imports
|
||||
@import '~react-select/less/control.less';
|
||||
@import '~react-select/less/menu.less';
|
||||
@import '~react-select/less/mixins.less';
|
||||
@import '~react-select/less/multi.less';
|
||||
@import '~react-select/less/spinner.less';
|
||||
|
||||
.Select--multi {
|
||||
.Select-multi-value-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.Select-value {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.Select-input > input {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.VirtualizedSelectOption {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.VirtualizedSelectFocusedOption {
|
||||
background-color: fade(@darkest, @opacity-light);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.VirtualizedSelectDisabledOption {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.VirtualizedSelectSelectedOption {
|
||||
font-weight: @font-weight-bold;
|
||||
}
|
|
@ -356,10 +356,6 @@ table.table-no-hover tr:hover {
|
|||
padding-right: 2;
|
||||
}
|
||||
|
||||
.Select-menu-outer {
|
||||
z-index: @z-index-dropdown !important;
|
||||
}
|
||||
|
||||
/** not found record **/
|
||||
.panel b {
|
||||
display: inline-block;
|
||||
|
|
|
@ -49,7 +49,6 @@ def gen_filter(
|
|||
"expressionType": "SIMPLE",
|
||||
"operator": operator,
|
||||
"subject": subject,
|
||||
"fromFormData": True,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -342,7 +342,6 @@ def load_deck_dash() -> None:
|
|||
"verbose_name": None,
|
||||
},
|
||||
"expressionType": "SIMPLE",
|
||||
"fromFormData": True,
|
||||
"hasCustomLabel": True,
|
||||
"label": "Population",
|
||||
"optionName": "metric_t2v4qbfiz1_w6qgpx4h2p",
|
||||
|
@ -380,7 +379,6 @@ def load_deck_dash() -> None:
|
|||
"aggregate": None,
|
||||
"column": None,
|
||||
"expressionType": "SQL",
|
||||
"fromFormData": None,
|
||||
"hasCustomLabel": None,
|
||||
"label": "Density",
|
||||
"optionName": "metric_c5rvwrzoo86_293h6yrv2ic",
|
||||
|
|
|
@ -946,6 +946,7 @@ class Superset(BaseSupersetView):
|
|||
payload = json.dumps(
|
||||
datasource.values_for_column(column, config["FILTER_SELECT_ROW_LIMIT"]),
|
||||
default=utils.json_int_dttm_ser,
|
||||
ignore_nan=True,
|
||||
)
|
||||
return json_success(payload)
|
||||
|
||||
|
|
Loading…
Reference in New Issue