From d39dce81eef1a5d397e06285576002293ab6570a Mon Sep 17 00:00:00 2001 From: Anton Kulyk <kuliks.anton@gmail.com> Date: Tue, 13 Dec 2022 10:15:34 +0000 Subject: [PATCH] Convert drill code to TypeScript (#27165) * Extend `field` types * Add series types * Fix `FieldId` type import * Add `metabase/visualizations/types` * Add drill types * Specify `Question.prototype.pivot` return type * Update `AutomaticDashboardDrill` * Update `ColumnFilterDrill` * Update `CompareToRestDrill` * Update `DashboardClickDrill` * Update `DistributionDrill` * Update `ForeignKeyDrill` * Update `ObjectDetailDrill` * Update `PivotByCategoryDrill` * Update `PivotByLocationDrill` * Update `PivotByTimeDrill` * Update `QuickFilterDrill` * Update `SummarizeColumnByTimeDrill` * Update `SummarizeColumnDrill` * Update `UnderlyingRecordsDrill` * Update `ZoomDrill` * Update `FormatDrill` (former `FormatAction`) * Update `SortDrill` (former `SortAction`) * Update `NativeDrillFallback` * Fix types * Fix handling missing `clicked` object * Simplify drill type hierarchy --- frontend/src/metabase-lib/Question.ts | 5 +- frontend/src/metabase-lib/references.ts | 2 +- frontend/src/metabase-types/api/dataset.ts | 20 +++- frontend/src/metabase-types/api/field.ts | 57 +++++++++-- .../src/metabase-types/api/mocks/field.ts | 17 +++- frontend/src/metabase-types/api/query.ts | 2 +- ...rdDrill.jsx => AutomaticDashboardDrill.ts} | 7 +- .../components/drill/ColumnFilterDrill.jsx | 41 -------- .../components/drill/ColumnFilterDrill.tsx | 49 +++++++++ ...reToRestDrill.js => CompareToRestDrill.ts} | 7 +- ...ClickDrill.jsx => DashboardClickDrill.tsx} | 28 +++++- ...ributionDrill.jsx => DistributionDrill.ts} | 7 +- ...ForeignKeyDrill.jsx => ForeignKeyDrill.ts} | 25 +++-- .../modes/components/drill/FormatAction.jsx | 75 -------------- .../drill/FormatDrill/FormatDrill.styled.tsx | 7 ++ .../drill/FormatDrill/FormatDrill.tsx | 78 +++++++++++++++ .../components/drill/FormatDrill/index.ts | 1 + .../NativeDrillFallback.styled.tsx | 1 - .../NativeDrillFallback.tsx | 13 +-- ...ctDetailDrill.jsx => ObjectDetailDrill.ts} | 29 +++++- .../components/drill/PivotByCategoryDrill.jsx | 45 --------- .../components/drill/PivotByCategoryDrill.tsx | 58 +++++++++++ .../components/drill/PivotByLocationDrill.jsx | 45 --------- .../components/drill/PivotByLocationDrill.tsx | 58 +++++++++++ .../components/drill/PivotByTimeDrill.jsx | 43 -------- .../components/drill/PivotByTimeDrill.tsx | 54 ++++++++++ ...ckFilterDrill.jsx => QuickFilterDrill.tsx} | 14 +-- .../drill/{SortAction.jsx => SortDrill.ts} | 11 ++- ...Drill.js => SummarizeColumnByTimeDrill.ts} | 9 +- .../components/drill/SummarizeColumnDrill.js | 42 -------- .../components/drill/SummarizeColumnDrill.ts | 71 +++++++++++++ ...rdsDrill.jsx => UnderlyingRecordsDrill.ts} | 9 +- .../drill/{ZoomDrill.jsx => ZoomDrill.ts} | 9 +- .../modes/components/modes/DefaultMode.jsx | 8 +- frontend/src/metabase/modes/types.ts | 99 +++++++++++++++++++ ...reakoutPopover.jsx => BreakoutPopover.tsx} | 25 ++++- frontend/src/metabase/visualizations/types.ts | 8 ++ 37 files changed, 711 insertions(+), 368 deletions(-) rename frontend/src/metabase/modes/components/drill/{AutomaticDashboardDrill.jsx => AutomaticDashboardDrill.ts} (79%) delete mode 100644 frontend/src/metabase/modes/components/drill/ColumnFilterDrill.jsx create mode 100644 frontend/src/metabase/modes/components/drill/ColumnFilterDrill.tsx rename frontend/src/metabase/modes/components/drill/{CompareToRestDrill.js => CompareToRestDrill.ts} (80%) rename frontend/src/metabase/modes/components/drill/{DashboardClickDrill.jsx => DashboardClickDrill.tsx} (70%) rename frontend/src/metabase/modes/components/drill/{DistributionDrill.jsx => DistributionDrill.ts} (76%) rename frontend/src/metabase/modes/components/drill/{ForeignKeyDrill.jsx => ForeignKeyDrill.ts} (51%) delete mode 100644 frontend/src/metabase/modes/components/drill/FormatAction.jsx create mode 100644 frontend/src/metabase/modes/components/drill/FormatDrill/FormatDrill.styled.tsx create mode 100644 frontend/src/metabase/modes/components/drill/FormatDrill/FormatDrill.tsx create mode 100644 frontend/src/metabase/modes/components/drill/FormatDrill/index.ts rename frontend/src/metabase/modes/components/drill/{ObjectDetailDrill.jsx => ObjectDetailDrill.ts} (63%) delete mode 100644 frontend/src/metabase/modes/components/drill/PivotByCategoryDrill.jsx create mode 100644 frontend/src/metabase/modes/components/drill/PivotByCategoryDrill.tsx delete mode 100644 frontend/src/metabase/modes/components/drill/PivotByLocationDrill.jsx create mode 100644 frontend/src/metabase/modes/components/drill/PivotByLocationDrill.tsx delete mode 100644 frontend/src/metabase/modes/components/drill/PivotByTimeDrill.jsx create mode 100644 frontend/src/metabase/modes/components/drill/PivotByTimeDrill.tsx rename frontend/src/metabase/modes/components/drill/{QuickFilterDrill.jsx => QuickFilterDrill.tsx} (69%) rename frontend/src/metabase/modes/components/drill/{SortAction.jsx => SortDrill.ts} (78%) rename frontend/src/metabase/modes/components/drill/{SummarizeColumnByTimeDrill.js => SummarizeColumnByTimeDrill.ts} (75%) delete mode 100644 frontend/src/metabase/modes/components/drill/SummarizeColumnDrill.js create mode 100644 frontend/src/metabase/modes/components/drill/SummarizeColumnDrill.ts rename frontend/src/metabase/modes/components/drill/{UnderlyingRecordsDrill.jsx => UnderlyingRecordsDrill.ts} (84%) rename frontend/src/metabase/modes/components/drill/{ZoomDrill.jsx => ZoomDrill.ts} (77%) create mode 100644 frontend/src/metabase/modes/types.ts rename frontend/src/metabase/query_builder/components/{BreakoutPopover.jsx => BreakoutPopover.tsx} (59%) create mode 100644 frontend/src/metabase/visualizations/types.ts diff --git a/frontend/src/metabase-lib/Question.ts b/frontend/src/metabase-lib/Question.ts index 51ed8d30095..fa25a650a0b 100644 --- a/frontend/src/metabase-lib/Question.ts +++ b/frontend/src/metabase-lib/Question.ts @@ -527,7 +527,10 @@ class QuestionInner { return filter(this, operator, column, value) || this; } - pivot(breakouts = [], dimensions = []): Question { + pivot( + breakouts: (Breakout | Dimension | Field)[] = [], + dimensions = [], + ): Question { return pivot(this, breakouts, dimensions) || this; } diff --git a/frontend/src/metabase-lib/references.ts b/frontend/src/metabase-lib/references.ts index bdd7a4cc122..f54c9a4a97d 100644 --- a/frontend/src/metabase-lib/references.ts +++ b/frontend/src/metabase-lib/references.ts @@ -9,7 +9,7 @@ import { FieldReference, ReferenceOptions, TemplateTagReference, -} from "metabase-types/api/query"; +} from "metabase-types/api"; export const isFieldReference = (mbql: any): mbql is FieldReference => { return Array.isArray(mbql) && mbql.length === 3 && mbql[0] === "field"; diff --git a/frontend/src/metabase-types/api/dataset.ts b/frontend/src/metabase-types/api/dataset.ts index 14b1ccbf8cd..22ae14e5d6a 100644 --- a/frontend/src/metabase-types/api/dataset.ts +++ b/frontend/src/metabase-types/api/dataset.ts @@ -1,4 +1,6 @@ +import { Card } from "./card"; import { DatabaseId } from "./database"; +import { FieldId } from "./field"; import { DatetimeUnit, DimensionReference } from "./query"; import { DownloadPermission } from "./permissions"; @@ -6,10 +8,10 @@ export type RowValue = string | number | null | boolean; export type RowValues = RowValue[]; export interface DatasetColumn { - id?: number; + id?: FieldId; + name: string; display_name: string; source: string; - name: string; // FIXME: this prop does not come from API remapped_to_column?: DatasetColumn; unit?: DatetimeUnit; @@ -41,3 +43,17 @@ export interface Dataset { export interface NativeQueryForm { query: string; } + +export type SingleSeries = { + card: Card; + data: DatasetData; + error_type?: string; + error?: { + status: number; // HTTP status code + data?: string; + }; +}; + +export type RawSeries = SingleSeries[]; +export type TransformedSeries = RawSeries & { _raw: Series }; +export type Series = RawSeries | TransformedSeries; diff --git a/frontend/src/metabase-types/api/field.ts b/frontend/src/metabase-types/api/field.ts index 5b065268859..851383d7de0 100644 --- a/frontend/src/metabase-types/api/field.ts +++ b/frontend/src/metabase-types/api/field.ts @@ -1,3 +1,8 @@ +import { RowValue } from "./dataset"; +import { TableId } from "./table"; + +export type FieldId = number; + export type TextFieldFingerprint = { "average-length": number; "percent-email": number; @@ -32,19 +37,51 @@ export interface FieldFingerprint { }; } +export type FieldVisibilityType = + | "details-only" + | "hidden" + | "normal" + | "retired"; + +type HumanReadableFieldValue = string; +type FieldValue = [RowValue] | [RowValue, HumanReadableFieldValue]; + +export type FieldDimension = { + name: string; +}; + export interface Field { - id?: number; - dimensions?: FieldDimension; - display_name: string; - table_id: number | string; + id?: FieldId; + table_id: TableId; + name: string; - base_type: string; + display_name: string; description: string | null; - nfc_path: string[] | null; + base_type: string; + effective_type?: string; + semantic_type: string; + + active: boolean; + visibility_type: FieldVisibilityType; + preview_display: boolean; + position: number; + + parent_id?: FieldId; + fk_target_field_id?: FieldId; + values?: FieldValue[]; + dimensions?: FieldDimension; + + max_value?: number; + min_value?: number; + + caveats?: string | null; + points_of_interest?: string; + + nfc_path: string[] | null; fingerprint?: FieldFingerprint; -} -export type FieldDimension = { - name: string; -}; + last_analyzed: string; + created_at: string; + updated_at: string; +} diff --git a/frontend/src/metabase-types/api/mocks/field.ts b/frontend/src/metabase-types/api/mocks/field.ts index de75b99377e..26ae847fe99 100644 --- a/frontend/src/metabase-types/api/mocks/field.ts +++ b/frontend/src/metabase-types/api/mocks/field.ts @@ -2,11 +2,24 @@ import { Field } from "metabase-types/api"; export const createMockField = (opts?: Partial<Field>): Field => ({ id: 1, + + name: "mock_field", display_name: "Mock Field", + description: null, + table_id: 1, - name: "mock_field", + base_type: "type/Text", - description: null, + semantic_type: "type/Text", + + active: true, + visibility_type: "normal", + preview_display: true, + position: 1, nfc_path: null, + + last_analyzed: new Date().toISOString(), + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), ...opts, }); diff --git a/frontend/src/metabase-types/api/query.ts b/frontend/src/metabase-types/api/query.ts index 0b3007fb365..0a150f079e4 100644 --- a/frontend/src/metabase-types/api/query.ts +++ b/frontend/src/metabase-types/api/query.ts @@ -1,4 +1,5 @@ import { DatabaseId } from "./database"; +import { FieldId } from "./field"; import { TableId } from "./table"; export interface StructuredQuery { @@ -74,7 +75,6 @@ export type ReferenceOptionsKeys = | "temporal-unit" | "binning"; -export type FieldId = number; export type ColumnName = string; export type FieldReference = [ "field", diff --git a/frontend/src/metabase/modes/components/drill/AutomaticDashboardDrill.jsx b/frontend/src/metabase/modes/components/drill/AutomaticDashboardDrill.ts similarity index 79% rename from frontend/src/metabase/modes/components/drill/AutomaticDashboardDrill.jsx rename to frontend/src/metabase/modes/components/drill/AutomaticDashboardDrill.ts index 6bc59cb08f7..53d3e63eabb 100644 --- a/frontend/src/metabase/modes/components/drill/AutomaticDashboardDrill.jsx +++ b/frontend/src/metabase/modes/components/drill/AutomaticDashboardDrill.ts @@ -4,8 +4,9 @@ import { automaticDashboardDrill, automaticDashboardDrillUrl, } from "metabase-lib/queries/drills/automatic-dashboard-drill"; +import type { Drill } from "../../types"; -export default ({ question, clicked }) => { +const AutomaticDashboardDrill: Drill = ({ question, clicked }) => { const enableXrays = MetabaseSettings.get("enable-xrays"); if (!automaticDashboardDrill({ question, clicked, enableXrays })) { return []; @@ -14,11 +15,13 @@ export default ({ question, clicked }) => { return [ { name: "exploratory-dashboard", + title: t`X-ray`, section: "auto", icon: "bolt", buttonType: "token", - title: t`X-ray`, url: () => automaticDashboardDrillUrl({ question, clicked }), }, ]; }; + +export default AutomaticDashboardDrill; diff --git a/frontend/src/metabase/modes/components/drill/ColumnFilterDrill.jsx b/frontend/src/metabase/modes/components/drill/ColumnFilterDrill.jsx deleted file mode 100644 index 7ab0aa6777f..00000000000 --- a/frontend/src/metabase/modes/components/drill/ColumnFilterDrill.jsx +++ /dev/null @@ -1,41 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from "react"; -import { t } from "ttag"; - -import FilterPopover from "metabase/query_builder/components/filters/FilterPopover"; -import { columnFilterDrill } from "metabase-lib/queries/drills/column-filter-drill"; - -export default function ColumnFilterDrill({ question, clicked }) { - const drill = columnFilterDrill({ question, clicked }); - if (!drill) { - return []; - } - - const { query, initialFilter } = drill; - - return [ - { - name: "filter-column", - section: "summarize", - title: t`Filter by this column`, - buttonType: "horizontal", - icon: "filter", - // eslint-disable-next-line react/display-name - popover: ({ onChangeCardAndRun, onResize, onClose }) => ( - <FilterPopover - query={query} - filter={initialFilter} - onClose={onClose} - onResize={onResize} - onChangeFilter={filter => { - const nextCard = query.filter(filter).question().card(); - onChangeCardAndRun({ nextCard }); - onClose(); - }} - showFieldPicker={false} - isNew={true} - /> - ), - }, - ]; -} diff --git a/frontend/src/metabase/modes/components/drill/ColumnFilterDrill.tsx b/frontend/src/metabase/modes/components/drill/ColumnFilterDrill.tsx new file mode 100644 index 00000000000..7b07b8cad9f --- /dev/null +++ b/frontend/src/metabase/modes/components/drill/ColumnFilterDrill.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { t } from "ttag"; + +import FilterPopover from "metabase/query_builder/components/filters/FilterPopover"; +import { columnFilterDrill } from "metabase-lib/queries/drills/column-filter-drill"; + +import type { Drill, ClickActionPopoverProps } from "../../types"; + +const ColumnFilterDrill: Drill = ({ question, clicked }) => { + const drill = columnFilterDrill({ question, clicked }); + if (!drill) { + return []; + } + + const { query, initialFilter } = drill; + + const ColumnFilterDrillPopover = ({ + onChangeCardAndRun, + onResize, + onClose, + }: ClickActionPopoverProps) => ( + <FilterPopover + isNew + query={query} + filter={initialFilter} + showFieldPicker={false} + onClose={onClose} + onResize={onResize} + onChangeFilter={filter => { + const nextCard = query.filter(filter).question().card(); + onChangeCardAndRun({ nextCard }); + onClose(); + }} + /> + ); + + return [ + { + name: "filter-column", + section: "summarize", + title: t`Filter by this column`, + buttonType: "horizontal", + icon: "filter", + popover: ColumnFilterDrillPopover, + }, + ]; +}; + +export default ColumnFilterDrill; diff --git a/frontend/src/metabase/modes/components/drill/CompareToRestDrill.js b/frontend/src/metabase/modes/components/drill/CompareToRestDrill.ts similarity index 80% rename from frontend/src/metabase/modes/components/drill/CompareToRestDrill.js rename to frontend/src/metabase/modes/components/drill/CompareToRestDrill.ts index 85626a1cc26..76ab129fa62 100644 --- a/frontend/src/metabase/modes/components/drill/CompareToRestDrill.js +++ b/frontend/src/metabase/modes/components/drill/CompareToRestDrill.ts @@ -5,8 +5,9 @@ import { compareToRestDrill, compareToRestDrillUrl, } from "metabase-lib/queries/drills/compare-to-rest-drill"; +import type { Drill } from "../../types"; -export default ({ question, clicked }) => { +const CompareToRestDrill: Drill = ({ question, clicked }) => { const enableXrays = MetabaseSettings.get("enable-xrays"); if (!compareToRestDrill({ question, clicked, enableXrays })) { return []; @@ -15,11 +16,13 @@ export default ({ question, clicked }) => { return [ { name: "compare-dashboard", + title: t`Compare to the rest`, section: "auto", icon: "bolt", buttonType: "token", - title: t`Compare to the rest`, url: () => compareToRestDrillUrl({ question, clicked }), }, ]; }; + +export default CompareToRestDrill; diff --git a/frontend/src/metabase/modes/components/drill/DashboardClickDrill.jsx b/frontend/src/metabase/modes/components/drill/DashboardClickDrill.tsx similarity index 70% rename from frontend/src/metabase/modes/components/drill/DashboardClickDrill.jsx rename to frontend/src/metabase/modes/components/drill/DashboardClickDrill.tsx index 7ed37820384..53552250ab5 100644 --- a/frontend/src/metabase/modes/components/drill/DashboardClickDrill.jsx +++ b/frontend/src/metabase/modes/components/drill/DashboardClickDrill.tsx @@ -3,6 +3,10 @@ import { setOrUnsetParameterValues, setParameterValue, } from "metabase/dashboard/actions"; + +import type { ReduxAction } from "metabase-types/store"; +import type Question from "metabase-lib/Question"; + import { getDashboardDrillLinkUrl, getDashboardDrillPageUrl, @@ -12,7 +16,21 @@ import { getDashboardDrillUrl, } from "metabase-lib/queries/drills/dashboard-click-drill"; -function getAction(type, question, clicked) { +import type { ClickAction, ClickObject, Drill } from "../../types"; + +type DashboardDrillType = + | "link-url" + | "question-url" + | "page-url" + | "dashboard-url" + | "dashboard-filter" + | "dashboard-reset"; + +function getAction( + type: DashboardDrillType, + question: Question, + clicked: ClickObject, +): Partial<ClickAction> { switch (type) { case "link-url": return { @@ -24,7 +42,9 @@ function getAction(type, question, clicked) { url: () => getDashboardDrillQuestionUrl(question, clicked), }; case "page-url": - return { action: () => push(getDashboardDrillPageUrl(clicked)) }; + return { + action: () => push(getDashboardDrillPageUrl(clicked)) as ReduxAction, + }; case "dashboard-url": return { url: () => getDashboardDrillUrl(clicked) }; case "dashboard-filter": @@ -46,7 +66,7 @@ function getAction(type, question, clicked) { } } -export default ({ question, clicked }) => { +const DashboardClickDrill: Drill = ({ question, clicked = {} }) => { const type = getDashboardDrillType(clicked); if (!type) { return []; @@ -60,3 +80,5 @@ export default ({ question, clicked }) => { }, ]; }; + +export default DashboardClickDrill; diff --git a/frontend/src/metabase/modes/components/drill/DistributionDrill.jsx b/frontend/src/metabase/modes/components/drill/DistributionDrill.ts similarity index 76% rename from frontend/src/metabase/modes/components/drill/DistributionDrill.jsx rename to frontend/src/metabase/modes/components/drill/DistributionDrill.ts index 18015b2472f..fbd5afc862f 100644 --- a/frontend/src/metabase/modes/components/drill/DistributionDrill.jsx +++ b/frontend/src/metabase/modes/components/drill/DistributionDrill.ts @@ -3,8 +3,9 @@ import { distributionDrill, distributionDrillQuestion, } from "metabase-lib/queries/drills/distribution-drill"; +import type { Drill } from "../../types"; -export default ({ question, clicked }) => { +const DistributionDrill: Drill = ({ question, clicked }) => { if (!distributionDrill({ question, clicked })) { return []; } @@ -13,10 +14,12 @@ export default ({ question, clicked }) => { { name: "distribution", title: t`Distribution`, - buttonType: "horizontal", section: "summarize", icon: "bar", + buttonType: "horizontal", question: () => distributionDrillQuestion({ question, clicked }), }, ]; }; + +export default DistributionDrill; diff --git a/frontend/src/metabase/modes/components/drill/ForeignKeyDrill.jsx b/frontend/src/metabase/modes/components/drill/ForeignKeyDrill.ts similarity index 51% rename from frontend/src/metabase/modes/components/drill/ForeignKeyDrill.jsx rename to frontend/src/metabase/modes/components/drill/ForeignKeyDrill.ts index a7911808a95..fe5ec25dc71 100644 --- a/frontend/src/metabase/modes/components/drill/ForeignKeyDrill.jsx +++ b/frontend/src/metabase/modes/components/drill/ForeignKeyDrill.ts @@ -4,8 +4,9 @@ import { foreignKeyDrill, foreignKeyDrillQuestion, } from "metabase-lib/queries/drills/foreign-key-drill"; +import type { Drill } from "../../types"; -export default function ForeignKeyDrill({ question, clicked }) { +const ForeignKeyDrill: Drill = ({ question, clicked }) => { const drill = foreignKeyDrill({ question, clicked }); if (!drill) { return []; @@ -15,12 +16,16 @@ export default function ForeignKeyDrill({ question, clicked }) { const columnTitle = singularize(columnName); const tableTitle = pluralize(tableName); - return { - name: "view-fks", - section: "standalone_filter", - buttonType: "horizontal", - icon: "filter", - title: t`View this ${columnTitle}'s ${tableTitle}`, - question: () => foreignKeyDrillQuestion({ question, clicked }), - }; -} + return [ + { + name: "view-fks", + title: t`View this ${columnTitle}'s ${tableTitle}`, + section: "standalone_filter", + icon: "filter", + buttonType: "horizontal", + question: () => foreignKeyDrillQuestion({ question, clicked }), + }, + ]; +}; + +export default ForeignKeyDrill; diff --git a/frontend/src/metabase/modes/components/drill/FormatAction.jsx b/frontend/src/metabase/modes/components/drill/FormatAction.jsx deleted file mode 100644 index 9ebfb408209..00000000000 --- a/frontend/src/metabase/modes/components/drill/FormatAction.jsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from "react"; -import styled from "@emotion/styled"; - -/* eslint-disable react/prop-types */ -import { t } from "ttag"; - -import { getSettingsWidgetsForSeries } from "metabase/visualizations/lib/settings/visualization"; -import ChartSettingsWidget from "metabase/visualizations/components/ChartSettingsWidget"; -import { updateSettings } from "metabase/visualizations/lib/settings"; -import { getColumnKey } from "metabase-lib/queries/utils/get-column-key"; - -export default ({ question, clicked }) => { - if ( - !clicked || - clicked.value !== undefined || - !clicked.column || - !question.query().isEditable() - ) { - return []; - } - const { column } = clicked; - - return [ - { - name: "formatting", - title: "Column formatting", - section: "sort", - buttonType: "formatting", - icon: "gear", - tooltip: t`Column formatting`, - popoverProps: { - placement: "right-end", - offset: [0, 20], - }, - popover: function FormatPopover({ series, onChange }) { - const handleChangeSettings = changedSettings => { - onChange( - updateSettings( - series[0].card.visualization_settings, - changedSettings, - ), - ); - }; - - const columnSettingsWidget = getSettingsWidgetsForSeries( - series, - handleChangeSettings, - false, - ).find(widget => widget.id === "column_settings"); - - return ( - <PopoverRoot> - <ChartSettingsWidget - key={columnSettingsWidget.id} - {...{ - ...columnSettingsWidget, - props: { - ...columnSettingsWidget.props, - initialKey: getColumnKey(column), - }, - }} - hidden={false} - /> - </PopoverRoot> - ); - }, - }, - ]; -}; - -const PopoverRoot = styled.div` - padding-top: 1.5rem; - max-height: 600px; - overflow-y: auto; -`; diff --git a/frontend/src/metabase/modes/components/drill/FormatDrill/FormatDrill.styled.tsx b/frontend/src/metabase/modes/components/drill/FormatDrill/FormatDrill.styled.tsx new file mode 100644 index 00000000000..31687267f55 --- /dev/null +++ b/frontend/src/metabase/modes/components/drill/FormatDrill/FormatDrill.styled.tsx @@ -0,0 +1,7 @@ +import styled from "@emotion/styled"; + +export const PopoverRoot = styled.div` + padding-top: 1.5rem; + max-height: 600px; + overflow-y: auto; +`; diff --git a/frontend/src/metabase/modes/components/drill/FormatDrill/FormatDrill.tsx b/frontend/src/metabase/modes/components/drill/FormatDrill/FormatDrill.tsx new file mode 100644 index 00000000000..53ce0731d47 --- /dev/null +++ b/frontend/src/metabase/modes/components/drill/FormatDrill/FormatDrill.tsx @@ -0,0 +1,78 @@ +import React from "react"; +import { t } from "ttag"; + +import { getSettingsWidgetsForSeries } from "metabase/visualizations/lib/settings/visualization"; +import ChartSettingsWidget from "metabase/visualizations/components/ChartSettingsWidget"; +import { updateSettings } from "metabase/visualizations/lib/settings"; + +import type { VisualizationSettings } from "metabase-types/api"; + +import { getColumnKey } from "metabase-lib/queries/utils/get-column-key"; + +import type { ClickActionPopoverProps, Drill } from "../../../types"; +import { PopoverRoot } from "./FormatDrill.styled"; + +const FormatDrill: Drill = ({ question, clicked }) => { + if ( + !clicked || + clicked.value !== undefined || + !clicked.column || + !question.query().isEditable() + ) { + return []; + } + + const { column } = clicked; + + const FormatPopover = ({ series, onChange }: ClickActionPopoverProps) => { + const handleChangeSettings = (settings: VisualizationSettings) => { + onChange(updateSettings(series[0].card.visualization_settings, settings)); + }; + + const widgets = getSettingsWidgetsForSeries( + series, + handleChangeSettings, + false, + ); + + const columnSettingsWidget = widgets.find( + widget => widget.id === "column_settings", + ); + + const extraProps = { + ...columnSettingsWidget, + props: { + ...columnSettingsWidget.props, + initialKey: getColumnKey(column), + }, + }; + + return ( + <PopoverRoot> + <ChartSettingsWidget + {...extraProps} + key={columnSettingsWidget.id} + hidden={false} + /> + </PopoverRoot> + ); + }; + + return [ + { + name: "formatting", + title: t`Column formatting`, + section: "sort", + buttonType: "formatting", + icon: "gear", + tooltip: t`Column formatting`, + popoverProps: { + placement: "right-end", + offset: [0, 20], + }, + popover: FormatPopover, + }, + ]; +}; + +export default FormatDrill; diff --git a/frontend/src/metabase/modes/components/drill/FormatDrill/index.ts b/frontend/src/metabase/modes/components/drill/FormatDrill/index.ts new file mode 100644 index 00000000000..57a961ff48c --- /dev/null +++ b/frontend/src/metabase/modes/components/drill/FormatDrill/index.ts @@ -0,0 +1 @@ +export { default } from "./FormatDrill"; diff --git a/frontend/src/metabase/modes/components/drill/NativeDrillFallback/NativeDrillFallback.styled.tsx b/frontend/src/metabase/modes/components/drill/NativeDrillFallback/NativeDrillFallback.styled.tsx index 44a05f68f3b..42de24d0ec4 100644 --- a/frontend/src/metabase/modes/components/drill/NativeDrillFallback/NativeDrillFallback.styled.tsx +++ b/frontend/src/metabase/modes/components/drill/NativeDrillFallback/NativeDrillFallback.styled.tsx @@ -1,6 +1,5 @@ import styled from "@emotion/styled"; import { color } from "metabase/lib/colors"; -import Icon from "metabase/components/Icon"; import ExternalLink from "metabase/core/components/ExternalLink"; export const DrillRoot = styled.div` diff --git a/frontend/src/metabase/modes/components/drill/NativeDrillFallback/NativeDrillFallback.tsx b/frontend/src/metabase/modes/components/drill/NativeDrillFallback/NativeDrillFallback.tsx index bc950209e17..6b573adf5a2 100644 --- a/frontend/src/metabase/modes/components/drill/NativeDrillFallback/NativeDrillFallback.tsx +++ b/frontend/src/metabase/modes/components/drill/NativeDrillFallback/NativeDrillFallback.tsx @@ -1,21 +1,18 @@ import React from "react"; import { t } from "ttag"; +import Icon from "metabase/components/Icon"; import MetabaseSettings from "metabase/lib/settings"; import { getEngineNativeType } from "metabase/lib/engine"; -import Icon from "metabase/components/Icon"; -import Question from "metabase-lib/Question"; import { nativeDrillFallback } from "metabase-lib/queries/drills/native-drill-fallback"; + +import type { ClickAction, Drill } from "../../../types"; import { DrillLearnLink, DrillMessage, DrillRoot, } from "./NativeDrillFallback.styled"; -interface NativeDrillFallbackProps { - question: Question; -} - -const NativeDrillFallback = ({ question }: NativeDrillFallbackProps) => { +const NativeDrillFallback: Drill = ({ question }) => { const drill = nativeDrillFallback({ question }); if (!drill) { return []; @@ -43,7 +40,7 @@ const NativeDrillFallback = ({ question }: NativeDrillFallbackProps) => { </DrillLearnLink> </DrillRoot> ), - }, + } as ClickAction, ]; }; diff --git a/frontend/src/metabase/modes/components/drill/ObjectDetailDrill.jsx b/frontend/src/metabase/modes/components/drill/ObjectDetailDrill.ts similarity index 63% rename from frontend/src/metabase/modes/components/drill/ObjectDetailDrill.jsx rename to frontend/src/metabase/modes/components/drill/ObjectDetailDrill.ts index b2a974249bc..f3e9e107854 100644 --- a/frontend/src/metabase/modes/components/drill/ObjectDetailDrill.jsx +++ b/frontend/src/metabase/modes/components/drill/ObjectDetailDrill.ts @@ -1,12 +1,25 @@ import { t } from "ttag"; import { zoomInRow } from "metabase/query_builder/actions"; + +import type { RowValue } from "metabase-types/api"; +import type Question from "metabase-lib/Question"; + import { objectDetailDrill, objectDetailFKDrillQuestion, objectDetailPKDrillQuestion, } from "metabase-lib/queries/drills/object-detail-drill"; -function getAction({ question, clicked, type, objectId }) { +import type { Drill, DrillOptions } from "../../types"; + +type DrillType = "pk" | "fk" | "zoom" | "dashboard"; + +function getAction({ + question, + clicked, + type, + objectId, +}: DrillOptions & { question: Question; type: DrillType; objectId: RowValue }) { switch (type) { case "pk": return { @@ -23,7 +36,13 @@ function getAction({ question, clicked, type, objectId }) { } } -function getActionExtraData({ objectId, hasManyPKColumns }) { +function getActionExtraData({ + objectId, + hasManyPKColumns, +}: { + objectId: RowValue; + hasManyPKColumns: boolean; +}) { if (!hasManyPKColumns) { return { extra: () => ({ objectId }), @@ -31,7 +50,7 @@ function getActionExtraData({ objectId, hasManyPKColumns }) { } } -export default ({ question, clicked }) => { +const ObjectDetailDrill: Drill = ({ question, clicked }) => { const drill = objectDetailDrill({ question, clicked }); if (!drill) { return []; @@ -47,8 +66,10 @@ export default ({ question, clicked }) => { buttonType: "horizontal", icon: "document", default: true, - ...getAction({ question, clicked, type, objectId }), + ...getAction({ question, clicked, type: type as DrillType, objectId }), ...getActionExtraData({ objectId, hasManyPKColumns }), }, ]; }; + +export default ObjectDetailDrill; diff --git a/frontend/src/metabase/modes/components/drill/PivotByCategoryDrill.jsx b/frontend/src/metabase/modes/components/drill/PivotByCategoryDrill.jsx deleted file mode 100644 index 78bfe23d098..00000000000 --- a/frontend/src/metabase/modes/components/drill/PivotByCategoryDrill.jsx +++ /dev/null @@ -1,45 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from "react"; -import { t, jt } from "ttag"; -import BreakoutPopover from "metabase/query_builder/components/BreakoutPopover"; -import { pivotByCategoryDrill } from "metabase-lib/queries/drills/pivot-drill"; - -export default ({ question, clicked }) => { - const drill = pivotByCategoryDrill({ question, clicked }); - if (!drill) { - return []; - } - - const { query, dimensions, breakoutOptions } = drill; - - return [ - { - name: "pivot-by-category", - section: "breakout", - buttonType: "token", - title: clicked ? ( - t`Category` - ) : ( - <span> - {jt`Break out by ${( - <span className="text-dark">{t`category`}</span> - )}`} - </span> - ), - popover: function PivotDrillPopover({ onChangeCardAndRun, onClose }) { - return ( - <BreakoutPopover - query={query} - breakoutOptions={breakoutOptions} - onChangeBreakout={breakout => { - const nextCard = question.pivot([breakout], dimensions).card(); - onChangeCardAndRun({ nextCard }); - }} - onClose={onClose} - alwaysExpanded - /> - ); - }, - }, - ]; -}; diff --git a/frontend/src/metabase/modes/components/drill/PivotByCategoryDrill.tsx b/frontend/src/metabase/modes/components/drill/PivotByCategoryDrill.tsx new file mode 100644 index 00000000000..7b6749c4b2e --- /dev/null +++ b/frontend/src/metabase/modes/components/drill/PivotByCategoryDrill.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { t, jt } from "ttag"; + +import BreakoutPopover from "metabase/query_builder/components/BreakoutPopover"; + +import type { Card } from "metabase-types/api"; +import { pivotByCategoryDrill } from "metabase-lib/queries/drills/pivot-drill"; + +import type { ClickActionPopoverProps, Drill } from "../../types"; + +const PivotByCategoryDrill: Drill = ({ question, clicked }) => { + const drill = pivotByCategoryDrill({ question, clicked }); + if (!drill) { + return []; + } + + const { query, dimensions, breakoutOptions } = drill; + + const PivotDrillPopover = ({ + onChangeCardAndRun, + onClose, + }: ClickActionPopoverProps) => { + return ( + <BreakoutPopover + query={query} + breakoutOptions={breakoutOptions} + onChangeBreakout={breakout => { + const nextCard = question.pivot([breakout], dimensions).card(); + + // Casting deprecated `metabase-types/Card` to `metabase-types/api/Card` + onChangeCardAndRun({ nextCard: nextCard as Card }); + }} + onClose={onClose} + alwaysExpanded + /> + ); + }; + + return [ + { + name: "pivot-by-category", + title: clicked ? ( + t`Category` + ) : ( + <span> + {jt`Break out by ${( + <span className="text-dark">{t`category`}</span> + )}`} + </span> + ), + section: "breakout", + buttonType: "token", + popover: PivotDrillPopover, + }, + ]; +}; + +export default PivotByCategoryDrill; diff --git a/frontend/src/metabase/modes/components/drill/PivotByLocationDrill.jsx b/frontend/src/metabase/modes/components/drill/PivotByLocationDrill.jsx deleted file mode 100644 index d75feb7f669..00000000000 --- a/frontend/src/metabase/modes/components/drill/PivotByLocationDrill.jsx +++ /dev/null @@ -1,45 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from "react"; -import { t, jt } from "ttag"; -import BreakoutPopover from "metabase/query_builder/components/BreakoutPopover"; -import { pivotByLocationDrill } from "metabase-lib/queries/drills/pivot-drill"; - -export default ({ question, clicked }) => { - const drill = pivotByLocationDrill({ question, clicked }); - if (!drill) { - return []; - } - - const { query, dimensions, breakoutOptions } = drill; - - return [ - { - name: "pivot-by-location", - section: "breakout", - buttonType: "token", - title: clicked ? ( - t`Location` - ) : ( - <span> - {jt`Break out by ${( - <span className="text-dark">{t`location`}</span> - )}`} - </span> - ), - popover: function PivotDrillPopover({ onChangeCardAndRun, onClose }) { - return ( - <BreakoutPopover - query={query} - breakoutOptions={breakoutOptions} - onChangeBreakout={breakout => { - const nextCard = question.pivot([breakout], dimensions).card(); - onChangeCardAndRun({ nextCard }); - }} - onClose={onClose} - alwaysExpanded - /> - ); - }, - }, - ]; -}; diff --git a/frontend/src/metabase/modes/components/drill/PivotByLocationDrill.tsx b/frontend/src/metabase/modes/components/drill/PivotByLocationDrill.tsx new file mode 100644 index 00000000000..815c88d5eaa --- /dev/null +++ b/frontend/src/metabase/modes/components/drill/PivotByLocationDrill.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { t, jt } from "ttag"; + +import BreakoutPopover from "metabase/query_builder/components/BreakoutPopover"; + +import type { Card } from "metabase-types/api"; +import { pivotByLocationDrill } from "metabase-lib/queries/drills/pivot-drill"; + +import type { ClickActionPopoverProps, Drill } from "../../types"; + +const PivotByLocationDrill: Drill = ({ question, clicked }) => { + const drill = pivotByLocationDrill({ question, clicked }); + if (!drill) { + return []; + } + + const { query, dimensions, breakoutOptions } = drill; + + const PivotDrillPopover = ({ + onChangeCardAndRun, + onClose, + }: ClickActionPopoverProps) => { + return ( + <BreakoutPopover + query={query} + breakoutOptions={breakoutOptions} + onChangeBreakout={breakout => { + const nextCard = question.pivot([breakout], dimensions).card(); + + // Casting deprecated `metabase-types/Card` to `metabase-types/api/Card` + onChangeCardAndRun({ nextCard: nextCard as Card }); + }} + onClose={onClose} + alwaysExpanded + /> + ); + }; + + return [ + { + name: "pivot-by-location", + title: clicked ? ( + t`Location` + ) : ( + <span> + {jt`Break out by ${( + <span className="text-dark">{t`location`}</span> + )}`} + </span> + ), + section: "breakout", + buttonType: "token", + popover: PivotDrillPopover, + }, + ]; +}; + +export default PivotByLocationDrill; diff --git a/frontend/src/metabase/modes/components/drill/PivotByTimeDrill.jsx b/frontend/src/metabase/modes/components/drill/PivotByTimeDrill.jsx deleted file mode 100644 index 33ca646c00f..00000000000 --- a/frontend/src/metabase/modes/components/drill/PivotByTimeDrill.jsx +++ /dev/null @@ -1,43 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from "react"; -import { t, jt } from "ttag"; -import BreakoutPopover from "metabase/query_builder/components/BreakoutPopover"; -import { pivotByTimeDrill } from "metabase-lib/queries/drills/pivot-drill"; - -export default ({ question, clicked }) => { - const drill = pivotByTimeDrill({ question, clicked }); - if (!drill) { - return []; - } - - const { query, dimensions, breakoutOptions } = drill; - - return [ - { - name: "pivot-by-time", - section: "breakout", - buttonType: "token", - title: clicked ? ( - t`Time` - ) : ( - <span> - {jt`Break out by ${(<span className="text-dark">{t`time`}</span>)}`} - </span> - ), - popover: function PivotDrillPopover({ onChangeCardAndRun, onClose }) { - return ( - <BreakoutPopover - query={query} - breakoutOptions={breakoutOptions} - onChangeBreakout={breakout => { - const nextCard = question.pivot([breakout], dimensions).card(); - onChangeCardAndRun({ nextCard }); - }} - onClose={onClose} - alwaysExpanded - /> - ); - }, - }, - ]; -}; diff --git a/frontend/src/metabase/modes/components/drill/PivotByTimeDrill.tsx b/frontend/src/metabase/modes/components/drill/PivotByTimeDrill.tsx new file mode 100644 index 00000000000..c4ce6723f6d --- /dev/null +++ b/frontend/src/metabase/modes/components/drill/PivotByTimeDrill.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import { t, jt } from "ttag"; + +import BreakoutPopover from "metabase/query_builder/components/BreakoutPopover"; + +import type { Card } from "metabase-types/api"; +import { pivotByTimeDrill } from "metabase-lib/queries/drills/pivot-drill"; + +import type { ClickActionPopoverProps, Drill } from "../../types"; + +const PivotByTimeDrill: Drill = ({ question, clicked }) => { + const drill = pivotByTimeDrill({ question, clicked }); + if (!drill) { + return []; + } + + const { query, dimensions, breakoutOptions } = drill; + + const PivotDrillPopover = ({ + onChangeCardAndRun, + onClose, + }: ClickActionPopoverProps) => ( + <BreakoutPopover + query={query} + breakoutOptions={breakoutOptions} + onChangeBreakout={breakout => { + const nextCard = question.pivot([breakout], dimensions).card(); + + // Casting deprecated `metabase-types/Card` to `metabase-types/api/card` + onChangeCardAndRun({ nextCard: nextCard as Card }); + }} + onClose={onClose} + alwaysExpanded + /> + ); + + return [ + { + name: "pivot-by-time", + title: clicked ? ( + t`Time` + ) : ( + <span> + {jt`Break out by ${(<span className="text-dark">{t`time`}</span>)}`} + </span> + ), + section: "breakout", + buttonType: "token", + popover: PivotDrillPopover, + }, + ]; +}; + +export default PivotByTimeDrill; diff --git a/frontend/src/metabase/modes/components/drill/QuickFilterDrill.jsx b/frontend/src/metabase/modes/components/drill/QuickFilterDrill.tsx similarity index 69% rename from frontend/src/metabase/modes/components/drill/QuickFilterDrill.jsx rename to frontend/src/metabase/modes/components/drill/QuickFilterDrill.tsx index 6e368900d26..1f4053c17f4 100644 --- a/frontend/src/metabase/modes/components/drill/QuickFilterDrill.jsx +++ b/frontend/src/metabase/modes/components/drill/QuickFilterDrill.tsx @@ -1,23 +1,23 @@ -/* eslint-disable react/prop-types */ import React from "react"; import { quickFilterDrill, quickFilterDrillQuestion, } from "metabase-lib/queries/drills/quick-filter-drill"; +import type { Drill } from "../../types"; -export default function QuickFilterDrill({ question, clicked }) { +const QuickFilterDrill: Drill = ({ question, clicked }) => { const drill = quickFilterDrill({ question, clicked }); if (!drill) { return []; } - const { operators } = drill; - - return operators.map(({ name, operator }) => ({ + return drill.operators.map(({ name, operator }) => ({ name: operator, + title: <span className="h2">{name}</span>, section: "filter", buttonType: "token-filter", - title: <span className="h2">{name}</span>, question: () => quickFilterDrillQuestion({ question, clicked, operator }), })); -} +}; + +export default QuickFilterDrill; diff --git a/frontend/src/metabase/modes/components/drill/SortAction.jsx b/frontend/src/metabase/modes/components/drill/SortDrill.ts similarity index 78% rename from frontend/src/metabase/modes/components/drill/SortAction.jsx rename to frontend/src/metabase/modes/components/drill/SortDrill.ts index ecb174eeeeb..d25cd8afef8 100644 --- a/frontend/src/metabase/modes/components/drill/SortAction.jsx +++ b/frontend/src/metabase/modes/components/drill/SortDrill.ts @@ -3,25 +3,26 @@ import { sortDrill, sortDrillQuestion, } from "metabase-lib/queries/drills/sort-drill"; +import type { ClickActionBase, Drill } from "../../types"; -const ACTIONS = { +const ACTIONS: Record<string, ClickActionBase> = { asc: { name: "sort-ascending", + icon: "arrow_up", section: "sort", buttonType: "sort", - icon: "arrow_up", tooltip: t`Sort ascending`, }, desc: { name: "sort-descending", + icon: "arrow_down", section: "sort", buttonType: "sort", - icon: "arrow_down", tooltip: t`Sort descending`, }, }; -export default ({ question, clicked }) => { +const SortDrill: Drill = ({ question, clicked }) => { const drill = sortDrill({ question, clicked }); if (!drill) { return []; @@ -33,3 +34,5 @@ export default ({ question, clicked }) => { question: () => sortDrillQuestion({ question, clicked, sortDirection }), })); }; + +export default SortDrill; diff --git a/frontend/src/metabase/modes/components/drill/SummarizeColumnByTimeDrill.js b/frontend/src/metabase/modes/components/drill/SummarizeColumnByTimeDrill.ts similarity index 75% rename from frontend/src/metabase/modes/components/drill/SummarizeColumnByTimeDrill.js rename to frontend/src/metabase/modes/components/drill/SummarizeColumnByTimeDrill.ts index 097a99ab765..e41a2d9998c 100644 --- a/frontend/src/metabase/modes/components/drill/SummarizeColumnByTimeDrill.js +++ b/frontend/src/metabase/modes/components/drill/SummarizeColumnByTimeDrill.ts @@ -3,8 +3,9 @@ import { summarizeColumnByTimeDrill, summarizeColumnByTimeDrillQuestion, } from "metabase-lib/queries/drills/summarize-column-by-time-drill"; +import type { Drill } from "../../types"; -export default ({ question, clicked = {} }) => { +const SummarizeColumnByTimeDrill: Drill = ({ question, clicked = {} }) => { if (!summarizeColumnByTimeDrill({ question, clicked })) { return []; } @@ -12,11 +13,13 @@ export default ({ question, clicked = {} }) => { return [ { name: "summarize-by-time", - buttonType: "horizontal", + title: t`Sum over time`, section: "summarize", icon: "line", - title: t`Sum over time`, + buttonType: "horizontal", question: () => summarizeColumnByTimeDrillQuestion({ question, clicked }), }, ]; }; + +export default SummarizeColumnByTimeDrill; diff --git a/frontend/src/metabase/modes/components/drill/SummarizeColumnDrill.js b/frontend/src/metabase/modes/components/drill/SummarizeColumnDrill.js deleted file mode 100644 index 3fc2a687c34..00000000000 --- a/frontend/src/metabase/modes/components/drill/SummarizeColumnDrill.js +++ /dev/null @@ -1,42 +0,0 @@ -import { t } from "ttag"; -import { - summarizeColumnDrill, - summarizeColumnDrillQuestion, -} from "metabase-lib/queries/drills/summarize-column-drill"; - -const ACTIONS = { - sum: { - title: t`Sum`, - section: "sum", - buttonType: "token", - }, - avg: { - title: t`Avg`, - section: "sum", - buttonType: "token", - }, - distinct: { - title: t`Distinct values`, - section: "sum", - buttonType: "token", - }, -}; - -export default ({ question, clicked = {} }) => { - const drill = summarizeColumnDrill({ question, clicked }); - if (!drill) { - return []; - } - - const { aggregationOperators } = drill; - - return aggregationOperators.map(aggregationOperator => ({ - ...ACTIONS[aggregationOperator.short], - name: aggregationOperator.short, - question: () => - summarizeColumnDrillQuestion({ question, clicked, aggregationOperator }), - action: () => dispatch => - // HACK: drill through closes sidebars, so open sidebar asynchronously - setTimeout(() => dispatch({ type: "metabase/qb/EDIT_SUMMARY" })), - })); -}; diff --git a/frontend/src/metabase/modes/components/drill/SummarizeColumnDrill.ts b/frontend/src/metabase/modes/components/drill/SummarizeColumnDrill.ts new file mode 100644 index 00000000000..fe4531c623a --- /dev/null +++ b/frontend/src/metabase/modes/components/drill/SummarizeColumnDrill.ts @@ -0,0 +1,71 @@ +import { t } from "ttag"; + +import { + summarizeColumnDrill, + summarizeColumnDrillQuestion, +} from "metabase-lib/queries/drills/summarize-column-drill"; + +import type { + ClickAction, + ClickActionBase, + Drill, + DrillOptions, +} from "../../types"; + +type AggregationOperator = { + short: string; +}; + +const ACTIONS: Record<string, Omit<ClickActionBase, "name">> = { + sum: { + title: t`Sum`, + section: "sum", + buttonType: "token", + }, + avg: { + title: t`Avg`, + section: "sum", + buttonType: "token", + }, + distinct: { + title: t`Distinct values`, + section: "sum", + buttonType: "token", + }, +}; + +function getAction( + operator: AggregationOperator, + { question, clicked }: DrillOptions, +): ClickAction { + return { + ...ACTIONS[operator.short], + name: operator.short, + question: () => + summarizeColumnDrillQuestion({ + question, + clicked, + aggregationOperator: operator, + }), + action: () => dispatch => + // HACK: drill through closes sidebars, so open sidebar asynchronously + setTimeout(() => dispatch({ type: "metabase/qb/EDIT_SUMMARY" })), + }; +} + +const SummarizeColumnDrill: Drill = (opts: DrillOptions) => { + const { question, clicked } = opts; + + const drill = summarizeColumnDrill({ question, clicked }); + if (!drill) { + return []; + } + + const { aggregationOperators } = drill; + + return aggregationOperators + .filter(operator => operator) + .map(operator => getAction(operator as AggregationOperator, opts)); +}; + +export default SummarizeColumnDrill; diff --git a/frontend/src/metabase/modes/components/drill/UnderlyingRecordsDrill.jsx b/frontend/src/metabase/modes/components/drill/UnderlyingRecordsDrill.ts similarity index 84% rename from frontend/src/metabase/modes/components/drill/UnderlyingRecordsDrill.jsx rename to frontend/src/metabase/modes/components/drill/UnderlyingRecordsDrill.ts index 3ecb08c0e07..851a2a0aa85 100644 --- a/frontend/src/metabase/modes/components/drill/UnderlyingRecordsDrill.jsx +++ b/frontend/src/metabase/modes/components/drill/UnderlyingRecordsDrill.ts @@ -4,8 +4,9 @@ import { underlyingRecordsDrill, underlyingRecordsDrillQuestion, } from "metabase-lib/queries/drills/underlying-records-drill"; +import type { Drill } from "../../types"; -export default ({ question, clicked }) => { +export const UnderlyingRecordsDrill: Drill = ({ question, clicked }) => { const drill = underlyingRecordsDrill({ question, clicked }); if (!drill) { return []; @@ -26,11 +27,13 @@ export default ({ question, clicked }) => { return [ { name: "underlying-records", + title: actionTitle, section: "records", - buttonType: "horizontal", icon: "table_spaced", - title: actionTitle, + buttonType: "horizontal", question: () => underlyingRecordsDrillQuestion({ question, clicked }), }, ]; }; + +export default UnderlyingRecordsDrill; diff --git a/frontend/src/metabase/modes/components/drill/ZoomDrill.jsx b/frontend/src/metabase/modes/components/drill/ZoomDrill.ts similarity index 77% rename from frontend/src/metabase/modes/components/drill/ZoomDrill.jsx rename to frontend/src/metabase/modes/components/drill/ZoomDrill.ts index f524641c6c6..251e81a86a5 100644 --- a/frontend/src/metabase/modes/components/drill/ZoomDrill.jsx +++ b/frontend/src/metabase/modes/components/drill/ZoomDrill.ts @@ -3,8 +3,9 @@ import { zoomDrill, zoomDrillQuestion, } from "metabase-lib/queries/drills/zoom-drill"; +import type { Drill } from "../../types"; -export default ({ question, clicked }) => { +const ZoomDrill: Drill = ({ question, clicked }) => { if (!zoomDrill({ question, clicked })) { return []; } @@ -12,11 +13,13 @@ export default ({ question, clicked }) => { return [ { name: "timeseries-zoom", - section: "zoom", title: t`Zoom in`, - buttonType: "horizontal", + section: "zoom", icon: "zoom_in", + buttonType: "horizontal", question: () => zoomDrillQuestion({ question, clicked }), }, ]; }; + +export default ZoomDrill; diff --git a/frontend/src/metabase/modes/components/modes/DefaultMode.jsx b/frontend/src/metabase/modes/components/modes/DefaultMode.jsx index 991b03ad837..5a3a2328206 100644 --- a/frontend/src/metabase/modes/components/modes/DefaultMode.jsx +++ b/frontend/src/metabase/modes/components/modes/DefaultMode.jsx @@ -1,4 +1,4 @@ -import SortAction from "../drill/SortAction"; +import SortDrill from "../drill/SortDrill"; import ObjectDetailDrill from "../drill/ObjectDetailDrill"; import QuickFilterDrill from "../drill/QuickFilterDrill"; import ForeignKeyDrill from "../drill/ForeignKeyDrill"; @@ -7,14 +7,14 @@ import UnderlyingRecordsDrill from "../drill/UnderlyingRecordsDrill"; import AutomaticDashboardDrill from "../drill/AutomaticDashboardDrill"; import CompareToRestDrill from "../drill/CompareToRestDrill"; import ZoomDrill from "../drill/ZoomDrill"; -import FormatAction from "../drill/FormatAction"; +import FormatDrill from "../drill/FormatDrill"; import DashboardClickDrill from "../drill/DashboardClickDrill"; const DefaultMode = { name: "default", drills: [ ZoomDrill, - SortAction, + SortDrill, ObjectDetailDrill, QuickFilterDrill, ForeignKeyDrill, @@ -22,7 +22,7 @@ const DefaultMode = { UnderlyingRecordsDrill, AutomaticDashboardDrill, CompareToRestDrill, - FormatAction, + FormatDrill, DashboardClickDrill, ], }; diff --git a/frontend/src/metabase/modes/types.ts b/frontend/src/metabase/modes/types.ts new file mode 100644 index 00000000000..f624c2aa66a --- /dev/null +++ b/frontend/src/metabase/modes/types.ts @@ -0,0 +1,99 @@ +import React from "react"; +import type { + DatasetColumn, + RowValue, + Series, + VisualizationSettings, +} from "metabase-types/api"; +import type { Dispatch, ReduxAction } from "metabase-types/store"; +import type { OnChangeCardAndRun } from "metabase/visualizations/types"; +import type Question from "metabase-lib/Question"; + +type DimensionValue = { + value: RowValue; + column: DatasetColumn; +}; + +export type ClickObject = { + value?: RowValue; + column?: DatasetColumn; + dimensions?: DimensionValue[]; + settings?: Record<string, unknown>; + extraData?: Record<string, unknown>; + seriesIndex?: number; + origin?: { + row: RowValue; + cols: DatasetColumn[]; + }; + event?: MouseEvent; + element?: HTMLElement; +}; + +type Dispatcher = (dispatch: Dispatch) => void; + +export type ClickActionPopoverProps = { + series: Series; + onChangeCardAndRun: OnChangeCardAndRun; + onChange: (settings: VisualizationSettings) => void; + onResize: (...args: unknown[]) => void; + onClose: () => void; +}; + +export type ClickActionButtonType = + | "formatting" + | "horizontal" + | "info" + | "token" + | "token-filter" + | "sort"; + +export type ClickActionBase = { + name: string; + title?: React.ReactNode; + section: string; + icon?: string; + buttonType: ClickActionButtonType; + default?: boolean; + tooltip?: string; + extra?: () => Record<string, unknown>; +}; + +type ReduxClickAction = ClickActionBase & { + action: () => ReduxAction | Dispatcher; +}; + +type QuestionChangeClickAction = ClickActionBase & { + question: () => Question; +}; + +type PopoverClickAction = ClickActionBase & { + popoverProps?: Record<string, unknown>; + popover: (props: ClickActionPopoverProps) => JSX.Element; +}; + +type UrlClickAction = ClickActionBase & { + ignoreSiteUrl?: boolean; + url: () => string; +}; + +type RegularClickAction = + | ReduxClickAction + | QuestionChangeClickAction + | PopoverClickAction + | UrlClickAction; + +type AlwaysDefaultClickAction = Omit< + RegularClickAction, + "title" | "section" | "default" | "buttonType" | "tooltip" +> & { + defaultAlways: true; +}; + +export type ClickAction = RegularClickAction | AlwaysDefaultClickAction; + +export type DrillOptions = { + question: Question; + clicked?: ClickObject; +}; + +export type Drill = (options: DrillOptions) => ClickAction[]; diff --git a/frontend/src/metabase/query_builder/components/BreakoutPopover.jsx b/frontend/src/metabase/query_builder/components/BreakoutPopover.tsx similarity index 59% rename from frontend/src/metabase/query_builder/components/BreakoutPopover.jsx rename to frontend/src/metabase/query_builder/components/BreakoutPopover.tsx index 79afac4de53..90893b399a3 100644 --- a/frontend/src/metabase/query_builder/components/BreakoutPopover.jsx +++ b/frontend/src/metabase/query_builder/components/BreakoutPopover.tsx @@ -1,18 +1,35 @@ -/* eslint-disable react/prop-types */ import React from "react"; + +import type Field from "metabase-lib/metadata/Field"; +import type Breakout from "metabase-lib/queries/structured/Breakout"; +import type DimensionOptions from "metabase-lib/DimensionOptions"; +import type StructuredQuery from "metabase-lib/queries/StructuredQuery"; + import { BreakoutFieldList } from "./BreakoutPopover.styled"; +interface BreakoutPopoverProps { + className?: string; + query: StructuredQuery; + breakout?: Breakout; + breakoutOptions: DimensionOptions; + width?: number; + maxHeight?: number; + alwaysExpanded?: boolean; + onChangeBreakout: (breakout: Field) => void; + onClose: () => void; +} + const BreakoutPopover = ({ className, + query, breakout, onChangeBreakout, - query, breakoutOptions, onClose, maxHeight, alwaysExpanded, width = 400, -}) => { +}: BreakoutPopoverProps) => { const table = query.table(); // FieldList requires table if (!table) { @@ -29,7 +46,7 @@ const BreakoutPopover = ({ query={query} metadata={query.metadata()} fieldOptions={fieldOptions} - onFieldChange={field => { + onFieldChange={(field: Field) => { onChangeBreakout(field); if (onClose) { onClose(); diff --git a/frontend/src/metabase/visualizations/types.ts b/frontend/src/metabase/visualizations/types.ts new file mode 100644 index 00000000000..7b20a7bd55a --- /dev/null +++ b/frontend/src/metabase/visualizations/types.ts @@ -0,0 +1,8 @@ +import type { Card } from "metabase-types/api"; + +type OnChangeCardAndRunOpts = { + previousCard?: Card; + nextCard: Card; +}; + +export type OnChangeCardAndRun = (opts: OnChangeCardAndRunOpts) => void; -- GitLab