Skip to content
Snippets Groups Projects
Unverified Commit 5dae153b authored by Anton Kulyk's avatar Anton Kulyk Committed by GitHub
Browse files

Handle data permission error for dashboard actions (#29092)

* Handle data permission error for dashboard actions

* Extract `setupUnauthorizedDatabaseEndpoints`

* Type error as `unknown`
parent 5bde4b88
No related branches found
No related tags found
No related merge requests found
......@@ -7,6 +7,8 @@ import { executeRowAction } from "metabase/dashboard/actions";
import Tooltip from "metabase/core/components/Tooltip";
import { getResponseErrorMessage } from "metabase/core/utils/errors";
import type {
ActionDashboardCard,
ParametersForActionExecution,
......@@ -37,15 +39,21 @@ import ActionVizForm from "./ActionVizForm";
import ActionButtonView from "./ActionButtonView";
import { FullContainer } from "./ActionButton.styled";
export interface ActionProps extends VisualizationProps {
interface OwnProps {
dashcard: ActionDashboardCard;
dashboard: Dashboard;
dispatch: Dispatch;
parameterValues: { [id: string]: ParameterValueOrArray };
isEditingDashcard: boolean;
dispatch: Dispatch;
}
interface DatabaseLoaderProps {
database: Database;
error?: unknown;
}
export type ActionProps = VisualizationProps & OwnProps & DatabaseLoaderProps;
function ActionComponent({
dashcard,
dashboard,
......@@ -133,14 +141,17 @@ function ActionFn(props: ActionProps) {
const {
database,
dashcard: { action },
error,
} = props;
const hasActionsEnabled = database?.hasActionsEnabled?.();
if (!action || !hasActionsEnabled) {
const tooltip = !action
? t`No action assigned`
: t`Actions are not enabled for this database`;
if (error || !action || !hasActionsEnabled) {
const tooltip = getErrorTooltip({
hasActionAssigned: !!action,
hasActionsEnabled,
error,
});
return (
<Tooltip tooltip={tooltip}>
......@@ -160,10 +171,32 @@ function ActionFn(props: ActionProps) {
return <ConnectedActionComponent {...props} />;
}
function getErrorTooltip({
hasActionAssigned,
hasActionsEnabled,
error,
}: {
hasActionAssigned: boolean;
hasActionsEnabled: boolean;
error?: unknown;
}) {
if (error) {
return getResponseErrorMessage(error);
}
if (!hasActionAssigned) {
return t`No action assigned`;
}
if (!hasActionsEnabled) {
return t`Actions are not enabled for this database`;
}
return t`Something's gone wrong`;
}
export default _.compose(
Databases.load({
id: (state: State, props: ActionProps) =>
props.dashcard?.action?.database_id,
loadingAndErrorWrapper: false,
}),
connect(mapStateToProps),
)(ActionFn);
......@@ -2,14 +2,11 @@ import React from "react";
import fetchMock from "fetch-mock";
import userEvent from "@testing-library/user-event";
import { renderWithProviders, screen, getIcon, waitFor } from "__support__/ui";
import {
renderWithProviders,
screen,
getIcon,
waitFor,
waitForElementToBeRemoved,
} from "__support__/ui";
import { setupDatabasesEndpoints } from "__support__/server-mocks";
setupDatabasesEndpoints,
setupUnauthorizedDatabasesEndpoints,
} from "__support__/server-mocks";
import type { ActionDashboardCard } from "metabase-types/api";
import type { ParameterTarget } from "metabase-types/types/Parameter";
......@@ -76,7 +73,7 @@ function createMockActionDashboardCard(
}
type SetupOpts = Partial<ActionProps> & {
awaitLoading?: boolean;
hasDataPermissions?: boolean;
};
async function setup({
......@@ -84,14 +81,17 @@ async function setup({
dashcard = createMockActionDashboardCard(),
settings = {},
parameterValues = {},
awaitLoading = true,
hasDataPermissions = true,
...props
}: SetupOpts = {}) {
setupDatabasesEndpoints([DATABASE, DATABASE_WITHOUT_ACTIONS]);
const databases = [DATABASE, DATABASE_WITHOUT_ACTIONS];
fetchMock.post(ACTION_EXEC_MOCK_PATH, {
"rows-updated": [1],
});
if (hasDataPermissions) {
setupDatabasesEndpoints(databases);
fetchMock.post(ACTION_EXEC_MOCK_PATH, { "rows-updated": [1] });
} else {
setupUnauthorizedDatabasesEndpoints(databases);
}
renderWithProviders(
<Action
......@@ -107,11 +107,8 @@ async function setup({
/>,
);
if (awaitLoading) {
await waitForElementToBeRemoved(() =>
screen.queryAllByTestId("loading-spinner"),
);
}
// Wait until UI is ready
await screen.findByRole("button");
}
describe("Actions > ActionViz > Action", () => {
......@@ -119,7 +116,6 @@ describe("Actions > ActionViz > Action", () => {
it("should render an empty state for a button with no action", async () => {
await setup({
dashcard: createMockActionDashboardCard({ action: undefined }),
awaitLoading: false,
});
expect(getIcon("bolt")).toBeInTheDocument();
expect(screen.getByRole("button")).toBeDisabled();
......@@ -141,6 +137,15 @@ describe("Actions > ActionViz > Action", () => {
).toBeInTheDocument();
});
it("should render a disabled state if the user doesn't have permissions to action database", async () => {
await setup({ hasDataPermissions: false });
expect(getIcon("bolt")).toBeInTheDocument();
expect(screen.getByRole("button")).toBeDisabled();
expect(
screen.getByLabelText(/don't have permission/i),
).toBeInTheDocument();
});
it("should render an enabled state when the action is valid", async () => {
await setup();
expect(screen.getByRole("button")).toBeEnabled();
......
......@@ -2,6 +2,7 @@ import fetchMock from "fetch-mock";
import _ from "underscore";
import { SAVED_QUESTIONS_DATABASE } from "metabase/databases/constants";
import { Database } from "metabase-types/api";
import { PERMISSION_ERROR } from "./constants";
import { setupTableEndpoints } from "./table";
export function setupDatabaseEndpoints(db: Database) {
......@@ -39,3 +40,14 @@ export const setupSchemaEndpoints = (db: Database) => {
);
});
};
export function setupUnauthorizedDatabaseEndpoints(db: Database) {
fetchMock.get(`path:/api/database/${db.id}`, {
status: 403,
body: PERMISSION_ERROR,
});
}
export function setupUnauthorizedDatabasesEndpoints(dbs: Database[]) {
dbs.forEach(db => setupUnauthorizedDatabaseEndpoints(db));
}
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