Skip to content
Snippets Groups Projects
Unverified Commit 3305c815 authored by Kamil Mielnik's avatar Kamil Mielnik Committed by GitHub
Browse files

Previous period comparison shortcut in notebook mode (#43220)

* Sort functions

* Add boilerplate for aggregateOffset and tests

* Simplify assertion

* Make basic case work

* Handle name generation

* Do not require "lib/uuid" attribute to be present since MLv2 will normalize it under the hood anyway

* Fix typing

* Group tests

* Refactor offsetClause to return a new clause instead of a new query
- Move offsetClause to expressions.ts
- Add assertion

* Revert "Sort functions"

This reverts commit ab9ce2b24ea6bdad5ff7e9bed8ef38d4b5923f00.

* Move tests

* Handle names dynamically

* Shorten a test

* Update comment

* Add TODO

* Update expression types

* Add diffOffsetClause & percentDiffOffsetClause

* Add a test for diffOffsetClause

* Add TODOs

* Add tests for diffOffsetClause and percentDiffOffsetClause

* Unwrap tests

* Add skeleton for new tests

* Refactor first offsetClause tests

* Add tests for names

* Fix case of non-datetime columns

* Handle a case with offset < 1 and no breakouts

* Handle a case with offset < 1 and breakouts on non-datetime column

* Handle a case with offset < 1 and breakouts binned datetime column

* Handle a case with offset < 1 and breakout on non-binned datetime column

* Refactor

* Refactor

* Remove TODO

* Add tests for diffOffsetClause

* Add tests for percentDiffOffsetClause

* Move offset stuff to offset.ts and offset.unit.spec.ts

* Use template string for prefixes

* Use breakoutColumn + isDate to check column type

* Refactor

* Fix error message

* Add boilerplate for CompareAggregations

* Fix title

* Render aggregations list

* Style AccordionList

* Sort props

* Fix bucket name

* Use displayName instead shortName

* Support parseValue prop in NumberInput

* Add period input accepting integers only

* Accept non-negative values only

* Do not accept zeros

* Add state

* Add submit button

* Export offset functions via Lib

* Make it possible to skip rightSection

* Add column picker

* Map offset user input to api input (negative value)

* Add label

* Fix crash

* Extract shouldCreate

* Make onSelect in AggregationPicker support multiple values

* Extract ReferenceAggregationPicker

* Extract ColumnPicker

* Extract getAggregations

* Rename

* Add custom items

* Refactor item component

* Extract OffsetInput

* Remove unused data-testid

* Style OffsetInput

* Generate titles according to specs

* Generate label

* Generate help

* Extract utils

* Use different width for the 1st step

* Format code

* Use MultiSelect directly

* Avoid custom parseValue

* Revert MultiaAutocomplete changes

* Improve typing in describeTemporalInterval and
 describeRelativeDatetime

* Use describeTemporalUnit to pluralize

* Use interface

* Avoid setting value as DOM attribute

* Fix test

* Add onAdd prop to AggregationPicker and revert the change to have onSelect pass multiple aggregations

* Reduce number of props

* Render checkboxes in custom items

* Introduce and use --mb-color-brand-lighter

* Avoid !important

* Remove redundant prop

* Fix warning about isSelected being used as DOM attribute

* Fix positioning in case dir attribute is not present in any parent component

* Add type attribute to all ListItems
parent 2f0bc497
No related branches found
No related tags found
No related merge requests found
Showing
with 506 additions and 21 deletions
...@@ -4,7 +4,7 @@ import type { HTMLAttributes } from "react"; ...@@ -4,7 +4,7 @@ import type { HTMLAttributes } from "react";
import { getRootStyle } from "metabase/css/core/base.styled"; import { getRootStyle } from "metabase/css/core/base.styled";
import { defaultFontFiles } from "metabase/css/core/fonts.styled"; import { defaultFontFiles } from "metabase/css/core/fonts.styled";
import { alpha } from "metabase/lib/colors"; import { alpha, lighten } from "metabase/lib/colors";
import { useSelector } from "metabase/lib/redux"; import { useSelector } from "metabase/lib/redux";
import { aceEditorStyles } from "metabase/query_builder/components/NativeQueryEditor/NativeQueryEditor.styled"; import { aceEditorStyles } from "metabase/query_builder/components/NativeQueryEditor/NativeQueryEditor.styled";
import { getFontFiles } from "metabase/styled-components/selectors"; import { getFontFiles } from "metabase/styled-components/selectors";
...@@ -37,6 +37,8 @@ const SdkContentWrapperInner = styled.div< ...@@ -37,6 +37,8 @@ const SdkContentWrapperInner = styled.div<
--mb-color-bg-light: ${({ theme }) => theme.fn.themeColor("bg-light")}; --mb-color-bg-light: ${({ theme }) => theme.fn.themeColor("bg-light")};
--mb-color-bg-dark: ${({ theme }) => theme.fn.themeColor("bg-dark")}; --mb-color-bg-dark: ${({ theme }) => theme.fn.themeColor("bg-dark")};
--mb-color-brand: ${({ theme }) => theme.fn.themeColor("brand")}; --mb-color-brand: ${({ theme }) => theme.fn.themeColor("brand")};
--mb-color-brand-lighter: ${({ theme }) =>
lighten(theme.fn.themeColor("brand"), 0.598)};
--mb-color-brand-alpha-04: ${({ theme }) => --mb-color-brand-alpha-04: ${({ theme }) =>
alpha(theme.fn.themeColor("brand"), 0.04)}; alpha(theme.fn.themeColor("brand"), 0.04)};
--mb-color-brand-alpha-88: ${({ theme }) => --mb-color-brand-alpha-88: ${({ theme }) =>
......
...@@ -17,6 +17,7 @@ export * from "./limit"; ...@@ -17,6 +17,7 @@ export * from "./limit";
export * from "./metadata"; export * from "./metadata";
export * from "./metrics"; export * from "./metrics";
export * from "./native"; export * from "./native";
export * from "./offset";
export * from "./order_by"; export * from "./order_by";
export * from "./query"; export * from "./query";
export * from "./segments"; export * from "./segments";
......
import { t } from "ttag"; import { t } from "ttag";
import { inflect } from "metabase/lib/formatting";
import { breakoutColumn, breakouts } from "./breakout"; import { breakoutColumn, breakouts } from "./breakout";
import { isDate } from "./column_types"; import { isDate } from "./column_types";
import { expressionClause, withExpressionName } from "./expression"; import { expressionClause, withExpressionName } from "./expression";
import { displayInfo } from "./metadata"; import { describeTemporalUnit, displayInfo } from "./metadata";
import { temporalBucket } from "./temporal_bucket"; import { temporalBucket } from "./temporal_bucket";
import type { AggregationClause, ExpressionClause, Query } from "./types"; import type { AggregationClause, ExpressionClause, Query } from "./types";
...@@ -103,8 +101,10 @@ function getOffsetClauseName( ...@@ -103,8 +101,10 @@ function getOffsetClauseName(
} }
const bucketInfo = displayInfo(query, stageIndex, bucket); const bucketInfo = displayInfo(query, stageIndex, bucket);
const bucketName = bucketInfo.displayName.toLowerCase(); const period = describeTemporalUnit(
const period = inflect(bucketName, absoluteOffset); bucketInfo.shortName,
absoluteOffset,
).toLowerCase();
return absoluteOffset === 1 return absoluteOffset === 1
? t`${displayName} (${prefix}previous ${period})` ? t`${displayName} (${prefix}previous ${period})`
......
...@@ -52,14 +52,14 @@ type IntervalAmount = number | "current" | "next" | "last"; ...@@ -52,14 +52,14 @@ type IntervalAmount = number | "current" | "next" | "last";
export function describeTemporalInterval( export function describeTemporalInterval(
n: IntervalAmount, n: IntervalAmount,
unit?: string, unit?: BucketName,
): string { ): string {
return ML.describe_temporal_interval(n, unit); return ML.describe_temporal_interval(n, unit);
} }
export function describeRelativeDatetime( export function describeRelativeDatetime(
n: IntervalAmount, n: IntervalAmount,
unit?: string, unit?: BucketName,
): string { ): string {
return ML.describe_relative_datetime(n, unit); return ML.describe_relative_datetime(n, unit);
} }
......
...@@ -4,6 +4,10 @@ import { t } from "ttag"; ...@@ -4,6 +4,10 @@ import { t } from "ttag";
import AccordionList from "metabase/core/components/AccordionList"; import AccordionList from "metabase/core/components/AccordionList";
import { useToggle } from "metabase/hooks/use-toggle"; import { useToggle } from "metabase/hooks/use-toggle";
import { useSelector } from "metabase/lib/redux"; import { useSelector } from "metabase/lib/redux";
import {
CompareAggregations,
getOffsetPeriod,
} from "metabase/query_builder/components/CompareAggregations";
import { ExpressionWidget } from "metabase/query_builder/components/expressions/ExpressionWidget"; import { ExpressionWidget } from "metabase/query_builder/components/expressions/ExpressionWidget";
import { ExpressionWidgetHeader } from "metabase/query_builder/components/expressions/ExpressionWidgetHeader"; import { ExpressionWidgetHeader } from "metabase/query_builder/components/expressions/ExpressionWidgetHeader";
import { getMetadata } from "metabase/selectors/metadata"; import { getMetadata } from "metabase/selectors/metadata";
...@@ -27,20 +31,29 @@ interface AggregationPickerProps { ...@@ -27,20 +31,29 @@ interface AggregationPickerProps {
clauseIndex?: number; clauseIndex?: number;
operators: Lib.AggregationOperator[]; operators: Lib.AggregationOperator[];
hasExpressionInput?: boolean; hasExpressionInput?: boolean;
onSelect: (operator: Lib.Aggregable) => void; onAdd: (aggregations: Lib.Aggregable[]) => void;
onSelect: (aggregation: Lib.Aggregable) => void;
onClose?: () => void; onClose?: () => void;
} }
type OperatorListItem = Lib.AggregationOperatorDisplayInfo & { type OperatorListItem = Lib.AggregationOperatorDisplayInfo & {
type: "operator";
operator: Lib.AggregationOperator; operator: Lib.AggregationOperator;
}; };
type MetricListItem = Lib.MetricDisplayInfo & { type MetricListItem = Lib.MetricDisplayInfo & {
type: "metric";
metric: Lib.MetricMetadata; metric: Lib.MetricMetadata;
selected: boolean; selected: boolean;
}; };
type ListItem = OperatorListItem | MetricListItem; type CompareListItem = {
type: "compare";
displayName: string;
selected?: boolean;
};
type ListItem = OperatorListItem | MetricListItem | CompareListItem;
type Section = { type Section = {
name?: string; name?: string;
...@@ -50,10 +63,6 @@ type Section = { ...@@ -50,10 +63,6 @@ type Section = {
type?: string; type?: string;
}; };
function isOperatorListItem(item: ListItem): item is OperatorListItem {
return "operator" in item;
}
export function AggregationPicker({ export function AggregationPicker({
className, className,
query, query,
...@@ -62,6 +71,7 @@ export function AggregationPicker({ ...@@ -62,6 +71,7 @@ export function AggregationPicker({
clauseIndex, clauseIndex,
operators, operators,
hasExpressionInput = true, hasExpressionInput = true,
onAdd,
onSelect, onSelect,
onClose, onClose,
}: AggregationPickerProps) { }: AggregationPickerProps) {
...@@ -76,6 +86,7 @@ export function AggregationPicker({ ...@@ -76,6 +86,7 @@ export function AggregationPicker({
] = useToggle( ] = useToggle(
isExpressionEditorInitiallyOpen(query, stageIndex, clause, operators), isExpressionEditorInitiallyOpen(query, stageIndex, clause, operators),
); );
const [isComparing, setIsComparing] = useState(false);
// For really simple inline expressions like Average([Price]), // For really simple inline expressions like Average([Price]),
// MLv2 can figure out that "Average" operator is used. // MLv2 can figure out that "Average" operator is used.
...@@ -97,14 +108,17 @@ export function AggregationPicker({ ...@@ -97,14 +108,17 @@ export function AggregationPicker({
const database = metadata.database(databaseId); const database = metadata.database(databaseId);
const canUseExpressions = database?.hasFeature("expression-aggregations"); const canUseExpressions = database?.hasFeature("expression-aggregations");
const isMetricBased = Lib.isMetricBased(query, stageIndex); const isMetricBased = Lib.isMetricBased(query, stageIndex);
const compareItem = getCompareListItem(query, stageIndex);
if ((compareItem || operators.length > 0) && !isMetricBased) {
const operatorItems = operators.map(operator =>
getOperatorListItem(query, stageIndex, operator),
);
if (operators.length > 0 && !isMetricBased) {
sections.push({ sections.push({
key: "operators", key: "operators",
name: t`Basic Metrics`, name: t`Basic Metrics`,
items: operators.map(operator => items: compareItem ? [compareItem, ...operatorItems] : operatorItems,
getOperatorListItem(query, stageIndex, operator),
),
icon: "table2", icon: "table2",
}); });
} }
...@@ -175,15 +189,25 @@ export function AggregationPicker({ ...@@ -175,15 +189,25 @@ export function AggregationPicker({
[onSelect, onClose], [onSelect, onClose],
); );
const handleCompareSelect = useCallback(() => {
setIsComparing(true);
}, []);
const handleCompareClose = useCallback(() => {
setIsComparing(false);
}, []);
const handleChange = useCallback( const handleChange = useCallback(
(item: ListItem) => { (item: ListItem) => {
if (isOperatorListItem(item)) { if (item.type === "operator") {
handleOperatorSelect(item); handleOperatorSelect(item);
} else { } else if (item.type === "metric") {
handleMetricSelect(item); handleMetricSelect(item);
} else if (item.type === "compare") {
handleCompareSelect();
} }
}, },
[handleOperatorSelect, handleMetricSelect], [handleOperatorSelect, handleMetricSelect, handleCompareSelect],
); );
const handleSectionChange = useCallback( const handleSectionChange = useCallback(
...@@ -204,6 +228,25 @@ export function AggregationPicker({ ...@@ -204,6 +228,25 @@ export function AggregationPicker({
[onSelect, onClose], [onSelect, onClose],
); );
const handleCompareSubmit = useCallback(
(aggregations: Lib.Aggregable[]) => {
onAdd(aggregations);
onClose?.();
},
[onAdd, onClose],
);
if (isComparing) {
return (
<CompareAggregations
query={query}
stageIndex={stageIndex}
onSubmit={handleCompareSubmit}
onClose={handleCompareClose}
/>
);
}
if (isEditingExpression) { if (isEditingExpression) {
return ( return (
<ExpressionWidget <ExpressionWidget
...@@ -330,6 +373,7 @@ function getOperatorListItem( ...@@ -330,6 +373,7 @@ function getOperatorListItem(
const operatorInfo = Lib.displayInfo(query, stageIndex, operator); const operatorInfo = Lib.displayInfo(query, stageIndex, operator);
return { return {
...operatorInfo, ...operatorInfo,
type: "operator",
operator, operator,
}; };
} }
...@@ -343,12 +387,41 @@ function getMetricListItem( ...@@ -343,12 +387,41 @@ function getMetricListItem(
const metricInfo = Lib.displayInfo(query, stageIndex, metric); const metricInfo = Lib.displayInfo(query, stageIndex, metric);
return { return {
...metricInfo, ...metricInfo,
type: "metric",
metric, metric,
selected: selected:
clauseIndex != null && metricInfo.aggregationPosition === clauseIndex, clauseIndex != null && metricInfo.aggregationPosition === clauseIndex,
}; };
} }
function getCompareListItem(
query: Lib.Query,
stageIndex: number,
): CompareListItem | undefined {
const aggregations = Lib.aggregations(query, stageIndex);
if (aggregations.length === 0) {
return undefined;
}
const period = getOffsetPeriod(query, stageIndex);
if (aggregations.length > 1) {
return {
type: "compare",
displayName: t`Compare to previous ${period} ...`,
};
}
const [aggregation] = aggregations;
const info = Lib.displayInfo(query, stageIndex, aggregation);
return {
type: "compare",
displayName: t`Compare “${info.displayName}” to previous ${period} ...`,
};
}
function checkIsColumnSelected(columnInfo: Lib.ColumnDisplayInfo) { function checkIsColumnSelected(columnInfo: Lib.ColumnDisplayInfo) {
return !!columnInfo.selected; return !!columnInfo.selected;
} }
...@@ -131,6 +131,7 @@ function setup({ ...@@ -131,6 +131,7 @@ function setup({
: baseOperators; : baseOperators;
const onSelect = jest.fn(); const onSelect = jest.fn();
const onAdd = jest.fn();
renderWithProviders( renderWithProviders(
<AggregationPicker <AggregationPicker
...@@ -139,6 +140,7 @@ function setup({ ...@@ -139,6 +140,7 @@ function setup({
stageIndex={stageIndex} stageIndex={stageIndex}
operators={operators} operators={operators}
hasExpressionInput={hasExpressionInput} hasExpressionInput={hasExpressionInput}
onAdd={onAdd}
onSelect={onSelect} onSelect={onSelect}
/>, />,
{ storeInitialState: state }, { storeInitialState: state },
......
import type { FormEvent } from "react";
import { useMemo, useState } from "react";
import { t } from "ttag";
import { Box, Button, Flex, Stack } from "metabase/ui";
import * as Lib from "metabase-lib";
import { ExpressionWidgetHeader } from "../expressions/ExpressionWidgetHeader";
import {
ColumnPicker,
OffsetInput,
ReferenceAggregationPicker,
} from "./components";
import type { ColumnType } from "./types";
import { canSubmit, getAggregations, getTitle } from "./utils";
interface Props {
query: Lib.Query;
stageIndex: number;
onClose: () => void;
onSubmit: (aggregations: Lib.ExpressionClause[]) => void;
}
const DEFAULT_OFFSET = 1;
const DEFAULT_COLUMNS: ColumnType[] = ["offset", "percent-diff-offset"];
const STEP_1_WIDTH = 378;
const STEP_2_WIDTH = 472;
export const CompareAggregations = ({
query,
stageIndex,
onClose,
onSubmit,
}: Props) => {
const aggregations = useMemo(() => {
return Lib.aggregations(query, stageIndex);
}, [query, stageIndex]);
const hasManyAggregations = aggregations.length > 1;
const [aggregation, setAggregation] = useState<
Lib.AggregationClause | Lib.ExpressionClause | undefined
>(hasManyAggregations ? undefined : aggregations[0]);
const [offset, setOffset] = useState<number | "">(DEFAULT_OFFSET);
const [columns, setColumns] = useState<ColumnType[]>(DEFAULT_COLUMNS);
const width = aggregation ? STEP_2_WIDTH : STEP_1_WIDTH;
const title = useMemo(
() => getTitle(query, stageIndex, aggregation),
[query, stageIndex, aggregation],
);
const handleBack = () => {
if (hasManyAggregations && aggregation) {
setAggregation(undefined);
} else {
onClose();
}
};
const handleSubmit = (event: FormEvent) => {
event.preventDefault();
if (aggregation && offset !== "") {
const aggregations = getAggregations(
query,
stageIndex,
aggregation,
columns,
offset,
);
onSubmit(aggregations);
onClose();
}
};
return (
<Box miw={width} maw={width}>
<ExpressionWidgetHeader title={title} onBack={handleBack} />
{!aggregation && (
<ReferenceAggregationPicker
query={query}
stageIndex={stageIndex}
onChange={setAggregation}
/>
)}
{aggregation && (
<form onSubmit={handleSubmit}>
<Stack p="lg" spacing="xl">
<Stack spacing="md">
<OffsetInput
query={query}
stageIndex={stageIndex}
value={offset}
onChange={setOffset}
/>
<ColumnPicker value={columns} onChange={setColumns} />
</Stack>
<Flex justify="flex-end">
<Button
disabled={!canSubmit(offset, columns)}
type="submit"
variant="filled"
>{t`Done`}</Button>
</Flex>
</Stack>
</form>
)}
</Box>
);
};
import type { ComponentPropsWithoutRef } from "react";
import { forwardRef, useCallback } from "react";
import { t } from "ttag";
import { Checkbox, Flex, MultiSelect, Text } from "metabase/ui";
import type { ColumnType } from "../../types";
import S from "./ColumnPicker.module.css";
interface ItemType {
example: string;
label: string;
value: ColumnType;
}
interface Props {
value: ColumnType[];
onChange: (value: ColumnType[]) => void;
}
const COLUMN_OPTIONS: ItemType[] = [
{
example: "1826, 3004",
label: t`Previous value`,
value: "offset",
},
{
example: "+2.3%, -0.1%",
label: t`Percentage difference`,
value: "percent-diff-offset",
},
{
example: "+42, -3",
label: t`Value difference`,
value: "diff-offset",
},
];
export const ColumnPicker = ({ value, onChange }: Props) => {
const handleChange = useCallback(
(values: string[]) => {
onChange(values as ColumnType[]);
},
[onChange],
);
return (
<MultiSelect
data={COLUMN_OPTIONS}
disableSelectedItemFiltering
itemComponent={Item}
label={t`Columns to create`}
placeholder={t`Columns to create`}
styles={{
item: {
"&[data-selected]": {
backgroundColor: "transparent",
},
"&[data-selected]:hover": {
backgroundColor: "var(--mb-color-brand-lighter)",
},
"&[data-selected][data-hovered]": {
backgroundColor: "var(--mb-color-brand-lighter)",
},
"&[data-hovered]": {
backgroundColor: "var(--mb-color-brand-lighter)",
},
},
}}
value={value}
onChange={handleChange}
/>
);
};
const Item = forwardRef<
HTMLDivElement,
ItemType & ComponentPropsWithoutRef<"div"> & { selected: boolean }
>(function Item({ example, label, selected, value, ...props }, ref) {
return (
<div ref={ref} {...props}>
<Flex align="center" gap="sm">
<Checkbox checked={selected} readOnly />
<Flex align="center" className={S.itemContent} justify="space-between">
<Text>{label}</Text>
<Text c="text-light" size="sm">
{example}
</Text>
</Flex>
</Flex>
</div>
);
});
export * from "./ColumnPicker";
.input {
max-width: 50px;
}
.wrapper {
margin-top: 0.25rem;
}
.help {
position: absolute;
left: 50px;
}
import { useCallback, useMemo } from "react";
import { Flex, NumberInput, Text } from "metabase/ui";
import type * as Lib from "metabase-lib";
import S from "./OffsetInput.module.css";
import { getHelp, getLabel } from "./utils";
interface Props {
query: Lib.Query;
stageIndex: number;
value: number | "";
onChange: (value: number | "") => void;
}
export const OffsetInput = ({ query, stageIndex, value, onChange }: Props) => {
const label = useMemo(() => getLabel(query, stageIndex), [query, stageIndex]);
const help = useMemo(() => getHelp(query, stageIndex), [query, stageIndex]);
const handleChange = useCallback(
(value: number | "") => {
if (typeof value === "number") {
onChange(Math.floor(Math.max(Math.abs(value), 1)));
} else {
onChange(value);
}
},
[onChange],
);
return (
<Flex align="flex-end" pos="relative">
<NumberInput
classNames={{
input: S.input,
wrapper: S.wrapper,
}}
label={label}
min={1}
precision={0}
size="md"
step={1}
type="number"
value={value}
onChange={handleChange}
/>
<Text className={S.help} c="text-light" p="sm">
{help}
</Text>
</Flex>
);
};
export * from "./OffsetInput";
import { t } from "ttag";
import * as Lib from "metabase-lib";
export const getLabel = (query: Lib.Query, stageIndex: number): string => {
const firstBreakout = Lib.breakouts(query, stageIndex)[0];
if (firstBreakout) {
const firstBreakoutColumn = Lib.breakoutColumn(
query,
stageIndex,
firstBreakout,
);
if (!Lib.isDate(firstBreakoutColumn)) {
return t`Row for comparison`;
}
}
return t`Previous period`;
};
export const getHelp = (query: Lib.Query, stageIndex: number): string => {
const firstBreakout = Lib.breakouts(query, stageIndex)[0];
if (!firstBreakout) {
return t`periods ago based on grouping`;
}
const firstBreakoutColumn = Lib.breakoutColumn(
query,
stageIndex,
firstBreakout,
);
const firstBreakoutColumnInfo = Lib.displayInfo(
query,
stageIndex,
firstBreakoutColumn,
);
if (!Lib.isDate(firstBreakoutColumn)) {
return t`rows above based on “${firstBreakoutColumnInfo.displayName}”`;
}
const bucket = Lib.temporalBucket(firstBreakout);
if (!bucket) {
return t`periods ago based on “${firstBreakoutColumnInfo.displayName}”`;
}
const bucketInfo = Lib.displayInfo(query, stageIndex, bucket);
const periodPlural = Lib.describeTemporalUnit(
bucketInfo.shortName,
2,
).toLowerCase();
return t`${periodPlural} ago based on “${firstBreakoutColumnInfo.displayName}”`;
};
.accordionList {
color: var(--mb-color-summarize);
}
import { useCallback, useMemo } from "react";
import AccordionList from "metabase/core/components/AccordionList";
import * as Lib from "metabase-lib";
import S from "./ReferenceAggregationPicker.module.css";
type AggregationItem = Lib.AggregationClauseDisplayInfo & {
aggregation: Lib.AggregationClause;
};
interface Props {
query: Lib.Query;
stageIndex: number;
onChange: (aggregation: Lib.AggregationClause | Lib.ExpressionClause) => void;
}
export const ReferenceAggregationPicker = ({
query,
stageIndex,
onChange,
}: Props) => {
const sections = useMemo(
() => getSections(query, stageIndex),
[query, stageIndex],
);
const handleChange = useCallback(
(item: AggregationItem) => {
onChange(item.aggregation);
},
[onChange],
);
return (
<AccordionList
alwaysExpanded
className={S.accordionList}
maxHeight={Infinity}
renderItemDescription={renderItemDescription}
renderItemName={renderItemName}
sections={sections}
width="100%"
onChange={handleChange}
/>
);
};
const renderItemName = (item: AggregationItem) => item.displayName;
const renderItemDescription = () => null;
const getSections = (query: Lib.Query, stageIndex: number) => {
const aggregations = Lib.aggregations(query, stageIndex);
const items = aggregations.map<AggregationItem>(aggregation => {
const info = Lib.displayInfo(query, stageIndex, aggregation);
return { ...info, aggregation };
});
const sections = [{ items }];
return sections;
};
export * from "./ReferenceAggregationPicker";
export * from "./ColumnPicker";
export * from "./OffsetInput";
export * from "./ReferenceAggregationPicker";
export * from "./CompareAggregations";
export * from "./utils";
export type ColumnType = "offset" | "diff-offset" | "percent-diff-offset";
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment