diff --git a/frontend/src/metabase/query_builder/components/DatasetEditor/ResizableNotebook/ResizableNotebook.jsx b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/DatasetNotebook.jsx similarity index 78% rename from frontend/src/metabase/query_builder/components/DatasetEditor/ResizableNotebook/ResizableNotebook.jsx rename to frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/DatasetNotebook.jsx index 5dbf8f7cbe3346a13b7f78ae8394e1d873cfa06c..d175ea2b6e592ea75dc63b93ddb79f2e45f9f815 100644 --- a/frontend/src/metabase/query_builder/components/DatasetEditor/ResizableNotebook/ResizableNotebook.jsx +++ b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/DatasetNotebook.jsx @@ -8,7 +8,10 @@ import { darken } from "metabase/lib/colors"; import { Notebook } from "metabase/querying/notebook/components/Notebook"; import { Box, Flex, rem } from "metabase/ui"; +import { MetricSidebar } from "./MetricSidebar"; + const propTypes = { + question: PropTypes.object.isRequired, isResizing: PropTypes.bool.isRequired, resizableBoxProps: PropTypes.object.isRequired, onResizeStop: PropTypes.func.isRequired, @@ -50,7 +53,8 @@ const Handle = forwardRef(function Handle(props, ref) { ); }); -function ResizableNotebook({ +export function DatasetNotebook({ + question, isResizing, onResizeStop, resizableBoxProps, @@ -67,13 +71,18 @@ function ResizableNotebook({ onResizeStop(...args); }} > - <Box w="100%" style={{ overflowY: getOverflow(isResizing) }}> - <Notebook {...notebookProps} hasVisualizeButton={false} /> - </Box> + <Flex w="100%" style={{ overflowY: getOverflow(isResizing) }}> + <Box w="100%"> + <Notebook + {...notebookProps} + question={question} + hasVisualizeButton={false} + /> + </Box> + {question.type() === "metric" && <MetricSidebar />} + </Flex> </ResizableBox> ); } -ResizableNotebook.propTypes = propTypes; - -export default ResizableNotebook; +DatasetNotebook.propTypes = propTypes; diff --git a/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/DatasetNotebook.unit.spec.tsx b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/DatasetNotebook.unit.spec.tsx new file mode 100644 index 0000000000000000000000000000000000000000..15e20d07ce0bd93cbaa01b3fec354d03ee95e8ef --- /dev/null +++ b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/DatasetNotebook.unit.spec.tsx @@ -0,0 +1,71 @@ +import type { ResizableBoxProps } from "react-resizable"; + +import { renderWithProviders, screen } from "__support__/ui"; +import Question from "metabase-lib/v1/Question"; +import { createMockCard } from "metabase-types/api/mocks"; + +import { DatasetNotebook } from "./DatasetNotebook"; + +type SetupOpts = { + question: Question; + reportTimezone?: string; + isDirty?: boolean; + isRunnable?: boolean; + isResultDirty?: boolean; + isResizing?: boolean; + resizableBoxProps?: ResizableBoxProps; +}; + +function setup({ + question, + reportTimezone = "UTC", + isDirty = false, + isRunnable = false, + isResultDirty = false, + isResizing = false, + resizableBoxProps = { + axis: "y", + height: 100, + }, +}: SetupOpts) { + const updateQuestion = jest.fn(); + const setQueryBuilderMode = jest.fn(); + const onResizeStop = jest.fn(); + + renderWithProviders( + <DatasetNotebook + question={question} + reportTimezone={reportTimezone} + isDirty={isDirty} + isRunnable={isRunnable} + isResultDirty={isResultDirty} + isResizing={isResizing} + resizableBoxProps={resizableBoxProps} + updateQuestion={updateQuestion} + setQueryBuilderMode={setQueryBuilderMode} + onResizeStop={onResizeStop} + />, + ); +} + +describe("DatasetNotebook", () => { + it("should render a metric docs link for metrics", () => { + const question = new Question(createMockCard({ type: "metric" })); + setup({ question }); + const link = screen.getByRole("link", { name: /Docs/ }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute("target", "_blank"); + expect(link).toHaveAttribute( + "href", + expect.stringContaining("data-modeling/segments-and-metrics"), + ); + }); + + it("should not render a metric docs link for non-metrics", () => { + const question = new Question(createMockCard({ type: "question" })); + setup({ question }); + expect( + screen.queryByRole("link", { name: /Docs/ }), + ).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/MetricSidebar/MetricSidebar.tsx b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/MetricSidebar/MetricSidebar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..720f358f9f2838c7320d38aaa08a8eb427ced8d1 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/MetricSidebar/MetricSidebar.tsx @@ -0,0 +1,32 @@ +import { t } from "ttag"; + +import ExternalLink from "metabase/core/components/ExternalLink"; +import { useSelector } from "metabase/lib/redux"; +import { getDocsUrl } from "metabase/selectors/settings"; +import { getShowMetabaseLinks } from "metabase/selectors/whitelabel"; +import { Box, Button, Icon } from "metabase/ui"; + +export function MetricSidebar() { + const showMetabaseLinks = useSelector(getShowMetabaseLinks); + const docsUrl = useSelector(state => + getDocsUrl(state, { + page: "data-modeling/segments-and-metrics", + anchor: "creating-a-metric", + }), + ); + + return ( + <Box pt="md" pr={{ sm: "sm", lg: "md" }}> + {showMetabaseLinks && ( + <Button + component={ExternalLink} + href={docsUrl} + variant="subtle" + rightIcon={<Icon name="external" size={16} />} + > + {t`Docs`} + </Button> + )} + </Box> + ); +} diff --git a/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/MetricSidebar/index.ts b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/MetricSidebar/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f4f5e14386adc509c5b06ce8a9fc219a3fd911fc --- /dev/null +++ b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/MetricSidebar/index.ts @@ -0,0 +1 @@ +export * from "./MetricSidebar"; diff --git a/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/MetricSidebar/tests/common.unit.spec.tsx b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/MetricSidebar/tests/common.unit.spec.tsx new file mode 100644 index 0000000000000000000000000000000000000000..94aa8a3f82e6a82757d7d5b6d1bd62987d8607c0 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/MetricSidebar/tests/common.unit.spec.tsx @@ -0,0 +1,16 @@ +import { screen } from "__support__/ui"; + +import { setup } from "./setup"; + +describe("MetricSidebar (OSS)", () => { + it("should render the metric docs link", () => { + setup(); + const link = screen.getByRole("link", { name: /Docs/ }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute("target", "_blank"); + expect(link).toHaveAttribute( + "href", + expect.stringContaining("data-modeling/segments-and-metrics"), + ); + }); +}); diff --git a/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/MetricSidebar/tests/enterprise.unit.spec.tsx b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/MetricSidebar/tests/enterprise.unit.spec.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6f31d75d4fe63cac80b551bbf15daeb1ba524d0b --- /dev/null +++ b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/MetricSidebar/tests/enterprise.unit.spec.tsx @@ -0,0 +1,19 @@ +import { screen } from "__support__/ui"; + +import { type SetupOpts, setup as baseSetup } from "./setup"; + +function setup(opts: SetupOpts = {}) { + baseSetup({ hasEnterprisePlugins: true, ...opts }); +} + +describe("MetricSidebar (EE without a token)", () => { + it("should render the metric docs link by default", () => { + setup({ showMetabaseLinks: true }); + expect(screen.getByRole("link", { name: /Docs/ })).toBeInTheDocument(); + }); + + it("should render the metric docs link even if the setting is turned off", () => { + setup({ showMetabaseLinks: false }); + expect(screen.getByRole("link", { name: /Docs/ })).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/MetricSidebar/tests/setup.tsx b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/MetricSidebar/tests/setup.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a67c1a22b8029b51b3b56ed92498776504f4ef5b --- /dev/null +++ b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/MetricSidebar/tests/setup.tsx @@ -0,0 +1,33 @@ +import { setupEnterprisePlugins } from "__support__/enterprise"; +import { mockSettings } from "__support__/settings"; +import { renderWithProviders } from "__support__/ui"; +import type { TokenFeatures } from "metabase-types/api"; +import { createMockTokenFeatures } from "metabase-types/api/mocks"; +import { createMockState } from "metabase-types/store/mocks"; + +import { MetricSidebar } from "../MetricSidebar"; + +export type SetupOpts = { + tokenFeatures?: Partial<TokenFeatures>; + showMetabaseLinks?: boolean; + hasEnterprisePlugins?: boolean; +}; + +export function setup({ + tokenFeatures, + showMetabaseLinks, + hasEnterprisePlugins, +}: SetupOpts = {}) { + const state = createMockState({ + settings: mockSettings({ + "show-metabase-links": showMetabaseLinks, + "token-features": createMockTokenFeatures(tokenFeatures), + }), + }); + + if (hasEnterprisePlugins) { + setupEnterprisePlugins(); + } + + renderWithProviders(<MetricSidebar />, { storeInitialState: state }); +} diff --git a/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/MetricSidebar/tests/whitelabel.unit.spec.tsx b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/MetricSidebar/tests/whitelabel.unit.spec.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a6c50309e7894040739b6b24c8459b8ab503d266 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/MetricSidebar/tests/whitelabel.unit.spec.tsx @@ -0,0 +1,25 @@ +import { screen } from "__support__/ui"; + +import { type SetupOpts, setup as baseSetup } from "./setup"; + +function setup(opts: SetupOpts = {}) { + baseSetup({ + tokenFeatures: { whitelabel: true }, + hasEnterprisePlugins: true, + ...opts, + }); +} + +describe("MetricSidebar (EE with a whitelabel token)", () => { + it("should render the metric docs link by default", () => { + setup({ showMetabaseLinks: true }); + expect(screen.getByRole("link", { name: /Docs/ })).toBeInTheDocument(); + }); + + it("should not render the metric docs link when the setting is turned off", () => { + setup({ showMetabaseLinks: false }); + expect( + screen.queryByRole("link", { name: /Docs/ }), + ).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/index.ts b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..fa20edb296629c50eb87421542d5a014062441c8 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetNotebook/index.ts @@ -0,0 +1 @@ +export * from "./DatasetNotebook"; diff --git a/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetQueryEditor.jsx b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetQueryEditor.jsx index aafb7760e772e712137791e30ff726fddaee2204..801b904cfe3557995c93d142cdbe9d48003039bd 100644 --- a/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetQueryEditor.jsx +++ b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetQueryEditor.jsx @@ -6,7 +6,7 @@ import { isReducedMotionPreferred } from "metabase/lib/dom"; import NativeQueryEditor from "metabase/query_builder/components/NativeQueryEditor"; import * as Lib from "metabase-lib"; -import ResizableNotebook from "./ResizableNotebook"; +import { DatasetNotebook } from "./DatasetNotebook"; const QueryEditorContainer = styled.div` visibility: ${props => (props.isActive ? "visible" : "hidden")}; @@ -83,7 +83,7 @@ function DatasetQueryEditor({ onSetDatabaseId={onSetDatabaseId} /> ) : ( - <ResizableNotebook + <DatasetNotebook {...props} question={question} isResizing={isResizing} diff --git a/frontend/src/metabase/query_builder/components/DatasetEditor/ResizableNotebook/index.js b/frontend/src/metabase/query_builder/components/DatasetEditor/ResizableNotebook/index.js deleted file mode 100644 index ab9a6783494f11cc6ca938220840a7621070da2b..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/query_builder/components/DatasetEditor/ResizableNotebook/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./ResizableNotebook";