diff --git a/superset-frontend/.eslintrc.js b/superset-frontend/.eslintrc.js index c25f689898e1..b7306914ae09 100644 --- a/superset-frontend/.eslintrc.js +++ b/superset-frontend/.eslintrc.js @@ -135,7 +135,6 @@ module.exports = { 'react/no-unused-prop-types': 0, 'react/prop-types': 0, 'react/require-default-props': 0, - 'react/sort-comp': 0, // TODO: re-enable in separate PR 'react/static-property-placement': 0, // re-enable up for discussion 'prettier/prettier': 'error', }, @@ -247,7 +246,6 @@ module.exports = { 'react/no-unused-prop-types': 0, 'react/prop-types': 0, 'react/require-default-props': 0, - 'react/sort-comp': 0, // TODO: re-enable in separate PR 'react/static-property-placement': 0, // disabled temporarily 'prettier/prettier': 'error', }, diff --git a/superset-frontend/src/SqlLab/components/ExploreResultsButton.jsx b/superset-frontend/src/SqlLab/components/ExploreResultsButton.jsx index fc8c8ac017d6..1f337100d75a 100644 --- a/superset-frontend/src/SqlLab/components/ExploreResultsButton.jsx +++ b/superset-frontend/src/SqlLab/components/ExploreResultsButton.jsx @@ -62,12 +62,6 @@ class ExploreResultsButton extends React.PureComponent { return []; } - getQueryDuration() { - return moment - .duration(this.props.query.endDttm - this.props.query.startDttm) - .asSeconds(); - } - getInvalidColumns() { const re1 = /__\d+$/; // duplicate column name pattern const re2 = /^__timestamp/i; // reserved temporal column alias @@ -77,15 +71,10 @@ class ExploreResultsButton extends React.PureComponent { .filter(col => re1.test(col) || re2.test(col)); } - datasourceName() { - const { query } = this.props; - const uniqueId = shortid.generate(); - let datasourceName = uniqueId; - if (query) { - datasourceName = query.user ? `${query.user}-` : ''; - datasourceName += `${query.tab}-${uniqueId}`; - } - return datasourceName; + getQueryDuration() { + return moment + .duration(this.props.query.endDttm - this.props.query.startDttm) + .asSeconds(); } buildVizOptions() { @@ -100,6 +89,37 @@ class ExploreResultsButton extends React.PureComponent { }; } + datasourceName() { + const { query } = this.props; + const uniqueId = shortid.generate(); + let datasourceName = uniqueId; + if (query) { + datasourceName = query.user ? `${query.user}-` : ''; + datasourceName += `${query.tab}-${uniqueId}`; + } + return datasourceName; + } + + renderInvalidColumnMessage() { + const invalidColumns = this.getInvalidColumns(); + if (invalidColumns.length === 0) { + return null; + } + return ( +
+ {t('Column name(s) ')} + + {invalidColumns.join(', ')} + + {t(`cannot be used as a column name. The column name/alias "__timestamp" + is reserved for the main temporal expression, and column aliases ending with + double underscores followed by a numeric value (e.g. "my_col__1") are reserved + for deduplicating duplicate column names. Please use aliases to rename the + invalid column names.`)} +
+ ); + } + renderTimeoutWarning() { return ( @@ -124,26 +144,6 @@ class ExploreResultsButton extends React.PureComponent { ); } - renderInvalidColumnMessage() { - const invalidColumns = this.getInvalidColumns(); - if (invalidColumns.length === 0) { - return null; - } - return ( -
- {t('Column name(s) ')} - - {invalidColumns.join(', ')} - - {t(`cannot be used as a column name. The column name/alias "__timestamp" - is reserved for the main temporal expression, and column aliases ending with - double underscores followed by a numeric value (e.g. "my_col__1") are reserved - for deduplicating duplicate column names. Please use aliases to rename the - invalid column names.`)} -
- ); - } - render() { const allowsSubquery = this.props.database && this.props.database.allows_subquery; diff --git a/superset-frontend/src/SqlLab/components/HighlightedSql.jsx b/superset-frontend/src/SqlLab/components/HighlightedSql.jsx index b143b37c74f7..35a5efccff75 100644 --- a/superset-frontend/src/SqlLab/components/HighlightedSql.jsx +++ b/superset-frontend/src/SqlLab/components/HighlightedSql.jsx @@ -49,6 +49,31 @@ class HighlightedSql extends React.Component { }; } + generateModal() { + let rawSql; + if (this.props.rawSql && this.props.rawSql !== this.props.sql) { + rawSql = ( +
+

{t('Raw SQL')}

+ + {this.props.rawSql} + +
+ ); + } + this.setState({ + modalBody: ( +
+

{t('Source SQL')}

+ + {this.props.sql} + + {rawSql} +
+ ), + }); + } + shrinkSql() { const ssql = this.props.sql || ''; let lines = ssql.split('\n'); @@ -77,31 +102,6 @@ class HighlightedSql extends React.Component { ); } - generateModal() { - let rawSql; - if (this.props.rawSql && this.props.rawSql !== this.props.sql) { - rawSql = ( -
-

{t('Raw SQL')}

- - {this.props.rawSql} - -
- ); - } - this.setState({ - modalBody: ( -
-

{t('Source SQL')}

- - {this.props.sql} - - {rawSql} -
- ), - }); - } - render() { return ( { - this.refreshQueries(); - }); + onChange(db) { + const val = db ? db.value : null; + this.setState({ databaseId: val }); } onDbClicked(dbId) { @@ -105,17 +104,18 @@ class QuerySearch extends React.PureComponent { }); } - onChange(db) { - const val = db ? db.value : null; - this.setState({ databaseId: val }); - } - onKeyDown(event) { if (event.keyCode === 13) { this.refreshQueries(); } } + onUserClicked(userId) { + this.setState({ userId }, () => { + this.refreshQueries(); + }); + } + getTimeFromSelection(selection) { switch (selection) { case 'now': @@ -142,21 +142,8 @@ class QuerySearch extends React.PureComponent { this.setState({ from: val }); } - changeTo(status) { - const val = status ? status.value : null; - this.setState({ to: val }); - } - - changeUser(user) { - const val = user ? user.value : null; - this.setState({ userId: val }); - } - - insertParams(baseUrl, params) { - const validParams = params.filter(function (p) { - return p !== ''; - }); - return `${baseUrl}?${validParams.join('&')}`; + changeSearch(event) { + this.setState({ searchText: event.target.value }); } changeStatus(status) { @@ -164,22 +151,14 @@ class QuerySearch extends React.PureComponent { this.setState({ status: val }); } - changeSearch(event) { - this.setState({ searchText: event.target.value }); - } - - userLabel(user) { - if (user.first_name && user.last_name) { - return `${user.first_name} ${user.last_name}`; - } - return user.username; + changeTo(status) { + const val = status ? status.value : null; + this.setState({ to: val }); } - userMutator(data) { - return data.result.map(({ value, text }) => ({ - label: text, - value, - })); + changeUser(user) { + const val = user ? user.value : null; + this.setState({ userId: val }); } dbMutator(data) { @@ -196,6 +175,13 @@ class QuerySearch extends React.PureComponent { return options; } + insertParams(baseUrl, params) { + const validParams = params.filter(function (p) { + return p !== ''; + }); + return `${baseUrl}?${validParams.join('&')}`; + } + refreshQueries() { this.setState({ queriesLoading: true }); const params = [ @@ -222,6 +208,20 @@ class QuerySearch extends React.PureComponent { }); } + userLabel(user) { + if (user.first_name && user.last_name) { + return `${user.first_name} ${user.last_name}`; + } + return user.username; + } + + userMutator(data) { + return data.result.map(({ value, text }) => ({ + label: text, + value, + })); + } + render() { return ( diff --git a/superset-frontend/src/SqlLab/components/ScheduleQueryButton.jsx b/superset-frontend/src/SqlLab/components/ScheduleQueryButton.jsx index a1d2411b197a..9a7a8c377083 100644 --- a/superset-frontend/src/SqlLab/components/ScheduleQueryButton.jsx +++ b/superset-frontend/src/SqlLab/components/ScheduleQueryButton.jsx @@ -110,6 +110,18 @@ class ScheduleQueryButton extends React.PureComponent { this.onDescriptionChange = this.onDescriptionChange.bind(this); } + onCancel() { + this.saveModal.close(); + } + + onDescriptionChange(e) { + this.setState({ description: e.target.value }); + } + + onLabelChange(e) { + this.setState({ label: e.target.value }); + } + onSchedule({ formData }) { const query = { label: this.state.label, @@ -123,18 +135,6 @@ class ScheduleQueryButton extends React.PureComponent { this.saveModal.close(); } - onCancel() { - this.saveModal.close(); - } - - onLabelChange(e) { - this.setState({ label: e.target.value }); - } - - onDescriptionChange(e) { - this.setState({ description: e.target.value }); - } - toggleSchedule() { this.setState(prevState => ({ showSchedule: !prevState.showSchedule })); } diff --git a/superset-frontend/src/SqlLab/components/SqlEditor.jsx b/superset-frontend/src/SqlLab/components/SqlEditor.jsx index 75028eae7fc6..dc8d5119f390 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor.jsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor.jsx @@ -228,12 +228,6 @@ class SqlEditor extends React.PureComponent { window.removeEventListener('resize', this.handleWindowResize); } - onResizeStart() { - // Set the heights on the ace editor and the ace content area after drag starts - // to smooth out the visual transition to the new heights when drag ends - document.getElementsByClassName('ace_content')[0].style.height = '100%'; - } - onResizeEnd([northPercent, southPercent]) { this.setState({ northPercent, southPercent }); @@ -246,6 +240,12 @@ class SqlEditor extends React.PureComponent { } } + onResizeStart() { + // Set the heights on the ace editor and the ace content area after drag starts + // to smooth out the visual transition to the new heights when drag ends + document.getElementsByClassName('ace_content')[0].style.height = '100%'; + } + onSqlChanged(sql) { this.setState({ sql }); this.setQueryEditorSqlWithDebounce(sql); @@ -256,13 +256,6 @@ class SqlEditor extends React.PureComponent { } } - // One layer of abstraction for easy spying in unit tests - getSqlEditorHeight() { - return this.sqlEditorRef.current - ? this.sqlEditorRef.current.clientHeight - SQL_EDITOR_PADDING * 2 - : 0; - } - // Return the heights for the ace editor and the south pane as an object // given the height of the sql editor, north pane percent and south pane percent. getAceEditorAndSouthPaneHeights(height, northPercent, southPercent) { @@ -320,14 +313,6 @@ class SqlEditor extends React.PureComponent { ]; } - setQueryEditorSql(sql) { - this.props.queryEditorSetSql(this.props.queryEditor, sql); - } - - setQueryLimit(queryLimit) { - this.props.queryEditorSetQueryLimit(this.props.queryEditor, queryLimit); - } - getQueryCostEstimate() { if (this.props.database) { const qe = this.props.queryEditor; @@ -342,36 +327,19 @@ class SqlEditor extends React.PureComponent { } } - handleToggleAutocompleteEnabled = () => { - this.setState(prevState => ({ - autocompleteEnabled: !prevState.autocompleteEnabled, - })); - }; - - handleWindowResize() { - this.setState({ height: this.getSqlEditorHeight() }); + // One layer of abstraction for easy spying in unit tests + getSqlEditorHeight() { + return this.sqlEditorRef.current + ? this.sqlEditorRef.current.clientHeight - SQL_EDITOR_PADDING * 2 + : 0; } - elementStyle(dimension, elementSize, gutterSize) { - return { - [dimension]: `calc(${elementSize}% - ${ - gutterSize + SQL_EDITOR_GUTTER_MARGIN - }px)`, - }; + setQueryEditorSql(sql) { + this.props.queryEditorSetSql(this.props.queryEditor, sql); } - requestValidation() { - if (this.props.database) { - const qe = this.props.queryEditor; - const query = { - dbId: qe.dbId, - sql: this.state.sql, - sqlEditorId: qe.id, - schema: qe.schema, - templateParams: qe.templateParams, - }; - this.props.validateQuery(query); - } + setQueryLimit(queryLimit) { + this.props.queryEditorSetQueryLimit(this.props.queryEditor, queryLimit); } canValidateQuery() { @@ -384,47 +352,10 @@ class SqlEditor extends React.PureComponent { return false; } - runQuery() { - if (this.props.database) { - this.startQuery(); - } - } - convertToNumWithSpaces(num) { return num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1 '); } - startQuery(ctas = false, ctas_method = CtasEnum.TABLE) { - const qe = this.props.queryEditor; - const query = { - dbId: qe.dbId, - sql: qe.selectedText ? qe.selectedText : this.state.sql, - sqlEditorId: qe.id, - tab: qe.title, - schema: qe.schema, - tempTable: ctas ? this.state.ctas : '', - templateParams: qe.templateParams, - queryLimit: qe.queryLimit || this.props.defaultQueryLimit, - runAsync: this.props.database - ? this.props.database.allow_run_async - : false, - ctas, - ctas_method, - updateTabState: !qe.selectedText, - }; - this.props.runQuery(query); - this.props.setActiveSouthPaneTab('Results'); - } - - stopQuery() { - if ( - this.props.latestQuery && - ['running', 'pending'].indexOf(this.props.latestQuery.state) >= 0 - ) { - this.props.postStopQuery(this.props.latestQuery); - } - } - createTableAs() { this.startQuery(true, CtasEnum.TABLE); this.setState({ showCreateAsModal: false, ctas: '' }); @@ -439,6 +370,24 @@ class SqlEditor extends React.PureComponent { this.setState({ ctas: event.target.value }); } + elementStyle(dimension, elementSize, gutterSize) { + return { + [dimension]: `calc(${elementSize}% - ${ + gutterSize + SQL_EDITOR_GUTTER_MARGIN + }px)`, + }; + } + + handleToggleAutocompleteEnabled = () => { + this.setState(prevState => ({ + autocompleteEnabled: !prevState.autocompleteEnabled, + })); + }; + + handleWindowResize() { + this.setState({ height: this.getSqlEditorHeight() }); + } + queryPane() { const hotkeys = this.getHotkeyConfig(); const { @@ -492,6 +441,57 @@ class SqlEditor extends React.PureComponent { ); } + requestValidation() { + if (this.props.database) { + const qe = this.props.queryEditor; + const query = { + dbId: qe.dbId, + sql: this.state.sql, + sqlEditorId: qe.id, + schema: qe.schema, + templateParams: qe.templateParams, + }; + this.props.validateQuery(query); + } + } + + runQuery() { + if (this.props.database) { + this.startQuery(); + } + } + + startQuery(ctas = false, ctas_method = CtasEnum.TABLE) { + const qe = this.props.queryEditor; + const query = { + dbId: qe.dbId, + sql: qe.selectedText ? qe.selectedText : this.state.sql, + sqlEditorId: qe.id, + tab: qe.title, + schema: qe.schema, + tempTable: ctas ? this.state.ctas : '', + templateParams: qe.templateParams, + queryLimit: qe.queryLimit || this.props.defaultQueryLimit, + runAsync: this.props.database + ? this.props.database.allow_run_async + : false, + ctas, + ctas_method, + updateTabState: !qe.selectedText, + }; + this.props.runQuery(query); + this.props.setActiveSouthPaneTab('Results'); + } + + stopQuery() { + if ( + this.props.latestQuery && + ['running', 'pending'].indexOf(this.props.latestQuery.state) >= 0 + ) { + this.props.postStopQuery(this.props.latestQuery); + } + } + renderDropdown() { const qe = this.props.queryEditor; const successful = this.props.latestQuery?.state === 'success'; @@ -538,23 +538,6 @@ class SqlEditor extends React.PureComponent { ); } - renderQueryLimit() { - const menuDropdown = ( - - {LIMIT_DROPDOWN.map(limit => ( - this.setQueryLimit(limit)}> - {/* // eslint-disable-line no-use-before-define */} - - {this.convertToNumWithSpaces(limit)} - {' '} - - ))} - - ); - - return menuDropdown; - } - renderEditorBottomBar() { const { queryEditor: qe } = this.props; let limitWarning = null; @@ -693,6 +676,23 @@ class SqlEditor extends React.PureComponent { ); } + renderQueryLimit() { + const menuDropdown = ( + + {LIMIT_DROPDOWN.map(limit => ( + this.setQueryLimit(limit)}> + {/* // eslint-disable-line no-use-before-define */} + + {this.convertToNumWithSpaces(limit)} + {' '} + + ))} + + ); + + return menuDropdown; + } + render() { const createViewModalTitle = this.state.createAs === CtasEnum.VIEW diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar.jsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar.jsx index 7b2179fb6a86..1b24f4ee3cdc 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar.jsx +++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar.jsx @@ -60,6 +60,10 @@ export default class SqlEditorLeftBar extends React.PureComponent { this.onTableChange = this.onTableChange.bind(this); } + onDbChange(db) { + this.props.actions.queryEditorSetDb(this.props.queryEditor, db.id); + } + onSchemaChange(schema) { this.props.actions.queryEditorSetSchema(this.props.queryEditor, schema); } @@ -71,6 +75,10 @@ export default class SqlEditorLeftBar extends React.PureComponent { ); } + onTableChange(tableName, schemaName) { + this.props.actions.addTable(this.props.queryEditor, tableName, schemaName); + } + onTablesLoad(tables) { this.props.actions.queryEditorSetTableOptions( this.props.queryEditor, @@ -78,18 +86,20 @@ export default class SqlEditorLeftBar extends React.PureComponent { ); } - onDbChange(db) { - this.props.actions.queryEditorSetDb(this.props.queryEditor, db.id); + getDbList(dbs) { + this.props.actions.setDatabases(dbs); } - onTableChange(tableName, schemaName) { + changeTable(tableOpt) { + if (!tableOpt) { + return; + } + const schemaName = tableOpt.value.schema; + const tableName = tableOpt.value.table; + this.props.actions.queryEditorSetSchema(this.props.queryEditor, schemaName); this.props.actions.addTable(this.props.queryEditor, tableName, schemaName); } - getDbList(dbs) { - this.props.actions.setDatabases(dbs); - } - dbMutator(data) { const options = data.result.map(db => ({ value: db.id, @@ -108,16 +118,6 @@ export default class SqlEditorLeftBar extends React.PureComponent { this.props.actions.resetState(); } - changeTable(tableOpt) { - if (!tableOpt) { - return; - } - const schemaName = tableOpt.value.schema; - const tableName = tableOpt.value.table; - this.props.actions.queryEditorSetSchema(this.props.queryEditor, schemaName); - this.props.actions.addTable(this.props.queryEditor, tableName, schemaName); - } - render() { const shouldShowReset = window.location.search === '?reset=1'; const tableMetaDataHeight = this.props.height - 130; // 130 is the height of the selects above diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors.jsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors.jsx index ce5c23fafb03..4a5eafd78dcb 100644 --- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors.jsx +++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors.jsx @@ -226,20 +226,6 @@ class TabbedSqlEditors extends React.PureComponent { } } - popNewTab() { - queryCount += 1; - // Clean the url in browser history - window.history.replaceState({}, document.title, this.state.sqlLabUrl); - } - - renameTab(qe) { - /* eslint no-alert: 0 */ - const newTitle = prompt(t('Enter a new title for the tab')); - if (newTitle) { - this.props.actions.queryEditorSetTitle(qe, newTitle); - } - } - activeQueryEditor() { if (this.props.tabHistory.length === 0) { return this.props.queryEditors[0]; @@ -248,6 +234,31 @@ class TabbedSqlEditors extends React.PureComponent { return this.props.queryEditors.find(qe => qe.id === qeid) || null; } + duplicateQueryEditor(qe) { + this.props.actions.cloneQueryToNewTab(qe, false); + } + + handleEdit(key, action) { + if (action === 'remove') { + const qe = this.props.queryEditors.find(qe => qe.id === key); + this.removeQueryEditor(qe); + } + if (action === 'add') { + this.newQueryEditor(); + } + } + + handleSelect(key) { + const qeid = this.props.tabHistory[this.props.tabHistory.length - 1]; + if (key !== qeid) { + const queryEditor = this.props.queryEditors.find(qe => qe.id === key); + this.props.actions.switchQueryEditor( + queryEditor, + this.props.displayLimit, + ); + } + } + newQueryEditor() { queryCount += 1; const activeQueryEditor = this.activeQueryEditor(); @@ -273,29 +284,10 @@ class TabbedSqlEditors extends React.PureComponent { this.props.actions.addQueryEditor(qe); } - handleSelect(key) { - const qeid = this.props.tabHistory[this.props.tabHistory.length - 1]; - if (key !== qeid) { - const queryEditor = this.props.queryEditors.find(qe => qe.id === key); - this.props.actions.switchQueryEditor( - queryEditor, - this.props.displayLimit, - ); - } - } - - handleEdit(key, action) { - if (action === 'remove') { - const qe = this.props.queryEditors.find(qe => qe.id === key); - this.removeQueryEditor(qe); - } - if (action === 'add') { - this.newQueryEditor(); - } - } - - removeQueryEditor(qe) { - this.props.actions.removeQueryEditor(qe); + popNewTab() { + queryCount += 1; + // Clean the url in browser history + window.history.replaceState({}, document.title, this.state.sqlLabUrl); } removeAllOtherQueryEditors(cqe) { @@ -304,8 +296,16 @@ class TabbedSqlEditors extends React.PureComponent { ); } - duplicateQueryEditor(qe) { - this.props.actions.cloneQueryToNewTab(qe, false); + removeQueryEditor(qe) { + this.props.actions.removeQueryEditor(qe); + } + + renameTab(qe) { + /* eslint no-alert: 0 */ + const newTitle = prompt(t('Enter a new title for the tab')); + if (newTitle) { + this.props.actions.queryEditorSetTitle(qe, newTitle); + } } toggleLeftBar() { diff --git a/superset-frontend/src/SqlLab/components/TableElement.jsx b/superset-frontend/src/SqlLab/components/TableElement.jsx index b32e774982a5..f8c6d411d931 100644 --- a/superset-frontend/src/SqlLab/components/TableElement.jsx +++ b/superset-frontend/src/SqlLab/components/TableElement.jsx @@ -80,13 +80,8 @@ class TableElement extends React.PureComponent { this.props.actions.addQueryEditor(qe); } - toggleTable(e) { - e.preventDefault(); - if (this.props.table.expanded) { - this.props.actions.collapseTable(this.props.table); - } else { - this.props.actions.expandTable(this.props.table); - } + removeFromStore() { + this.props.actions.removeTable(this.props.table); } removeTable() { @@ -98,44 +93,46 @@ class TableElement extends React.PureComponent { this.setState(prevState => ({ sortColumns: !prevState.sortColumns })); } - removeFromStore() { - this.props.actions.removeTable(this.props.table); + toggleTable(e) { + e.preventDefault(); + if (this.props.table.expanded) { + this.props.actions.collapseTable(this.props.table); + } else { + this.props.actions.expandTable(this.props.table); + } } - renderWell() { + renderBody() { const { table } = this.props; - let header; - if (table.partitions) { - let partitionQuery; - let partitionClipBoard; - if (table.partitions.partitionQuery) { - ({ partitionQuery } = table.partitions.partitionQuery); - const tt = t('Copy partition query to clipboard'); - partitionClipBoard = ( - } - /> - ); + let cols; + if (table.columns) { + cols = table.columns.slice(); + if (this.state.sortColumns) { + cols.sort((a, b) => { + const colA = a.name.toUpperCase(); + const colB = b.name.toUpperCase(); + if (colA < colB) { + return -1; + } + if (colA > colB) { + return 1; + } + return 0; + }); } - let latest = Object.entries(table.partitions?.latest || []).map( - ([key, value]) => `${key}=${value}`, - ); - latest = latest.join('/'); - header = ( - -
- - {t('latest partition:')} {latest} - {' '} - {partitionClipBoard} -
-
- ); } - return header; + const metadata = ( + +
+ {this.renderWell()} +
+ {cols && + cols.map(col => )} +
+
+
+ ); + return metadata; } renderControls() { @@ -250,37 +247,40 @@ class TableElement extends React.PureComponent { ); } - renderBody() { + renderWell() { const { table } = this.props; - let cols; - if (table.columns) { - cols = table.columns.slice(); - if (this.state.sortColumns) { - cols.sort((a, b) => { - const colA = a.name.toUpperCase(); - const colB = b.name.toUpperCase(); - if (colA < colB) { - return -1; - } - if (colA > colB) { - return 1; - } - return 0; - }); + let header; + if (table.partitions) { + let partitionQuery; + let partitionClipBoard; + if (table.partitions.partitionQuery) { + ({ partitionQuery } = table.partitions.partitionQuery); + const tt = t('Copy partition query to clipboard'); + partitionClipBoard = ( + } + /> + ); } - } - const metadata = ( - -
- {this.renderWell()} -
- {cols && - cols.map(col => )} + let latest = Object.entries(table.partitions?.latest || []).map( + ([key, value]) => `${key}=${value}`, + ); + latest = latest.join('/'); + header = ( + +
+ + {t('latest partition:')} {latest} + {' '} + {partitionClipBoard}
-
- - ); - return metadata; + + ); + } + return header; } render() { diff --git a/superset-frontend/src/chart/Chart.jsx b/superset-frontend/src/chart/Chart.jsx index 3f1596cc45d6..b98b29094dc1 100644 --- a/superset-frontend/src/chart/Chart.jsx +++ b/superset-frontend/src/chart/Chart.jsx @@ -110,6 +110,25 @@ class Chart extends React.PureComponent { } } + handleRenderContainerFailure(error, info) { + const { actions, chartId } = this.props; + logging.warn(error); + actions.chartRenderingFailed( + error.toString(), + chartId, + info ? info.componentStack : null, + ); + + actions.logEvent(LOG_ACTIONS_RENDER_CHART, { + slice_id: chartId, + has_err: true, + error_details: error.toString(), + start_offset: this.renderStartTime, + ts: new Date().getTime(), + duration: Logger.getTimestamp() - this.renderStartTime, + }); + } + runQuery() { if (this.props.chartId > 0 && isFeatureEnabled(FeatureFlag.CLIENT_CACHE)) { // Load saved chart with a GET request @@ -132,25 +151,6 @@ class Chart extends React.PureComponent { } } - handleRenderContainerFailure(error, info) { - const { actions, chartId } = this.props; - logging.warn(error); - actions.chartRenderingFailed( - error.toString(), - chartId, - info ? info.componentStack : null, - ); - - actions.logEvent(LOG_ACTIONS_RENDER_CHART, { - slice_id: chartId, - has_err: true, - error_details: error.toString(), - start_offset: this.renderStartTime, - ts: new Date().getTime(), - duration: Logger.getTimestamp() - this.renderStartTime, - }); - } - renderErrorMessage(queryResponse) { const { chartAlert, chartStackTrace, dashboardId, owners } = this.props; diff --git a/superset-frontend/src/chart/ChartRenderer.jsx b/superset-frontend/src/chart/ChartRenderer.jsx index 0c21a3a5c065..765a55b21c74 100644 --- a/superset-frontend/src/chart/ChartRenderer.jsx +++ b/superset-frontend/src/chart/ChartRenderer.jsx @@ -103,25 +103,6 @@ class ChartRenderer extends React.Component { this.props.addFilter(col, vals, merge, refresh); } - handleRenderSuccess() { - const { actions, chartStatus, chartId, vizType } = this.props; - if (['loading', 'rendered'].indexOf(chartStatus) < 0) { - actions.chartRenderingSucceeded(chartId); - } - - // only log chart render time which is triggered by query results change - // currently we don't log chart re-render time, like window resize etc - if (this.hasQueryResponseChange) { - actions.logEvent(LOG_ACTIONS_RENDER_CHART, { - slice_id: chartId, - viz_type: vizType, - start_offset: this.renderStartTime, - ts: new Date().getTime(), - duration: Logger.getTimestamp() - this.renderStartTime, - }); - } - } - handleRenderFailure(error, info) { const { actions, chartId } = this.props; logging.warn(error); @@ -144,6 +125,25 @@ class ChartRenderer extends React.Component { } } + handleRenderSuccess() { + const { actions, chartStatus, chartId, vizType } = this.props; + if (['loading', 'rendered'].indexOf(chartStatus) < 0) { + actions.chartRenderingSucceeded(chartId); + } + + // only log chart render time which is triggered by query results change + // currently we don't log chart re-render time, like window resize etc + if (this.hasQueryResponseChange) { + actions.logEvent(LOG_ACTIONS_RENDER_CHART, { + slice_id: chartId, + viz_type: vizType, + start_offset: this.renderStartTime, + ts: new Date().getTime(), + duration: Logger.getTimestamp() - this.renderStartTime, + }); + } + } + handleSetControlValue(...args) { const { setControlValue } = this.props; if (setControlValue) { diff --git a/superset-frontend/src/components/AlteredSliceTag.jsx b/superset-frontend/src/components/AlteredSliceTag.jsx index 53a5b8ca23db..5a013afbc76e 100644 --- a/superset-frontend/src/components/AlteredSliceTag.jsx +++ b/superset-frontend/src/components/AlteredSliceTag.jsx @@ -71,14 +71,6 @@ export default class AlteredSliceTag extends React.Component { })); } - getRowsFromDiffs(diffs, controlsMap) { - return Object.entries(diffs).map(([key, diff]) => ({ - control: (controlsMap[key] && controlsMap[key].label) || key, - before: this.formatValue(diff.before, key, controlsMap), - after: this.formatValue(diff.after, key, controlsMap), - })); - } - getDiffs(props) { // Returns all properties that differ in the // current form data and the saved form data @@ -101,8 +93,12 @@ export default class AlteredSliceTag extends React.Component { return diffs; } - isEqualish(val1, val2) { - return isEqual(alterForComparison(val1), alterForComparison(val2)); + getRowsFromDiffs(diffs, controlsMap) { + return Object.entries(diffs).map(([key, diff]) => ({ + control: (controlsMap[key] && controlsMap[key].label) || key, + before: this.formatValue(diff.before, key, controlsMap), + after: this.formatValue(diff.after, key, controlsMap), + })); } formatValue(value, key, controlsMap) { @@ -146,6 +142,10 @@ export default class AlteredSliceTag extends React.Component { return safeStringify(value); } + isEqualish(val1, val2) { + return isEqual(alterForComparison(val1), alterForComparison(val2)); + } + renderModalBody() { const columns = [ { diff --git a/superset-frontend/src/components/CachedLabel.jsx b/superset-frontend/src/components/CachedLabel.jsx index db4bd2e46dba..7ed3fb85f1be 100644 --- a/superset-frontend/src/components/CachedLabel.jsx +++ b/superset-frontend/src/components/CachedLabel.jsx @@ -39,6 +39,15 @@ class CacheLabel extends React.PureComponent { }; } + mouseOut() { + this.setState({ hovered: false }); + } + + mouseOver() { + this.updateTooltipContent(); + this.setState({ hovered: true }); + } + updateTooltipContent() { const cachedText = this.props.cachedTimestamp ? ( @@ -57,15 +66,6 @@ class CacheLabel extends React.PureComponent { this.setState({ tooltipContent }); } - mouseOver() { - this.updateTooltipContent(); - this.setState({ hovered: true }); - } - - mouseOut() { - this.setState({ hovered: false }); - } - render() { const labelStyle = this.state.hovered ? 'primary' : 'default'; return ( diff --git a/superset-frontend/src/components/CopyToClipboard.jsx b/superset-frontend/src/components/CopyToClipboard.jsx index 5caf9d610cfa..86f9d677931c 100644 --- a/superset-frontend/src/components/CopyToClipboard.jsx +++ b/superset-frontend/src/components/CopyToClipboard.jsx @@ -56,11 +56,6 @@ class CopyToClipboard extends React.Component { this.onClick = this.onClick.bind(this); } - onMouseOut() { - // delay to avoid flash of text change on tooltip - setTimeout(this.resetTooltipText, 200); - } - onClick() { if (this.props.getText) { this.props.getText(d => { @@ -71,6 +66,11 @@ class CopyToClipboard extends React.Component { } } + onMouseOut() { + // delay to avoid flash of text change on tooltip + setTimeout(this.resetTooltipText, 200); + } + getDecoratedCopyNode() { return React.cloneElement(this.props.copyNode, { style: { cursor: 'pointer' }, @@ -79,10 +79,6 @@ class CopyToClipboard extends React.Component { }); } - resetTooltipText() { - this.setState({ tooltipText: this.props.tooltipText }); - } - copyToClipboard(textToCopy) { copyTextToClipboard(textToCopy) .then(() => { @@ -100,20 +96,8 @@ class CopyToClipboard extends React.Component { }); } - renderNotWrapped() { - return ( - - {this.getDecoratedCopyNode()} - - ); + resetTooltipText() { + this.setState({ tooltipText: this.props.tooltipText }); } renderLink() { @@ -136,6 +120,22 @@ class CopyToClipboard extends React.Component { ); } + renderNotWrapped() { + return ( + + {this.getDecoratedCopyNode()} + + ); + } + render() { const { wrapped } = this.props; if (!wrapped) { diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder.jsx b/superset-frontend/src/dashboard/components/DashboardBuilder.jsx index 861f3ad7c7b1..822ef2bbb834 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder.jsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder.jsx @@ -112,15 +112,6 @@ const StyledDashboardContent = styled.div` `; class DashboardBuilder extends React.Component { - static shouldFocusTabs(event, container) { - // don't focus the tabs when we click on a tab - return ( - event.target.className === 'ant-tabs-nav-wrap' || - (/icon-button/.test(event.target.className) && - container.contains(event.target)) - ); - } - static getRootLevelTabIndex(dashboardLayout, directPathToChild) { return Math.max( 0, @@ -141,6 +132,15 @@ class DashboardBuilder extends React.Component { : dashboardLayout[rootChildId]; } + static shouldFocusTabs(event, container) { + // don't focus the tabs when we click on a tab + return ( + event.target.className === 'ant-tabs-nav-wrap' || + (/icon-button/.test(event.target.className) && + container.contains(event.target)) + ); + } + constructor(props) { super(props); @@ -185,20 +185,6 @@ class DashboardBuilder extends React.Component { } } - toggleDashboardFiltersOpen(visible) { - if (visible === undefined) { - this.setState(state => ({ - ...state, - dashboardFiltersOpen: !state.dashboardFiltersOpen, - })); - } else { - this.setState(state => ({ - ...state, - dashboardFiltersOpen: visible, - })); - } - } - handleChangeTab({ pathToTabIndex }) { this.props.setDirectPathToChild(pathToTabIndex); } @@ -214,6 +200,20 @@ class DashboardBuilder extends React.Component { this.props.setDirectPathToChild(firstTab); } + toggleDashboardFiltersOpen(visible) { + if (visible === undefined) { + this.setState(state => ({ + ...state, + dashboardFiltersOpen: !state.dashboardFiltersOpen, + })); + } else { + this.setState(state => ({ + ...state, + dashboardFiltersOpen: visible, + })); + } + } + render() { const { handleComponentDrop, diff --git a/superset-frontend/src/dashboard/components/DashboardGrid.jsx b/superset-frontend/src/dashboard/components/DashboardGrid.jsx index 6889c91ab3de..6214557bd82f 100644 --- a/superset-frontend/src/dashboard/components/DashboardGrid.jsx +++ b/superset-frontend/src/dashboard/components/DashboardGrid.jsx @@ -70,6 +70,16 @@ class DashboardGrid extends React.PureComponent { this.grid = ref; } + handleChangeTab({ pathToTabIndex }) { + this.props.setDirectPathToChild(pathToTabIndex); + } + + handleResize({ ref, direction }) { + if (direction === 'bottom' || direction === 'bottomRight') { + this.setState(() => ({ rowGuideTop: this.getRowGuidePosition(ref) })); + } + } + handleResizeStart({ ref, direction }) { let rowGuideTop = null; if (direction === 'bottom' || direction === 'bottomRight') { @@ -82,12 +92,6 @@ class DashboardGrid extends React.PureComponent { })); } - handleResize({ ref, direction }) { - if (direction === 'bottom' || direction === 'bottomRight') { - this.setState(() => ({ rowGuideTop: this.getRowGuidePosition(ref) })); - } - } - handleResizeStop({ id, widthMultiple: width, heightMultiple: height }) { this.props.resizeComponent({ id, width, height }); @@ -110,10 +114,6 @@ class DashboardGrid extends React.PureComponent { } } - handleChangeTab({ pathToTabIndex }) { - this.props.setDirectPathToChild(pathToTabIndex); - } - render() { const { gridComponent, diff --git a/superset-frontend/src/dashboard/components/Header.jsx b/superset-frontend/src/dashboard/components/Header.jsx index 9ef547197f32..68ebda1ef9ef 100644 --- a/superset-frontend/src/dashboard/components/Header.jsx +++ b/superset-frontend/src/dashboard/components/Header.jsx @@ -174,6 +174,25 @@ class Header extends React.PureComponent { clearTimeout(this.ctrlZTimeout); } + forceRefresh() { + if (!this.props.isLoading) { + const chartList = Object.keys(this.props.charts); + this.props.logEvent(LOG_ACTIONS_FORCE_REFRESH_DASHBOARD, { + force: true, + interval: 0, + chartCount: chartList.length, + }); + + return this.props.fetchCharts( + chartList, + true, + 0, + this.props.dashboardInfo.id, + ); + } + return false; + } + handleChangeText(nextText) { const { updateDashboardTitle, onChange } = this.props; if (nextText && this.props.dashboardTitle !== nextText) { @@ -202,77 +221,8 @@ class Header extends React.PureComponent { }); } - forceRefresh() { - if (!this.props.isLoading) { - const chartList = Object.keys(this.props.charts); - this.props.logEvent(LOG_ACTIONS_FORCE_REFRESH_DASHBOARD, { - force: true, - interval: 0, - chartCount: chartList.length, - }); - - return this.props.fetchCharts( - chartList, - true, - 0, - this.props.dashboardInfo.id, - ); - } - return false; - } - - startPeriodicRender(interval) { - let intervalMessage; - if (interval) { - const predefinedValue = PeriodicRefreshOptions.find( - option => option.value === interval / 1000, - ); - if (predefinedValue) { - intervalMessage = predefinedValue.label; - } else { - intervalMessage = moment.duration(interval, 'millisecond').humanize(); - } - } - - const periodicRender = () => { - const { fetchCharts, logEvent, charts, dashboardInfo } = this.props; - const { metadata } = dashboardInfo; - const immune = metadata.timed_refresh_immune_slices || []; - const affectedCharts = Object.values(charts) - .filter(chart => immune.indexOf(chart.id) === -1) - .map(chart => chart.id); - - logEvent(LOG_ACTIONS_PERIODIC_RENDER_DASHBOARD, { - interval, - chartCount: affectedCharts.length, - }); - this.props.addWarningToast( - t( - `This dashboard is currently force refreshing; the next force refresh will be in %s.`, - intervalMessage, - ), - ); - - return fetchCharts( - affectedCharts, - true, - interval * 0.2, - dashboardInfo.id, - ); - }; - - this.refreshTimer = setPeriodicRunner({ - interval, - periodicRender, - refreshTimer: this.refreshTimer, - }); - } - - toggleEditMode() { - this.props.logEvent(LOG_ACTIONS_TOGGLE_EDIT_DASHBOARD, { - edit_mode: !this.props.editMode, - }); - this.props.setEditMode(!this.props.editMode); + hidePropertiesModal() { + this.setState({ showingPropertiesModal: false }); } overwriteDashboard() { @@ -344,8 +294,58 @@ class Header extends React.PureComponent { this.setState({ showingPropertiesModal: true }); } - hidePropertiesModal() { - this.setState({ showingPropertiesModal: false }); + startPeriodicRender(interval) { + let intervalMessage; + if (interval) { + const predefinedValue = PeriodicRefreshOptions.find( + option => option.value === interval / 1000, + ); + if (predefinedValue) { + intervalMessage = predefinedValue.label; + } else { + intervalMessage = moment.duration(interval, 'millisecond').humanize(); + } + } + + const periodicRender = () => { + const { fetchCharts, logEvent, charts, dashboardInfo } = this.props; + const { metadata } = dashboardInfo; + const immune = metadata.timed_refresh_immune_slices || []; + const affectedCharts = Object.values(charts) + .filter(chart => immune.indexOf(chart.id) === -1) + .map(chart => chart.id); + + logEvent(LOG_ACTIONS_PERIODIC_RENDER_DASHBOARD, { + interval, + chartCount: affectedCharts.length, + }); + this.props.addWarningToast( + t( + `This dashboard is currently force refreshing; the next force refresh will be in %s.`, + intervalMessage, + ), + ); + + return fetchCharts( + affectedCharts, + true, + interval * 0.2, + dashboardInfo.id, + ); + }; + + this.refreshTimer = setPeriodicRunner({ + interval, + periodicRender, + refreshTimer: this.refreshTimer, + }); + } + + toggleEditMode() { + this.props.logEvent(LOG_ACTIONS_TOGGLE_EDIT_DASHBOARD, { + edit_mode: !this.props.editMode, + }); + this.props.setEditMode(!this.props.editMode); } render() { diff --git a/superset-frontend/src/dashboard/components/PropertiesModal.jsx b/superset-frontend/src/dashboard/components/PropertiesModal.jsx index 1cc75b12ed6b..ff6e6879c14b 100644 --- a/superset-frontend/src/dashboard/components/PropertiesModal.jsx +++ b/superset-frontend/src/dashboard/components/PropertiesModal.jsx @@ -130,6 +130,11 @@ class PropertiesModal extends React.PureComponent { JsonEditor.preload(); } + onChange(e) { + const { name, value } = e.target; + this.updateFormState(name, value); + } + onColorSchemeChange(value, { updateMetadata = true } = {}) { // check that color_scheme is valid const colorChoices = getCategoricalSchemeRegistry().keys(); @@ -159,17 +164,12 @@ class PropertiesModal extends React.PureComponent { this.updateFormState('colorScheme', value); } - onOwnersChange(value) { - this.updateFormState('owners', value); - } - onMetadataChange(metadata) { this.updateFormState('json_metadata', metadata); } - onChange(e) { - const { name, value } = e.target; - this.updateFormState(name, value); + onOwnersChange(value) { + this.updateFormState('owners', value); } fetchDashboardDetails() { @@ -206,21 +206,6 @@ class PropertiesModal extends React.PureComponent { }, handleErrorResponse); } - updateFormState(name, value) { - this.setState(state => ({ - values: { - ...state.values, - [name]: value, - }, - })); - } - - toggleAdvanced() { - this.setState(state => ({ - isAdvancedOpen: !state.isAdvancedOpen, - })); - } - submit(e) { e.preventDefault(); e.stopPropagation(); @@ -282,6 +267,21 @@ class PropertiesModal extends React.PureComponent { } } + toggleAdvanced() { + this.setState(state => ({ + isAdvancedOpen: !state.isAdvancedOpen, + })); + } + + updateFormState(name, value) { + this.setState(state => ({ + values: { + ...state.values, + [name]: value, + }, + })); + } + render() { const { values, isDashboardLoaded, isAdvancedOpen, errors } = this.state; const { onHide, onlyApply } = this.props; diff --git a/superset-frontend/src/dashboard/components/SliceAdder.jsx b/superset-frontend/src/dashboard/components/SliceAdder.jsx index 141ccac14a72..0ea089850a1b 100644 --- a/superset-frontend/src/dashboard/components/SliceAdder.jsx +++ b/superset-frontend/src/dashboard/components/SliceAdder.jsx @@ -136,16 +136,6 @@ class SliceAdder extends React.Component { } } - searchUpdated(searchTerm) { - this.setState(prevState => ({ - searchTerm, - filteredSlices: this.getFilteredSortedSlices( - searchTerm, - prevState.sortBy, - ), - })); - } - handleSelect(sortBy) { this.setState(prevState => ({ sortBy, @@ -202,6 +192,16 @@ class SliceAdder extends React.Component { ); } + searchUpdated(searchTerm) { + this.setState(prevState => ({ + searchTerm, + filteredSlices: this.getFilteredSortedSlices( + searchTerm, + prevState.sortBy, + ), + })); + } + render() { const slicesListHeight = this.props.height - diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls.jsx b/superset-frontend/src/dashboard/components/SliceHeaderControls.jsx index f985fe83c8cb..d75e58405ace 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls.jsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls.jsx @@ -114,21 +114,6 @@ class SliceHeaderControls extends React.PureComponent { }; } - refreshChart() { - if (this.props.updatedDttm) { - this.props.forceRefresh( - this.props.slice.slice_id, - this.props.dashboardId, - ); - } - } - - toggleControls() { - this.setState(prevState => ({ - showControls: !prevState.showControls, - })); - } - handleMenuClick({ key, domEvent }) { switch (key) { case MENU_KEYS.FORCE_REFRESH: @@ -166,6 +151,21 @@ class SliceHeaderControls extends React.PureComponent { } } + refreshChart() { + if (this.props.updatedDttm) { + this.props.forceRefresh( + this.props.slice.slice_id, + this.props.dashboardId, + ); + } + } + + toggleControls() { + this.setState(prevState => ({ + showControls: !prevState.showControls, + })); + } + render() { const { slice, diff --git a/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.jsx b/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.jsx index 6cfcc0eee0a8..4f19cff80921 100644 --- a/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.jsx +++ b/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.jsx @@ -178,6 +178,74 @@ export default class FilterScopeSelector extends React.PureComponent { this.onSave = this.onSave.bind(this); } + onChangeFilterField(filterField = {}) { + const { layout } = this.props; + const nextActiveFilterField = filterField.value; + const { + activeFilterField: currentActiveFilterField, + checkedFilterFields, + filterScopeMap, + } = this.state; + + // we allow single edit and multiple edit in the same view. + // if user click on the single filter field, + // will show filter scope for the single field. + // if user click on the same filter filed again, + // will toggle off the single filter field, + // and allow multi-edit all checked filter fields. + if (nextActiveFilterField === currentActiveFilterField) { + const filterScopeTreeEntry = buildFilterScopeTreeEntry({ + checkedFilterFields, + activeFilterField: null, + filterScopeMap, + layout, + }); + + this.setState({ + activeFilterField: null, + filterScopeMap: { + ...filterScopeMap, + ...filterScopeTreeEntry, + }, + }); + } else if (this.allfilterFields.includes(nextActiveFilterField)) { + const filterScopeTreeEntry = buildFilterScopeTreeEntry({ + checkedFilterFields, + activeFilterField: nextActiveFilterField, + filterScopeMap, + layout, + }); + + this.setState({ + activeFilterField: nextActiveFilterField, + filterScopeMap: { + ...filterScopeMap, + ...filterScopeTreeEntry, + }, + }); + } + } + + onCheckFilterField(checkedFilterFields = []) { + const { layout } = this.props; + const { filterScopeMap } = this.state; + const filterScopeTreeEntry = buildFilterScopeTreeEntry({ + checkedFilterFields, + activeFilterField: null, + filterScopeMap, + layout, + }); + + this.setState(() => ({ + activeFilterField: null, + checkedFilterFields, + filterScopeMap: { + ...filterScopeMap, + ...filterScopeTreeEntry, + }, + })); + } + onCheckFilterScope(checked = []) { const { activeFilterField, @@ -212,6 +280,16 @@ export default class FilterScopeSelector extends React.PureComponent { })); } + onClose() { + this.props.onCloseModal(); + } + + onExpandFilterField(expandedFilterIds = []) { + this.setState(() => ({ + expandedFilterIds, + })); + } + onExpandFilterScope(expanded = []) { const { activeFilterField, @@ -234,88 +312,6 @@ export default class FilterScopeSelector extends React.PureComponent { })); } - onCheckFilterField(checkedFilterFields = []) { - const { layout } = this.props; - const { filterScopeMap } = this.state; - const filterScopeTreeEntry = buildFilterScopeTreeEntry({ - checkedFilterFields, - activeFilterField: null, - filterScopeMap, - layout, - }); - - this.setState(() => ({ - activeFilterField: null, - checkedFilterFields, - filterScopeMap: { - ...filterScopeMap, - ...filterScopeTreeEntry, - }, - })); - } - - onExpandFilterField(expandedFilterIds = []) { - this.setState(() => ({ - expandedFilterIds, - })); - } - - onChangeFilterField(filterField = {}) { - const { layout } = this.props; - const nextActiveFilterField = filterField.value; - const { - activeFilterField: currentActiveFilterField, - checkedFilterFields, - filterScopeMap, - } = this.state; - - // we allow single edit and multiple edit in the same view. - // if user click on the single filter field, - // will show filter scope for the single field. - // if user click on the same filter filed again, - // will toggle off the single filter field, - // and allow multi-edit all checked filter fields. - if (nextActiveFilterField === currentActiveFilterField) { - const filterScopeTreeEntry = buildFilterScopeTreeEntry({ - checkedFilterFields, - activeFilterField: null, - filterScopeMap, - layout, - }); - - this.setState({ - activeFilterField: null, - filterScopeMap: { - ...filterScopeMap, - ...filterScopeTreeEntry, - }, - }); - } else if (this.allfilterFields.includes(nextActiveFilterField)) { - const filterScopeTreeEntry = buildFilterScopeTreeEntry({ - checkedFilterFields, - activeFilterField: nextActiveFilterField, - filterScopeMap, - layout, - }); - - this.setState({ - activeFilterField: nextActiveFilterField, - filterScopeMap: { - ...filterScopeMap, - ...filterScopeTreeEntry, - }, - }); - } - } - - onSearchInputChange(e) { - this.setState({ searchText: e.target.value }, this.filterTree); - } - - onClose() { - this.props.onCloseModal(); - } - onSave() { const { filterScopeMap } = this.state; @@ -343,6 +339,27 @@ export default class FilterScopeSelector extends React.PureComponent { this.props.onCloseModal(); } + onSearchInputChange(e) { + this.setState({ searchText: e.target.value }, this.filterTree); + } + + filterNodes(filtered = [], node = {}) { + const { searchText } = this.state; + const children = (node.children || []).reduce(this.filterNodes, []); + + if ( + // Node's label matches the search string + node.label.toLocaleLowerCase().indexOf(searchText.toLocaleLowerCase()) > + -1 || + // Or a children has a matching node + children.length + ) { + filtered.push({ ...node, children }); + } + + return filtered; + } + filterTree() { // Reset nodes back to unfiltered state if (!this.state.searchText) { @@ -403,21 +420,27 @@ export default class FilterScopeSelector extends React.PureComponent { } } - filterNodes(filtered = [], node = {}) { - const { searchText } = this.state; - const children = (node.children || []).reduce(this.filterNodes, []); - - if ( - // Node's label matches the search string - node.label.toLocaleLowerCase().indexOf(searchText.toLocaleLowerCase()) > - -1 || - // Or a children has a matching node - children.length - ) { - filtered.push({ ...node, children }); - } + renderEditingFiltersName() { + const { dashboardFilters } = this.props; + const { activeFilterField, checkedFilterFields } = this.state; + const currentFilterLabels = [] + .concat(activeFilterField || checkedFilterFields) + .map(key => { + const { chartId, column } = getChartIdAndColumnFromFilterKey(key); + return dashboardFilters[chartId].labels[column] || column; + }); - return filtered; + return ( +
+ {currentFilterLabels.length === 0 && t('No filter is selected.')} + {currentFilterLabels.length === 1 && t('Editing 1 filter:')} + {currentFilterLabels.length > 1 && + t('Batch editing %d filters:', currentFilterLabels.length)} + + {currentFilterLabels.join(', ')} + +
+ ); } renderFilterFieldList() { @@ -480,29 +503,6 @@ export default class FilterScopeSelector extends React.PureComponent { ); } - renderEditingFiltersName() { - const { dashboardFilters } = this.props; - const { activeFilterField, checkedFilterFields } = this.state; - const currentFilterLabels = [] - .concat(activeFilterField || checkedFilterFields) - .map(key => { - const { chartId, column } = getChartIdAndColumnFromFilterKey(key); - return dashboardFilters[chartId].labels[column] || column; - }); - - return ( -
- {currentFilterLabels.length === 0 && t('No filter is selected.')} - {currentFilterLabels.length === 1 && t('Editing 1 filter:')} - {currentFilterLabels.length > 1 && - t('Batch editing %d filters:', currentFilterLabels.length)} - - {currentFilterLabels.join(', ')} - -
- ); - } - render() { const { showSelector } = this.state; diff --git a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx index 7dd01e97873e..dcae97bf5cfb 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx @@ -184,11 +184,6 @@ export default class Chart extends React.Component { this.headerRef = ref; } - resize() { - const { width, height } = this.props; - this.setState(() => ({ width, height })); - } - changeFilter(newSelectedValues = {}) { this.props.logEvent(LOG_ACTIONS_CHANGE_DASHBOARD_FILTER, { id: this.props.chart.id, @@ -197,14 +192,6 @@ export default class Chart extends React.Component { this.props.changeFilter(this.props.chart.id, newSelectedValues); } - handleFilterMenuOpen(chartId, column) { - this.props.setFocusedFilterField(chartId, column); - } - - handleFilterMenuClose(chartId, column) { - this.props.unsetFocusedFilterField(chartId, column); - } - exploreChart() { this.props.logEvent(LOG_ACTIONS_EXPLORE_DASHBOARD_CHART, { slice_id: this.props.slice.slice_id, @@ -237,6 +224,19 @@ export default class Chart extends React.Component { ); } + handleFilterMenuClose(chartId, column) { + this.props.unsetFocusedFilterField(chartId, column); + } + + handleFilterMenuOpen(chartId, column) { + this.props.setFocusedFilterField(chartId, column); + } + + resize() { + const { width, height } = this.props; + this.setState(() => ({ width, height })); + } + render() { const { id, diff --git a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx index 61a0cf2e45e0..453570a9cce2 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx @@ -120,17 +120,6 @@ const FilterFocusHighlight = React.forwardRef( ); class ChartHolder extends React.Component { - static renderInFocusCSS(columnName) { - return ( - - ); - } - static getDerivedStateFromProps(props, state) { const { component, directPathToChild, directPathLastUpdated } = props; const { @@ -151,6 +140,17 @@ class ChartHolder extends React.Component { return null; } + static renderInFocusCSS(columnName) { + return ( + + ); + } + constructor(props) { super(props); this.state = { @@ -175,21 +175,6 @@ class ChartHolder extends React.Component { this.hideOutline(prevState, this.state); } - hideOutline(prevState, state) { - const { outlinedComponentId: timerKey } = state; - const { outlinedComponentId: prevTimerKey } = prevState; - - // because of timeout, there might be multiple charts showing outline - if (!!timerKey && !prevTimerKey) { - setTimeout(() => { - this.setState(() => ({ - outlinedComponentId: null, - outlinedColumnName: null, - })); - }, 2000); - } - } - handleChangeFocus(nextFocus) { this.setState(() => ({ isFocused: nextFocus })); } @@ -199,6 +184,10 @@ class ChartHolder extends React.Component { deleteComponent(id, parentId); } + handleToggleFullSize() { + this.setState(prevState => ({ isFullSize: !prevState.isFullSize })); + } + handleUpdateSliceName(nextName) { const { component, updateComponents } = this.props; updateComponents({ @@ -212,8 +201,19 @@ class ChartHolder extends React.Component { }); } - handleToggleFullSize() { - this.setState(prevState => ({ isFullSize: !prevState.isFullSize })); + hideOutline(prevState, state) { + const { outlinedComponentId: timerKey } = state; + const { outlinedComponentId: prevTimerKey } = prevState; + + // because of timeout, there might be multiple charts showing outline + if (!!timerKey && !prevTimerKey) { + setTimeout(() => { + this.setState(() => ({ + outlinedComponentId: null, + outlinedColumnName: null, + })); + }, 2000); + } } render() { diff --git a/superset-frontend/src/dashboard/components/gridComponents/Column.jsx b/superset-frontend/src/dashboard/components/gridComponents/Column.jsx index 78d272b551c0..0c07bb3d1239 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Column.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Column.jsx @@ -74,15 +74,15 @@ class Column extends React.PureComponent { this.handleDeleteComponent = this.handleDeleteComponent.bind(this); } + handleChangeFocus(nextFocus) { + this.setState(() => ({ isFocused: Boolean(nextFocus) })); + } + handleDeleteComponent() { const { deleteComponent, id, parentId } = this.props; deleteComponent(id, parentId); } - handleChangeFocus(nextFocus) { - this.setState(() => ({ isFocused: Boolean(nextFocus) })); - } - handleUpdateMeta(metaKey, nextValue) { const { updateComponents, component } = this.props; if (nextValue && component.meta[metaKey] !== nextValue) { diff --git a/superset-frontend/src/dashboard/components/gridComponents/Header.jsx b/superset-frontend/src/dashboard/components/gridComponents/Header.jsx index 3e380773f185..8e5df059e6e6 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Header.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Header.jsx @@ -73,6 +73,11 @@ class Header extends React.PureComponent { this.setState(() => ({ isFocused: nextFocus })); } + handleDeleteComponent() { + const { deleteComponent, id, parentId } = this.props; + deleteComponent(id, parentId); + } + handleUpdateMeta(metaKey, nextValue) { const { updateComponents, component } = this.props; if (nextValue && component.meta[metaKey] !== nextValue) { @@ -88,11 +93,6 @@ class Header extends React.PureComponent { } } - handleDeleteComponent() { - const { deleteComponent, id, parentId } = this.props; - deleteComponent(id, parentId); - } - render() { const { isFocused } = this.state; diff --git a/superset-frontend/src/dashboard/components/gridComponents/Markdown.jsx b/superset-frontend/src/dashboard/components/gridComponents/Markdown.jsx index b071a9982dfe..d48dd846e404 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Markdown.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Markdown.jsx @@ -89,33 +89,10 @@ function isSafeMarkup(node) { } class Markdown extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - isFocused: false, - markdownSource: props.component.meta.code, - editor: null, - editorMode: 'preview', - undoLength: props.undoLength, - redoLength: props.redoLength, + static getDerivedStateFromError() { + return { + hasError: true, }; - this.renderStartTime = Logger.getTimestamp(); - - this.handleChangeFocus = this.handleChangeFocus.bind(this); - this.handleChangeEditorMode = this.handleChangeEditorMode.bind(this); - this.handleMarkdownChange = this.handleMarkdownChange.bind(this); - this.handleDeleteComponent = this.handleDeleteComponent.bind(this); - this.handleResizeStart = this.handleResizeStart.bind(this); - this.setEditor = this.setEditor.bind(this); - } - - componentDidMount() { - this.props.logEvent(LOG_ACTIONS_RENDER_CHART, { - viz_type: 'markdown', - start_offset: this.renderStartTime, - ts: new Date().getTime(), - duration: Logger.getTimestamp() - this.renderStartTime, - }); } static getDerivedStateFromProps(nextProps, state) { @@ -155,10 +132,33 @@ class Markdown extends React.PureComponent { return state; } - static getDerivedStateFromError() { - return { - hasError: true, + constructor(props) { + super(props); + this.state = { + isFocused: false, + markdownSource: props.component.meta.code, + editor: null, + editorMode: 'preview', + undoLength: props.undoLength, + redoLength: props.redoLength, }; + this.renderStartTime = Logger.getTimestamp(); + + this.handleChangeFocus = this.handleChangeFocus.bind(this); + this.handleChangeEditorMode = this.handleChangeEditorMode.bind(this); + this.handleMarkdownChange = this.handleMarkdownChange.bind(this); + this.handleDeleteComponent = this.handleDeleteComponent.bind(this); + this.handleResizeStart = this.handleResizeStart.bind(this); + this.setEditor = this.setEditor.bind(this); + } + + componentDidMount() { + this.props.logEvent(LOG_ACTIONS_RENDER_CHART, { + viz_type: 'markdown', + start_offset: this.renderStartTime, + ts: new Date().getTime(), + duration: Logger.getTimestamp() - this.renderStartTime, + }); } componentDidUpdate(prevProps) { @@ -192,13 +192,6 @@ class Markdown extends React.PureComponent { }); } - handleChangeFocus(nextFocus) { - const nextFocused = !!nextFocus; - const nextEditMode = nextFocused ? 'edit' : 'preview'; - this.setState(() => ({ isFocused: nextFocused })); - this.handleChangeEditorMode(nextEditMode); - } - handleChangeEditorMode(mode) { const nextState = { ...this.state, @@ -212,19 +205,16 @@ class Markdown extends React.PureComponent { this.setState(nextState); } - updateMarkdownContent() { - const { updateComponents, component } = this.props; - if (component.meta.code !== this.state.markdownSource) { - updateComponents({ - [component.id]: { - ...component, - meta: { - ...component.meta, - code: this.state.markdownSource, - }, - }, - }); - } + handleChangeFocus(nextFocus) { + const nextFocused = !!nextFocus; + const nextEditMode = nextFocused ? 'edit' : 'preview'; + this.setState(() => ({ isFocused: nextFocused })); + this.handleChangeEditorMode(nextEditMode); + } + + handleDeleteComponent() { + const { deleteComponent, id, parentId } = this.props; + deleteComponent(id, parentId); } handleMarkdownChange(nextValue) { @@ -233,11 +223,6 @@ class Markdown extends React.PureComponent { }); } - handleDeleteComponent() { - const { deleteComponent, id, parentId } = this.props; - deleteComponent(id, parentId); - } - handleResizeStart(e) { const { editorMode } = this.state; const { editMode, onResizeStart } = this.props; @@ -248,6 +233,21 @@ class Markdown extends React.PureComponent { } } + updateMarkdownContent() { + const { updateComponents, component } = this.props; + if (component.meta.code !== this.state.markdownSource) { + updateComponents({ + [component.id]: { + ...component, + meta: { + ...component.meta, + code: this.state.markdownSource, + }, + }, + }); + } + } + renderEditMode() { return ( ({ isFocused: Boolean(nextFocus) })); } + handleDeleteComponent() { + const { deleteComponent, component, parentId } = this.props; + deleteComponent(component.id, parentId); + } + handleUpdateMeta(metaKey, nextValue) { const { updateComponents, component } = this.props; if (nextValue && component.meta[metaKey] !== nextValue) { @@ -90,11 +95,6 @@ class Row extends React.PureComponent { } } - handleDeleteComponent() { - const { deleteComponent, component, parentId } = this.props; - deleteComponent(component.id, parentId); - } - render() { const { component: rowComponent, diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx index cda808c024bb..7c6d0c6b8cff 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx @@ -108,6 +108,51 @@ export default class Tab extends React.PureComponent { } } + renderTab() { + const { + component, + parentComponent, + index, + depth, + editMode, + filters, + isFocused, + } = this.props; + + return ( + + {({ dropIndicatorProps, dragSourceRef }) => ( +
+ + {!editMode && ( + = 5 ? 'left' : 'right'} + /> + )} + + {dropIndicatorProps &&
} +
+ )} + + ); + } + renderTabContent() { const { component: tabComponent, @@ -183,51 +228,6 @@ export default class Tab extends React.PureComponent { ); } - renderTab() { - const { - component, - parentComponent, - index, - depth, - editMode, - filters, - isFocused, - } = this.props; - - return ( - - {({ dropIndicatorProps, dragSourceRef }) => ( -
- - {!editMode && ( - = 5 ? 'left' : 'right'} - /> - )} - - {dropIndicatorProps &&
} -
- )} - - ); - } - render() { const { renderType } = this.props; return renderType === RENDER_TAB diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx index 455a41a94c38..e84e50ec3661 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx @@ -163,48 +163,6 @@ class Tabs extends React.PureComponent { } } - showDeleteConfirmModal = key => { - const { component, deleteComponent } = this.props; - Modal.confirm({ - title: t('Delete dashboard tab?'), - content: ( - - Deleting a tab will remove all content within it. You may still - reverse this action with the undo button (cmd + z) until you - save your changes. - - ), - onOk: () => { - deleteComponent(key, component.id); - const tabIndex = component.children.indexOf(key); - this.handleClickTab(Math.max(0, tabIndex - 1)); - }, - okType: 'danger', - okText: 'DELETE', - cancelText: 'CANCEL', - icon: null, - }); - }; - - handleEdit = (key, action) => { - const { component, createComponent } = this.props; - if (action === 'add') { - createComponent({ - destination: { - id: component.id, - type: component.type, - index: component.children.length, - }, - dragging: { - id: NEW_TAB_ID, - type: TAB_TYPE, - }, - }); - } else if (action === 'remove') { - this.showDeleteConfirmModal(key); - } - }; - handleClickTab(tabIndex) { const { component } = this.props; @@ -248,6 +206,48 @@ class Tabs extends React.PureComponent { } } + handleEdit = (key, action) => { + const { component, createComponent } = this.props; + if (action === 'add') { + createComponent({ + destination: { + id: component.id, + type: component.type, + index: component.children.length, + }, + dragging: { + id: NEW_TAB_ID, + type: TAB_TYPE, + }, + }); + } else if (action === 'remove') { + this.showDeleteConfirmModal(key); + } + }; + + showDeleteConfirmModal = key => { + const { component, deleteComponent } = this.props; + Modal.confirm({ + title: t('Delete dashboard tab?'), + content: ( + + Deleting a tab will remove all content within it. You may still + reverse this action with the undo button (cmd + z) until you + save your changes. + + ), + onOk: () => { + deleteComponent(key, component.id); + const tabIndex = component.children.indexOf(key); + this.handleClickTab(Math.max(0, tabIndex - 1)); + }, + okType: 'danger', + okText: 'DELETE', + cancelText: 'CANCEL', + icon: null, + }); + }; + render() { const { depth, diff --git a/superset-frontend/src/dashboard/components/resizable/ResizableContainer.jsx b/superset-frontend/src/dashboard/components/resizable/ResizableContainer.jsx index ff576101f491..a179f164401b 100644 --- a/superset-frontend/src/dashboard/components/resizable/ResizableContainer.jsx +++ b/superset-frontend/src/dashboard/components/resizable/ResizableContainer.jsx @@ -93,6 +93,13 @@ class ResizableContainer extends React.PureComponent { this.handleResizeStop = this.handleResizeStop.bind(this); } + handleResize(event, direction, ref) { + const { onResize, id } = this.props; + if (onResize) { + onResize({ id, direction, ref }); + } + } + handleResizeStart(event, direction, ref) { const { id, onResizeStart } = this.props; @@ -103,13 +110,6 @@ class ResizableContainer extends React.PureComponent { this.setState(() => ({ isResizing: true })); } - handleResize(event, direction, ref) { - const { onResize, id } = this.props; - if (onResize) { - onResize({ id, direction, ref }); - } - } - handleResizeStop(event, direction, ref, delta) { const { id, diff --git a/superset-frontend/src/datasource/DatasourceEditor.jsx b/superset-frontend/src/datasource/DatasourceEditor.jsx index 347947aca30c..3b9d58074c58 100644 --- a/superset-frontend/src/datasource/DatasourceEditor.jsx +++ b/superset-frontend/src/datasource/DatasourceEditor.jsx @@ -358,8 +358,60 @@ class DatasourceEditor extends React.PureComponent { this.setState(obj, this.validateAndChange); } - validateAndChange() { - this.validate(this.onChange); + findDuplicates(arr, accessor) { + const seen = {}; + const dups = []; + arr.forEach(obj => { + const item = accessor(obj); + if (item in seen) { + dups.push(item); + } else { + seen[item] = null; + } + }); + return dups; + } + + handleTabSelect(activeTabKey) { + this.setState({ activeTabKey }); + } + + syncMetadata() { + const { datasource } = this.state; + const endpoint = `/datasource/external_metadata/${ + datasource.type || datasource.datasource_type + }/${datasource.id}/`; + this.setState({ metadataLoading: true }); + + SupersetClient.get({ endpoint }) + .then(({ json }) => { + const results = this.updateColumns(json); + if (results.modified.length) { + this.props.addSuccessToast( + t('Modified columns: %s', results.modified.join(', ')), + ); + } + if (results.removed.length) { + this.props.addSuccessToast( + t('Removed columns: %s', results.removed.join(', ')), + ); + } + if (results.added.length) { + this.props.addSuccessToast( + t('New columns added: %s', results.added.join(', ')), + ); + } + this.props.addSuccessToast(t('Metadata has been synced')); + this.setState({ metadataLoading: false }); + }) + .catch(response => + getClientErrorObject(response).then(({ error, statusText }) => { + this.props.addDangerToast( + error || statusText || t('An error has occurred'), + ); + this.setState({ metadataLoading: false }); + }), + ); } updateColumns(cols) { @@ -414,58 +466,6 @@ class DatasourceEditor extends React.PureComponent { return results; } - syncMetadata() { - const { datasource } = this.state; - const endpoint = `/datasource/external_metadata/${ - datasource.type || datasource.datasource_type - }/${datasource.id}/`; - this.setState({ metadataLoading: true }); - - SupersetClient.get({ endpoint }) - .then(({ json }) => { - const results = this.updateColumns(json); - if (results.modified.length) { - this.props.addSuccessToast( - t('Modified columns: %s', results.modified.join(', ')), - ); - } - if (results.removed.length) { - this.props.addSuccessToast( - t('Removed columns: %s', results.removed.join(', ')), - ); - } - if (results.added.length) { - this.props.addSuccessToast( - t('New columns added: %s', results.added.join(', ')), - ); - } - this.props.addSuccessToast(t('Metadata has been synced')); - this.setState({ metadataLoading: false }); - }) - .catch(response => - getClientErrorObject(response).then(({ error, statusText }) => { - this.props.addDangerToast( - error || statusText || t('An error has occurred'), - ); - this.setState({ metadataLoading: false }); - }), - ); - } - - findDuplicates(arr, accessor) { - const seen = {}; - const dups = []; - arr.forEach(obj => { - const item = accessor(obj); - if (item in seen) { - dups.push(item); - } else { - seen[item] = null; - } - }); - return dups; - } - validate(callback) { let errors = []; let dups; @@ -496,8 +496,180 @@ class DatasourceEditor extends React.PureComponent { this.setState({ errors }, callback); } - handleTabSelect(activeTabKey) { - this.setState({ activeTabKey }); + validateAndChange() { + this.validate(this.onChange); + } + + renderAdvancedFieldset() { + const { datasource } = this.state; + return ( +
+ } + /> + } + /> + {this.state.isSqla && ( + } + /> + )} +
+ ); + } + + renderErrors() { + if (this.state.errors.length > 0) { + return ( + + {this.state.errors.map(err => ( +
{err}
+ ))} +
+ ); + } + return null; + } + + renderMetricCollection() { + return ( + +
+ } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> +
+ + } + collection={this.state.datasource.metrics} + allowAddItem + onChange={this.onDatasourcePropChange.bind(this, 'metrics')} + itemGenerator={() => ({ + metric_name: '', + verbose_name: '', + expression: '', + })} + itemRenderers={{ + metric_name: (v, onChange, _, record) => ( + + {record.is_certified && ( + + )} + + + ), + verbose_name: (v, onChange) => ( + + ), + expression: (v, onChange) => ( + + ), + description: (v, onChange, label) => ( + } + /> + ), + d3format: (v, onChange, label) => ( + } + /> + ), + }} + allowDeletes + /> + ); } renderSettingsFieldset() { @@ -584,72 +756,6 @@ class DatasourceEditor extends React.PureComponent { ); } - renderAdvancedFieldset() { - const { datasource } = this.state; - return ( -
- } - /> - } - /> - {this.state.isSqla && ( - } - /> - )} -
- ); - } - - renderSpatialTab() { - const { datasource } = this.state; - const { spatials, all_cols: allCols } = datasource; - return ( - } - key={4} - > - ({ - name: '', - type: '', - config: null, - })} - collection={spatials} - allowDeletes - itemRenderers={{ - name: (d, onChange) => ( - - ), - config: (v, onChange) => ( - - ), - }} - /> - - ); - } - renderSourceFieldset() { const { datasource } = this.state; return ( @@ -810,140 +916,34 @@ class DatasourceEditor extends React.PureComponent { ); } - renderErrors() { - if (this.state.errors.length > 0) { - return ( - - {this.state.errors.map(err => ( -
{err}
- ))} -
- ); - } - return null; - } - - renderMetricCollection() { + renderSpatialTab() { + const { datasource } = this.state; + const { spatials, all_cols: allCols } = datasource; return ( - -
- } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> -
- - } - collection={this.state.datasource.metrics} - allowAddItem - onChange={this.onDatasourcePropChange.bind(this, 'metrics')} - itemGenerator={() => ({ - metric_name: '', - verbose_name: '', - expression: '', - })} - itemRenderers={{ - metric_name: (v, onChange, _, record) => ( - - {record.is_certified && ( - - )} - - - ), - verbose_name: (v, onChange) => ( - - ), - expression: (v, onChange) => ( - - ), - description: (v, onChange, label) => ( - } - /> - ), - d3format: (v, onChange, label) => ( - } - /> - ), - }} - allowDeletes - /> + } + key={4} + > + ({ + name: '', + type: '', + config: null, + })} + collection={spatials} + allowDeletes + itemRenderers={{ + name: (d, onChange) => ( + + ), + config: (v, onChange) => ( + + ), + }} + /> + ); } diff --git a/superset-frontend/src/explore/components/AdhocFilterEditPopover.jsx b/superset-frontend/src/explore/components/AdhocFilterEditPopover.jsx index 005d43774298..5d97e12cebfc 100644 --- a/superset-frontend/src/explore/components/AdhocFilterEditPopover.jsx +++ b/superset-frontend/src/explore/components/AdhocFilterEditPopover.jsx @@ -85,11 +85,6 @@ export default class AdhocFilterEditPopover extends React.Component { this.setState({ adhocFilter }); } - onSave() { - this.props.onChange(this.state.adhocFilter); - this.props.onClose(); - } - onDragDown(e) { this.dragStartX = e.clientX; this.dragStartY = e.clientY; @@ -116,6 +111,11 @@ export default class AdhocFilterEditPopover extends React.Component { document.removeEventListener('mousemove', this.onMouseMove); } + onSave() { + this.props.onChange(this.state.adhocFilter); + this.props.onClose(); + } + adjustHeight(heightDifference) { this.setState(state => ({ height: state.height + heightDifference })); } diff --git a/superset-frontend/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx b/superset-frontend/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx index 5856bfafe78e..420f64a966ec 100644 --- a/superset-frontend/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx +++ b/superset-frontend/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx @@ -117,35 +117,19 @@ export default class AdhocFilterEditPopoverSimpleTabContent extends React.Compon } } - onSubjectChange(id) { - const option = this.props.options.find( - option => option.id === id || option.optionName === id, - ); - - let subject; - let clause; - // infer the new clause based on what subject was selected. - if (option && option.column_name) { - subject = option.column_name; - clause = CLAUSES.WHERE; - } else if (option && (option.saved_metric_name || option.label)) { - subject = option.saved_metric_name || option.label; - clause = CLAUSES.HAVING; - } - const { operator } = this.props.adhocFilter; + onComparatorChange(comparator) { this.props.onChange( this.props.adhocFilter.duplicateWith({ - subject, - clause, - operator: - operator && this.isOperatorRelevant(operator, subject) - ? operator - : null, + comparator, expressionType: EXPRESSION_TYPES.SIMPLE, }), ); } + onInputComparatorChange(event) { + this.onComparatorChange(event.target.value); + } + onOperatorChange(operator) { const currentComparator = this.props.adhocFilter.comparator; let newComparator; @@ -182,19 +166,73 @@ export default class AdhocFilterEditPopoverSimpleTabContent extends React.Compon } } - onInputComparatorChange(event) { - this.onComparatorChange(event.target.value); - } + onSubjectChange(id) { + const option = this.props.options.find( + option => option.id === id || option.optionName === id, + ); - onComparatorChange(comparator) { + let subject; + let clause; + // infer the new clause based on what subject was selected. + if (option && option.column_name) { + subject = option.column_name; + clause = CLAUSES.WHERE; + } else if (option && (option.saved_metric_name || option.label)) { + subject = option.saved_metric_name || option.label; + clause = CLAUSES.HAVING; + } + const { operator } = this.props.adhocFilter; this.props.onChange( this.props.adhocFilter.duplicateWith({ - comparator, + subject, + clause, + operator: + operator && this.isOperatorRelevant(operator, subject) + ? operator + : null, expressionType: EXPRESSION_TYPES.SIMPLE, }), ); } + createSuggestionsPlaceholder() { + const optionsRemaining = this.optionsRemaining(); + const placeholder = t('%s option(s)', optionsRemaining); + return optionsRemaining ? placeholder : ''; + } + + focusComparator(ref, shouldFocus) { + if (ref && shouldFocus) { + ref.focus(); + } + } + + isOperatorRelevant(operator, subject) { + if (operator && CUSTOM_OPERATORS.has(operator)) { + const { partitionColumn } = this.props; + return partitionColumn && subject && subject === partitionColumn; + } + + return !( + (this.props.datasource.type === 'druid' && + TABLE_ONLY_OPERATORS.indexOf(operator) >= 0) || + (this.props.datasource.type === 'table' && + DRUID_ONLY_OPERATORS.indexOf(operator) >= 0) || + (this.props.adhocFilter.clause === CLAUSES.HAVING && + HAVING_OPERATORS.indexOf(operator) === -1) + ); + } + + optionsRemaining() { + const { suggestions } = this.state; + const { comparator } = this.props.adhocFilter; + // if select is multi/value is array, we show the options not selected + const valuesFromSuggestionsLength = Array.isArray(comparator) + ? comparator.filter(v => suggestions.includes(v)).length + : 0; + return suggestions?.length - valuesFromSuggestionsLength ?? 0; + } + refreshComparatorSuggestions() { const { datasource } = this.props; const col = this.props.adhocFilter.subject; @@ -230,44 +268,6 @@ export default class AdhocFilterEditPopoverSimpleTabContent extends React.Compon } } - isOperatorRelevant(operator, subject) { - if (operator && CUSTOM_OPERATORS.has(operator)) { - const { partitionColumn } = this.props; - return partitionColumn && subject && subject === partitionColumn; - } - - return !( - (this.props.datasource.type === 'druid' && - TABLE_ONLY_OPERATORS.indexOf(operator) >= 0) || - (this.props.datasource.type === 'table' && - DRUID_ONLY_OPERATORS.indexOf(operator) >= 0) || - (this.props.adhocFilter.clause === CLAUSES.HAVING && - HAVING_OPERATORS.indexOf(operator) === -1) - ); - } - - focusComparator(ref, shouldFocus) { - if (ref && shouldFocus) { - ref.focus(); - } - } - - optionsRemaining() { - const { suggestions } = this.state; - const { comparator } = this.props.adhocFilter; - // if select is multi/value is array, we show the options not selected - const valuesFromSuggestionsLength = Array.isArray(comparator) - ? comparator.filter(v => suggestions.includes(v)).length - : 0; - return suggestions?.length - valuesFromSuggestionsLength ?? 0; - } - - createSuggestionsPlaceholder() { - const optionsRemaining = this.optionsRemaining(); - const placeholder = t('%s option(s)', optionsRemaining); - return optionsRemaining ? placeholder : ''; - } - renderSubjectOptionLabel(option) { return ; } diff --git a/superset-frontend/src/explore/components/AdhocFilterEditPopoverSqlTabContent.jsx b/superset-frontend/src/explore/components/AdhocFilterEditPopoverSqlTabContent.jsx index ffd01751f5c1..500231ce1505 100644 --- a/superset-frontend/src/explore/components/AdhocFilterEditPopoverSqlTabContent.jsx +++ b/superset-frontend/src/explore/components/AdhocFilterEditPopoverSqlTabContent.jsx @@ -61,19 +61,19 @@ export default class AdhocFilterEditPopoverSqlTabContent extends React.Component } } - onSqlExpressionClauseChange(clause) { + onSqlExpressionChange(sqlExpression) { this.props.onChange( this.props.adhocFilter.duplicateWith({ - clause, + sqlExpression, expressionType: EXPRESSION_TYPES.SQL, }), ); } - onSqlExpressionChange(sqlExpression) { + onSqlExpressionClauseChange(clause) { this.props.onChange( this.props.adhocFilter.duplicateWith({ - sqlExpression, + clause, expressionType: EXPRESSION_TYPES.SQL, }), ); diff --git a/superset-frontend/src/explore/components/AdhocMetricEditPopover.jsx b/superset-frontend/src/explore/components/AdhocMetricEditPopover.jsx index fe6b0194fc40..a2f4180e4b70 100644 --- a/superset-frontend/src/explore/components/AdhocMetricEditPopover.jsx +++ b/superset-frontend/src/explore/components/AdhocMetricEditPopover.jsx @@ -93,6 +93,64 @@ export default class AdhocMetricEditPopover extends React.Component { document.removeEventListener('mousemove', this.onMouseMove); } + onAggregateChange(aggregate) { + // we construct this object explicitly to overwrite the value in the case aggregate is null + this.setState(prevState => ({ + adhocMetric: prevState.adhocMetric.duplicateWith({ + aggregate, + expressionType: EXPRESSION_TYPES.SIMPLE, + }), + savedMetric: undefined, + })); + } + + onColumnChange(columnId) { + const column = this.props.columns.find(column => column.id === columnId); + this.setState(prevState => ({ + adhocMetric: prevState.adhocMetric.duplicateWith({ + column, + expressionType: EXPRESSION_TYPES.SIMPLE, + }), + savedMetric: undefined, + })); + } + + onDragDown(e) { + this.dragStartX = e.clientX; + this.dragStartY = e.clientY; + this.dragStartWidth = this.state.width; + this.dragStartHeight = this.state.height; + document.addEventListener('mousemove', this.onMouseMove); + } + + onMouseMove(e) { + this.props.onResize(); + this.setState({ + width: Math.max( + this.dragStartWidth + (e.clientX - this.dragStartX), + startingWidth, + ), + height: Math.max( + this.dragStartHeight + (e.clientY - this.dragStartY) * 2, + startingHeight, + ), + }); + } + + onMouseUp() { + document.removeEventListener('mousemove', this.onMouseMove); + } + + onResetStateAndClose() { + this.setState( + { + adhocMetric: this.props.adhocMetric, + savedMetric: this.props.savedMetric, + }, + this.props.onClose, + ); + } + onSave() { const { title } = this.props; const { hasCustomLabel } = title; @@ -118,38 +176,6 @@ export default class AdhocMetricEditPopover extends React.Component { this.props.onClose(); } - onResetStateAndClose() { - this.setState( - { - adhocMetric: this.props.adhocMetric, - savedMetric: this.props.savedMetric, - }, - this.props.onClose, - ); - } - - onColumnChange(columnId) { - const column = this.props.columns.find(column => column.id === columnId); - this.setState(prevState => ({ - adhocMetric: prevState.adhocMetric.duplicateWith({ - column, - expressionType: EXPRESSION_TYPES.SIMPLE, - }), - savedMetric: undefined, - })); - } - - onAggregateChange(aggregate) { - // we construct this object explicitly to overwrite the value in the case aggregate is null - this.setState(prevState => ({ - adhocMetric: prevState.adhocMetric.duplicateWith({ - aggregate, - expressionType: EXPRESSION_TYPES.SIMPLE, - }), - savedMetric: undefined, - })); - } - onSavedMetricChange(savedMetricId) { const savedMetric = this.props.savedMetrics.find( metric => metric.id === savedMetricId, @@ -175,32 +201,6 @@ export default class AdhocMetricEditPopover extends React.Component { })); } - onDragDown(e) { - this.dragStartX = e.clientX; - this.dragStartY = e.clientY; - this.dragStartWidth = this.state.width; - this.dragStartHeight = this.state.height; - document.addEventListener('mousemove', this.onMouseMove); - } - - onMouseMove(e) { - this.props.onResize(); - this.setState({ - width: Math.max( - this.dragStartWidth + (e.clientX - this.dragStartX), - startingWidth, - ), - height: Math.max( - this.dragStartHeight + (e.clientY - this.dragStartY) * 2, - startingHeight, - ), - }); - } - - onMouseUp() { - document.removeEventListener('mousemove', this.onMouseMove); - } - handleAceEditorRef(ref) { if (ref) { this.aceEditorRef = ref; diff --git a/superset-frontend/src/explore/components/AdhocMetricEditPopoverTitle.jsx b/superset-frontend/src/explore/components/AdhocMetricEditPopoverTitle.jsx index 08eb9e8553fa..875e879a6648 100644 --- a/superset-frontend/src/explore/components/AdhocMetricEditPopoverTitle.jsx +++ b/superset-frontend/src/explore/components/AdhocMetricEditPopoverTitle.jsx @@ -43,22 +43,14 @@ export default class AdhocMetricEditPopoverTitle extends React.Component { }; } - onMouseOver() { - this.setState({ isHovered: true }); - } - - onMouseOut() { - this.setState({ isHovered: false }); + onBlur() { + this.setState({ isEditable: false }); } onClick() { this.setState({ isEditable: true }); } - onBlur() { - this.setState({ isEditable: false }); - } - onInputBlur(e) { if (e.target.value === '') { this.props.onChange(e); @@ -66,6 +58,14 @@ export default class AdhocMetricEditPopoverTitle extends React.Component { this.onBlur(); } + onMouseOut() { + this.setState({ isHovered: false }); + } + + onMouseOver() { + this.setState({ isHovered: true }); + } + render() { const { title, onChange } = this.props; diff --git a/superset-frontend/src/explore/components/ControlPanelsContainer.jsx b/superset-frontend/src/explore/components/ControlPanelsContainer.jsx index 70c347cffd00..90d65feb85cd 100644 --- a/superset-frontend/src/explore/components/ControlPanelsContainer.jsx +++ b/superset-frontend/src/explore/components/ControlPanelsContainer.jsx @@ -87,6 +87,10 @@ class ControlPanelsContainer extends React.Component { this.renderControlPanelSection = this.renderControlPanelSection.bind(this); } + removeAlert() { + this.props.actions.removeControlPanelAlert(); + } + sectionsToRender() { return sectionsToRender( this.props.form_data.viz_type, @@ -94,10 +98,6 @@ class ControlPanelsContainer extends React.Component { ); } - removeAlert() { - this.props.actions.removeControlPanelAlert(); - } - renderControl({ name, config }) { const { actions, controls, form_data: formData } = this.props; const { visibility } = config; diff --git a/superset-frontend/src/explore/components/EmbedCodeButton.jsx b/superset-frontend/src/explore/components/EmbedCodeButton.jsx index 04efde16fa2c..d219de6cd22a 100644 --- a/superset-frontend/src/explore/components/EmbedCodeButton.jsx +++ b/superset-frontend/src/explore/components/EmbedCodeButton.jsx @@ -56,13 +56,6 @@ export default class EmbedCodeButton extends React.Component { .catch(this.props.addDangerToast); } - handleInputChange(e) { - const { value, name } = e.currentTarget; - const data = {}; - data[name] = value; - this.setState(data); - } - generateEmbedHTML() { const srcLink = `${window.location.origin + getURIDirectory()}?r=${ this.state.shortUrlId @@ -80,6 +73,13 @@ export default class EmbedCodeButton extends React.Component { ); } + handleInputChange(e) { + const { value, name } = e.currentTarget; + const data = {}; + data[name] = value; + this.setState(data); + } + renderPopoverContent() { const html = this.generateEmbedHTML(); return ( diff --git a/superset-frontend/src/explore/components/ExploreChartHeader.jsx b/superset-frontend/src/explore/components/ExploreChartHeader.jsx index 9c1df9f3a8d6..61ca255654bd 100644 --- a/superset-frontend/src/explore/components/ExploreChartHeader.jsx +++ b/superset-frontend/src/explore/components/ExploreChartHeader.jsx @@ -102,13 +102,10 @@ export class ExploreChartHeader extends React.PureComponent { return this.props.sliceName || t('%s - untitled', this.props.table_name); } - postChartFormData() { - this.props.actions.postChartFormData( - this.props.form_data, - true, - this.props.timeout, - this.props.chart.id, - ); + closePropertiesModal() { + this.setState({ + isPropertiesModalOpen: false, + }); } openPropertiesModal() { @@ -117,10 +114,13 @@ export class ExploreChartHeader extends React.PureComponent { }); } - closePropertiesModal() { - this.setState({ - isPropertiesModalOpen: false, - }); + postChartFormData() { + this.props.actions.postChartFormData( + this.props.form_data, + true, + this.props.timeout, + this.props.chart.id, + ); } render() { diff --git a/superset-frontend/src/explore/components/controls/AdhocFilterControl.jsx b/superset-frontend/src/explore/components/controls/AdhocFilterControl.jsx index f5c9f5d88102..8914c1bc039f 100644 --- a/superset-frontend/src/explore/components/controls/AdhocFilterControl.jsx +++ b/superset-frontend/src/explore/components/controls/AdhocFilterControl.jsx @@ -166,14 +166,22 @@ class AdhocFilterControl extends React.Component { } } - onRemoveFilter(index) { - const valuesCopy = [...this.state.values]; - valuesCopy.splice(index, 1); - this.setState(prevState => ({ - ...prevState, - values: valuesCopy, - })); - this.props.onChange(valuesCopy); + onChange(opts) { + const options = (opts || []) + .map(option => this.mapOption(option)) + .filter(option => option); + this.props.onChange(options); + } + + onFilterEdit(changedFilter) { + this.props.onChange( + this.state.values.map(value => { + if (value.filterOptionName === changedFilter.filterOptionName) { + return changedFilter; + } + return value; + }), + ); } onNewFilter(newFilter) { @@ -191,22 +199,14 @@ class AdhocFilterControl extends React.Component { } } - onFilterEdit(changedFilter) { - this.props.onChange( - this.state.values.map(value => { - if (value.filterOptionName === changedFilter.filterOptionName) { - return changedFilter; - } - return value; - }), - ); - } - - onChange(opts) { - const options = (opts || []) - .map(option => this.mapOption(option)) - .filter(option => option); - this.props.onChange(options); + onRemoveFilter(index) { + const valuesCopy = [...this.state.values]; + valuesCopy.splice(index, 1); + this.setState(prevState => ({ + ...prevState, + values: valuesCopy, + })); + this.props.onChange(valuesCopy); } getMetricExpression(savedMetricName) { @@ -215,15 +215,18 @@ class AdhocFilterControl extends React.Component { ).expression; } - moveLabel(dragIndex, hoverIndex) { - const { values } = this.state; - - const newValues = [...values]; - [newValues[hoverIndex], newValues[dragIndex]] = [ - newValues[dragIndex], - newValues[hoverIndex], - ]; - this.setState({ values: newValues }); + addNewFilterPopoverTrigger(trigger) { + return ( + + {trigger} + + ); } mapOption(option) { @@ -277,6 +280,17 @@ class AdhocFilterControl extends React.Component { return null; } + moveLabel(dragIndex, hoverIndex) { + const { values } = this.state; + + const newValues = [...values]; + [newValues[hoverIndex], newValues[dragIndex]] = [ + newValues[dragIndex], + newValues[hoverIndex], + ]; + this.setState({ values: newValues }); + } + optionsForSelect(props) { const options = [ ...props.columns, @@ -316,20 +330,6 @@ class AdhocFilterControl extends React.Component { ); } - addNewFilterPopoverTrigger(trigger) { - return ( - - {trigger} - - ); - } - render() { const { theme } = this.props; return ( diff --git a/superset-frontend/src/explore/components/controls/AnnotationLayer.jsx b/superset-frontend/src/explore/components/controls/AnnotationLayer.jsx index 1c65c56e7c55..a6405f9f43eb 100644 --- a/superset-frontend/src/explore/components/controls/AnnotationLayer.jsx +++ b/superset-frontend/src/explore/components/controls/AnnotationLayer.jsx @@ -203,69 +203,45 @@ export default class AnnotationLayer extends React.PureComponent { return sources; } - isValidFormula(value, annotationType) { - if (annotationType === ANNOTATION_TYPES.FORMULA) { - try { - mathjsParse(value).compile().evaluate({ x: 0 }); - } catch (err) { - return true; - } - } - return false; - } + applyAnnotation() { + if (this.isValidForm()) { + const annotationFields = [ + 'name', + 'annotationType', + 'sourceType', + 'color', + 'opacity', + 'style', + 'width', + 'showMarkers', + 'hideLine', + 'value', + 'overrides', + 'show', + 'titleColumn', + 'descriptionColumns', + 'timeColumn', + 'intervalEndColumn', + ]; + const newAnnotation = {}; + annotationFields.forEach(field => { + if (this.state[field] !== null) { + newAnnotation[field] = this.state[field]; + } + }); - isValidForm() { - const { - name, - annotationType, - sourceType, - value, - timeColumn, - intervalEndColumn, - } = this.state; - const errors = [ - validateNonEmpty(name), - validateNonEmpty(annotationType), - validateNonEmpty(value), - ]; - if (sourceType !== ANNOTATION_SOURCE_TYPES.NATIVE) { - if (annotationType === ANNOTATION_TYPES.EVENT) { - errors.push(validateNonEmpty(timeColumn)); - } - if (annotationType === ANNOTATION_TYPES.INTERVAL) { - errors.push(validateNonEmpty(timeColumn)); - errors.push(validateNonEmpty(intervalEndColumn)); + if (newAnnotation.color === AUTOMATIC_COLOR) { + newAnnotation.color = null; } - } - errors.push(this.isValidFormula(value, annotationType)); - return !errors.filter(x => x).length; - } - - handleAnnotationType(annotationType) { - this.setState({ - annotationType, - sourceType: null, - value: null, - }); - } - - handleAnnotationSourceType(sourceType) { - const { sourceType: prevSourceType } = this.state; - if (prevSourceType !== sourceType) { - this.setState({ sourceType, value: null, isLoadingOptions: true }); + this.props.addAnnotationLayer(newAnnotation); + this.setState({ isNew: false }); } } - handleValue(value) { - this.setState({ - value, - descriptionColumns: null, - intervalEndColumn: null, - timeColumn: null, - titleColumn: null, - overrides: { time_range: null }, - }); + deleteAnnotation() { + this.props.removeAnnotationLayer(); + this.props.close(); } fetchOptions(annotationType, sourceType, isLoadingOptions) { @@ -311,45 +287,69 @@ export default class AnnotationLayer extends React.PureComponent { } } - deleteAnnotation() { - this.props.removeAnnotationLayer(); - this.props.close(); + handleAnnotationSourceType(sourceType) { + const { sourceType: prevSourceType } = this.state; + + if (prevSourceType !== sourceType) { + this.setState({ sourceType, value: null, isLoadingOptions: true }); + } } - applyAnnotation() { - if (this.isValidForm()) { - const annotationFields = [ - 'name', - 'annotationType', - 'sourceType', - 'color', - 'opacity', - 'style', - 'width', - 'showMarkers', - 'hideLine', - 'value', - 'overrides', - 'show', - 'titleColumn', - 'descriptionColumns', - 'timeColumn', - 'intervalEndColumn', - ]; - const newAnnotation = {}; - annotationFields.forEach(field => { - if (this.state[field] !== null) { - newAnnotation[field] = this.state[field]; - } - }); + handleAnnotationType(annotationType) { + this.setState({ + annotationType, + sourceType: null, + value: null, + }); + } - if (newAnnotation.color === AUTOMATIC_COLOR) { - newAnnotation.color = null; + handleValue(value) { + this.setState({ + value, + descriptionColumns: null, + intervalEndColumn: null, + timeColumn: null, + titleColumn: null, + overrides: { time_range: null }, + }); + } + + isValidForm() { + const { + name, + annotationType, + sourceType, + value, + timeColumn, + intervalEndColumn, + } = this.state; + const errors = [ + validateNonEmpty(name), + validateNonEmpty(annotationType), + validateNonEmpty(value), + ]; + if (sourceType !== ANNOTATION_SOURCE_TYPES.NATIVE) { + if (annotationType === ANNOTATION_TYPES.EVENT) { + errors.push(validateNonEmpty(timeColumn)); + } + if (annotationType === ANNOTATION_TYPES.INTERVAL) { + errors.push(validateNonEmpty(timeColumn)); + errors.push(validateNonEmpty(intervalEndColumn)); } + } + errors.push(this.isValidFormula(value, annotationType)); + return !errors.filter(x => x).length; + } - this.props.addAnnotationLayer(newAnnotation); - this.setState({ isNew: false }); + isValidFormula(value, annotationType) { + if (annotationType === ANNOTATION_TYPES.FORMULA) { + try { + mathjsParse(value).compile().evaluate({ x: 0 }); + } catch (err) { + return true; + } } + return false; } submitAnnotation() { @@ -357,78 +357,115 @@ export default class AnnotationLayer extends React.PureComponent { this.props.close(); } - renderOption(option) { - return ( - - {option.label} - - ); - } - - renderValueConfiguration() { + renderDisplayConfiguration() { const { + color, + opacity, + style, + width, + showMarkers, + hideLine, annotationType, - sourceType, - value, - valueOptions, - isLoadingOptions, } = this.state; - let label = ''; - let description = ''; - if (requiresQuery(sourceType)) { - if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) { - label = 'Annotation Layer'; - description = 'Select the Annotation Layer you would like to use.'; - } else { - label = t('Chart'); - description = `Use a pre defined Superset Chart as a source for annotations and overlays. - your chart must be one of these visualization types: - [${this.getSupportedSourceTypes(annotationType) - .map(x => x.label) - .join(', ')}]`; - } - } else if (annotationType === ANNOTATION_TYPES.FORMULA) { - label = 'Formula'; - description = `Expects a formula with depending time parameter 'x' - in milliseconds since epoch. mathjs is used to evaluate the formulas. - Example: '2x+5'`; + const colorScheme = getCategoricalSchemeRegistry() + .get(this.props.colorScheme) + .colors.concat(); + if ( + color && + color !== AUTOMATIC_COLOR && + !colorScheme.find(x => x.toLowerCase() === color.toLowerCase()) + ) { + colorScheme.push(color); } - if (requiresQuery(sourceType)) { - return ( + return ( + {}} + title={t('Display configuration')} + info={t('Configure your how you overlay is displayed here.')} + > this.setState({ style: v })} /> - ); - } - if (annotationType === ANNOTATION_TYPES.FORMULA) { - return ( + this.setState({ opacity: v })} + /> +
+ +
+ this.setState({ color: v.hex })} + /> + +
+
this.setState({ width: v })} /> - ); - } - return ''; + {annotationType === ANNOTATION_TYPES.TIME_SERIES && ( + this.setState({ showMarkers: v })} + /> + )} + {annotationType === ANNOTATION_TYPES.TIME_SERIES && ( + this.setState({ hideLine: v })} + /> + )} +
+ ); + } + + renderOption(option) { + return ( + + {option.label} + + ); } renderSliceConfiguration() { @@ -574,107 +611,70 @@ export default class AnnotationLayer extends React.PureComponent { return ''; } - renderDisplayConfiguration() { + renderValueConfiguration() { const { - color, - opacity, - style, - width, - showMarkers, - hideLine, annotationType, + sourceType, + value, + valueOptions, + isLoadingOptions, } = this.state; - const colorScheme = getCategoricalSchemeRegistry() - .get(this.props.colorScheme) - .colors.concat(); - if ( - color && - color !== AUTOMATIC_COLOR && - !colorScheme.find(x => x.toLowerCase() === color.toLowerCase()) - ) { - colorScheme.push(color); + let label = ''; + let description = ''; + if (requiresQuery(sourceType)) { + if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) { + label = 'Annotation Layer'; + description = 'Select the Annotation Layer you would like to use.'; + } else { + label = t('Chart'); + description = `Use a pre defined Superset Chart as a source for annotations and overlays. + your chart must be one of these visualization types: + [${this.getSupportedSourceTypes(annotationType) + .map(x => x.label) + .join(', ')}]`; + } + } else if (annotationType === ANNOTATION_TYPES.FORMULA) { + label = 'Formula'; + description = `Expects a formula with depending time parameter 'x' + in milliseconds since epoch. mathjs is used to evaluate the formulas. + Example: '2x+5'`; } - return ( - {}} - title={t('Display configuration')} - info={t('Configure your how you overlay is displayed here.')} - > - this.setState({ style: v })} - /> + if (requiresQuery(sourceType)) { + return ( this.setState({ opacity: v })} + name="annotation-layer-value" + showHeader + hovered + description={description} + label={label} + placeholder="" + options={valueOptions} + isLoading={isLoadingOptions} + value={value} + onChange={this.handleValue} + validationErrors={!value ? ['Mandatory'] : []} + optionRenderer={this.renderOption} /> -
- -
- this.setState({ color: v.hex })} - /> - -
-
+ ); + } + if (annotationType === ANNOTATION_TYPES.FORMULA) { + return ( this.setState({ width: v })} + name="annotation-layer-value" + hovered + showHeader + description={description} + label={label} + placeholder="" + value={value} + onChange={this.handleValue} + validationErrors={ + this.isValidFormula(value, annotationType) ? ['Bad formula.'] : [] + } /> - {annotationType === ANNOTATION_TYPES.TIME_SERIES && ( - this.setState({ showMarkers: v })} - /> - )} - {annotationType === ANNOTATION_TYPES.TIME_SERIES && ( - this.setState({ hideLine: v })} - /> - )} -
- ); + ); + } + return ''; } render() { diff --git a/superset-frontend/src/explore/components/controls/AnnotationLayerControl.jsx b/superset-frontend/src/explore/components/controls/AnnotationLayerControl.jsx index e47388c3470a..63b5dd128ee6 100644 --- a/superset-frontend/src/explore/components/controls/AnnotationLayerControl.jsx +++ b/superset-frontend/src/explore/components/controls/AnnotationLayerControl.jsx @@ -109,6 +109,28 @@ class AnnotationLayerControl extends React.PureComponent { this.props.onChange(annotations); } + renderInfo(anno) { + const { annotationError, annotationQuery } = this.props; + if (annotationQuery[anno.name]) { + return ( + + ); + } + if (annotationError[anno.name]) { + return ( + + ); + } + if (!anno.show) { + return Hidden ; + } + return ''; + } + renderPopover(popoverKey, annotation, error) { const id = annotation?.name || '_new'; @@ -132,28 +154,6 @@ class AnnotationLayerControl extends React.PureComponent { ); } - renderInfo(anno) { - const { annotationError, annotationQuery } = this.props; - if (annotationQuery[anno.name]) { - return ( - - ); - } - if (annotationError[anno.name]) { - return ( - - ); - } - if (!anno.show) { - return Hidden ; - } - return ''; - } - render() { const { addedAnnotationIndex } = this.state; const addedAnnotation = this.props.value[addedAnnotationIndex]; diff --git a/superset-frontend/src/explore/components/controls/BoundsControl.jsx b/superset-frontend/src/explore/components/controls/BoundsControl.jsx index b56b840a8c7c..e7510e49f750 100644 --- a/superset-frontend/src/explore/components/controls/BoundsControl.jsx +++ b/superset-frontend/src/explore/components/controls/BoundsControl.jsx @@ -46,26 +46,6 @@ export default class BoundsControl extends React.Component { this.onMaxChange = this.onMaxChange.bind(this); } - onMinChange(event) { - const min = event.target.value; - this.setState( - prevState => ({ - minMax: [min, prevState.minMax[1]], - }), - this.onChange, - ); - } - - onMaxChange(event) { - const max = event.target.value; - this.setState( - prevState => ({ - minMax: [prevState.minMax[0], max], - }), - this.onChange, - ); - } - onChange() { const mm = this.state.minMax; const errors = []; @@ -82,6 +62,26 @@ export default class BoundsControl extends React.Component { } } + onMaxChange(event) { + const max = event.target.value; + this.setState( + prevState => ({ + minMax: [prevState.minMax[0], max], + }), + this.onChange, + ); + } + + onMinChange(event) { + const min = event.target.value; + this.setState( + prevState => ({ + minMax: [min, prevState.minMax[1]], + }), + this.onChange, + ); + } + render() { return (
diff --git a/superset-frontend/src/explore/components/controls/CollectionControl.jsx b/superset-frontend/src/explore/components/controls/CollectionControl.jsx index 198df6d5cc51..bb860eee46b3 100644 --- a/superset-frontend/src/explore/components/controls/CollectionControl.jsx +++ b/superset-frontend/src/explore/components/controls/CollectionControl.jsx @@ -69,15 +69,15 @@ export default class CollectionControl extends React.Component { this.onAdd = this.onAdd.bind(this); } + onAdd() { + this.props.onChange(this.props.value.concat([this.props.itemGenerator()])); + } + onChange(i, value) { Object.assign(this.props.value[i], value); this.props.onChange(this.props.value); } - onAdd() { - this.props.onChange(this.props.value.concat([this.props.itemGenerator()])); - } - onSortEnd({ oldIndex, newIndex }) { this.props.onChange(arrayMove(this.props.value, oldIndex, newIndex)); } diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl.jsx index e30aebe8763c..1526125a4a1b 100644 --- a/superset-frontend/src/explore/components/controls/DatasourceControl.jsx +++ b/superset-frontend/src/explore/components/controls/DatasourceControl.jsx @@ -114,24 +114,6 @@ class DatasourceControl extends React.PureComponent { } } - toggleShowDatasource() { - this.setState(({ showDatasource }) => ({ - showDatasource: !showDatasource, - })); - } - - toggleChangeDatasourceModal() { - this.setState(({ showChangeDatasourceModal }) => ({ - showChangeDatasourceModal: !showChangeDatasourceModal, - })); - } - - toggleEditDatasourceModal() { - this.setState(({ showEditDatasourceModal }) => ({ - showEditDatasourceModal: !showEditDatasourceModal, - })); - } - handleMenuItemClick({ key }) { if (key === CHANGE_DATASET) { this.toggleChangeDatasourceModal(); @@ -149,6 +131,24 @@ class DatasourceControl extends React.PureComponent { } } + toggleChangeDatasourceModal() { + this.setState(({ showChangeDatasourceModal }) => ({ + showChangeDatasourceModal: !showChangeDatasourceModal, + })); + } + + toggleEditDatasourceModal() { + this.setState(({ showEditDatasourceModal }) => ({ + showEditDatasourceModal: !showEditDatasourceModal, + })); + } + + toggleShowDatasource() { + this.setState(({ showDatasource }) => ({ + showDatasource: !showDatasource, + })); + } + render() { const { showChangeDatasourceModal, showEditDatasourceModal } = this.state; const { datasource, onChange } = this.props; diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl.jsx b/superset-frontend/src/explore/components/controls/DateFilterControl.jsx index 5b0f15a5e7e2..a2b0d6a79601 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl.jsx +++ b/superset-frontend/src/explore/components/controls/DateFilterControl.jsx @@ -308,22 +308,6 @@ class DateFilterControl extends React.Component { } } - handleClick(e) { - const { target } = e; - // switch to `TYPES.CUSTOM_START_END` when the calendar is clicked - if (this.startEndSectionRef && this.startEndSectionRef.contains(target)) { - this.setTypeCustomStartEnd(); - } - - // if user click outside popover, popover will hide and we will call onCloseDateFilterControl, - // but need to exclude OverlayTrigger component to avoid handle click events twice. - if (target.getAttribute('name') !== 'popover-trigger') { - if (this.popoverContainer && !this.popoverContainer.contains(target)) { - this.props.onCloseDateFilterControl(); - } - } - } - close() { let val; if ( @@ -345,6 +329,31 @@ class DateFilterControl extends React.Component { }); } + handleClick(e) { + const { target } = e; + // switch to `TYPES.CUSTOM_START_END` when the calendar is clicked + if (this.startEndSectionRef && this.startEndSectionRef.contains(target)) { + this.setTypeCustomStartEnd(); + } + + // if user click outside popover, popover will hide and we will call onCloseDateFilterControl, + // but need to exclude OverlayTrigger component to avoid handle click events twice. + if (target.getAttribute('name') !== 'popover-trigger') { + if (this.popoverContainer && !this.popoverContainer.contains(target)) { + this.props.onCloseDateFilterControl(); + } + } + } + + handleVisibleChange(visible) { + if (visible) { + this.props.onOpenDateFilterControl(); + } else { + this.props.onCloseDateFilterControl(); + } + this.setState({ popoverVisible: visible }); + } + isValidSince(date) { return ( !isValidMoment(this.state.until) || @@ -375,15 +384,6 @@ class DateFilterControl extends React.Component { this.setState(nextState); } - handleVisibleChange(visible) { - if (visible) { - this.props.onOpenDateFilterControl(); - } else { - this.props.onCloseDateFilterControl(); - } - this.setState({ popoverVisible: visible }); - } - renderInput(props, key) { return ( diff --git a/superset-frontend/src/explore/components/controls/FixedOrMetricControl.jsx b/superset-frontend/src/explore/components/controls/FixedOrMetricControl.jsx index d7cf050b2150..e5fd7f548168 100644 --- a/superset-frontend/src/explore/components/controls/FixedOrMetricControl.jsx +++ b/superset-frontend/src/explore/components/controls/FixedOrMetricControl.jsx @@ -77,10 +77,6 @@ export default class FixedOrMetricControl extends React.Component { }); } - setType(type) { - this.setState({ type }, this.onChange); - } - setFixedValue(fixedValue) { this.setState({ fixedValue }, this.onChange); } @@ -89,6 +85,10 @@ export default class FixedOrMetricControl extends React.Component { this.setState({ metricValue }, this.onChange); } + setType(type) { + this.setState({ type }, this.onChange); + } + toggle() { this.setState(prevState => ({ expanded: !prevState.expanded, diff --git a/superset-frontend/src/explore/components/controls/MetricsControl.jsx b/superset-frontend/src/explore/components/controls/MetricsControl.jsx index f643e2b769ee..cba2ae218a3c 100644 --- a/superset-frontend/src/explore/components/controls/MetricsControl.jsx +++ b/superset-frontend/src/explore/components/controls/MetricsControl.jsx @@ -171,16 +171,29 @@ class MetricsControl extends React.PureComponent { } } - onNewMetric(newMetric) { - this.setState( - prevState => ({ - ...prevState, - value: [...prevState.value, newMetric], - }), - () => { - this.onChange(this.state.value); - }, - ); + onChange(opts) { + // if clear out options + if (opts === null) { + this.props.onChange(null); + return; + } + + let transformedOpts; + if (Array.isArray(opts)) { + transformedOpts = opts; + } else { + transformedOpts = opts ? [opts] : []; + } + const optionValues = transformedOpts + .map(option => { + // pre-defined metric + if (option.metric_name) { + return option.metric_name; + } + return option; + }) + .filter(option => option); + this.props.onChange(this.props.multi ? optionValues : optionValues[0]); } onMetricEdit(changedMetric, oldMetric) { @@ -204,6 +217,18 @@ class MetricsControl extends React.PureComponent { ); } + onNewMetric(newMetric) { + this.setState( + prevState => ({ + ...prevState, + value: [...prevState.value, newMetric], + }), + () => { + this.onChange(this.state.value); + }, + ); + } + onRemoveMetric(index) { if (!Array.isArray(this.state.value)) { return; @@ -217,46 +242,6 @@ class MetricsControl extends React.PureComponent { this.props.onChange(valuesCopy); } - onChange(opts) { - // if clear out options - if (opts === null) { - this.props.onChange(null); - return; - } - - let transformedOpts; - if (Array.isArray(opts)) { - transformedOpts = opts; - } else { - transformedOpts = opts ? [opts] : []; - } - const optionValues = transformedOpts - .map(option => { - // pre-defined metric - if (option.metric_name) { - return option.metric_name; - } - return option; - }) - .filter(option => option); - this.props.onChange(this.props.multi ? optionValues : optionValues[0]); - } - - moveLabel(dragIndex, hoverIndex) { - const { value } = this.state; - - const newValues = [...value]; - [newValues[hoverIndex], newValues[dragIndex]] = [ - newValues[dragIndex], - newValues[hoverIndex], - ]; - this.setState({ value: newValues }); - } - - isAddNewMetricDisabled() { - return !this.props.multi && this.state.value.length > 0; - } - addNewMetricPopoverTrigger(trigger) { if (this.isAddNewMetricDisabled()) { return trigger; @@ -286,6 +271,28 @@ class MetricsControl extends React.PureComponent { this.setState({ aggregateInInput }); } + isAddNewMetricDisabled() { + return !this.props.multi && this.state.value.length > 0; + } + + isAutoGeneratedMetric(savedMetric) { + if (this.props.datasourceType === 'druid') { + return druidAutoGeneratedMetricRegex.test(savedMetric.verbose_name); + } + return sqlaAutoGeneratedMetricNameRegex.test(savedMetric.metric_name); + } + + moveLabel(dragIndex, hoverIndex) { + const { value } = this.state; + + const newValues = [...value]; + [newValues[hoverIndex], newValues[dragIndex]] = [ + newValues[dragIndex], + newValues[hoverIndex], + ]; + this.setState({ value: newValues }); + } + optionsForSelect(props) { const { columns, savedMetrics } = props; const aggregates = @@ -315,13 +322,6 @@ class MetricsControl extends React.PureComponent { }, []); } - isAutoGeneratedMetric(savedMetric) { - if (this.props.datasourceType === 'druid') { - return druidAutoGeneratedMetricRegex.test(savedMetric.verbose_name); - } - return sqlaAutoGeneratedMetricNameRegex.test(savedMetric.metric_name); - } - selectFilterOption({ data: option }, filterValue) { if (this.state.aggregateInInput) { let endIndex = filterValue.length; diff --git a/superset-frontend/src/explore/components/controls/SelectControl.jsx b/superset-frontend/src/explore/components/controls/SelectControl.jsx index ccd0574f579d..284226ede6b3 100644 --- a/superset-frontend/src/explore/components/controls/SelectControl.jsx +++ b/superset-frontend/src/explore/components/controls/SelectControl.jsx @@ -125,13 +125,6 @@ export default class SelectControl extends React.PureComponent { this.props.onChange(optionValue); } - getSelectRef(instance) { - this.select = instance; - if (this.props.selectRef) { - this.props.selectRef(instance); - } - } - getOptions(props) { let options = []; if (props.options) { @@ -172,6 +165,26 @@ export default class SelectControl extends React.PureComponent { return options; } + getSelectRef(instance) { + this.select = instance; + if (this.props.selectRef) { + this.props.selectRef(instance); + } + } + + createMetaSelectAllOption() { + const option = { label: 'Select All', meta: true }; + option[this.props.valueKey] = 'Select All'; + return option; + } + + createPlaceholder() { + const optionsRemaining = this.optionsRemaining(); + const placeholder = + this.props.placeholder || t('%s option(s)', optionsRemaining); + return optionsRemaining ? placeholder : ''; + } + handleKeyDownForCreate(event) { const { key } = event; if (key === 'Tab' || (this.props.commaChoosesOption && key === ',')) { @@ -203,19 +216,6 @@ export default class SelectControl extends React.PureComponent { return remainingOptions; } - createPlaceholder() { - const optionsRemaining = this.optionsRemaining(); - const placeholder = - this.props.placeholder || t('%s option(s)', optionsRemaining); - return optionsRemaining ? placeholder : ''; - } - - createMetaSelectAllOption() { - const option = { label: 'Select All', meta: true }; - option[this.props.valueKey] = 'Select All'; - return option; - } - render() { // Tab, comma or Enter will trigger a new option created for FreeFormSelect const { diff --git a/superset-frontend/src/explore/components/controls/SpatialControl.jsx b/superset-frontend/src/explore/components/controls/SpatialControl.jsx index 5c34ec3ca685..fcda7a5cc9f1 100644 --- a/superset-frontend/src/explore/components/controls/SpatialControl.jsx +++ b/superset-frontend/src/explore/components/controls/SpatialControl.jsx @@ -131,35 +131,6 @@ export default class SpatialControl extends React.Component { return null; } - renderSelect(name, type) { - return ( - { - this.setType(type); - }} - onChange={value => { - this.setState({ [name]: value }, this.onChange); - }} - /> - ); - } - - renderReverseCheckbox() { - return ( - - {t('Reverse lat/long ')} - - - ); - } - renderPopoverContent() { return (
@@ -213,6 +184,35 @@ export default class SpatialControl extends React.Component { ); } + renderReverseCheckbox() { + return ( + + {t('Reverse lat/long ')} + + + ); + } + + renderSelect(name, type) { + return ( + { + this.setType(type); + }} + onChange={value => { + this.setState({ [name]: value }, this.onChange); + }} + /> + ); + } + render() { return (
diff --git a/superset-frontend/src/explore/components/controls/TextAreaControl.jsx b/superset-frontend/src/explore/components/controls/TextAreaControl.jsx index 0bcf2d12860d..5a9698bcd4a1 100644 --- a/superset-frontend/src/explore/components/controls/TextAreaControl.jsx +++ b/superset-frontend/src/explore/components/controls/TextAreaControl.jsx @@ -66,14 +66,14 @@ export default class TextAreaControl extends React.Component { }, 300); } - onControlChange(event) { - this.props.onChange(event.target.value); - } - onAceChange(value) { this.props.onChange(value); } + onControlChange(event) { + this.props.onChange(event.target.value); + } + renderEditor(inModal = false) { const value = this.props.value || ''; const minLines = inModal ? 40 : this.props.minLines || 12; diff --git a/superset-frontend/src/explore/components/controls/TimeSeriesColumnControl.jsx b/superset-frontend/src/explore/components/controls/TimeSeriesColumnControl.jsx index e98172053784..3943a61ceba0 100644 --- a/superset-frontend/src/explore/components/controls/TimeSeriesColumnControl.jsx +++ b/superset-frontend/src/explore/components/controls/TimeSeriesColumnControl.jsx @@ -98,10 +98,18 @@ export default class TimeSeriesColumnControl extends React.Component { this.onChange = this.onChange.bind(this); } + onBoundsChange(bounds) { + this.setState({ bounds }, this.onChange); + } + onChange() { this.props.onChange(this.state); } + onCheckboxChange(attr, value) { + this.setState({ [attr]: value }, this.onChange); + } + onSelectChange(attr, opt) { this.setState({ [attr]: opt.value }, this.onChange); } @@ -110,24 +118,12 @@ export default class TimeSeriesColumnControl extends React.Component { this.setState({ [attr]: event.target.value }, this.onChange); } - onCheckboxChange(attr, value) { - this.setState({ [attr]: value }, this.onChange); - } - - onBoundsChange(bounds) { - this.setState({ bounds }, this.onChange); - } - onYAxisBoundsChange(yAxisBounds) { this.setState({ yAxisBounds }, this.onChange); } setType() {} - textSummary() { - return `${this.state.label}`; - } - edit() {} formRow(label, tooltip, ttLabel, control) { @@ -146,6 +142,10 @@ export default class TimeSeriesColumnControl extends React.Component { ); } + textSummary() { + return `${this.state.label}`; + } + renderPopover() { return (
diff --git a/superset-frontend/src/explore/components/controls/ViewportControl.jsx b/superset-frontend/src/explore/components/controls/ViewportControl.jsx index b76e6dd34aa4..51631d563475 100644 --- a/superset-frontend/src/explore/components/controls/ViewportControl.jsx +++ b/superset-frontend/src/explore/components/controls/ViewportControl.jsx @@ -68,6 +68,23 @@ export default class ViewportControl extends React.Component { }); } + renderLabel() { + if (this.props.value.longitude && this.props.value.latitude) { + return `${decimal2sexagesimal( + this.props.value.longitude, + )} | ${decimal2sexagesimal(this.props.value.latitude)}`; + } + return 'N/A'; + } + + renderPopover() { + return ( +
+ {PARAMS.map(ctrl => this.renderTextControl(ctrl))} +
+ ); + } + renderTextControl(ctrl) { return (
@@ -81,23 +98,6 @@ export default class ViewportControl extends React.Component { ); } - renderPopover() { - return ( -
- {PARAMS.map(ctrl => this.renderTextControl(ctrl))} -
- ); - } - - renderLabel() { - if (this.props.value.longitude && this.props.value.latitude) { - return `${decimal2sexagesimal( - this.props.value.longitude, - )} | ${decimal2sexagesimal(this.props.value.latitude)}`; - } - return 'N/A'; - } - render() { return (
diff --git a/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx b/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx index 4880a385b335..0504b616e225 100644 --- a/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx +++ b/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx @@ -118,20 +118,20 @@ class FilterBox extends React.PureComponent { this.onFilterMenuClose = this.onFilterMenuClose.bind(this); } - onFilterMenuOpen(column) { - return this.props.onFilterMenuOpen(this.props.chartId, column); - } + onCloseDateFilterControl = () => this.onFilterMenuClose(TIME_RANGE); onFilterMenuClose(column) { return this.props.onFilterMenuClose(this.props.chartId, column); } + onFilterMenuOpen(column) { + return this.props.onFilterMenuOpen(this.props.chartId, column); + } + onOpenDateFilterControl() { return this.onFilterMenuOpen(TIME_RANGE); } - onCloseDateFilterControl = () => this.onFilterMenuClose(TIME_RANGE); - getControlData(controlName) { const { selectedValues } = this.state; const control = { @@ -156,13 +156,6 @@ class FilterBox extends React.PureComponent { return this.maxValueCache[key]; } - clickApply() { - const { selectedValues } = this.state; - this.setState({ hasChanged: false }, () => { - this.props.onChange(selectedValues, false); - }); - } - changeFilter(filter, options) { const fltr = TIME_FILTER_MAP[filter] || filter; let vals = null; @@ -192,6 +185,13 @@ class FilterBox extends React.PureComponent { ); } + clickApply() { + const { selectedValues } = this.state; + this.setState({ hasChanged: false }, () => { + this.props.onChange(selectedValues, false); + }); + } + /** * Generate a debounce function that loads options for a specific column */ @@ -204,26 +204,6 @@ class FilterBox extends React.PureComponent { return this.debouncerCache[key]; } - /** - * Transform select options, add bar background - */ - transformOptions(options, max) { - const maxValue = max === undefined ? d3Max(options, x => x.metric) : max; - return options.map(opt => { - const perc = Math.round((opt.metric / maxValue) * 100); - const color = 'lightgrey'; - const backgroundImage = `linear-gradient(to right, ${color}, ${color} ${perc}%, rgba(0,0,0,0) ${perc}%`; - const style = { backgroundImage }; - let label = opt.id; - if (label === true) { - label = BOOL_TRUE_DISPLAY; - } else if (label === false) { - label = BOOL_FALSE_DISPLAY; - } - return { value: opt.id, label, style }; - }); - } - async loadOptions(key, inputValue = '') { const input = inputValue.toLowerCase(); const sortAsc = this.props.filtersFields.find(x => x.key === key).asc; @@ -267,32 +247,24 @@ class FilterBox extends React.PureComponent { return this.transformOptions(options, this.getKnownMax(key, options)); } - renderDateFilter() { - const { showDateFilter } = this.props; - const label = TIME_FILTER_LABELS.time_range; - if (showDateFilter) { - return ( -
-
- { - this.changeFilter(TIME_RANGE, newValue); - }} - onOpenDateFilterControl={this.onOpenDateFilterControl} - onCloseDateFilterControl={this.onCloseDateFilterControl} - value={this.state.selectedValues[TIME_RANGE] || 'No filter'} - /> -
-
- ); - } - return null; + /** + * Transform select options, add bar background + */ + transformOptions(options, max) { + const maxValue = max === undefined ? d3Max(options, x => x.metric) : max; + return options.map(opt => { + const perc = Math.round((opt.metric / maxValue) * 100); + const color = 'lightgrey'; + const backgroundImage = `linear-gradient(to right, ${color}, ${color} ${perc}%, rgba(0,0,0,0) ${perc}%`; + const style = { backgroundImage }; + let label = opt.id; + if (label === true) { + label = BOOL_TRUE_DISPLAY; + } else if (label === false) { + label = BOOL_FALSE_DISPLAY; + } + return { value: opt.id, label, style }; + }); } renderDatasourceFilters() { @@ -334,6 +306,47 @@ class FilterBox extends React.PureComponent { return datasourceFilters; } + renderDateFilter() { + const { showDateFilter } = this.props; + const label = TIME_FILTER_LABELS.time_range; + if (showDateFilter) { + return ( +
+
+ { + this.changeFilter(TIME_RANGE, newValue); + }} + onOpenDateFilterControl={this.onOpenDateFilterControl} + onCloseDateFilterControl={this.onCloseDateFilterControl} + value={this.state.selectedValues[TIME_RANGE] || 'No filter'} + /> +
+
+ ); + } + return null; + } + + renderFilters() { + const { filtersFields = [] } = this.props; + return filtersFields.map(filterConfig => { + const { label, key } = filterConfig; + return ( +
+ {label} + {this.renderSelect(filterConfig)} +
+ ); + }); + } + renderSelect(filterConfig) { const { filtersChoices } = this.props; const { selectedValues } = this.state; @@ -413,19 +426,6 @@ class FilterBox extends React.PureComponent { ); } - renderFilters() { - const { filtersFields = [] } = this.props; - return filtersFields.map(filterConfig => { - const { label, key } = filterConfig; - return ( -
- {label} - {this.renderSelect(filterConfig)} -
- ); - }); - } - render() { const { instantFiltering } = this.props; return (