From 21fb561ea5ac830ea24cc0fe0fa9587f4769edc4 Mon Sep 17 00:00:00 2001 From: Romeo Van Snick <romeo@romeovansnick.be> Date: Tue, 1 Oct 2024 18:20:42 +0200 Subject: [PATCH] List metrics in native editor data reference (#48191) * Add metrics to table pane * Add test for metric in data reference sidebar * Clean up other reference tests * Add test for when the table has no metrics * Use helper for sidebar header title * Rename sidebar to create distinction from existing helper --- e2e/test/scenarios/native/native.cy.spec.js | 130 ++++++++++++------ .../components/dataref/TablePane.tsx | 113 ++++++++++----- 2 files changed, 166 insertions(+), 77 deletions(-) diff --git a/e2e/test/scenarios/native/native.cy.spec.js b/e2e/test/scenarios/native/native.cy.spec.js index 09360b37e78..4bf4812deda 100644 --- a/e2e/test/scenarios/native/native.cy.spec.js +++ b/e2e/test/scenarios/native/native.cy.spec.js @@ -3,8 +3,10 @@ import { USER_GROUPS, WRITABLE_DB_ID, } from "e2e/support/cypress_data"; +import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database"; import { THIRD_COLLECTION_ID } from "e2e/support/cypress_sample_instance_data"; import { + createQuestion, entityPickerModal, filter, filterField, @@ -19,6 +21,19 @@ import { visitQuestionAdhoc, } from "e2e/support/helpers"; +const { ORDERS_ID } = SAMPLE_DATABASE; + +const ORDERS_SCALAR_METRIC = { + name: "Count of orders", + type: "metric", + description: "A metric", + query: { + "source-table": ORDERS_ID, + aggregation: [["count"]], + }, + display: "scalar", +}; + describe("scenarios > question > native", () => { beforeEach(() => { cy.intercept("POST", "api/card").as("card"); @@ -495,32 +510,25 @@ describe("scenarios > native question > data reference sidebar", () => { it("should show tables", () => { openNativeEditor(); - cy.icon("reference").click(); - cy.get("[data-testid='sidebar-header-title']").findByText( - "Sample Database", - ); - // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText("ORDERS").click(); - // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText( - "Confirmed Sample Company orders for a product, from a user.", - ); - // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText("9 columns"); - // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText("QUANTITY").click(); - // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText("Number of products bought."); - // clicking the title should navigate back - // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText("QUANTITY").click(); - // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText("ORDERS").click(); - cy.get("[data-testid='sidebar-header-title']") - .findByText("Sample Database") - .click(); - // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText("Data Reference"); + referenceButton().click(); + + sidebarHeaderTitle().should("have.text", "Sample Database"); + + dataReferenceSidebar().within(() => { + cy.findByText("ORDERS").click(); + cy.findByText( + "Confirmed Sample Company orders for a product, from a user.", + ); + cy.findByText("9 columns"); + cy.findByText("QUANTITY").click(); + cy.findByText("Number of products bought."); + + cy.log("clicking the title should navigate back"); + cy.findByText("QUANTITY").click(); + cy.findByText("ORDERS").click(); + sidebarHeaderTitle().findByText("Sample Database").click(); + cy.findByText("Data Reference"); + }); }); it("should show models", () => { @@ -544,24 +552,64 @@ describe("scenarios > native question > data reference sidebar", () => { }); openNativeEditor(); - cy.icon("reference").click(); - // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText("2 models"); - // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText("Native Products Model").click(); - // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText("A model of the Products table"); // description - // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText("Bobby Tables's Personal Collection"); // collection - // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText("1 column"); - // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText("RENAMED_ID").click(); - // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText("No description"); + referenceButton().click(); + + dataReferenceSidebar().within(() => { + cy.findByText("2 models"); + cy.findByText("Native Products Model").click(); + cy.findByText("A model of the Products table"); // description + cy.findByText("Bobby Tables's Personal Collection"); // collection + cy.findByText("1 column"); + cy.findByText("RENAMED_ID").click(); + cy.findByText("No description"); + }); + }); + + describe("metrics", () => { + it("should not show metrics when they are not defined on the selected table", () => { + openNativeEditor(); + referenceButton().click(); + sidebarHeaderTitle().should("have.text", "Sample Database"); + + dataReferenceSidebar().within(() => { + cy.findByText("ORDERS").click(); + cy.findByText(/metric/).should("not.exist"); + }); + }); + + it("should show metrics defined on tables", () => { + createQuestion(ORDERS_SCALAR_METRIC); + + openNativeEditor(); + referenceButton().click(); + sidebarHeaderTitle().should("have.text", "Sample Database"); + + dataReferenceSidebar().within(() => { + cy.findByText("ORDERS").click(); + cy.findByText("1 metric").should("be.visible"); + + cy.findByText("Count of orders").should("be.visible").click(); + cy.findByText("A metric").should("be.visible"); + + cy.log("clicking the title should navigate back"); + cy.findByText("Count of orders").should("be.visible").click(); + }); + }); }); }); +function referenceButton() { + return cy.icon("reference"); +} + +function sidebarHeaderTitle() { + return cy.findByTestId("sidebar-header-title"); +} + +function dataReferenceSidebar() { + return cy.findByTestId("sidebar-right"); +} + const runQuery = () => { cy.findByTestId("native-query-editor-container").within(() => { cy.button("Get Answer").click(); diff --git a/frontend/src/metabase/query_builder/components/dataref/TablePane.tsx b/frontend/src/metabase/query_builder/components/dataref/TablePane.tsx index 6aaac01275a..55bd55c136a 100644 --- a/frontend/src/metabase/query_builder/components/dataref/TablePane.tsx +++ b/frontend/src/metabase/query_builder/components/dataref/TablePane.tsx @@ -1,5 +1,5 @@ import { connect } from "react-redux"; -import { t } from "ttag"; +import { msgid, ngettext, t } from "ttag"; import _ from "underscore"; import { @@ -14,6 +14,15 @@ import type Table from "metabase-lib/v1/metadata/Table"; import type { State } from "metabase-types/store"; import FieldList from "./FieldList"; +import { + NodeListIcon, + NodeListItemIcon, + NodeListItemLink, + NodeListItemName, + NodeListTitle, + NodeListTitleText, + QuestionId, +} from "./NodeList.styled"; import { PaneContent } from "./Pane.styled"; import TableInfoLoader from "./TableInfoLoader"; @@ -28,42 +37,74 @@ const mapStateToProps = (state: State, props: TablePaneProps) => ({ table: Tables.selectors.getObject(state, { entityId: props.table.id }), }); -const TablePane = ({ table, onItemClick, onBack, onClose }: TablePaneProps) => ( - <SidebarContent - title={table.name} - icon={"table"} - onBack={onBack} - onClose={onClose} - > - <PaneContent> - <TableInfoLoader table={table}> - <div className={CS.ml1}> - {table.description ? ( - <Description>{table.description}</Description> - ) : ( - <EmptyDescription>{t`No description`}</EmptyDescription> - )} - </div> - <div className={CS.my2}> - {table.fields?.length ? ( - <> - <FieldList - fields={table.fields} - onFieldClick={f => onItemClick("field", f)} - /> - {table.connectedTables() && ( - <ConnectedTableList - tables={table.connectedTables()} - onTableClick={t => onItemClick("table", t)} +function TablePane({ table, onItemClick, onBack, onClose }: TablePaneProps) { + return ( + <SidebarContent + title={table.name} + icon={"table"} + onBack={onBack} + onClose={onClose} + > + <PaneContent> + <TableInfoLoader table={table}> + <div className={CS.ml1}> + {table.description ? ( + <Description>{table.description}</Description> + ) : ( + <EmptyDescription>{t`No description`}</EmptyDescription> + )} + </div> + <div className={CS.my2}> + {table.fields?.length ? ( + <> + <FieldList + fields={table.fields} + onFieldClick={f => onItemClick("field", f)} /> - )} - </> - ) : null} - </div> - </TableInfoLoader> - </PaneContent> - </SidebarContent> -); + {table.connectedTables() && ( + <ConnectedTableList + tables={table.connectedTables()} + onTableClick={t => onItemClick("table", t)} + /> + )} + </> + ) : null} + {table.metrics?.length ? ( + <> + <NodeListTitle> + <NodeListIcon name="metric" /> + <NodeListTitleText> + {ngettext( + msgid`${table.metrics.length} metric`, + `${table.metrics.length} metrics`, + table.metrics.length, + )} + </NodeListTitleText> + </NodeListTitle> + <ul> + {table.metrics?.map(metric => ( + <li key={metric.card().id}> + <NodeListItemLink + onClick={() => onItemClick("question", metric.card())} + > + <NodeListItemIcon name="metric" /> + <NodeListItemName> + {metric.card().name} + </NodeListItemName> + <QuestionId>{`#${metric.id()}`}</QuestionId> + </NodeListItemLink> + </li> + ))} + </ul> + <br></br> + </> + ) : null} + </div> + </TableInfoLoader> + </PaneContent> + </SidebarContent> + ); +} // eslint-disable-next-line import/no-default-export -- deprecated usage export default _.compose( -- GitLab