-
Notifications
You must be signed in to change notification settings - Fork 14k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Explore] Adding custom expressions to adhoc metrics #4736
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,29 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { Button, ControlLabel, FormGroup, Popover } from 'react-bootstrap'; | ||
import { Button, ControlLabel, FormGroup, Popover, Tab, Tabs } from 'react-bootstrap'; | ||
import VirtualizedSelect from 'react-virtualized-select'; | ||
import AceEditor from 'react-ace'; | ||
import 'brace/mode/sql'; | ||
import 'brace/theme/github'; | ||
import 'brace/ext/language_tools'; | ||
|
||
import { AGGREGATES } from '../constants'; | ||
import { t } from '../../locales'; | ||
import VirtualizedRendererWrap from '../../components/VirtualizedRendererWrap'; | ||
import OnPasteSelect from '../../components/OnPasteSelect'; | ||
import AdhocMetricEditPopoverTitle from './AdhocMetricEditPopoverTitle'; | ||
import columnType from '../propTypes/columnType'; | ||
import AdhocMetric from '../AdhocMetric'; | ||
import AdhocMetric, { EXPRESSION_TYPES } from '../AdhocMetric'; | ||
import ColumnOption from '../../components/ColumnOption'; | ||
import { sqlWords } from '../../SqlLab/components/AceEditorWrapper'; | ||
|
||
const langTools = ace.acequire('ace/ext/language_tools'); | ||
|
||
const propTypes = { | ||
adhocMetric: PropTypes.instanceOf(AdhocMetric).isRequired, | ||
onChange: PropTypes.func.isRequired, | ||
onClose: PropTypes.func.isRequired, | ||
onResize: PropTypes.func.isRequired, | ||
columns: PropTypes.arrayOf(columnType), | ||
datasourceType: PropTypes.string, | ||
}; | ||
|
@@ -24,14 +32,25 @@ const defaultProps = { | |
columns: [], | ||
}; | ||
|
||
const startingWidth = 300; | ||
const startingHeight = 180; | ||
|
||
export default class AdhocMetricEditPopover extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.onSave = this.onSave.bind(this); | ||
this.onColumnChange = this.onColumnChange.bind(this); | ||
this.onAggregateChange = this.onAggregateChange.bind(this); | ||
this.onSqlExpressionChange = this.onSqlExpressionChange.bind(this); | ||
this.onLabelChange = this.onLabelChange.bind(this); | ||
this.state = { adhocMetric: this.props.adhocMetric }; | ||
this.onDragDown = this.onDragDown.bind(this); | ||
this.onMouseMove = this.onMouseMove.bind(this); | ||
this.onMouseUp = this.onMouseUp.bind(this); | ||
this.state = { | ||
adhocMetric: this.props.adhocMetric, | ||
width: startingWidth, | ||
height: startingHeight, | ||
}; | ||
this.selectProps = { | ||
multi: false, | ||
name: 'select-column', | ||
|
@@ -40,6 +59,23 @@ export default class AdhocMetricEditPopover extends React.Component { | |
clearable: true, | ||
selectWrap: VirtualizedSelect, | ||
}; | ||
if (langTools) { | ||
const words = sqlWords.concat(this.props.columns.map(column => ( | ||
{ name: column.column_name, value: column.column_name, score: 50, meta: 'column' } | ||
))); | ||
const completer = { | ||
getCompletions: (aceEditor, session, pos, prefix, callback) => { | ||
callback(null, words); | ||
}, | ||
}; | ||
langTools.setCompleters([completer]); | ||
} | ||
document.addEventListener('mouseup', this.onMouseUp); | ||
} | ||
|
||
componentWillUnmount() { | ||
document.removeEventListener('mouseup', this.onMouseUp); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unlikely sitch, but could also call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oooh good call |
||
document.removeEventListener('mousemove', this.onMouseMove); | ||
} | ||
|
||
onSave() { | ||
|
@@ -48,14 +84,27 @@ export default class AdhocMetricEditPopover extends React.Component { | |
} | ||
|
||
onColumnChange(column) { | ||
this.setState({ adhocMetric: this.state.adhocMetric.duplicateWith({ column }) }); | ||
this.setState({ adhocMetric: this.state.adhocMetric.duplicateWith({ | ||
column, | ||
expressionType: EXPRESSION_TYPES.SIMPLE, | ||
}) }); | ||
} | ||
|
||
onAggregateChange(aggregate) { | ||
// we construct this object explicitly to overwrite the value in the case aggregate is null | ||
this.setState({ | ||
adhocMetric: this.state.adhocMetric.duplicateWith({ | ||
aggregate: aggregate && aggregate.aggregate, | ||
expressionType: EXPRESSION_TYPES.SIMPLE, | ||
}), | ||
}); | ||
} | ||
|
||
onSqlExpressionChange(sqlExpression) { | ||
this.setState({ | ||
adhocMetric: this.state.adhocMetric.duplicateWith({ | ||
sqlExpression, | ||
expressionType: EXPRESSION_TYPES.SQL, | ||
}), | ||
}); | ||
} | ||
|
@@ -68,13 +117,44 @@ 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); | ||
} | ||
|
||
render() { | ||
const { adhocMetric, columns, onChange, onClose, datasourceType, ...popoverProps } = this.props; | ||
const { | ||
adhocMetric: propsAdhocMetric, | ||
columns, | ||
onChange, | ||
onClose, | ||
onResize, | ||
datasourceType, | ||
...popoverProps | ||
} = this.props; | ||
|
||
const { adhocMetric } = this.state; | ||
|
||
const columnSelectProps = { | ||
placeholder: t('%s column(s)', columns.length), | ||
options: columns, | ||
value: this.state.adhocMetric.column && this.state.adhocMetric.column.column_name, | ||
value: (adhocMetric.column && adhocMetric.column.column_name) || | ||
adhocMetric.inferSqlExpressionColumn(), | ||
onChange: this.onColumnChange, | ||
optionRenderer: VirtualizedRendererWrap(option => ( | ||
<ColumnOption column={option} showType /> | ||
|
@@ -86,7 +166,7 @@ export default class AdhocMetricEditPopover extends React.Component { | |
const aggregateSelectProps = { | ||
placeholder: t('%s aggregates(s)', Object.keys(AGGREGATES).length), | ||
options: Object.keys(AGGREGATES).map(aggregate => ({ aggregate })), | ||
value: this.state.adhocMetric.aggregate, | ||
value: adhocMetric.aggregate || adhocMetric.inferSqlExpressionAggregate(), | ||
onChange: this.onAggregateChange, | ||
optionRenderer: VirtualizedRendererWrap(aggregate => aggregate.aggregate), | ||
valueRenderer: aggregate => aggregate.aggregate, | ||
|
@@ -101,38 +181,68 @@ export default class AdhocMetricEditPopover extends React.Component { | |
|
||
const popoverTitle = ( | ||
<AdhocMetricEditPopoverTitle | ||
adhocMetric={this.state.adhocMetric} | ||
adhocMetric={adhocMetric} | ||
onChange={this.onLabelChange} | ||
/> | ||
); | ||
|
||
const stateIsValid = this.state.adhocMetric.column && this.state.adhocMetric.aggregate; | ||
const hasUnsavedChanges = this.state.adhocMetric.equals(this.props.adhocMetric); | ||
const stateIsValid = adhocMetric.isValid(); | ||
const hasUnsavedChanges = !adhocMetric.equals(propsAdhocMetric); | ||
|
||
return ( | ||
<Popover | ||
id="metrics-edit-popover" | ||
title={popoverTitle} | ||
{...popoverProps} | ||
> | ||
<FormGroup> | ||
<ControlLabel><strong>column</strong></ControlLabel> | ||
<OnPasteSelect {...this.selectProps} {...columnSelectProps} /> | ||
</FormGroup> | ||
<FormGroup> | ||
<ControlLabel><strong>aggregate</strong></ControlLabel> | ||
<OnPasteSelect {...this.selectProps} {...aggregateSelectProps} /> | ||
</FormGroup> | ||
<Button | ||
disabled={!stateIsValid} | ||
bsStyle={(hasUnsavedChanges || !stateIsValid) ? 'default' : 'primary'} | ||
bsSize="small" | ||
className="m-r-5" | ||
onClick={this.onSave} | ||
<Tabs | ||
id="adhoc-metric-edit-tabs" | ||
defaultActiveKey={adhocMetric.expressionType} | ||
className="adhoc-metric-edit-tabs" | ||
style={{ height: this.state.height, width: this.state.width }} | ||
> | ||
Save | ||
</Button> | ||
<Button bsSize="small" onClick={this.props.onClose}>Close</Button> | ||
<Tab className="adhoc-metric-edit-tab" eventKey={EXPRESSION_TYPES.SIMPLE} title="Simple"> | ||
<FormGroup> | ||
<ControlLabel><strong>column</strong></ControlLabel> | ||
<OnPasteSelect {...this.selectProps} {...columnSelectProps} /> | ||
</FormGroup> | ||
<FormGroup> | ||
<ControlLabel><strong>aggregate</strong></ControlLabel> | ||
<OnPasteSelect {...this.selectProps} {...aggregateSelectProps} /> | ||
</FormGroup> | ||
</Tab> | ||
{ | ||
this.props.datasourceType !== 'druid' && | ||
<Tab className="adhoc-metric-edit-tab" eventKey={EXPRESSION_TYPES.SQL} title="Custom SQL"> | ||
<FormGroup> | ||
<AceEditor | ||
mode="sql" | ||
theme="github" | ||
height={(this.state.height - 40) + 'px'} | ||
onChange={this.onSqlExpressionChange} | ||
width="100%" | ||
showGutter={false} | ||
value={adhocMetric.sqlExpression || adhocMetric.getDefaultLabel()} | ||
editorProps={{ $blockScrolling: true }} | ||
enableLiveAutocompletion | ||
/> | ||
</FormGroup> | ||
</Tab> | ||
} | ||
</Tabs> | ||
<div> | ||
<Button | ||
disabled={!stateIsValid} | ||
bsStyle={(hasUnsavedChanges && stateIsValid) ? 'primary' : 'default'} | ||
bsSize="small" | ||
className="m-r-5" | ||
onClick={this.onSave} | ||
> | ||
Save | ||
</Button> | ||
<Button bsSize="small" onClick={this.props.onClose}>Close</Button> | ||
<i onMouseDown={this.onDragDown} className="glyphicon glyphicon-resize-full edit-popover-resize" /> | ||
</div> | ||
</Popover> | ||
); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,11 @@ export default class AdhocMetricOption extends React.PureComponent { | |
constructor(props) { | ||
super(props); | ||
this.closeMetricEditOverlay = this.closeMetricEditOverlay.bind(this); | ||
this.onPopoverResize = this.onPopoverResize.bind(this); | ||
} | ||
|
||
onPopoverResize() { | ||
this.forceUpdate(); | ||
} | ||
|
||
closeMetricEditOverlay() { | ||
|
@@ -28,6 +33,7 @@ export default class AdhocMetricOption extends React.PureComponent { | |
const { adhocMetric } = this.props; | ||
const overlay = ( | ||
<AdhocMetricEditPopover | ||
onResize={this.onPopoverResize} | ||
adhocMetric={adhocMetric} | ||
onChange={this.props.onMetricEdit} | ||
onClose={this.closeMetricEditOverlay} | ||
|
@@ -44,6 +50,7 @@ export default class AdhocMetricOption extends React.PureComponent { | |
disabled | ||
overlay={overlay} | ||
rootClose | ||
shouldUpdatePosition | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎉 thanks again for adding this! |
||
defaultOverlayShown={!adhocMetric.fromFormData} | ||
> | ||
<Label style={{ margin: this.props.multi ? 0 : 3, cursor: 'pointer' }}> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Boolean()
also works fyiThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Airbnb js style guide prefer
!!
toBoolean(...)
- not sure if we have a different style guide for this project, but I like their advice on the whole.https://github.com/airbnb/javascript#coercion--booleans