diff --git a/frontend/src/metabase/query_builder/components/AggregationPopover/AggregationPopover.jsx b/frontend/src/metabase/query_builder/components/AggregationPopover/AggregationPopover.jsx index f03c26190612e3e23fc89ad73841579707e7771b..6807fa5ae7b016be340bc561b3aa5fdaebb4339c 100644 --- a/frontend/src/metabase/query_builder/components/AggregationPopover/AggregationPopover.jsx +++ b/frontend/src/metabase/query_builder/components/AggregationPopover/AggregationPopover.jsx @@ -106,26 +106,26 @@ export default class AggregationPopover extends Component { const aggregation = this._getAggregation(); if (dimension) { - if (item.aggregation && item.aggregation.requiresField) { + if (item.aggregation?.requiresField) { this.commitAggregation( AGGREGATION.setField(item.value, dimension.mbql()), ); } } else if (item.custom) { // use the existing aggregation if it's valid - const value = aggregation && aggregation.isValid() ? aggregation : null; + const value = aggregation?.isValid() ? aggregation : null; this.setState({ aggregation: value, editingAggregation: true, }); - } else if (item.aggregation && item.aggregation.requiresField) { + } else if (item.aggregation?.requiresField) { // check if this aggregation requires a field, if so then force user to pick that now, otherwise we are done this.setState({ aggregation: item.value, choosingField: true, }); } else { - // this includse picking a METRIC or picking an aggregation which doesn't require a field + // this includes picking a METRIC or picking an aggregation which doesn't require a field this.commitAggregation(item.value); } }; @@ -147,9 +147,12 @@ export default class AggregationPopover extends Component { const { aggregationOperators, query, dimension, showRawData } = this.props; return ( aggregationOperators || - (dimension && dimension.aggregationOperators()) || + dimension?.aggregationOperators() || query.table().aggregationOperators() - ).filter(agg => showRawData || agg.short !== "rows"); + ).filter( + aggregationOperator => + showRawData || aggregationOperator.short !== "rows", + ); } itemIsSelected(item) { @@ -158,7 +161,7 @@ export default class AggregationPopover extends Component { } renderItemExtra(item, itemIndex) { - if (item.aggregation && item.aggregation.description) { + if (item.aggregation?.description) { return ( <div className="p1"> <Tooltip tooltip={item.aggregation.description}> @@ -172,77 +175,29 @@ export default class AggregationPopover extends Component { } renderMetricTooltip(metric) { + const content = <QueryDefinitionTooltip type="metric" object={metric} />; + return ( <div className="p1"> - <Tooltip - tooltip={<QueryDefinitionTooltip type="metric" object={metric} />} - > + <Tooltip tooltip={content}> <span className="QuestionTooltipTarget" /> </Tooltip> </div> ); } - render() { - let { query, dimension, showCustom, showMetrics, alwaysExpanded } = - this.props; - - const table = query.table(); - const aggregationOperators = this._getAvailableAggregations(); - - if (dimension) { - showCustom = false; - showMetrics = false; - } - if (!table.database.hasFeature("expression-aggregations")) { - showCustom = false; - } + getSections(table, selectedAggregation) { + const { alwaysExpanded, dimension, showCustom } = this.props; + const aggregationItems = this.getAggregationItems(); + const metricItems = this.getMetricItems(table, selectedAggregation); - const { choosingField, editingAggregation } = this.state; - const aggregation = AGGREGATION.getContent(this.state.aggregation); - - let selectedAggregation; - if (AGGREGATION.isMetric(aggregation)) { - selectedAggregation = _.findWhere(table.metrics, { - id: AGGREGATION.getMetric(aggregation), - }); - } else if (AGGREGATION.isStandard(aggregation)) { - selectedAggregation = _.findWhere(aggregationOperators, { - short: AGGREGATION.getOperator(aggregation), - }); - } - - const aggregationItems = aggregationOperators.map(aggregation => ({ - name: dimension - ? aggregation.name.replace("of ...", "") - : aggregation.name, - value: [aggregation.short, ...aggregation.fields.map(field => null)], - isSelected: agg => - AGGREGATION.isStandard(agg) && - AGGREGATION.getOperator(agg) === aggregation.short, - aggregation: aggregation, - })); + const sections = []; - // we only want to consider active metrics, with the ONE exception that if the currently selected aggregation is a - // retired metric then we include it in the list to maintain continuity - const metrics = table.metrics - ? table.metrics.filter(metric => - showMetrics - ? !metric.archived || - (selectedAggregation && selectedAggregation.id === metric.id) - : // GA metrics are more like columns, so they should be displayed even when showMetrics is false - metric.googleAnalyics, - ) - : []; - const metricItems = metrics.map(metric => ({ - name: metric.name, - value: ["metric", metric.id], - isSelected: aggregation => - AGGREGATION.getMetric(aggregation) === metric.id, - metric: metric, - })); + const maybeOverriddenShowCustomProp = + dimension || !table.database.hasFeature("expression-aggregations") + ? false + : showCustom; - const sections = []; // "Basic Metrics", e.x. count, sum, avg, etc if (aggregationItems.length > 0) { sections.push({ @@ -251,6 +206,7 @@ export default class AggregationPopover extends Component { items: aggregationItems, }); } + // "Common Metrics" a.k.a. saved metrics if (metricItems.length > 0) { sections.push({ @@ -266,14 +222,16 @@ export default class AggregationPopover extends Component { aggregationItems, item => COMMON_AGGREGATIONS.has(item.aggregation.short), ); + // move COMMON_AGGREGATIONS into the "common metrics" section sections[0].items = basicAggregationItems; sections[1].items = [...commonAggregationItems, ...metricItems]; + // swap the order of the sections so "common metrics" are first sections.reverse(); } - if (showCustom) { + if (maybeOverriddenShowCustomProp) { // add "custom" as it's own section sections.push({ name: CUSTOM_SECTION_NAME, @@ -295,6 +253,80 @@ export default class AggregationPopover extends Component { sections[0].name = null; } + return sections; + } + + getSelectedAggregation(table, aggregation) { + const aggregationOperators = this._getAvailableAggregations(); + + if (AGGREGATION.isMetric(aggregation)) { + return _.findWhere(table.metrics, { + id: AGGREGATION.getMetric(aggregation), + }); + } + + return _.findWhere(aggregationOperators, { + short: AGGREGATION.getOperator(aggregation), + }); + } + + getAggregationItems() { + const { dimension } = this.props; + const aggregationOperators = this._getAvailableAggregations(); + + return aggregationOperators.map(aggregation => ({ + name: dimension + ? aggregation.name.replace("of ...", "") + : aggregation.name, + value: [aggregation.short, ...aggregation.fields.map(field => null)], + isSelected: agg => + AGGREGATION.isStandard(agg) && + AGGREGATION.getOperator(agg) === aggregation.short, + aggregation: aggregation, + })); + } + + getMetrics(table, selectedAggregation) { + const { dimension, showMetrics } = this.props; + const maybeOverriddenShowMetrics = dimension ? false : showMetrics; + + // we only want to consider active metrics, with the ONE exception that if the currently selected aggregation is a + // retired metric then we include it in the list to maintain continuity + + const filter = metric => + maybeOverriddenShowMetrics + ? !metric.archived || + (selectedAggregation && selectedAggregation.id === metric.id) + : // GA metrics are more like columns, so they should be displayed even when showMetrics is false + metric.googleAnalyics; + + if (table.metrics) { + return table.metrics.filter(filter); + } + + return []; + } + + getMetricItems(table, selectedAggregation) { + const metrics = this.getMetrics(table, selectedAggregation); + + return metrics.map(metric => ({ + name: metric.name, + value: ["metric", metric.id], + isSelected: aggregation => + AGGREGATION.getMetric(aggregation) === metric.id, + metric: metric, + })); + } + + render() { + const { query } = this.props; + const table = query.table(); + const { choosingField, editingAggregation } = this.state; + const aggregation = AGGREGATION.getContent(this.state.aggregation); + const selectedAggregation = this.getSelectedAggregation(table, aggregation); + const sections = this.getSections(table, selectedAggregation); + if (editingAggregation) { return ( <ExpressionPopoverRoot> @@ -362,10 +394,10 @@ export default class AggregationPopover extends Component { sections={sections} onChange={this.onPickAggregation} itemIsSelected={this.itemIsSelected.bind(this)} - renderSectionIcon={s => <Icon name={s.icon} size={18} />} + renderSectionIcon={section => <Icon name={section.icon} size={18} />} renderItemExtra={this.renderItemExtra.bind(this)} getItemClassName={item => - item.metric && item.metric.archived ? "text-medium" : null + item.metric?.archived ? "text-medium" : null } onChangeSection={(section, sectionIndex) => { if (section.custom) {