diff --git a/enterprise/frontend/src/embedding-sdk/README.md b/enterprise/frontend/src/embedding-sdk/README.md index 59a4db6318a455ec0ad4846ec16817b0d7cfe76d..80bbbe2bea794fcffe4dce22a3603e47cef5f76c 100644 --- a/enterprise/frontend/src/embedding-sdk/README.md +++ b/enterprise/frontend/src/embedding-sdk/README.md @@ -178,7 +178,7 @@ yarn add @metabase/embedding-sdk-react Once installed, you need to import `MetabaseProvider` and provide it with a `config` object. -```jsx +```typescript jsx import React from "react"; import { MetabaseProvider } from "@metabase/embedding-sdk-react"; @@ -218,7 +218,7 @@ After the SDK is configured, you can use embed your question using the `StaticQu The component has a default height, which can be customized by using the `height` prop. To inherit the height from the parent container, you can pass `100%` to the height prop. -```jsx +```typescript jsx import React from "react"; import { MetabaseProvider, StaticQuestion } from "@metabase/embedding-sdk-react"; @@ -237,7 +237,7 @@ export default function App() { ### Embedding an interactive question (with drill-down) -```jsx +```typescript jsx import React from "react"; import { MetabaseProvider, InteractiveQuestion } from "@metabase/embedding-sdk-react"; @@ -265,13 +265,13 @@ make your application unique. Therefore, we've added the ability to customize th Using the `InteractiveQuestion` with its default layout looks like this: -```jsx +```typescript jsx <InteractiveQuestion questionId={95} /> ``` To customize the layout, use namespaced components within the `InteractiveQuestion`. For example: -```jsx +```typescript jsx <InteractiveQuestion questionId={95}> <div style={{ @@ -340,7 +340,7 @@ After the SDK is configured, you can embed your dashboard using the `StaticDashb - **onLoad**: `(dashboard: Dashboard | null) => void;` - event handler that triggers after dashboard loads with all visible cards and their content. - **onLoadWithoutCards**: `(dashboard: Dashboard | null) => void;` - event handler that triggers after dashboard loads, but without its cards - at this stage dashboard title, tabs and cards grid is rendered, but cards content is not yet loaded. -```jsx +```typescript jsx import React from "react"; import { MetabaseProvider, StaticDashboard } from "@metabase/embedding-sdk-react"; @@ -384,7 +384,7 @@ After the SDK is configured, you can embed your dashboard using the `Interactive - **onLoad**: `(dashboard: Dashboard | null) => void;` - event handler that triggers after dashboard loads with all visible cards and their content. - **onLoadWithoutCards**: `(dashboard: Dashboard | null) => void;` - event handler that triggers after dashboard loads, but without its cards - at this stage dashboard title, tabs and cards grid is rendered, but cards content is not yet loaded. -```jsx +```typescript jsx import React from "react"; import { MetabaseProvider, InteractiveDashboard } from "@metabase/embedding-sdk-react"; @@ -605,13 +605,46 @@ const theme = { }; ``` -### Implementing custom actions +### Plugins + +The Metabase Embedding SDK supports plugins to customize the behavior of components. These plugins can be used in a +global context or on a per-component basis. This list of plugins will continue to grow as we add more options to each +component. + +To use a plugin globally, add the plugin to the `MetabaseProvider`'s `pluginsConfig` prop: + +```typescript jsx +<MetabaseProvider + config={config} + theme={theme} + pluginsConfig={{ + mapQuestionClickActions: [...] // Add your custom actions here + }} +> + {children} +</MetabaseProvider> +``` + +To use a plugin on a per-component basis, pass the plugin as a prop to the component: + +```typescript jsx +<InteractiveQuestion + questionId={1} + plugins={{ + mapQuestionClickActions: [...], + }} +/> +``` + +#### _Interactive Question_ -`MetabaseProvider` also supports `pluginsConfig`. You can use `pluginsConfig` to customize the behavior of components. Currently we only allow configuring `mapQuestionClickActions` which lets you add custom actions or remove Metabase default actions in `InteractiveQuestion` component. +###### `mapQuestionClickActions` -We'll support more plugins in next releases. Please share your uses cases for us! +This plugin allows you to add custom actions to +the click-through menu of an interactive question. You can add and +customize the appearance and behavior of the custom actions. -```jsx +```typescript jsx // You can provide a custom action with your own `onClick` logic. const createCustomAction = clicked => ({ buttonType: "horizontal", @@ -667,6 +700,115 @@ return ( ); ``` +#### _Interactive Dashboard_ + +###### `dashcardMenu` + +This plugin allows you to add, remove, and modify the custom actions on the overflow menu of dashboard cards. The plugin +appears as a dropdown menu on the top right corner of the card. + +The plugin's default configuration looks like this: + +```typescript jsx +const plugins = { + dashboard: { + dashcardMenu: { + withDownloads: true, + withEditLink: true, + customItems: [], + }, + }, +} +``` + +and can be used in the InteractiveDashboard like this: + +```typescript jsx +<InteractiveDashboard + questionId={1} + plugins={{ + dashboard: { + dashcardMenu: null, + }, + }} +/> +``` + +Take a look below to see how you can customize the plugin: + +###### Enabling/disabling default actions + +To remove the download button from the dashcard menu, set `withDownloads` to `false`. To remove the edit link from the +dashcard menu, set `withEditLink` to `false`. + +```typescript jsx +const plugins = { + dashboard: { + dashcardMenu: { + withDownloads: false, + withEditLink: false, + customItems: [], + } + } +}; +``` + +###### Adding custom actions to the existing menu: + +You can add custom actions to the dashcard menu by adding an object to the `customItems` array. Each element can either +be an object or a function that takes in the dashcard's question, and outputs a list of custom items in the form of: + +```typescript jsx +{ + iconName: string; + label: string; + onClick: () => void; + disabled?: boolean; +} +``` + +```typescript jsx +const plugins: SdkPluginsConfig = { + dashboard: { + dashcardMenu: { + customItems: [ + { + iconName: "chevronright", + label: "Custom action", + onClick: () => { + alert(`Custom action clicked`); + }, + }, + ({ question }) => { + return { + iconName: "chevronright", + label: "Custom action", + onClick: () => { + alert(`Custom action clicked ${question.name}`); + }, + }; + }, + ], + }, + }, +}; +``` + +###### Replacing the existing menu with your own component + +If you want to replace the existing menu with your own component, you can do so by providing a function that returns a +React component. This function also can receive the question as an argument. + +```typescript jsx +const plugins: SdkPluginsConfig = { + dashboard: { + dashcardMenu: ({ question }) => ( + <button onClick={() => console.log(question.name)}>Click me</button> + ), + }, +}; +``` + ### Adding global event handlers `MetabaseProvider` also supports `eventHandlers` configuration. This way you can add global handlers to react on events that happen in the SDK context. @@ -697,7 +839,7 @@ return ( In case you need to reload a Metabase component, for example, your users modify your application data and that data is used to render a question in Metabase. If you embed this question and want to force Metabase to reload the question to show the latest data, you can do so by using the `key` prop to force a component to reload. -```jsx +```typescript jsx // Inside your application component const [data, setData] = useState({}); // This is used to force reloading Metabase components @@ -731,7 +873,7 @@ return <InteractiveQuestion key={counter} questionId={yourQuestionId} />; You can customize how the SDK fetches the refresh token by specifying the `fetchRefreshToken` function in the `config` prop: -```jsx +```typescript jsx /** * This is the default implementation used in the SDK. * You can customize this function to fit your needs, such as adding headers or excluding cookies. diff --git a/enterprise/frontend/src/embedding-sdk/lib/plugins.ts b/enterprise/frontend/src/embedding-sdk/lib/plugins.ts index f0fce6836699209d19f027948123f3d78a63aee7..e6f8048c7e25b5f46d0b0b236269a79537a6c763 100644 --- a/enterprise/frontend/src/embedding-sdk/lib/plugins.ts +++ b/enterprise/frontend/src/embedding-sdk/lib/plugins.ts @@ -2,7 +2,7 @@ import type { ReactNode } from "react"; import type { DashCardMenuItem } from "metabase/dashboard/components/DashCard/DashCardMenu/DashCardMenu"; import type { ClickAction, ClickObject } from "metabase/visualizations/types"; -import type Question from "metabase-lib/v1/Question"; +import type { Card as QuestionType } from "metabase-types/api"; export type SdkDataPointObject = Pick< ClickObject, @@ -14,16 +14,16 @@ export type SdkClickActionPluginsConfig = ( clickedDataPoint: SdkDataPointObject, ) => ClickAction[]; -type DashCardMenuCustomElement = ({ +export type DashCardMenuCustomElement = ({ question, }: { - question: Question; + question: QuestionType; }) => ReactNode; -type CustomDashCardMenuItem = ({ +export type CustomDashCardMenuItem = ({ question, }: { - question?: Question; + question?: QuestionType; }) => DashCardMenuItem; export type DashCardCustomMenuItem = { diff --git a/frontend/src/metabase/dashboard/components/DashCard/DashCardMenu/DashCardMenu.tsx b/frontend/src/metabase/dashboard/components/DashCard/DashCardMenu/DashCardMenu.tsx index dfc2cb90663e7c95332c987c95e5efe41fdd850f..a2568d7db01705a86388727144a84ebdda8b0210 100644 --- a/frontend/src/metabase/dashboard/components/DashCard/DashCardMenu/DashCardMenu.tsx +++ b/frontend/src/metabase/dashboard/components/DashCard/DashCardMenu/DashCardMenu.tsx @@ -2,6 +2,7 @@ import { useDisclosure } from "@mantine/hooks"; import cx from "classnames"; import { isValidElement, useState } from "react"; +import type { SdkPluginsConfig } from "embedding-sdk"; import { useInteractiveDashboardContext } from "embedding-sdk/components/public/InteractiveDashboard/context"; import CS from "metabase/css/core/index.css"; import { @@ -48,6 +49,20 @@ export type DashCardMenuItem = { disabled?: boolean; } & MenuItemProps; +function isDashCardMenuEmpty(plugins?: SdkPluginsConfig) { + const dashcardMenu = plugins?.dashboard?.dashcardMenu; + + if (!plugins || !dashcardMenu || typeof dashcardMenu !== "object") { + return false; + } + + return ( + dashcardMenu?.withDownloads === false && + dashcardMenu?.withEditLink === false && + !dashcardMenu?.customItems?.length + ); +} + export const DashCardMenu = ({ question, result, @@ -76,7 +91,15 @@ export const DashCardMenu = ({ }, }); + if (isDashCardMenuEmpty(plugins)) { + return null; + } + const getMenuContent = () => { + if (typeof plugins?.dashboard?.dashcardMenu === "function") { + return plugins.dashboard.dashcardMenu({ question: question.card() }); + } + if (isValidElement(plugins?.dashboard?.dashcardMenu)) { return plugins.dashboard.dashcardMenu; } diff --git a/frontend/src/metabase/dashboard/components/DashCard/DashCardMenu/DashCardMenuItems.tsx b/frontend/src/metabase/dashboard/components/DashCard/DashCardMenu/DashCardMenuItems.tsx index f0fc066bd9ddae24c226781328d908fdd99a3142..16159f88ffdbe3dc9677b5ddc153dc89a1af6c95 100644 --- a/frontend/src/metabase/dashboard/components/DashCard/DashCardMenu/DashCardMenuItems.tsx +++ b/frontend/src/metabase/dashboard/components/DashCard/DashCardMenu/DashCardMenuItems.tsx @@ -30,6 +30,7 @@ export const DashCardMenuItems = ({ plugins, onEditQuestion = question => dispatch(editQuestion(question)), } = useInteractiveDashboardContext(); + const dashcardMenuItems = plugins?.dashboard?.dashcardMenu as | DashCardCustomMenuItem | undefined; @@ -69,7 +70,9 @@ export const DashCardMenuItems = ({ items.push( ...customItems.map(item => { const customItem = - typeof item === "function" ? item({ question }) : item; + typeof item === "function" + ? item({ question: question.card() }) + : item; return { ...customItem,