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:
Jesse Yang 2020-05-19 16:59:49 -07:00 committed by GitHub
parent 68832d2fa5
commit 81ab8dd8b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
97 changed files with 2027 additions and 1234 deletions

View File

@ -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

View File

@ -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');

View File

@ -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,
},
],
};

View File

@ -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');
});
});
});

View File

@ -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();
});

View File

@ -91,7 +91,6 @@ export default () =>
comparator: ['South Asia', 'North America'],
clause: 'WHERE',
sqlExpression: null,
fromFormData: true,
filterOptionName: 'filter_txje2ikiv6_wxmn0qwd1xo',
},
],

View File

@ -53,7 +53,6 @@ export default () =>
comparator: ['Aaron', 'Amy', 'Andrea'],
clause: 'WHERE',
sqlExpression: null,
fromFormData: true,
filterOptionName: 'filter_4y6teao56zs_ebjsvwy48c',
},
];

View File

@ -63,7 +63,6 @@ export default () =>
comparator: 'South Asia',
clause: 'WHERE',
sqlExpression: null,
fromFormData: true,
filterOptionName: 'filter_8aqxcf5co1a_x7lm2d1fq0l',
},
],

View File

@ -74,7 +74,6 @@ export default () =>
comparator: 'South+Asia',
clause: 'WHERE',
sqlExpression: null,
fromFormData: true,
filterOptionName: 'filter_b2tfg1rs8y_8kmrcyxvsqd',
},
],

View File

@ -82,7 +82,6 @@ export default () =>
comparator: 'boy',
clause: 'WHERE',
sqlExpression: null,
fromFormData: true,
filterOptionName: 'filter_tqx1en70hh_7nksse7nqic',
},
],

View File

@ -62,7 +62,6 @@ export default () =>
comparator: 'girl',
clause: 'WHERE',
sqlExpression: null,
fromFormData: true,
filterOptionName: 'filter_1ep6q50g8vk_48jj6qxdems',
},
],

View File

@ -72,7 +72,6 @@ export default () =>
comparator: 'boy',
clause: 'WHERE',
sqlExpression: null,
fromFormData: true,
filterOptionName: 'filter_tqx1en70hh_7nksse7nqic',
},
],

View File

@ -67,7 +67,6 @@ export default () =>
comparator: 'boy',
clause: 'WHERE',
sqlExpression: null,
fromFormData: true,
filterOptionName: 'filter_tqx1en70hh_7nksse7nqic',
},
],

View File

@ -50,7 +50,6 @@ export default () =>
},
aggregate: 'SUM',
hasCustomLabel: false,
fromFormData: false,
label: 'SUM(sum_boys)',
optionName: 'metric_gvpdjt0v2qf_6hkf56o012',
};

View File

@ -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',
},
],

View File

@ -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',
};

View File

@ -76,7 +76,6 @@ export default () =>
comparator: ['South Asia', 'North America'],
clause: 'WHERE',
sqlExpression: null,
fromFormData: true,
filterOptionName: 'filter_txje2ikiv6_wxmn0qwd1xo',
},
],

View File

@ -152,7 +152,6 @@ export default () =>
column: null,
aggregate: null,
hasCustomLabel: true,
fromFormData: true,
label: '%+Girls',
optionName: 'metric_6qwzgc8bh2v_zox7hil1mzs',
};

View File

@ -70,7 +70,6 @@ export default () =>
comparator: 'South Asia',
clause: 'WHERE',
sqlExpression: null,
fromFormData: true,
filterOptionName: 'filter_8aqxcf5co1a_x7lm2d1fq0l',
},
],

View File

@ -63,7 +63,6 @@ export default () =>
comparator: 'South Asia',
clause: 'WHERE',
sqlExpression: null,
fromFormData: true,
filterOptionName: 'filter_8aqxcf5co1a_x7lm2d1fq0l',
},
],

View File

@ -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": {

View File

@ -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",

View File

@ -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';

View File

@ -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', () => {

View File

@ -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';

View File

@ -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', () => {

View File

@ -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);
});
});

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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,

View File

@ -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', () => {

View File

@ -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);
});
});

View File

@ -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 }),
);

View File

@ -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);
});
});

View File

@ -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';

View File

@ -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 = {

View File

@ -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);
});

View File

@ -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,
);
});
});

View File

@ -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', () => {

View File

@ -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';

View File

@ -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;
}

View File

@ -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';

View File

@ -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';

View File

@ -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;

View File

@ -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 {

View File

@ -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)}

View File

@ -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, {

View File

@ -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),

View File

@ -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[];

View File

@ -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}`}

View File

@ -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,
};

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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';

View File

@ -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),
};
},
};

View File

@ -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);
}

View File

@ -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}
`;

View File

@ -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) {

View File

@ -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;
}

View File

@ -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',
};

View File

@ -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';

View File

@ -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(

View File

@ -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';

View File

@ -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;
}`}

View File

@ -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,
});
}

View File

@ -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,
});
}

View File

@ -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>

View File

@ -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"

View File

@ -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>
);
}
}

View File

@ -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>

View File

@ -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'}
&nbsp;
<i
className="fa fa-pencil"
style={{ color: this.state.isHovered ? 'black' : 'grey' }}
/>
) : (
<span className="inline-editable">
{adhocMetric.hasCustomLabel ? adhocMetric.label : 'My Metric'}
&nbsp;
<i
className="fa fa-pencil"
style={{ color: this.state.isHovered ? 'black' : 'grey' }}
/>
</span>
)}
</span>
</OverlayTrigger>
);
}

View File

@ -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>
);
}
}

View File

@ -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>
);
}

View File

@ -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 && (

View File

@ -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) {

View File

@ -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}

View File

@ -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(

View File

@ -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';

View File

@ -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>
);

View File

@ -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>
);
}

View File

@ -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>
);

View File

@ -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>
);
}

View File

@ -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';

View File

@ -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 = [

View File

@ -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 : [],

View File

@ -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;
}

View File

@ -16,5 +16,4 @@
* specific language governing permissions and limitations
* under the License.
*/
import '../stylesheets/react-select/select.less';
import '../stylesheets/superset.less';

View File

@ -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',

View File

@ -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')}
/>
);

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -49,7 +49,6 @@ def gen_filter(
"expressionType": "SIMPLE",
"operator": operator,
"subject": subject,
"fromFormData": True,
}

View File

@ -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",

View File

@ -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)