From 004ee0abd9e42762a1975f70edf0812fc4ebb3bf Mon Sep 17 00:00:00 2001 From: Alexander Polyankin <alexander.polyankin@metabase.com> Date: Mon, 17 Jul 2023 11:48:40 +0300 Subject: [PATCH] Do not show edit model menu options without write permissions (#32261) --- .../metabase-enterprise/moderation/index.js | 40 ++++---- frontend/src/metabase/plugins/index.ts | 2 +- .../components/QuestionActions.tsx | 18 ++-- .../components/QuestionActions.unit.spec.tsx | 99 ++++++++++++++----- 4 files changed, 108 insertions(+), 51 deletions(-) diff --git a/enterprise/frontend/src/metabase-enterprise/moderation/index.js b/enterprise/frontend/src/metabase-enterprise/moderation/index.js index c060b33c8f7..0194ab535b1 100644 --- a/enterprise/frontend/src/metabase-enterprise/moderation/index.js +++ b/enterprise/frontend/src/metabase-enterprise/moderation/index.js @@ -41,26 +41,30 @@ if (hasPremiumFeature("content_management")) { const isVerified = isItemVerified(latestModerationReview); if (isModerator) { - return { - title: isVerified - ? t`Remove verification` - : isDataset - ? t`Verify this model` - : t`Verify this question`, - icon: isVerified ? "close" : verifiedIconName, - action: () => { - if (isVerified) { - removeReview({ itemId: id, itemType: "card" }); - } else { - verifyItem({ itemId: id, itemType: "card" }); - } - reload(); + return [ + { + title: isVerified + ? t`Remove verification` + : isDataset + ? t`Verify this model` + : t`Verify this question`, + icon: isVerified ? "close" : verifiedIconName, + action: () => { + if (isVerified) { + removeReview({ itemId: id, itemType: "card" }); + } else { + verifyItem({ itemId: id, itemType: "card" }); + } + reload(); + }, + testId: isVerified + ? "moderation-remove-verification-action" + : "moderation-verify-action", }, - testId: isVerified - ? "moderation-remove-verification-action" - : "moderation-verify-action", - }; + ]; } + + return []; }, }); } diff --git a/frontend/src/metabase/plugins/index.ts b/frontend/src/metabase/plugins/index.ts index 2f47681eb44..d994e5a886b 100644 --- a/frontend/src/metabase/plugins/index.ts +++ b/frontend/src/metabase/plugins/index.ts @@ -198,7 +198,7 @@ export const PLUGIN_MODERATION = { question?: Question, isModerator?: boolean, reload?: () => void, - ) => ({}), + ) => [], }; export const PLUGIN_CACHING = { diff --git a/frontend/src/metabase/query_builder/components/QuestionActions.tsx b/frontend/src/metabase/query_builder/components/QuestionActions.tsx index f94c8eab538..5e7e6f6269b 100644 --- a/frontend/src/metabase/query_builder/components/QuestionActions.tsx +++ b/frontend/src/metabase/query_builder/components/QuestionActions.tsx @@ -128,14 +128,14 @@ const QuestionActions = ({ } extraButtons.push( - PLUGIN_MODERATION.getMenuItems( + ...PLUGIN_MODERATION.getMenuItems( question, isModerator, dispatchSoftReloadCard, ), ); - if (isDataset) { + if (canWrite && isDataset) { extraButtons.push( { title: t`Edit query definition`, @@ -237,12 +237,14 @@ const QuestionActions = ({ /> </ViewHeaderIconButtonContainer> </Tooltip> - <EntityMenu - triggerAriaLabel={t`Move, archive, and more...`} - items={extraButtons} - triggerIcon="ellipsis" - tooltip={t`Move, archive, and more...`} - /> + {extraButtons.length > 0 && ( + <EntityMenu + triggerAriaLabel={t`Move, archive, and more...`} + items={extraButtons} + triggerIcon="ellipsis" + tooltip={t`Move, archive, and more...`} + /> + )} </> ); }; diff --git a/frontend/src/metabase/query_builder/components/QuestionActions.unit.spec.tsx b/frontend/src/metabase/query_builder/components/QuestionActions.unit.spec.tsx index 9ffd482f8c9..4e20802efcd 100644 --- a/frontend/src/metabase/query_builder/components/QuestionActions.unit.spec.tsx +++ b/frontend/src/metabase/query_builder/components/QuestionActions.unit.spec.tsx @@ -1,17 +1,25 @@ import userEvent from "@testing-library/user-event"; -import { renderWithProviders, screen } from "__support__/ui"; +import { getMetadata } from "metabase/selectors/metadata"; +import { Card, Database } from "metabase-types/api"; +import { createMockCard, createMockNativeCard } from "metabase-types/api/mocks"; +import { createSampleDatabase } from "metabase-types/api/mocks/presets"; import { createMockState } from "metabase-types/store/mocks"; import { createMockEntitiesState } from "__support__/store"; -import QuestionActions from "metabase/query_builder/components/QuestionActions"; -import { createMockCard, createMockNativeCard } from "metabase-types/api/mocks"; -import { getMetadata } from "metabase/selectors/metadata"; -import { Card } from "metabase-types/api"; +import { + getIcon, + queryIcon, + renderWithProviders, + screen, +} from "__support__/ui"; import Question from "metabase-lib/Question"; +import QuestionActions from "./QuestionActions"; -const TEST_STRUCTURED_CARD = createMockCard(); -const TEST_NATIVE_CARD = createMockNativeCard(); +const ICON_CASES_CARDS = [ + createMockCard({ name: "GUI" }), + createMockNativeCard({ name: "SQL" }), +]; -const iconList = [ +const ICON_CASES_LABELS = [ { label: "bookmark icon", tooltipText: "Bookmark" }, { label: "info icon", tooltipText: "More info" }, { @@ -20,9 +28,19 @@ const iconList = [ }, ]; -function setup({ card }: { card: Card }) { +const ICON_CASES = ICON_CASES_CARDS.flatMap(card => + ICON_CASES_LABELS.map(labels => ({ ...labels, card })), +); + +interface SetupOpts { + card: Card; + databases?: Database[]; +} + +function setup({ card, databases = [createSampleDatabase()] }: SetupOpts) { const state = createMockState({ entities: createMockEntitiesState({ + databases, questions: [card], }), }); @@ -46,22 +64,55 @@ function setup({ card }: { card: Card }) { ); } -describe("Question Actions | Icons", () => { - ["structured", "native"].forEach(queryType => { - iconList.forEach(({ label, tooltipText }) => { - it(`should display the "${label}" icon with the "${tooltipText}" tooltip for ${queryType} questions`, async () => { - setup({ - card: - queryType === "structured" - ? TEST_STRUCTURED_CARD - : TEST_NATIVE_CARD, - }); +describe("QuestionActions", () => { + it.each(ICON_CASES)( + `should display the "$label" icon with the "$tooltipText" tooltip for $card.name questions`, + async ({ label, tooltipText, card }) => { + setup({ card }); + + await userEvent.hover(screen.getByRole("button", { name: label })); + const tooltip = screen.getByRole("tooltip", { name: tooltipText }); + expect(tooltip).toHaveAttribute("data-placement", "top"); + expect(tooltip).toHaveTextContent(tooltipText); + }, + ); - await userEvent.hover(screen.getByRole("button", { name: label })); - const tooltip = screen.getByRole("tooltip", { name: tooltipText }); - expect(tooltip).toHaveAttribute("data-placement", "top"); - expect(tooltip).toHaveTextContent(tooltipText); - }); + it("should allow to edit the model only with write permissions", () => { + setup({ + card: createMockCard({ + dataset: true, + can_write: true, + }), }); + + userEvent.click(getIcon("ellipsis")); + expect(screen.getByText("Edit query definition")).toBeInTheDocument(); + expect(screen.getByText("Edit metadata")).toBeInTheDocument(); + }); + + it("should not allow to edit the model without write permissions", () => { + setup({ + card: createMockCard({ + dataset: true, + can_write: false, + }), + }); + + userEvent.click(getIcon("ellipsis")); + expect(screen.queryByText("Edit query definition")).not.toBeInTheDocument(); + expect(screen.queryByText("Edit metadata")).not.toBeInTheDocument(); + }); + + it("should not render the menu when there are no menu items", () => { + setup({ + card: createMockCard({ + dataset: true, + can_write: false, + }), + databases: [], + }); + + expect(getIcon("info")).toBeInTheDocument(); + expect(queryIcon("ellipsis")).not.toBeInTheDocument(); }); }); -- GitLab