Skip to content
Snippets Groups Projects
Unverified Commit 97b47047 authored by Alexander Polyankin's avatar Alexander Polyankin Committed by GitHub
Browse files

Extract more drills to metabase-lib (#25802)

parent 4bfbc794
No related branches found
No related tags found
No related merge requests found
Showing
with 541 additions and 178 deletions
import { isExpressionField } from "metabase-lib/lib/queries/utils";
export function automaticDashboardDrill({ question, clicked, enableXrays }) {
const query = question.query();
if (!question.isStructured() || !query.isEditable()) {
return false;
}
// questions with a breakout
const dimensions = (clicked && clicked.dimensions) || [];
// ExpressionDimensions don't work right now (see metabase#16680)
const includesExpressionDimensions = dimensions.some(dimension => {
return isExpressionField(dimension.column.field_ref);
});
const isUnsupportedDrill =
!clicked ||
dimensions.length === 0 ||
!enableXrays ||
includesExpressionDimensions;
return !isUnsupportedDrill;
}
export function automaticDashboardDrillUrl({ question, clicked }) {
const query = question.query();
const dimensions = (clicked && clicked.dimensions) || [];
const filters = query
.clearFilters() // clear existing filters so we don't duplicate them
.question()
.drillUnderlyingRecords(dimensions)
.query()
.filters();
return question.getAutomaticDashboardUrl(filters);
}
import { isa, TYPE } from "metabase/lib/types";
const INVALID_TYPES = [TYPE.Structured];
export function columnFilterDrill({ question, clicked }) {
const query = question.query();
if (
!question.isStructured() ||
!query.isEditable() ||
!clicked ||
!clicked.column ||
INVALID_TYPES.some(type => isa(clicked.column.base_type, type)) ||
clicked.column.field_ref == null ||
clicked.value !== undefined
) {
return null;
}
const { dimension } = clicked;
const initialFilter = dimension.defaultFilterForDimension();
return { query, initialFilter };
}
import { isExpressionField } from "metabase-lib/lib/queries/utils";
export function compareToRestDrill({ question, clicked, enableXrays }) {
const query = question.query();
if (!question.isStructured() || !query.isEditable()) {
return false;
}
// questions with a breakout
const dimensions = (clicked && clicked.dimensions) || [];
// ExpressionDimensions don't work right now (see metabase#16680)
const includesExpressionDimensions = dimensions.some(dimension => {
return isExpressionField(dimension.column.field_ref);
});
const isUnsupportedDrill =
!clicked ||
dimensions.length === 0 ||
// xrays must be enabled for this to work
!enableXrays ||
includesExpressionDimensions;
return !isUnsupportedDrill;
}
export function compareToRestDrillUrl({ question, clicked }) {
const query = question.query();
const dimensions = (clicked && clicked.dimensions) || [];
const filters = query
.clearFilters() // clear existing filters so we don't duplicate them
.question()
.drillUnderlyingRecords(dimensions)
.query()
.filters();
return question.getComparisonDashboardUrl(filters);
}
import { isa, TYPE } from "metabase/lib/types";
const DENYLIST_TYPES = [
TYPE.PK,
TYPE.SerializedJSON,
TYPE.Description,
TYPE.Comment,
];
export function distributionDrill({ question, clicked }) {
return !(
!clicked ||
!clicked.column ||
clicked.value !== undefined ||
DENYLIST_TYPES.some(t => isa(clicked.column.semantic_type, t)) ||
!question.query().isEditable()
);
}
export function distributionDrillQuestion({ question, clicked }) {
const { column } = clicked;
return question.distribution(column);
}
import { isFK, isPK } from "metabase/lib/types";
import { stripId } from "metabase/lib/formatting/strings";
export function foreignKeyDrill({ question, clicked }) {
const query = question.query();
if (
!question.isStructured() ||
!query.isEditable() ||
!clicked ||
!clicked.column ||
clicked.value === undefined
) {
return null;
}
const { column } = clicked;
if (isPK(column.semantic_type) || !isFK(column.semantic_type)) {
return null;
}
const columnName = stripId(column.display_name);
const tableName = query.table().display_name;
return { columnName, tableName };
}
export function foreignKeyDrillQuestion({ question, clicked }) {
const { column, value } = clicked;
return question.filter("=", column, value);
}
import { isAddress, isCategory, isDate } from "metabase/lib/schema_metadata";
function pivotDrill({ question, clicked, fieldFilter }) {
const query = question.query();
if (!question.isStructured() || !query.isEditable()) {
return null;
}
if (
clicked &&
(clicked.value === undefined || clicked.column.source !== "aggregation")
) {
return null;
}
const breakoutOptions = query.breakoutOptions(null, fieldFilter);
if (breakoutOptions.count === 0) {
return null;
}
const dimensions = (clicked && clicked.dimensions) || [];
return { query, dimensions, breakoutOptions };
}
export function pivotByTimeDrill({ question, clicked }) {
const fieldFilter = field => isDate(field);
return pivotDrill({ question, clicked, fieldFilter });
}
export function pivotByLocationDrill({ question, clicked }) {
const fieldFilter = field => isAddress(field);
return pivotDrill({ question, clicked, fieldFilter });
}
export function pivotByCategoryDrill({ question, clicked }) {
const fieldFilter = field => isCategory(field) && !isAddress(field);
return pivotDrill({ question, clicked, fieldFilter });
}
import { isa, isFK, isPK, TYPE } from "metabase/lib/types";
import { isDate, isNumeric } from "metabase/lib/schema_metadata";
import { isLocalField } from "metabase-lib/lib/queries/utils";
const INVALID_TYPES = [TYPE.Structured];
export function quickFilterDrill({ question, clicked }) {
const query = question.query();
if (
!question.isStructured() ||
!query.isEditable() ||
!clicked ||
!clicked.column ||
clicked.value === undefined
) {
return null;
}
const { column } = clicked;
if (isPK(column.semantic_type) || isFK(column.semantic_type)) {
return null;
}
const operators = getOperatorsForColumn(column);
return { operators };
}
export function quickFilterDrillQuestion({ question, clicked, operator }) {
const { column, value } = clicked;
if (isLocalField(column.field_ref)) {
return question.filter(operator, column, value);
}
/**
* For aggregated and custom columns
* with field refs like ["aggregation", 0],
* we need to nest the query as filters like ["=", ["aggregation", 0], value] won't work
*
* So the query like
* {
* aggregations: [["count"]]
* source-table: 2,
* }
*
* Becomes
* {
* source-query: {
* aggregations: [["count"]]
* source-table: 2,
* },
* filter: ["=", [ "field", "count", {"base-type": "type/BigInteger"} ], value]
* }
*/
const nestedQuestion = question.query().nest().question();
return nestedQuestion.filter(
operator,
{
...column,
field_ref: getFieldLiteralFromColumn(column),
},
value,
);
}
function getOperatorsForColumn(column) {
if (isNumeric(column) || isDate(column)) {
return [
{ name: "<", operator: "<" },
{ name: ">", operator: ">" },
{ name: "=", operator: "=" },
{ name: "", operator: "!=" },
];
} else if (!INVALID_TYPES.some(type => isa(column.base_type, type))) {
return [
{ name: "=", operator: "=" },
{ name: "", operator: "!=" },
];
} else {
return [];
}
}
function getFieldLiteralFromColumn(column) {
return ["field", column.name, { "base-type": column.base_type }];
}
import { isa, TYPE } from "metabase/lib/types";
import Dimension from "metabase-lib/lib/Dimension";
const INVALID_TYPES = [TYPE.Structured];
export function sortDrill({ question, clicked }) {
const query = question.query();
if (!question.isStructured() || !query.isEditable()) {
return null;
}
if (
!clicked ||
!clicked.column ||
clicked.value !== undefined ||
INVALID_TYPES.some(type => isa(clicked.column.base_type, type)) ||
!clicked.column.source
) {
return null;
}
const { column } = clicked;
const fieldRef = query.fieldReferenceForColumn(column);
if (!fieldRef) {
return null;
}
const sorts = query.sorts();
const [sortDirection, sortFieldRef] = sorts[0] ?? [];
const isAlreadySorted =
sortFieldRef != null && Dimension.isEqual(fieldRef, sortFieldRef);
const sortDirections = [];
if (!isAlreadySorted || sortDirection === "asc") {
sortDirections.push("desc");
}
if (!isAlreadySorted || sortDirection === "desc") {
sortDirections.push("asc");
}
return {
sortDirections,
};
}
export function sortDrillQuestion({ question, clicked, sortDirection }) {
const { column } = clicked;
const query = question.query();
const fieldRef = query.fieldReferenceForColumn(column);
return query.replaceSort([sortDirection, fieldRef]).question();
}
import {
getAggregationOperator,
isCompatibleAggregationOperatorForField,
} from "metabase/lib/schema_metadata";
import { fieldRefForColumn } from "metabase-lib/lib/queries/utils/dataset";
export function summarizeColumnByTimeDrill({ question, clicked }) {
const { column, value } = clicked;
const query = question.query();
const isStructured = question.isStructured();
if (!column || value !== undefined || !isStructured || !query.isEditable()) {
return false;
}
const dimensionOptions = query.dimensionOptions(d => d.field().isDate());
const dateDimension = dimensionOptions.all()[0];
if (!dateDimension) {
return false;
}
const aggregator = getAggregationOperator("sum");
return isCompatibleAggregationOperatorForField(aggregator, column);
}
export function summarizeColumnByTimeDrillQuestion({ question, clicked }) {
const { column } = clicked;
const query = question.query();
const dimensionOptions = query.dimensionOptions(d => d.field().isDate());
const dateDimension = dimensionOptions.all()[0];
return question
.aggregate(["sum", fieldRefForColumn(column)])
.pivot([dateDimension.defaultBreakout()]);
}
......@@ -10,7 +10,12 @@ const AGGREGATIONS = ["sum", "avg", "distinct"];
const INVALID_TYPES = [TYPE.Structured];
export function summarizeColumnDrill({ question, clicked }) {
if (!clicked) {
return null;
}
const { column, value } = clicked;
if (
!column ||
value !== undefined ||
......
......@@ -2,16 +2,12 @@ import { drillDownForDimensions } from "metabase-lib/lib/queries/utils/drilldown
export function zoomDrill({ question, clicked }) {
if (!question.query().isEditable()) {
return null;
return false;
}
const dimensions = (clicked && clicked.dimensions) || [];
const dimensions = clicked?.dimensions ?? [];
const drilldown = drillDownForDimensions(dimensions, question.metadata());
if (!drilldown) {
return null;
}
return true;
return drilldown != null;
}
export function zoomDrillQuestion({ question, clicked }) {
......
import { t } from "ttag";
import MetabaseSettings from "metabase/lib/settings";
import { isExpressionField } from "metabase-lib/lib/queries/utils/field-ref";
import {
automaticDashboardDrill,
automaticDashboardDrillUrl,
} from "metabase-lib/lib/queries/drills/automatic-dashboard-drill";
export default ({ question, clicked }) => {
const query = question.query();
if (!question.isStructured() || !query.isEditable()) {
return [];
}
// questions with a breakout
const dimensions = (clicked && clicked.dimensions) || [];
// ExpressionDimensions don't work right now (see metabase#16680)
const includesExpressionDimensions = dimensions.some(dimension => {
return isExpressionField(dimension.column.field_ref);
});
const isUnsupportedDrill =
!clicked ||
dimensions.length === 0 ||
!MetabaseSettings.get("enable-xrays") ||
includesExpressionDimensions;
if (isUnsupportedDrill) {
const enableXrays = MetabaseSettings.get("enable-xrays");
if (!automaticDashboardDrill({ question, clicked, enableXrays })) {
return [];
}
......@@ -34,15 +18,7 @@ export default ({ question, clicked }) => {
icon: "bolt",
buttonType: "token",
title: t`X-ray`,
url: () => {
const filters = query
.clearFilters() // clear existing filters so we don't duplicate them
.question()
.drillUnderlyingRecords(dimensions)
.query()
.filters();
return question.getAutomaticDashboardUrl(filters);
},
url: () => automaticDashboardDrillUrl({ question, clicked }),
},
];
};
/* eslint-disable react/prop-types */
import React from "react";
import { t } from "ttag";
import { TYPE, isa } from "metabase/lib/types";
import FilterPopover from "metabase/query_builder/components/filters/FilterPopover";
const INVALID_TYPES = [TYPE.Structured];
import { columnFilterDrill } from "metabase-lib/lib/queries/drills/column-filter-drill";
export default function ColumnFilterDrill({ question, clicked }) {
const query = question.query();
if (
!question.isStructured() ||
!query.isEditable() ||
!clicked ||
!clicked.column ||
INVALID_TYPES.some(type => isa(clicked.column.base_type, type)) ||
clicked.column.field_ref == null ||
clicked.value !== undefined
) {
const drill = columnFilterDrill({ question, clicked });
if (!drill) {
return [];
}
const { dimension } = clicked;
const initialFilter = dimension.defaultFilterForDimension();
const { query, initialFilter } = drill;
return [
{
......
import { t } from "ttag";
import MetabaseSettings from "metabase/lib/settings";
import { isExpressionField } from "metabase-lib/lib/queries/utils/field-ref";
import {
compareToRestDrill,
compareToRestDrillUrl,
} from "metabase-lib/lib/queries/drills/compare-to-rest-drill";
export default ({ question, clicked }) => {
const query = question.query();
if (!question.isStructured() || !query.isEditable()) {
return [];
}
// questions with a breakout
const dimensions = (clicked && clicked.dimensions) || [];
// ExpressionDimensions don't work right now (see metabase#16680)
const includesExpressionDimensions = dimensions.some(dimension => {
return isExpressionField(dimension.column.field_ref);
});
const isUnsupportedDrill =
!clicked ||
dimensions.length === 0 ||
// xrays must be enabled for this to work
!MetabaseSettings.get("enable-xrays") ||
includesExpressionDimensions;
if (isUnsupportedDrill) {
const enableXrays = MetabaseSettings.get("enable-xrays");
if (!compareToRestDrill({ question, clicked, enableXrays })) {
return [];
}
......@@ -35,15 +19,7 @@ export default ({ question, clicked }) => {
icon: "bolt",
buttonType: "token",
title: t`Compare to the rest`,
url: () => {
const filters = query
.clearFilters() // clear existing filters so we don't duplicate them
.question()
.drillUnderlyingRecords(dimensions)
.query()
.filters();
return question.getComparisonDashboardUrl(filters);
},
url: () => compareToRestDrillUrl({ question, clicked }),
},
];
};
/* eslint-disable react/prop-types */
import { t } from "ttag";
import { TYPE, isa } from "metabase/lib/types";
const DENYLIST_TYPES = [
TYPE.PK,
TYPE.SerializedJSON,
TYPE.Description,
TYPE.Comment,
];
import {
distributionDrill,
distributionDrillQuestion,
} from "metabase-lib/lib/queries/drills/distribution-drill";
export default ({ question, clicked }) => {
if (
!clicked ||
!clicked.column ||
clicked.value !== undefined ||
DENYLIST_TYPES.some(t => isa(clicked.column.semantic_type, t)) ||
!question.query().isEditable()
) {
if (!distributionDrill({ question, clicked })) {
return [];
}
const { column } = clicked;
return [
{
......@@ -28,7 +16,7 @@ export default ({ question, clicked }) => {
buttonType: "horizontal",
section: "summarize",
icon: "bar",
question: () => question.distribution(column),
question: () => distributionDrillQuestion({ question, clicked }),
},
];
};
import { t } from "ttag";
import { pluralize, singularize } from "metabase/lib/formatting/strings";
import {
foreignKeyDrill,
foreignKeyDrillQuestion,
} from "metabase-lib/lib/queries/drills/foreign-key-drill";
export default function ForeignKeyDrill({ question, clicked }) {
const drill = foreignKeyDrill({ question, clicked });
if (!drill) {
return [];
}
const { columnName, tableName } = drill;
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 }),
};
}
import { t } from "ttag";
/* 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/lib/queries/drills/pivot-drill";
import { isCategory, isAddress } from "metabase/lib/schema_metadata";
export default ({ question, clicked }) => {
const drill = pivotByCategoryDrill({ question, clicked });
if (!drill) {
return [];
}
import PivotByDrill from "./PivotByDrill";
const { query, dimensions, breakoutOptions } = drill;
export default PivotByDrill(
t`Category`,
"label",
field => isCategory(field) && !isAddress(field),
);
return [
{
name: "pivot-by-category",
section: "breakout",
buttonType: "token",
title: (
<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
/>
);
},
},
];
};
/* eslint-disable react/prop-types */
import React from "react";
import { jt } from "ttag";
import BreakoutPopover from "metabase/query_builder/components/BreakoutPopover";
// PivotByDrill displays a breakout picker, and optionally filters by the
// clicked dimension values (and removes corresponding breakouts)
export default (name, icon, fieldFilter) =>
({ question, clicked }) => {
const query = question.query();
if (!question.isStructured() || !query.isEditable()) {
return [];
}
// Click target types: metric value
if (
clicked &&
(clicked.value === undefined || clicked.column.source !== "aggregation")
) {
return [];
}
const dimensions = (clicked && clicked.dimensions) || [];
const breakoutOptions = query.breakoutOptions(null, fieldFilter);
if (breakoutOptions.count === 0) {
return [];
}
return [
{
name: "pivot-by-" + name.toLowerCase(),
section: "breakout",
buttonType: "token",
title: clicked ? (
name
) : (
<span>
{jt`Break out by ${(
<span className="text-dark">{name.toLowerCase()}</span>
)}`}
</span>
),
// eslint-disable-next-line react/display-name
popover: ({ onChangeCardAndRun, onClose }) => (
<BreakoutPopover
query={query}
breakoutOptions={breakoutOptions}
onChangeBreakout={breakout => {
const nextCard = question.pivot([breakout], dimensions).card();
if (nextCard) {
onChangeCardAndRun({ nextCard });
}
}}
onClose={onClose}
alwaysExpanded
/>
),
},
];
};
import { t } from "ttag";
/* 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/lib/queries/drills/pivot-drill";
import { isAddress } from "metabase/lib/schema_metadata";
export default ({ question, clicked }) => {
const drill = pivotByLocationDrill({ question, clicked });
if (!drill) {
return [];
}
import PivotByDrill from "./PivotByDrill";
const { query, dimensions, breakoutOptions } = drill;
export default PivotByDrill(t`Location`, "location", field => isAddress(field));
return [
{
name: "pivot-by-location",
section: "breakout",
buttonType: "token",
title: (
<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
/>
);
},
},
];
};
import { t } from "ttag";
/* 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/lib/queries/drills/pivot-drill";
import { isDate } from "metabase/lib/schema_metadata";
export default ({ question, clicked }) => {
const drill = pivotByTimeDrill({ question, clicked });
if (!drill) {
return [];
}
import PivotByDrill from "./PivotByDrill";
const { query, dimensions, breakoutOptions } = drill;
export default PivotByDrill(t`Time`, "clock", field => isDate(field));
return [
{
name: "pivot-by-time",
section: "breakout",
buttonType: "token",
title: (
<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
/>
);
},
},
];
};
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