docs(sdk): Setup automatic documentation generation for @metabase/embedding-sdk package (#56366)

Co-authored-by: jeff bruemmer <jeff.bruemmer@gmail.com>
This commit is contained in:
Timofey Kachalov
2025-04-10 20:06:06 +04:00
committed by GitHub
parent 31f31e59f0
commit b373500c04
72 changed files with 1492 additions and 477 deletions

1
.gitignore vendored
View File

@@ -70,6 +70,7 @@
/storybook-static
/target
/test-report-*
/temp
**/cli-snippets-tmp/**
\#*\#
bin/node_modules/

View File

@@ -0,0 +1,170 @@
const INTERNAL_MODULE_NAME = "internal";
const GENERATED_DOC_RETURN_URL_LOCAL_STORAGE_KEY = "generatedDocReturnUrl";
const isEmbeddingSdkApiDocsPage = (href) =>
href.includes("/embedding/sdk/api/");
const storeReturnUrl = () => {
const ref = document.referrer;
const isRefFromNonApiPage = ref && !isEmbeddingSdkApiDocsPage(ref);
if (isRefFromNonApiPage) {
sessionStorage.setItem(GENERATED_DOC_RETURN_URL_LOCAL_STORAGE_KEY, ref);
}
};
/**
* Called from `navigationLinks` field of `typedoc.config.mjs
*/
// eslint-disable-next-line no-unused-vars
const navigateBack = ({ fallbackUrl }) => {
const returnUrl = sessionStorage.getItem(
GENERATED_DOC_RETURN_URL_LOCAL_STORAGE_KEY,
);
if (returnUrl) {
sessionStorage.removeItem(GENERATED_DOC_RETURN_URL_LOCAL_STORAGE_KEY);
location.href = returnUrl;
} else {
location.href = fallbackUrl;
}
};
/**
* Redirects from the "internal" module index page to the main index page
*/
const setupRedirectsFromInternalModule = () => {
const href = location.href;
const indexPage = href.replace(/(.*\/html)\/.*/, "$1/index.html");
const isInternalModule =
href.endsWith(`${INTERNAL_MODULE_NAME}.html`) ||
href.endsWith(INTERNAL_MODULE_NAME);
if (isInternalModule) {
location.replace(indexPage);
}
};
/**
* Inserts word breaks (<wbr> tags) into the text content of an element
*/
function insertWordBreaks(textContent) {
const regex = /[\s\S]*?(?:[^_-][_-](?=[^_-])|[^A-Z](?=[A-Z][^A-Z]))/g;
const result = [];
let lastIndex = 0;
let match;
while ((match = regex.exec(textContent)) !== null) {
result.push(match[0], "<wbr>");
lastIndex += match[0].length;
}
result.push(textContent.slice(lastIndex));
return result.join("");
}
/**
* Inserts word breaks (<wbr> tags) into the text content of specific elements
* Currently the typedoc automatically adds word breaks to the identifier names in the right column, but does not do
* it for the left column and the main content.
* This logic fixes it and does it for the left column and the main content.
* This fixes unwanted horizontal scrollbars and overflow issues.
*
* We have to use `MutationObserver` because links are added by the typedoc dynamically, so we need to wait for them to be added to the DOM
*/
const setupWordBreaks = () => {
const apply = () => {
const leftNavigationMenuItemElements =
document.querySelectorAll("ul > li > a > span");
const contentMenuItemElements =
document.querySelectorAll("dl > dt > span > a");
const itemElements = [
...leftNavigationMenuItemElements,
...contentMenuItemElements,
];
itemElements.forEach((element) => {
if (!element.dataset.wordBreakApplied) {
element.innerHTML = insertWordBreaks(element.textContent);
element.dataset.wordBreakApplied = "true";
}
});
};
const observer = new MutationObserver(apply);
observer.observe(document.body, {
childList: true,
subtree: true,
});
};
/**
* Removes the "internal" kind item from the main content menu of a main page
*/
const removeContentMenuInternalItem = () => {
const internalItem = document.querySelector(
`.col-content dt#${INTERNAL_MODULE_NAME}`,
);
if (!internalItem) {
return;
}
internalItem.remove();
};
/**
* Removes the `internal` menu item from the site (left) menu
*/
const removeSiteMenuInternalItems = () => {
const internalModuleItem = document.querySelector(
`.site-menu .tsd-accordion li:has(summary[data-key="other$${INTERNAL_MODULE_NAME}"])`,
);
if (!internalModuleItem) {
return;
}
internalModuleItem.remove();
};
/**
* Removes the `internal` menu item from the page (right) menu
*/
const removePageMenuInternalItems = () => {
const internalModuleItem = document.querySelector(
`.page-menu .tsd-accordion a[href="#${INTERNAL_MODULE_NAME}"]`,
);
if (!internalModuleItem) {
return;
}
internalModuleItem.remove();
};
const removePageElements = () => {
removeContentMenuInternalItem();
removeSiteMenuInternalItems();
removePageMenuInternalItems();
};
const observer = new MutationObserver(removePageElements);
observer.observe(document.body, {
childList: true,
subtree: true,
});
const init = () => {
storeReturnUrl();
setupRedirectsFromInternalModule();
setupWordBreaks();
};
document.addEventListener("DOMContentLoaded", init);

View File

@@ -0,0 +1,126 @@
:root {
--color-text-aside: #509ee3;
}
* {
box-sizing: border-box;
}
@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]) {
--color-background: #fafbfe;
--color-background-secondary: #fff;
--color-accent: #e8e8e8;
}
:root:not([data-theme="dark"]) .tsd-signature {
/**
To display code in code-blocks with the same colors both on light and dark themes
*/
--color-ts-keyword: var(--dark-color-ts-keyword);
--color-ts-project: var(--dark-color-ts-project);
--color-ts-module: var(--dark-color-ts-module);
--color-ts-namespace: var(--dark-color-ts-namespace);
--color-ts-enum: var(--dark-color-ts-enum);
--color-ts-enum-member: var(--dark-color-ts-enum-member);
--color-ts-variable: var(--dark-color-ts-variable);
--color-ts-function: var(--dark-color-ts-function);
--color-ts-class: var(--dark-color-ts-class);
--color-ts-interface: var(--dark-color-ts-interface);
--color-ts-constructor: var(--dark-color-ts-constructor);
--color-ts-property: var(--dark-color-ts-property);
--color-ts-method: var(--dark-color-ts-method);
--color-ts-reference: var(--dark-color-ts-reference);
--color-ts-call-signature: var(--dark-color-ts-call-signature);
--color-ts-index-signature: var(--dark-color-ts-index-signature);
--color-ts-constructor-signature: var(
--dark-color-ts-constructor-signature
);
--color-ts-parameter: var(--dark-color-ts-parameter);
--color-ts-type-parameter: var(--dark-color-ts-type-parameter);
--color-ts-accessor: var(--dark-color-ts-accessor);
--color-ts-get-signature: var(--dark-color-ts-get-signature);
--color-ts-set-signature: var(--dark-color-ts-set-signature);
--color-ts-type-alias: var(--dark-color-ts-type-alias);
--color-link: var(--dark-color-link);
--external-icon: var(--dark-external-icon);
}
}
:root[data-theme='light'] {
--color-background: #fafbfe;
--color-background-secondary: #fff;
--color-accent: #e8e8e8;
}
:root[data-theme='light'] .tsd-signature {
/**
To display code in code-blocks with the same colors both on light and dark themes
*/
--color-ts-keyword: var(--dark-color-ts-keyword);
--color-ts-project: var(--dark-color-ts-project);
--color-ts-module: var(--dark-color-ts-module);
--color-ts-namespace: var(--dark-color-ts-namespace);
--color-ts-enum: var(--dark-color-ts-enum);
--color-ts-enum-member: var(--dark-color-ts-enum-member);
--color-ts-variable: var(--dark-color-ts-variable);
--color-ts-function: var(--dark-color-ts-function);
--color-ts-class: var(--dark-color-ts-class);
--color-ts-interface: var(--dark-color-ts-interface);
--color-ts-constructor: var(--dark-color-ts-constructor);
--color-ts-property: var(--dark-color-ts-property);
--color-ts-method: var(--dark-color-ts-method);
--color-ts-reference: var(--dark-color-ts-reference);
--color-ts-call-signature: var(--dark-color-ts-call-signature);
--color-ts-index-signature: var(--dark-color-ts-index-signature);
--color-ts-constructor-signature: var(
--dark-color-ts-constructor-signature
);
--color-ts-parameter: var(--dark-color-ts-parameter);
--color-ts-type-parameter: var(--dark-color-ts-type-parameter);
--color-ts-accessor: var(--dark-color-ts-accessor);
--color-ts-get-signature: var(--dark-color-ts-get-signature);
--color-ts-set-signature: var(--dark-color-ts-set-signature);
--color-ts-type-alias: var(--dark-color-ts-type-alias);
--color-link: var(--dark-color-link);
--external-icon: var(--dark-external-icon);
}
.tsd-toolbar-contents > .title::before {
content: "";
display: inline-block;
background-image: url('/images/logo.svg');
background-size: contain;
background-repeat: no-repeat;
height: 36px;
width: 36px;
margin-right: 8px;
vertical-align: middle;
}
.tsd-kind-icon ~ span,
.tsd-breadcrumb a,
.tsd-navigation a,
.tsd-page-toolbar a {
color: var(--color-text-aside);
font-weight: bold;
}
.tsd-signature {
background: #12436e;
border-radius: .75rem;
color: #fff;
font-family: "Roboto Mono", monospace;
padding: 1.5rem;
white-space: unset;
}
@media (max-width: 769px) {
#tsd-toolbar-menu-trigger {
display: flex;
align-items: center;
justify-content: center;
}
}

View File

@@ -0,0 +1,4 @@
{
"extends": "../tsconfig.base.json",
"files": ["../resources/embedding-sdk/dist/index.d.ts"]
}

View File

@@ -0,0 +1,22 @@
import { ParameterType, PageEvent } from "typedoc";
/**
* Adds frontmatter globals to each page.
*/
export function load(app) {
app.options.addDeclaration({
name: "frontmatterGlobals",
help: "Frontmatter globals to be added to each page.",
type: ParameterType.Object,
defaultValue: {},
});
app.renderer.on(PageEvent.END, (page) => {
const frontmatterGlobals = app.options.getValue("frontmatterGlobals");
const yamlItems = Object.entries(frontmatterGlobals)
.map(([key, value]) => `${key}: ${value}`)
.join("\n");
page.contents = `---\n${yamlItems}\n---\n\n${page.contents}`;
});
}

120
.typedoc/typedoc.config.mjs Normal file
View File

@@ -0,0 +1,120 @@
const EMBEDDING_SDK_DOCS_MAIN_PAGE_URL =
"/docs/latest/embedding/sdk/introduction";
/** @type {Partial<import("typedoc").TypeDocOptions>} */
const config = {
name: "Embedding SDK API",
tsconfig: "./tsconfig.sdk-docs.json",
plugin: [
"typedoc-plugin-missing-exports",
"typedoc-plugin-mdn-links",
"./typedoc-plugin-frontmatter.js",
],
entryPoints: ["../resources/embedding-sdk/dist/index.d.ts"],
router: "structure",
internalModule: "internal",
outputs: [
{
name: "html",
path: "../docs/embedding/sdk/api",
options: {
favicon: "./resources/frontend_client/favicon.ico",
customJs: "./.typedoc/page-custom-logic.js",
customCss: "./.typedoc/page-custom-styles.css",
hideGenerator: true,
collapseInternalModule: true,
visibilityFilters: {},
frontmatterGlobals: {
layout: "docs-api",
},
pretty: true,
customFooterHtml:
'<script type="text/javascript" src="/gdpr-cookie-notice/dist/script.js"></script>' +
'<script type="text/javascript" src="/js/cookie-consent.js"></script>' +
'<link href="/gdpr-cookie-notice/dist/style.css" rel="stylesheet" />' +
'<link href="/css/gdpr.css" rel="stylesheet" />',
},
},
],
defaultCategory: "other",
kindSortOrder: [
"Reference",
"Project",
"Namespace",
"Function",
"Variable",
"Enum",
"EnumMember",
"Class",
"Interface",
"TypeAlias",
"Constructor",
"Property",
"Accessor",
"Method",
"Parameter",
"TypeParameter",
"TypeLiteral",
"CallSignature",
"ConstructorSignature",
"IndexSignature",
"GetSignature",
"SetSignature",
"Module",
],
readme: "none",
excludePrivate: true,
excludeExternals: true,
excludeInternal: true,
excludeNotDocumented: true,
excludeReferences: true,
excludeNotDocumentedKinds: [
"Module",
"Namespace",
"Enum",
"EnumMember",
"Variable",
"Function",
"Class",
"Constructor",
"Method",
"CallSignature",
"IndexSignature",
"ConstructorSignature",
"Accessor",
"GetSignature",
"SetSignature",
"Reference",
],
treatWarningsAsErrors: true,
disableSources: true,
includeHierarchySummary: false,
navigation: {
includeCategories: true,
includeGroups: false,
includeFolders: false,
compactFolders: true,
excludeReferences: true,
},
validation: {
notExported: true,
invalidLink: true,
rewrittenLink: true,
notDocumented: false,
unusedMergeModuleWith: true,
},
externalSymbolLinkMappings: {
"@mantine/core": {
"*": "https://v7.mantine.dev/overview",
ButtonProps: "https://v7.mantine.dev/core/button/?t=props",
MenuProps: "https://v7.mantine.dev/core/menu/?t=props",
PopoverProps: "https://v7.mantine.dev/core/popover/?t=props",
StackProps: "https://v7.mantine.dev/core/stack/?t=props",
},
},
navigationLinks: {
"Back to documentation": `javascript:navigateBack({ fallbackUrl: '${EMBEDDING_SDK_DOCS_MAIN_PAGE_URL}' })`,
},
};
export default config;

View File

@@ -8,14 +8,7 @@ title: Embedded analytics SDK - collections
You can embed Metabase's collection browser so that people can explore items in your Metabase from your application.
## `CollectionBrowser` props
| Prop | Type | Description |
| ------------------ | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| collectionId | `number \| 'root' \| 'personal'` | The numerical ID of the collection, "personal" for the user's personal collection, or "root" for the root collection. You can find this ID in the URL when accessing a collection in your Metabase instance. For example, the collection ID in `http://localhost:3000/collection/1-my-collection` would be `1`. Defaults to "personal" |
| onClick | `(item: CollectionItem) => void` | An optional click handler that emits the clicked entity. |
| pageSize | `number` | The number of items to display per page. The default is 25. |
| visibleEntityTypes | `["question", "model", "dashboard", "collection"]` | The types of entities that should be visible. If not provided, all entities will be shown. |
Docs: [CollectionBrowser](./api/CollectionBrowser.html)
## Example embedding code with `CollectionBrowser`
@@ -30,4 +23,4 @@ With static questions, you set a specific collection as the collection people ca
1. Set `isSaveEnabled` to true.
2. Set `targetCollection` to the collection ID you want people to save items to.
For more options, see [Question props](./questions.md#question-props).
For more options, see [Question props](./questions.md).

View File

@@ -6,24 +6,14 @@ title: Embedded analytics SDK - config
{% include plans-blockquote.html feature="Embedded analytics SDK" sdk=true %}
To use the SDK in your app, you need to import the `MetabaseProvider` component and provide it with an `authConfig` object, like so:
To use the SDK in your app, you need to import the `MetabaseProvider` component and provide it with an `authConfig` object.
Docs: [MetabaseProvider](./api/MetabaseProvider.html)
```typescript
{% include_file "{{ dirname }}/snippets/config/config-base.tsx" snippet="example" %}
```
You can also pass additional props to `MetabaseProvider`:
- `authConfig` (Required). Defines how to authenticate with Metabase.
- `theme` (Optional) See [Appearance](./appearance.md).
- `pluginsConfig` (Optional). See [Plugins](./plugins.md).
- `eventHandlers` (Optional). See [Global event handlers](#global-event-handlers).
- `className` (Optional). Classes to be added to the wrapper element.
- `locale` (Optional). Defines the display language. Accepts an ISO language code such as `en` or `de`.
- `loaderComponent` (Optional). A custom loader component to display while the SDK is loading.
- `errorComponent` (Optional). A custom error component to display when the SDK encounters an error.
- `allowConsoleLog` (Optional). If `true`, log messages will be printed to the console.
## Example `config` object passed to `MetabaseProvider`
```typescript
@@ -34,10 +24,7 @@ You can also pass additional props to `MetabaseProvider`:
`MetabaseProvider` also supports `eventHandlers`.
Currently, we support:
- `onDashboardLoad?: (dashboard: Dashboard | null) => void;`. Triggers when a dashboard loads with all visible cards and their content
- `onDashboardLoadWithoutCards?: (dashboard: Dashboard | null) => void;`. Triggers after a dashboard loads, but without its cards (at this stage only the dashboard title, tabs, and cards grid are rendered, but the contents of the cards have yet to load.
Docs: [SdkEventHandlersConfig](./api/internal/SdkEventHandlersConfig.html)
```typescript
{% include_file "{{ dirname }}/snippets/config/config-with-event-handlers.tsx" snippet="example" %}

View File

@@ -14,44 +14,23 @@ You can embed an interactive, editable, or static dashboard.
You can embed a dashboard using the one of the dashboard components:
- `StaticDashboard`
### `StaticDashboard`
A lightweight dashboard component. Use this component when you want to display results without letting people interact with the data.
- `InteractiveDashboard`
Docs: [StaticDashboard](./api/StaticDashboard.html)
### `InteractiveDashboard`
A dashboard component with drill downs, click behaviors, and the ability to view and click into questions. Use this component when you want to allow people to explore their data.
- `EditableDashboard`
Docs: [InteractiveDashboard](./api/InteractiveDashboard.html)
### `EditableDashboard`
A dashboard component with the features available in the `InteractiveDashboard` component, as well as the ability to add and update questions, layout, and content within your dashboard. Use this component when you want to give people the ability to modify your dashboards, for example in an admin panel in your app.
## Dashboard component props
| Prop | Type | Description |
| ---------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| dashboardId | `number \| string` | The ID of the dashboard. This is either:<br>- the numerical ID when accessing a dashboard link, i.e. `http://localhost:3000/dashboard/1-my-dashboard` where the ID is `1`<br>- the string ID found in the `entity_id` key of the dashboard object when using the API directly or using the SDK Collection Browser to return data |
| initialParameters\*\* | `Record<string, string \| string[]>` | Query parameters for the dashboard. For a single option, use a `string` value, and use a list of strings for multiple options. |
| withTitle | `boolean` | Whether the dashboard should display a title. |
| withCardTitle | `boolean` | Whether the dashboard cards should display a title. |
| withDownloads | `boolean \| null` | Whether to hide the download button. |
| hiddenParameters\*\* | `string[] \| null` | A list of [parameters to hide](../../embedding/public-links.md#appearance-parameters). |
| drillThroughQuestionHeight\* | `number \| null` | Height of a question component when drilled from the dashboard to a question level. |
| questionPlugins\* | `{ mapQuestionClickActions: Function } \| null` | Additional mapper function to override or add drill-down menu. See the implementing custom actions section for more details. |
| 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. |
| renderDrillThroughQuestion\* | `() => ReactNode` | A react component that renders [a question's layout](#customizing-drill-through-question-layout) shown after drilling through a question or clicking on a question card in the dashboard. |
| drillThroughQuestionProps\* | `[InteractiveQuestionProps](./questions.md#question-props)` | Props for the drill-through question |
_\* Not available for `StaticDashboard`._
_\*\* Combining `initialParameters` and `hiddenParameters` to filter data on the frontend is a [security risk](./authentication.md#security-warning-each-end-user-must-have-their-own-metabase-account). Combining `initialParameters` and `hiddenParameters` to declutter the user interface is fine._
By default, dashboard components take full page height (100vh). You can override this with custom styles passed via `style` or `className` props.
```tsx
{% include_file "{{ dirname }}/snippets/dashboards/custom-height.tsx" snippet="example" %}
```
Docs: [EditableDashboard](./api/EditableDashboard.html)
## Example embedded dashboard with `InteractiveDashboard` component
@@ -59,6 +38,14 @@ By default, dashboard components take full page height (100vh). You can override
{% include_file "{{ dirname }}/snippets/dashboards/interactive-dashboard.tsx" %}
```
## Customizing dashboard height
By default, dashboard components take full page height (100vh). You can override this with custom styles passed via `style` or `className` props.
```tsx
{% include_file "{{ dirname }}/snippets/dashboards/custom-height.tsx" snippet="example" %}
```
## Customizing drill-through question layout
When drilling through or clicking on a question card in the dashboard, you will be taken to the question view. By default, the question is shown in the [default layout](./questions.md#customizing-interactive-questions) for interactive questions.
@@ -127,28 +114,16 @@ Creating a dashboard could be done with `useCreateDashboardApi` hook or `CreateD
### Hook
Docs: [useCreateDashboardApi](./api/useCreateDashboardApi.html)
```typescript
{% include_file "{{ dirname }}/snippets/dashboards/create-dashboard.tsx" snippet="example-hook" %}
```
Props:
| Prop | Type | Description |
| ------------ | -------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| name | `string` | (required) Dashboard title |
| description | `string \| null` | Optional dashboard description |
| collectionId | `number \| 'root' \| 'personal'` | Collection in which to create a new dashboard. You can use predefined system values like `root` or `personal`. |
### Component
Docs: [CreateDashboardModal](./api/CreateDashboardModal.html)
```typescript
{% include_file "{{ dirname }}/snippets/dashboards/create-dashboard.tsx" snippet="example-component" %}
```
Supported component props:
| Prop | Type | Description |
| ------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| collectionId? | `number \| 'root' \| 'personal'` | Initial collection in which to create a dashboard. You can use predefined system values like `root` or `personal`. |
| onCreate | `(dashboard: Dashboard) => void` | Handler to react on dashboard creation. |
| onClose | `() => void` | Handler to close modal component |

View File

@@ -15,10 +15,12 @@ There are different ways you can embed questions:
## Embedding a static question
![Static question](../images/static-question.png)
You can embed a static question using the `StaticQuestion` component.
Docs: [StaticQuestion](./api/StaticQuestion.html)
![Static question](../images/static-question.png)
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.
```typescript
@@ -27,31 +29,16 @@ The component has a default height, which can be customized by using the `height
## Embedding an interactive question
![Interactive question](../images/interactive-question.png)
You can embed an interactive question using the `InteractiveQuestion` component.
Docs: [InteractiveQuestion](./api/InteractiveQuestion.html)
![Interactive question](../images/interactive-question.png)
```typescript
{% include_file "{{ dirname }}/snippets/questions/interactive-question.tsx" %}
```
## Question props
| Prop | Type | Description |
| ----------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `entityTypeFilter` | string array; options include "table", "question", "model", "metric" | (optional) An array that specifies which entity types are available in the data picker |
| `height` | number or string | (optional) A number or string specifying a CSS size value that specifies the height of the component |
| `initialSqlParameters` | `Record<string, string \| string[]>` | (optional) For SQL questions only. A mapping of [SQL parameter names to parameter values](#pass-sql-parameters-to-sql-questions-with-initialsqlparameters), such as `{ product_id: "42"}` |
| `isSaveEnabled` | boolean | (optional, default: `true`) Whether users can save the question |
| `onBeforeSave` | `() => void` | (optional) A callback function that triggers before saving. Only relevant when `isSaveEnabled = true` |
| `onSave` | `() => void` | (optional) A callback function that triggers when a user saves the question. Only relevant when `isSaveEnabled = true` |
| `plugins` | `{ mapQuestionClickActions: Function }` or null | Additional mapper function to override or add drill-down menu |
| `questionId` | number or string | (required) The ID of the question. This is either:<br>- The numerical ID when accessing a question link, e.g., `http://localhost:3000/question/1-my-question` where the ID is `1`<br>- The `entity_id` key of the question object. You can find a question's Entity ID in the info panel when viewing a question<br>- `new` to show the notebook editor for creating new questions. `isSaveEnabled` must be `true` to allow saving the question |
| `targetCollection` | number \| "root" \| "personal" | (optional) The collection to save the question to. This will hide the collection picker from the save modal. Only applicable to interactive questions |
| `title` | boolean \| string \| `ReactNode` \| `() => ReactNode` | (optional) Determines whether the question title is displayed, and allows a custom title to be displayed instead of the default question title. Shown by default. Only applicable to interactive questions when using the default layout |
| `withChartTypeSelector` | boolean | (optional, default: `true`) Determines whether the chart type selector and corresponding settings button are shown. Only relevant when using the default layout |
| `withResetButton` | boolean | (optional, default: `true`) Determines whether a reset button is displayed. Only relevant when using the default layout |
## Pass SQL parameters to SQL questions with `initialSqlParameters`
You can pass parameter values to questions defined with SQL via the `initialSqlParameters` prop, in the format of `{parameter_name: parameter_value}`. Learn more about [SQL parameters](../../questions/native-editor/sql-parameters.md).
@@ -82,210 +69,27 @@ To customize the layout, use namespaced components within the `InteractiveQuesti
These components are available via the `InteractiveQuestion` namespace (e.g., `<InteractiveQuestion.Filter />`).
_\* signifies a required prop_
### `InteractiveQuestion.BackButton`
A navigation button that returns to the previous view. Only renders when `onNavigateBack` prop from `InteractiveQuestion` is available.
Uses [Mantine ActionIcon props](https://v6.mantine.dev/core/action-icon/) under the hood, as well as:
| Prop | Type | Description |
| --------- | ------------------- | ----------------------------------------------- |
| className | string | Custom CSS class name for styling the component |
| style | React.CSSProperties | Inline styles to apply to the component |
### `InteractiveQuestion.Breakout`
A set of badges for managing data groupings (breakouts).
No props. Uses question context for breakout functionality.
### `InteractiveQuestion.BreakoutDropdown`
Dropdown button for the Breakout component.
Uses [Popover props](https://v6.mantine.dev/core/popover/?t=props) except `onClose`, `children`, and `opened` under the hood, as well as:
| Prop | Type | Description |
| --------- | ------------------- | ----------------------------------------------- |
| className | string | Custom CSS class name for styling the component |
| style | React.CSSProperties | Inline styles to apply to the component |
### `InteractiveQuestion.ChartTypeDropdown`
Dropdown for selecting the visualization type (bar chart, line chart, table, etc.). Automatically updates to show recommended visualization types for the current data.
Uses [Mantine Menu props](https://v6.mantine.dev/core/menu/?t=props) under the hood, as well as:
| Prop | Type | Description |
| --------- | ------------------- | ----------------------------------------------- |
| className | string | Custom CSS class name for styling the component |
| style | React.CSSProperties | Inline styles to apply to the component |
### `InteractiveQuestion.ChartTypeSelector`
Detailed chart type selection interface with recommended visualization options.
Uses [Mantine Stack props](https://v6.mantine.dev/core/stack/?t=props) under the hood, as well as:
| Prop | Type | Description |
| --------- | ------------------- | ----------------------------------------------- |
| className | string | Custom CSS class name for styling the component |
| style | React.CSSProperties | Inline styles to apply to the component |
### `InteractiveQuestion.Editor`
Advanced query editor that provides full access to question configuration. Includes filtering, aggregation, custom expressions, and joins.
_Replaces deprecated `InteractiveQuestion.Notebook`_
| Prop | Type | Description |
| ------- | ---------- | --------------------------------------------------- |
| onApply | () => void | Callback function executed when changes are applied |
### `InteractiveQuestion.EditorButton`
_Replaces deprecated `InteractiveQuestion.NotebookButton`_
Toggle button for showing/hiding the Editor interface. In custom layouts, the `EditorButton` _must_ have an `onClick` handler or the button won't do anything when clicked.
Uses [Mantine ActionIcon props](https://v6.mantine.dev/core/action-icon/) under the hood, as well as:
| Prop | Type | Description |
| --------- | ------------------- | ----------------------------------------------- |
| isOpen | boolean | Whether the editor is currently open |
| className | string | Custom CSS class name for styling the component |
| style | React.CSSProperties | Inline styles to apply to the component |
### `InteractiveQuestion.Filter`
A set of interactive filter badges that allow adding, editing, and removing filters. Displays current filters as badges with an "Add another filter" option.
| Prop | Type | Description |
| ------------------ | ------- | ---------------------------------------------------- |
| withColumnItemIcon | boolean | Whether to show column icons in the filter interface |
### `InteractiveQuestion.FilterDropdown`
A dropdown button for the Filter component.
| Prop | Type | Description |
| ------------------ | ------- | ---------------------------------------------------- |
| withColumnItemIcon | boolean | Whether to show column icons in the filter interface |
### `InteractiveQuestion.QuestionSettings`
Settings panel for configuring visualization options like axes, colors, and formatting.
No props. Uses question context for settings.
### `InteractiveQuestion.QuestionSettingsDropdown`
Dropdown button that contains the QuestionSettings component.
Uses [Popover props](https://v6.mantine.dev/core/popover/?t=props) except `onClose` and `opened` under the hood, as well as:
| Prop | Type | Description |
| --------- | ----------------------------- | ----------------------------------------------- |
| height | React.CSSProperties["height"] | Height for the dropdown menu |
| className | string | Custom CSS class name for styling the component |
| style | React.CSSProperties | Inline styles to apply to the component |
### `InteractiveQuestion.QuestionVisualization`
The main visualization component that renders the question results as a chart, table, or other visualization type.
| Prop | Type | Description |
| --------- | ------------------- | ----------------------------------------------- |
| height | number \| string | Height for visualization |
| width | number \| string | Width for visualization |
| className | string | Custom CSS class name for styling the component |
| style | React.CSSProperties | Inline styles to apply to the component |
### `InteractiveQuestion.ResetButton`
Button to reset question modifications. Only appears when there are unsaved changes to the question.
Uses [Mantine Button props](https://v6.mantine.dev/core/button/?t=props) under the hood, as well as:
| Prop | Type | Description |
| --------- | ------------------- | ----------------------------------------------- |
| className | string | Custom CSS class name for styling the component |
| style | React.CSSProperties | Inline styles to apply to the component |
### `InteractiveQuestion.SaveButton`
Button for saving question changes. Only enabled when there are unsaved modifications to the question.
_Note_: Currently, in custom layouts, the `SaveButton` must have an `onClick` handler or the button will not do anything when clicked.
Uses [Mantine Button props](https://v6.mantine.dev/core/button/?t=props) under the hood, as well as:
| Prop | Type | Description |
| --------- | ------------------- | ----------------------------------------------- |
| className | string | Custom CSS class name for styling the component |
| style | React.CSSProperties | Inline styles to apply to the component |
### `InteractiveQuestion.SaveQuestionForm`
Form for saving a question, including title and description. When saved:
- For new questions: Calls `onCreate` prop from InteractiveQuestion
- For existing questions: Calls `onSave` prop from InteractiveQuestion
- Both callbacks receive the updated question object
- Form can be cancelled via the `onCancel` prop
| Prop | Type | Description |
| -------- | ---------- | ------------------------------------------------- |
| onCancel | () => void | Callback function executed when save is cancelled |
### `InteractiveQuestion.Summarize`
Interface for adding and managing data summaries (like counts, sums, averages). Displays as a set of badges.
No props. Uses question context for summarization functionality.
### `InteractiveQuestion.SummarizeDropdown`
Dropdown button for the Summarize component.
Uses [Popover props](https://v6.mantine.dev/core/popover/?t=props) except `onClose`, `children`, and `opened` under the hood, as well as:
| Prop | Type | Description |
| --------- | ------------------- | ----------------------------------------------- |
| className | string | Custom CSS class name for styling the component |
| style | React.CSSProperties | Inline styles to apply to the component |
### InteractiveQuestion.DownloadWidget
Provides a UI widget for downloading data in different formats (`CSV`, `XLSX`, `JSON`, and `PNG` depending on the visualization).
No props
### InteractiveQuestion.DownloadWidgetDropdown
Provides a button that contains a dropdown that shows the `DownloadWidget`.
Uses [Popover props](https://mantine.dev/core/popover/?t=props) under the hood, as well as:
| Prop | Type | Description |
| --------- | ------------------- | ----------------------------------------------- |
| className | string | Custom CSS class name for styling the component |
| style | React.CSSProperties | Inline styles to apply to the component |
### `InteractiveQuestion.Title`
Displays a title based on the question's state. Shows:
- The question's display name if it's saved
- An auto-generated description for ad-hoc questions (non-native queries)
- "New question" as fallback or for new/native queries
| Prop | Type | Description |
| --------- | ------------- | ----------------------------------------------- |
| className | string | Custom CSS class name for styling the component |
| style | CSSProperties | Inline styles to apply to the component |
Docs:
- [InteractiveQuestion.BackButton](./api/InteractiveQuestion.html#backbutton)
- [InteractiveQuestion.Breakout](./api/InteractiveQuestion.html#breakout)
- [InteractiveQuestion.BreakoutDropdown](./api/InteractiveQuestion.html#breakoutdropdown)
- [InteractiveQuestion.ChartTypeDropdown](./api/InteractiveQuestion.html#charttypedropdown)
- [InteractiveQuestion.ChartTypeSelector](./api/InteractiveQuestion.html#charttypeselector)
- [InteractiveQuestion.Editor](./api/InteractiveQuestion.html#editor)
- [InteractiveQuestion.EditorButton](./api/InteractiveQuestion.html#editorbutton)
- [InteractiveQuestion.Filter](./api/InteractiveQuestion.html#filter)
- [InteractiveQuestion.FilterDropdown](./api/InteractiveQuestion.html#filterdropdown)
- [InteractiveQuestion.QuestionSettings](./api/InteractiveQuestion.html#questionsettings)
- [InteractiveQuestion.QuestionSettingsDropdown](./api/InteractiveQuestion.html#questionsettingsdropdown)
- [InteractiveQuestion.QuestionVisualization](./api/InteractiveQuestion.html#questionvisualization)
- [InteractiveQuestion.ResetButton](./api/InteractiveQuestion.html#resetbutton)
- [InteractiveQuestion.SaveButton](./api/InteractiveQuestion.html#savebutton)
- [InteractiveQuestion.SaveQuestionForm](./api/InteractiveQuestion.html#savequestionform)
- [InteractiveQuestion.Summarize](./api/InteractiveQuestion.html#summarize)
- [InteractiveQuestion.SummarizeDropdown](./api/InteractiveQuestion.html#summarizedropdown)
- [InteractiveQuestion.DownloadWidget](./api/InteractiveQuestion.html#downloadwidget)
- [InteractiveQuestion.DownloadWidgetDropdown](./api/InteractiveQuestion.html#downloadwidgetdropdown)
- [InteractiveQuestion.Title](./api/InteractiveQuestion.html#title)
## Interactive question plugins

View File

@@ -1,16 +1,27 @@
import type { CSSProperties, PropsWithChildren } from "react";
import type { CommonStylingProps } from "embedding-sdk/types/props";
import { Box } from "metabase/ui";
export const FLEXIBLE_SIZE_DEFAULT_HEIGHT = "600px";
export const FLEXIBLE_SIZE_DEFAULT_WIDTH = "100%";
export type FlexibleSizeProps = PropsWithChildren<{
height?: CSSProperties["height"];
width?: CSSProperties["width"];
className?: string;
style?: CSSProperties;
}>;
/**
* @interface
*/
export type FlexibleSizeProps = PropsWithChildren<
CommonStylingProps & {
/**
* A number or string specifying a CSS size value that specifies the width of the component
*/
width?: CSSProperties["width"];
/**
* A number or string specifying a CSS size value that specifies the height of the component
*/
height?: CSSProperties["height"];
}
>;
export const FlexibleSizeComponent = ({
height: propHeight = FLEXIBLE_SIZE_DEFAULT_HEIGHT,

View File

@@ -1,6 +1,6 @@
import { useMemo } from "react";
import type { InteractiveQuestionProps } from "embedding-sdk";
import type { BaseInteractiveQuestionProps } from "embedding-sdk/components/public/InteractiveQuestion";
import * as Urls from "metabase/lib/urls";
import { deserializeCard, parseHash } from "metabase/query_builder/actions";
@@ -38,7 +38,7 @@ export const InteractiveAdHocQuestion = ({
initialSqlParameters,
onNavigateBack,
}: InteractiveAdHocQuestionProps &
Omit<InteractiveQuestionProps, "questionId"> &
Omit<BaseInteractiveQuestionProps, "questionId"> &
InteractiveQuestionDefaultViewProps) => {
const { location, params } = useMemo(
() => getQuestionParameters(questionPath),

View File

@@ -5,9 +5,25 @@ import {
import { useInteractiveQuestionContext } from "../../context";
export const BackButton = (
actionIconProps: Omit<DashboardBackButtonProps, "noLink" | "onClick">,
) => {
/**
* @category InteractiveQuestion
*/
export type InteractiveQuestionBackButtonProps = Omit<
DashboardBackButtonProps,
"noLink" | "onClick"
>;
/**
* A navigation button that returns to the previous view.
* Only visible when rendered within the {@link InteractiveDashboardProps.renderDrillThroughQuestion} prop.
*
* @function
* @category InteractiveQuestion
* @param props
*/
export const BackButton = ({
...actionIconProps
}: InteractiveQuestionBackButtonProps) => {
const { onNavigateBack } = useInteractiveQuestionContext();
if (!onNavigateBack) {

View File

@@ -51,6 +51,13 @@ export const BreakoutInner = ({
);
};
/**
* A set of badges for managing data groupings (breakouts).
* Uses question context for breakout functionality.
*
* @function
* @category InteractiveQuestion
*/
export const Breakout = () => {
const { question, updateQuestion } = useInteractiveQuestionContext();

View File

@@ -15,7 +15,10 @@ import { BreakoutBadgeList } from "../BreakoutBadgeList";
import { BreakoutPicker } from "../BreakoutPicker";
import { type SDKBreakoutItem, useBreakoutData } from "../use-breakout-data";
type BreakoutDropdownProps = Omit<
/**
* @category InteractiveQuestion
*/
export type InteractiveQuestionBreakoutDropdownProps = Omit<
PopoverProps,
"children" | "onClose" | "opened"
>;
@@ -24,7 +27,7 @@ export const BreakoutDropdownInner = ({
question,
updateQuestion,
...popoverProps
}: QuestionStateParams & BreakoutDropdownProps) => {
}: QuestionStateParams & InteractiveQuestionBreakoutDropdownProps) => {
const items = useBreakoutData({ question, updateQuestion });
const [step, setStep] = useState<MultiStepState<"picker" | "list">>(null);
@@ -90,7 +93,16 @@ export const BreakoutDropdownInner = ({
);
};
export const BreakoutDropdown = (props: BreakoutDropdownProps) => {
/**
* Dropdown button for the Breakout component.
*
* @function
* @category InteractiveQuestion
* @param props
*/
export const BreakoutDropdown = (
props: InteractiveQuestionBreakoutDropdownProps,
) => {
const { question, updateQuestion } = useInteractiveQuestionContext();
if (!question) {

View File

@@ -5,7 +5,21 @@ import type { StackProps } from "metabase/ui";
import { useSensibleVisualizations } from "../hooks/use-sensible-visualizations";
export const ChartTypeSelector = (stackProps: StackProps) => {
/**
* @category InteractiveQuestion
*/
export type InteractiveQuestionChartTypeSelectorProps = StackProps;
/**
* Detailed chart type selection interface with recommended visualization options.
*
* @function
* @category InteractiveQuestion
* @param props
*/
export const ChartTypeSelector = ({
...stackProps
}: InteractiveQuestionChartTypeSelectorProps) => {
const { sensibleVisualizations, nonSensibleVisualizations } =
useSensibleVisualizations();

View File

@@ -12,7 +12,22 @@ import { useSensibleVisualizations } from "../../hooks/use-sensible-visualizatio
import ToolbarButtonS from "../../styles/ToolbarButton.module.css";
import { ToolbarButton } from "../util/ToolbarButton";
export const ChartTypeDropdown = (menuProps: MenuProps) => {
/**
* @category InteractiveQuestion
*/
export type InteractiveQuestionChartTypeDropdownProps = MenuProps;
/**
* Dropdown for selecting the visualization type (bar chart, line chart, table, etc.).
* Automatically updates to show recommended visualization types for the current data.
*
* @function
* @category InteractiveQuestion
* @param props
*/
export const ChartTypeDropdown = ({
...menuProps
}: InteractiveQuestionChartTypeDropdownProps) => {
const { selectedVisualization, updateQuestionVisualization } =
useQuestionVisualization();

View File

@@ -2,7 +2,10 @@ import { InteractiveQuestion } from "embedding-sdk";
import { CommonSdkStoryWrapper } from "embedding-sdk/test/CommonSdkStoryWrapper";
import { Box, type PopoverProps } from "metabase/ui";
import { DownloadWidget, type DownloadWidgetProps } from "./DownloadWidget";
import {
DownloadWidget,
type InteractiveQuestionDownloadWidgetProps,
} from "./DownloadWidget";
const QUESTION_ID = (window as any).QUESTION_ID || 12;
@@ -19,7 +22,7 @@ export default {
};
export const Default = {
render(args: DownloadWidgetProps) {
render(args: InteractiveQuestionDownloadWidgetProps) {
return (
<Box p="lg">
<InteractiveQuestion questionId={QUESTION_ID}>

View File

@@ -8,13 +8,16 @@ import type { StackProps } from "metabase/ui";
import { useInteractiveQuestionContext } from "../../context";
// TODO: Add props for formatting, file type, etc
export interface DownloadWidgetProps extends StackProps {}
/**
* @category InteractiveQuestion
*/
export type InteractiveQuestionDownloadWidgetProps = StackProps;
const DownloadWidgetInner = ({
question,
result,
...rest
}: DownloadWidgetProps &
}: InteractiveQuestionDownloadWidgetProps &
Pick<UseDownloadDataParams, "question" | "result">) => {
const { withDownloads } = useInteractiveQuestionContext();
const [, handleDownload] = useDownloadData({
@@ -34,7 +37,16 @@ const DownloadWidgetInner = ({
);
};
export const DownloadWidget = (props: DownloadWidgetProps) => {
/**
* Provides a UI widget for downloading data in different formats (`CSV`, `XLSX`, `JSON`, and `PNG` depending on the visualization).
*
* @function
* @category InteractiveQuestion
* @param props
*/
export const DownloadWidget = (
props: InteractiveQuestionDownloadWidgetProps,
) => {
const { question, queryResults } = useInteractiveQuestionContext();
const [result] = queryResults ?? [];

View File

@@ -4,7 +4,21 @@ import { Center, Icon, Popover, type PopoverProps } from "metabase/ui";
import { useInteractiveQuestionContext } from "../../context";
import { ToolbarButton } from "../util/ToolbarButton";
export const DownloadWidgetDropdown = (popoverProps: PopoverProps) => {
/**
* @category InteractiveQuestion
*/
export type InteractiveQuestionDownloadWidgetDropdownProps = PopoverProps;
/**
* Provides a button that contains a dropdown that shows the `DownloadWidget`.
*
* @function
* @category InteractiveQuestion
* @param props
*/
export const DownloadWidgetDropdown = ({
...popoverProps
}: InteractiveQuestionDownloadWidgetDropdownProps) => {
const { withDownloads } = useInteractiveQuestionContext();
return (
<Popover

View File

@@ -12,9 +12,28 @@ import { getSetting } from "metabase/selectors/settings";
import { ScrollArea } from "metabase/ui";
import type Question from "metabase-lib/v1/Question";
type EditorProps = { onApply?: () => void };
/**
* @interface
* @category InteractiveQuestion
*/
export type InteractiveQuestionEditorProps = {
/**
* Callback function executed when changes are applied
*/
onApply?: () => void;
};
export const Editor = ({ onApply = () => {} }: EditorProps) => {
/**
* Advanced query editor that provides full access to question configuration.
* Includes filtering, aggregation, custom expressions, and joins.
*
* @function
* @category InteractiveQuestion
* @param props
*/
export const Editor = ({
onApply = () => {},
}: InteractiveQuestionEditorProps) => {
// Loads databases and metadata so we can show notebook steps for the selected data source
useDatabaseListQuery();

View File

@@ -2,7 +2,10 @@ import { InteractiveQuestion } from "embedding-sdk";
import { CommonSdkStoryWrapper } from "embedding-sdk/test/CommonSdkStoryWrapper";
import { Box } from "metabase/ui";
import { EditorButton, type EditorButtonProps } from "./EditorButton";
import {
EditorButton,
type InteractiveQuestionEditorButtonProps,
} from "./EditorButton";
const QUESTION_ID = (window as any).QUESTION_ID || 12;
@@ -19,7 +22,7 @@ export default {
};
export const Default = {
render(args: EditorButtonProps) {
render(args: InteractiveQuestionEditorButtonProps) {
return (
<Box p="lg">
<InteractiveQuestion questionId={QUESTION_ID}>

View File

@@ -1,4 +1,4 @@
import type { HTMLAttributes } from "react";
import type { HTMLAttributes, MouseEventHandler } from "react";
import { useInteractiveQuestionContext } from "embedding-sdk/components/private/InteractiveQuestion/context";
import { QuestionNotebookButton } from "metabase/query_builder/components/view/ViewHeader/components";
@@ -6,15 +6,36 @@ import { ActionIcon, type ActionIconProps, Icon } from "metabase/ui";
import S from "./EditorButton.module.css";
export type EditorButtonProps = {
/**
* @interface
* @category InteractiveQuestion
*/
export type InteractiveQuestionEditorButtonProps = {
/**
* Whether the editor is currently open
* @defaultValue false
*/
isOpen?: boolean;
/**
* Callback function to be called when the button is clicked
*/
onClick?: MouseEventHandler<HTMLButtonElement>;
} & ActionIconProps &
HTMLAttributes<HTMLButtonElement>;
/**
* Toggle button for showing/hiding the Editor interface.
* In custom layouts, the `EditorButton` _must_ have an {@link InteractiveQuestionEditorButtonProps.onClick}` handler or the button won't do anything when clicked.
*
* @function
* @category InteractiveQuestion
* @param props
*/
export const EditorButton = ({
isOpen = false,
...actionIconProps
}: EditorButtonProps) => {
}: InteractiveQuestionEditorButtonProps) => {
const { question } = useInteractiveQuestionContext();
return (
question &&

View File

@@ -4,16 +4,16 @@ import { Group, Popover } from "metabase/ui";
import { BadgeListItem } from "../util/BadgeList/BadgeListItem";
import type { InteractiveQuestionFilterProps } from "./Filter";
import { FilterPicker } from "./FilterPicker";
import { type SDKFilterItem, useFilterData } from "./hooks/use-filter-data";
import type { FilterProps } from "./types";
const DropdownFilterBadgeListContent = ({
item,
withColumnItemIcon,
}: {
item: SDKFilterItem;
} & FilterProps) => {
} & InteractiveQuestionFilterProps) => {
const [opened, { open, close }] = useDisclosure(false);
return (
@@ -39,7 +39,7 @@ const DropdownFilterBadgeListContent = ({
export const DropdownFilterBadgeList = ({
withColumnItemIcon,
}: FilterProps) => {
}: InteractiveQuestionFilterProps) => {
const filterItems = useFilterData();
return (

View File

@@ -7,7 +7,16 @@ import { AddBadgeListItem } from "../util/BadgeList/AddBadgeListItem";
import { DropdownFilterBadgeList } from "./DropdownFilterBadgeList";
import { FilterPicker } from "./FilterPicker";
import type { FilterProps } from "./types";
/**
* @category InteractiveQuestion
*/
export type InteractiveQuestionFilterProps = {
/**
* Whether to show the icon for the column item
*/
withColumnItemIcon?: boolean;
};
const AddFilterPopover = () => {
const [opened, { close, toggle }] = useDisclosure();
@@ -24,7 +33,17 @@ const AddFilterPopover = () => {
);
};
export const Filter = ({ withColumnItemIcon }: FilterProps) => (
/**
* A set of interactive filter badges that allow adding, editing, and removing filters.
* Displays current filters as badges with an "Add another filter" option.
*
* @function
* @category InteractiveQuestion
* @param props
*/
export const Filter = ({
withColumnItemIcon,
}: InteractiveQuestionFilterProps) => (
<Group>
<DropdownFilterBadgeList withColumnItemIcon={withColumnItemIcon} />
<AddFilterPopover />

View File

@@ -9,7 +9,6 @@ import {
} from "embedding-sdk/components/private/util/MultiStepPopover";
import type { UpdateQueryHookProps } from "metabase/query_builder/hooks";
import { getFilterItems } from "metabase/querying/filters/components/FilterPanel/utils";
import type { FilterColumnPickerProps } from "metabase/querying/filters/components/FilterPicker/FilterColumnPicker";
import type { PopoverProps } from "metabase/ui";
import { useInteractiveQuestionContext } from "../../../context";
@@ -17,14 +16,22 @@ import { ToolbarButton } from "../../util/ToolbarButton";
import { FilterBadgeList } from "../FilterBadgeList";
import { FilterPicker } from "../FilterPicker/FilterPicker";
type FilterProps = Pick<FilterColumnPickerProps, "withColumnItemIcon">;
/**
* @category InteractiveQuestion
*/
export type InteractiveQuestionFilterDropdownProps = {
/**
* Whether to show the icon for the column item
*/
withColumnItemIcon?: boolean;
};
const FilterDropdownInner = ({
query,
withColumnItemIcon,
...popoverProps
}: Pick<UpdateQueryHookProps, "query"> &
FilterProps &
InteractiveQuestionFilterDropdownProps &
Omit<PopoverProps, "children" | "onClose" | "opened">) => {
const filters = useMemo(() => getFilterItems(query), [query]);
@@ -94,7 +101,16 @@ const FilterDropdownInner = ({
);
};
export const FilterDropdown = ({ withColumnItemIcon }: FilterProps) => {
/**
* A dropdown button for the Filter component.
*
* @function
* @category InteractiveQuestion
* @param props
*/
export const FilterDropdown = ({
withColumnItemIcon,
}: InteractiveQuestionFilterDropdownProps) => {
const { question } = useInteractiveQuestionContext();
if (!question) {

View File

@@ -1,3 +1,4 @@
import type { CommonStylingProps } from "embedding-sdk/types/props";
import type { UpdateQueryHookProps } from "metabase/query_builder/hooks";
import {
FilterPicker as InnerFilterPicker,
@@ -10,14 +11,18 @@ import { useInteractiveQuestionContext } from "../../../context";
import type { SDKFilterItem } from "../hooks/use-filter-data";
import { useFilterHandlers } from "../hooks/use-filter-handlers";
/**
* @interface
*/
type Props = {
className?: string;
filterItem?: SDKFilterItem;
withIcon?: boolean;
} & Pick<InnerFilterPickerProps, "onClose" | "onBack">;
} & CommonStylingProps &
Pick<InnerFilterPickerProps, "onClose" | "onBack">;
const FilterPickerInner = ({
className,
style,
filterItem,
withIcon = false,
onClose,
@@ -32,7 +37,7 @@ const FilterPickerInner = ({
onQueryChange,
});
return (
<Box className={className}>
<Box className={className} style={style}>
<InnerFilterPicker
query={query}
stageIndex={stageIndex}

View File

@@ -1,3 +0,0 @@
import type { FilterColumnPickerProps } from "metabase/querying/filters/components/FilterPicker/FilterColumnPicker";
export type FilterProps = Pick<FilterColumnPickerProps, "withColumnItemIcon">;

View File

@@ -14,6 +14,11 @@ import {
useInteractiveQuestionContext,
} from "../../context";
/**
* @category InteractiveQuestion
*/
export type InteractiveQuestionQuestionSettingsProps = StackProps;
const QuestionSettingsContent = ({
question,
queryResults,
@@ -67,7 +72,17 @@ const QuestionSettingsContent = ({
);
};
export const QuestionSettings = (stackProps: StackProps) => {
/**
* Settings panel for configuring visualization options like axes, colors, and formatting.
* Uses question context for settings.
*
* @function
* @category InteractiveQuestion
* @param props
*/
export const QuestionSettings = ({
...stackProps
}: InteractiveQuestionQuestionSettingsProps) => {
const { question, queryResults, updateQuestion } =
useInteractiveQuestionContext();

View File

@@ -7,12 +7,28 @@ import { Center, Icon, Popover, type PopoverProps } from "metabase/ui";
import ToolbarButtonS from "../../../styles/ToolbarButton.module.css";
import { ToolbarButton } from "../../util/ToolbarButton";
/**
* @interface
* @category InteractiveQuestion
*/
export type InteractiveQuestionQuestionSettingsDropdownProps = {
/**
* Height for the dropdown menu
*/
height?: CSSProperties["height"];
} & Omit<PopoverProps, "children">;
/**
* Dropdown button that contains the QuestionSettings component.
*
* @function
* @category InteractiveQuestion
* @param props
*/
export const QuestionSettingsDropdown = ({
height,
...popoverProps
}: {
height?: CSSProperties["height"];
} & Omit<PopoverProps, "children"> = {}) => (
}: InteractiveQuestionQuestionSettingsDropdownProps = {}) => (
<Popover position="bottom-end" {...popoverProps}>
<Popover.Target>
<ToolbarButton

View File

@@ -7,10 +7,23 @@ import * as Lib from "metabase-lib";
import { useInteractiveQuestionContext } from "../../context";
/**
* @interface
* @category InteractiveQuestion
*/
export type InteractiveQuestionResetButtonProps = ButtonProps;
/**
* Button to reset question modifications. Only appears when there are unsaved changes to the question.
*
* @function
* @category InteractiveQuestion
* @param props
*/
export const QuestionResetButton = ({
onClick,
...buttonProps
}: ButtonProps = {}) => {
}: InteractiveQuestionResetButtonProps = {}) => {
const { question, originalQuestion, onReset } =
useInteractiveQuestionContext();

View File

@@ -1,3 +1,5 @@
import type { MouseEventHandler } from "react";
import type { ButtonProps } from "metabase/ui";
import * as Lib from "metabase-lib";
import type Question from "metabase-lib/v1/Question";
@@ -6,6 +8,16 @@ import { useInteractiveQuestionContext } from "../context";
import { ToolbarButton } from "./util/ToolbarButton";
/**
* @category InteractiveQuestion
*/
export type InteractiveQuestionSaveButtonProps = {
/**
* A handler function to be called when the button is clicked
*/
onClick?: MouseEventHandler<HTMLButtonElement>;
} & ButtonProps;
export const shouldShowSaveButton = ({
question,
originalQuestion,
@@ -21,7 +33,18 @@ export const shouldShowSaveButton = ({
return Boolean(isQuestionChanged && canSave);
};
export const SaveButton = (buttonProps: ButtonProps = {}) => {
/**
* Button for saving question changes. Only enabled when there are unsaved modifications to the question.
*
* _Note_: Currently, in custom layouts, the `SaveButton` must have an `onClick` handler or the button will not do anything when clicked.
*
* @function
* @category InteractiveQuestion
* @param props
*/
export const SaveButton = ({
...buttonProps
}: InteractiveQuestionSaveButtonProps = {}) => {
const { question, originalQuestion } = useInteractiveQuestionContext();
const isSaveButtonEnabled = shouldShowSaveButton({

View File

@@ -8,11 +8,31 @@ import { Stack, Title } from "metabase/ui";
import { useInteractiveQuestionContext } from "../context";
export type SdkSaveQuestionFormProps = {
/**
* @interface
* @category InteractiveQuestion
*/
export type InteractiveQuestionSaveQuestionFormProps = {
/**
* Callback function executed when save is cancelled
*/
onCancel?: () => void;
};
export const SdkSaveQuestionForm = ({ onCancel }: SdkSaveQuestionFormProps) => {
/**
* Form for saving a question, including title and description. When saved:
*
* - For existing questions: Calls {@link InteractiveQuestionProps.onSave}
* - Both callbacks receive the updated question object
* - Form can be cancelled via the {@link InteractiveQuestionSaveQuestionFormProps.onCancel}
*
* @function
* @category InteractiveQuestion
* @param props
*/
export const SdkSaveQuestionForm = ({
onCancel,
}: InteractiveQuestionSaveQuestionFormProps) => {
const { question, originalQuestion, onSave, onCreate, targetCollection } =
useInteractiveQuestionContext();

View File

@@ -40,6 +40,13 @@ const SummarizePopover = ({
);
};
/**
* Interface for adding and managing data summaries (like counts, sums, averages). Displays as a set of badges.
* Uses question context for summarization functionality.
*
* @function
* @category InteractiveQuestion
*/
export const Summarize = () => {
const items = useSummarizeData();

View File

@@ -16,9 +16,24 @@ import {
useSummarizeData,
} from "../use-summarize-data";
export const SummarizeDropdown = (
popoverProps: Omit<PopoverProps, "children" | "onClose" | "opened">,
) => {
/**
* @category InteractiveQuestion
*/
export type InteractiveQuestionSummarizeDropdownProps = Omit<
PopoverProps,
"children" | "onClose" | "opened"
>;
/**
* Dropdown button for the Summarize component.
*
* @function
* @category InteractiveQuestion
* @param props
*/
export const SummarizeDropdown = ({
...popoverProps
}: InteractiveQuestionSummarizeDropdownProps) => {
const aggregationItems = useSummarizeData();
const label = match(aggregationItems.length)

View File

@@ -1,15 +1,25 @@
import type { CSSProperties } from "react";
import { useInteractiveQuestionContext } from "embedding-sdk/components/private/InteractiveQuestion/context";
import { QuestionTitle } from "embedding-sdk/components/private/QuestionTitle";
import type { CommonStylingProps } from "embedding-sdk/types/props";
export const Title = ({
className,
style,
}: {
className?: string;
style?: CSSProperties;
}) => {
/**
* @interface
* @category InteractiveQuestion
*/
export type InteractiveQuestionTitleProps = CommonStylingProps;
/**
* Displays a title based on the question's state. Shows:
*
* - The question's display name if it's saved
* - An auto-generated description for ad-hoc questions (non-native queries)
* - "New question" as fallback or for new/native queries
*
* @function
* @category InteractiveQuestion
* @param props
*/
export const Title = ({ className, style }: InteractiveQuestionTitleProps) => {
const { question } = useInteractiveQuestionContext();
return (

View File

@@ -15,12 +15,25 @@ import type Question from "metabase-lib/v1/Question";
import { useInteractiveQuestionContext } from "../context";
/**
* @interface
* @category InteractiveQuestion
*/
export type InteractiveQuestionQuestionVisualizationProps = FlexibleSizeProps;
/**
* The main visualization component that renders the question results as a chart, table, or other visualization type.
*
* @function
* @category InteractiveQuestion
* @param props
*/
export const QuestionVisualization = ({
height,
width,
className,
style,
}: FlexibleSizeProps) => {
}: InteractiveQuestionQuestionVisualizationProps) => {
const {
question,
queryResults,

View File

@@ -18,26 +18,56 @@ export type EntityTypeFilterKeys = "table" | "question" | "model" | "metric";
export type ParameterValues = Record<ParameterId, string | number>;
type InteractiveQuestionConfig = {
/**
* An array that specifies which entity types are available in the data picker
*/
entityTypeFilter?: EntityTypeFilterKeys[];
/**
* Whether to show the save button.
**/
isSaveEnabled?: boolean;
/**
* Initial values for the SQL parameters.
**/
initialSqlParameters?: ParameterValues;
/**
* Enables the ability to download results in the interactive question.
*/
withDownloads?: boolean;
/**
* The collection to save the question to. This will hide the collection picker from the save modal. Only applicable to interactive questions.
*/
targetCollection?: SdkCollectionId;
/**
* Additional mapper function to override or add drill-down menu
*/
componentPlugins?: MetabasePluginsConfig;
onNavigateBack?: () => void;
/**
* A callback function that triggers before saving. Only relevant when `isSaveEnabled = true`
*/
onBeforeSave?: (
question: MetabaseQuestion | undefined,
context: { isNewQuestion: boolean },
) => Promise<void>;
/**
* A callback function that triggers when a user saves the question. Only relevant when `isSaveEnabled = true`
*/
onSave?: (
question: MetabaseQuestion | undefined,
context: { isNewQuestion: boolean },
) => void;
entityTypeFilter?: EntityTypeFilterKeys[];
/** Is the save question button visible? */
isSaveEnabled?: boolean;
/** Initial values for the SQL parameters */
initialSqlParameters?: ParameterValues;
withDownloads?: boolean;
targetCollection?: SdkCollectionId;
/**
* A callback function that triggers when a user clicks the back button.
*/
onNavigateBack?: () => void;
};
export type QuestionMockLocationParameters = {

View File

@@ -33,8 +33,19 @@ import { DefaultViewTitle } from "./DefaultViewTitle";
import InteractiveQuestionS from "./InteractiveQuestionDefaultView.module.css";
export interface InteractiveQuestionDefaultViewProps extends FlexibleSizeProps {
/**
* Determines whether the question title is displayed, and allows a custom title to be displayed instead of the default question title. Shown by default. Only applicable to interactive questions when using the default layout.
*/
title?: SdkQuestionTitleProps;
/**
* Determines whether a reset button is displayed. Only relevant when using the default layout.
*/
withResetButton?: boolean;
/**
* Determines whether the chart type selector and corresponding settings button are shown. Only relevant when using the default layout.
*/
withChartTypeSelector?: boolean;
}

View File

@@ -1,4 +1,4 @@
import React, { type CSSProperties } from "react";
import React from "react";
import { t } from "ttag";
import { PublicComponentStylesWrapper } from "embedding-sdk/components/private/PublicComponentStylesWrapper";
@@ -6,14 +6,14 @@ import { SdkError } from "embedding-sdk/components/private/PublicComponentWrappe
import { SdkLoader } from "embedding-sdk/components/private/PublicComponentWrapper/SdkLoader";
import { useSdkSelector } from "embedding-sdk/store";
import { getLoginStatus, getUsageProblem } from "embedding-sdk/store/selectors";
import type { CommonStylingProps } from "embedding-sdk/types/props";
import { RenderOnlyInSdkProvider } from "../SdkContext";
export type PublicComponentWrapperProps = {
children: React.ReactNode;
className?: string;
style?: CSSProperties;
};
} & CommonStylingProps;
const PublicComponentWrapperInner = React.forwardRef<
HTMLDivElement,
PublicComponentWrapperProps

View File

@@ -1,14 +1,15 @@
import { useSdkSelector } from "embedding-sdk/store";
import { getLoaderComponent } from "embedding-sdk/store/selectors";
import type { CommonStylingProps } from "embedding-sdk/types/props";
import { Center, Loader } from "metabase/ui";
export const SdkLoader = ({ className }: { className?: string }) => {
export const SdkLoader = ({ className, style }: CommonStylingProps) => {
const CustomLoader = useSdkSelector(getLoaderComponent);
const LoaderComponent = CustomLoader || Loader;
return (
<Center className={className} h="100%" w="100%" mx="auto">
<Center className={className} style={style} h="100%" w="100%" mx="auto">
<LoaderComponent data-testid="loading-indicator" />
</Center>
);

View File

@@ -4,7 +4,7 @@ import { PublicComponentWrapper } from "./PublicComponentWrapper";
export function withPublicComponentWrapper<P extends object>(
WrappedComponent: React.ComponentType<P>,
): React.FC<P> {
): (props: P) => React.ReactNode {
const WithPublicComponentWrapper: React.FC<P> = (props) => {
return (
<PublicComponentWrapper>

View File

@@ -2,11 +2,13 @@ import { useDisclosure } from "@mantine/hooks";
import { useState } from "react";
import { t } from "ttag";
import { InteractiveQuestion } from "embedding-sdk/components/public/InteractiveQuestion";
import {
type BaseInteractiveQuestionProps,
InteractiveQuestion,
} from "embedding-sdk/components/public/InteractiveQuestion";
import { SaveQuestionModal } from "metabase/containers/SaveQuestionModal";
import { Box, Button, Group, Icon, Stack, Tabs } from "metabase/ui";
import type { InteractiveQuestionProps } from "../../public/InteractiveQuestion";
import { FlexibleSizeComponent } from "../FlexibleSizeComponent";
import { useInteractiveQuestionContext } from "../InteractiveQuestion/context";
@@ -144,7 +146,7 @@ export const QuestionEditor = ({
plugins,
entityTypeFilter,
targetCollection,
}: InteractiveQuestionProps) => (
}: BaseInteractiveQuestionProps) => (
<InteractiveQuestion
questionId={questionId}
plugins={plugins}

View File

@@ -1,7 +1,7 @@
import cx from "classnames";
import type { CSSProperties } from "react";
import { t } from "ttag";
import type { CommonStylingProps } from "embedding-sdk/types/props";
import CS from "metabase/css/core/index.css";
import {
getAdHocQuestionDescription,
@@ -42,10 +42,7 @@ export const getQuestionTitle = ({
return t`New question`;
};
type QuestionTitleProps = GetQuestionTitleProps & {
className?: string;
style?: CSSProperties;
};
type QuestionTitleProps = GetQuestionTitleProps & CommonStylingProps;
export const QuestionTitle = ({
question,

View File

@@ -29,7 +29,7 @@ export const RenderOnlyInSdkProvider = ({
export function renderOnlyInSdkProvider<P extends object>(
Component: React.ComponentType<P>,
) {
): (props: P) => React.ReactNode {
const WithRenderOnlyInSdkProvider = (props: P) => (
<RenderOnlyInSdkProvider>
<Component {...props} />

View File

@@ -1,9 +1,4 @@
import {
type CSSProperties,
type ComponentType,
useEffect,
useState,
} from "react";
import { type ComponentType, useEffect, useState } from "react";
import {
CollectionNotFoundError,
@@ -14,6 +9,7 @@ import { useTranslatedCollectionId } from "embedding-sdk/hooks/private/use-trans
import { getCollectionIdSlugFromReference } from "embedding-sdk/store/collections";
import { useSdkSelector } from "embedding-sdk/store/use-sdk-selector";
import type { SdkCollectionId } from "embedding-sdk/types/collection";
import type { CommonStylingProps } from "embedding-sdk/types/props";
import { COLLECTION_PAGE_SIZE } from "metabase/collections/components/CollectionContent";
import { CollectionItemsTable } from "metabase/collections/components/CollectionContent/CollectionItemsTable";
import { isNotNull } from "metabase/lib/types";
@@ -57,16 +53,41 @@ const ENTITY_NAME_MAP: Partial<
model: "dataset",
};
/**
* @interface
* @category CollectionBrowser
*/
export type CollectionBrowserProps = {
/**
* The numerical ID of the collection, "personal" for the user's personal collection, or "root" for the root collection. You can find this ID in the URL when accessing a collection in your Metabase instance. For example, the collection ID in `http://localhost:3000/collection/1-my-collection` would be `1`. Defaults to "personal"
*/
collectionId?: SdkCollectionId;
onClick?: (item: CollectionItem) => void;
/**
* The number of items to display per page. The default is 25.
*/
pageSize?: number;
/**
* The types of entities that should be visible. If not provided, all entities will be shown.
*/
visibleEntityTypes?: UserFacingEntityName[];
EmptyContentComponent?: ComponentType | null;
/**
* The columns to display in the collection items table. If not provided, all columns will be shown.
*/
visibleColumns?: CollectionBrowserListColumns[];
className?: string;
style?: CSSProperties;
};
/**
* A component to display when there are no items in the collection.
*/
EmptyContentComponent?: ComponentType | null;
/**
* A function to call when an item is clicked.
*/
onClick?: (item: CollectionItem) => void;
} & CommonStylingProps;
export const CollectionBrowserInner = ({
collectionId = "personal",
@@ -147,6 +168,12 @@ const CollectionBrowserWrapper = ({
return <CollectionBrowserInner collectionId={id} {...restProps} />;
};
/**
* A component that allows you to browse collections and their items.
*
* @function
* @category CollectionBrowser
*/
export const CollectionBrowser = withPublicComponentWrapper(
CollectionBrowserWrapper,
);

View File

@@ -11,10 +11,28 @@ import Collections from "metabase/entities/collections";
import type { Dashboard } from "metabase-types/api";
import type { State } from "metabase-types/store";
/**
* @category CreateDashboardModal
*/
export interface CreateDashboardModalProps {
/**
* Initial collection in which to create a dashboard. You can use predefined system values like `root` or `personal`.
*/
initialCollectionId?: SdkCollectionId;
/**
* Whether the modal is open or not.
*/
isOpen?: boolean;
/**
* Handler to react on dashboard creation.
*/
onCreate: (dashboard: Dashboard) => void;
/**
* Handler to close modal component
*/
onClose?: () => void;
}
@@ -50,6 +68,12 @@ const CreateDashboardModalCoreWithLoading = _.compose(
}),
)(CreateDashboardModalCore);
/**
* Creates a dashboard
*
* @function
* @category CreateDashboardModal
*/
export const CreateDashboardModal = withPublicComponentWrapper(
CreateDashboardModalInner,
);

View File

@@ -8,14 +8,26 @@ import type { CreateDashboardProperties } from "metabase/dashboard/containers/Cr
export interface CreateDashboardValues
extends Omit<CreateDashboardProperties, "collection_id"> {
/**
* Collection in which to create a new dashboard. You can use predefined system values like `root` or `personal`.
*/
collectionId: SdkCollectionId;
}
/**
* Creates a dashboard
*
* @function
* @category CreateDashboardModal
*/
export const useCreateDashboardApi = () => {
const store = useSdkStore();
const [createDashboard] = useCreateDashboardMutation();
/**
* @function
*/
const handleCreateDashboard = useCallback(
async ({ collectionId = "personal", ...rest }: CreateDashboardValues) => {
const realCollectionId = getCollectionNumericIdFromReference(
@@ -32,6 +44,9 @@ export const useCreateDashboardApi = () => {
);
return {
/**
* @param options
*/
createDashboard: handleCreateDashboard,
};
};

View File

@@ -1,13 +1,21 @@
import {
type BaseInteractiveQuestionProps,
InteractiveQuestion,
type InteractiveQuestionProps,
} from "../InteractiveQuestion";
/**
* @interface
* @category CreateQuestion
*/
export type CreateQuestionProps = Partial<
Omit<InteractiveQuestionProps, "questionId" | "children">
Omit<BaseInteractiveQuestionProps, "questionId" | "children">
>;
/** @deprecated Use `<InteractiveQuestion questionId="new" />` instead. */
/**
* @function
* @category CreateQuestion
* @deprecated Use `<InteractiveQuestion questionId="new" />` instead.
* */
export const CreateQuestion = (props: CreateQuestionProps = {}) => (
<InteractiveQuestion {...props} questionId="new" />
);

View File

@@ -4,6 +4,7 @@ import type { ConnectedProps } from "react-redux";
import _ from "underscore";
import type { MetabasePluginsConfig } from "embedding-sdk";
import type { CommonStylingProps } from "embedding-sdk/types/props";
import * as dashboardActions from "metabase/dashboard/actions";
import type { NavigateToNewCardFromDashboardOpts } from "metabase/dashboard/components/DashCard/types";
import { Dashboard } from "metabase/dashboard/components/Dashboard/Dashboard";
@@ -94,8 +95,8 @@ type ConnectedDashboardProps = {
) => void;
plugins?: MetabasePluginsConfig;
className?: string;
} & DashboardFullscreenControls &
} & Pick<CommonStylingProps, "className"> &
DashboardFullscreenControls &
DashboardRefreshPeriodControls &
DashboardLoaderWrapperProps &
PublicOrEmbeddedDashboardEventHandlersProps;

View File

@@ -1,9 +1,5 @@
import { type CSSProperties, useEffect } from "react";
import { useEffect } from "react";
import type {
InteractiveQuestionProps,
MetabasePluginsConfig,
} from "embedding-sdk";
import { InteractiveAdHocQuestion } from "embedding-sdk/components/private/InteractiveAdHocQuestion";
import type { InteractiveQuestionDefaultViewProps } from "embedding-sdk/components/private/InteractiveQuestionDefaultView";
import {
@@ -21,24 +17,47 @@ import {
SDK_DASHBOARD_VIEW_ACTIONS,
} from "metabase/dashboard/components/DashboardHeader/DashboardHeaderButtonRow/constants";
import { getIsEditing } from "metabase/dashboard/selectors";
import type { MetabasePluginsConfig } from "metabase/embedding-sdk/types/plugins";
import type { PublicOrEmbeddedDashboardEventHandlersProps } from "metabase/public/containers/PublicOrEmbeddedDashboard/types";
import { setErrorPage } from "metabase/redux/app";
import { getErrorPage } from "metabase/selectors/app";
import type { BaseInteractiveQuestionProps } from "../InteractiveQuestion";
import { ConnectedDashboard } from "./ConnectedDashboard";
import { InteractiveDashboardProvider } from "./context";
import { useCommonDashboardParams } from "./use-common-dashboard-params";
/**
* @interface
* @category InteractiveDashboard
*/
export type EditableDashboardProps = {
/**
* Height of a question component when drilled from the dashboard to a question level.
*/
drillThroughQuestionHeight?: number;
/**
* Additional mapper function to override or add drill-down menu. See the implementing custom actions section for more details.
*/
plugins?: MetabasePluginsConfig;
className?: string;
style?: CSSProperties;
drillThroughQuestionProps?: Omit<InteractiveQuestionProps, "questionId"> &
/**
* Props for the drill-through question
*/
drillThroughQuestionProps?: Omit<BaseInteractiveQuestionProps, "questionId"> &
InteractiveQuestionDefaultViewProps;
} & Omit<SdkDashboardDisplayProps, "withTitle" | "hiddenParameters"> &
PublicOrEmbeddedDashboardEventHandlersProps;
/**
* A dashboard component with the features available in the `InteractiveDashboard` component, as well as the ability to add and update questions, layout, and content within your dashboard.
*
* @function
* @category InteractiveDashboard
* @param props
*/
export const EditableDashboard = ({
dashboardId: initialDashboardId,
initialParameters = {},

View File

@@ -1,15 +1,6 @@
import {
type CSSProperties,
type ReactNode,
useCallback,
useEffect,
} from "react";
import { type ReactNode, useCallback, useEffect } from "react";
import _ from "underscore";
import type {
InteractiveQuestionProps,
MetabasePluginsConfig,
} from "embedding-sdk";
import { InteractiveAdHocQuestion } from "embedding-sdk/components/private/InteractiveAdHocQuestion";
import type { InteractiveQuestionDefaultViewProps } from "embedding-sdk/components/private/InteractiveQuestionDefaultView";
import {
@@ -26,6 +17,7 @@ import {
import { useSdkDispatch, useSdkSelector } from "embedding-sdk/store";
import { DASHBOARD_DISPLAY_ACTIONS } from "metabase/dashboard/components/DashboardHeader/DashboardHeaderButtonRow/constants";
import { useEmbedTheme } from "metabase/dashboard/hooks";
import type { MetabasePluginsConfig } from "metabase/embedding-sdk/types/plugins";
import { PublicOrEmbeddedDashboard } from "metabase/public/containers/PublicOrEmbeddedDashboard/PublicOrEmbeddedDashboard";
import type { PublicOrEmbeddedDashboardEventHandlersProps } from "metabase/public/containers/PublicOrEmbeddedDashboard/types";
import { setErrorPage } from "metabase/redux/app";
@@ -33,23 +25,37 @@ import { getErrorPage } from "metabase/selectors/app";
import { getEmbeddingMode } from "metabase/visualizations/click-actions/lib/modes";
import type { ClickActionModeGetter } from "metabase/visualizations/types";
import type { BaseInteractiveQuestionProps } from "../InteractiveQuestion";
import { InteractiveDashboardProvider } from "./context";
/**
* @interface
* @category InteractiveDashboard
*/
export type InteractiveDashboardProps = {
className?: string;
style?: CSSProperties;
/**
* Additional mapper function to override or add drill-down menu. See the implementing custom actions section for more details.
*/
plugins?: MetabasePluginsConfig;
// @todo pass the question context to the question view component,
// once we have a public-facing question context.
/**
* A custom React component to render the question layout.
* Use namespaced InteractiveQuestion components to build the layout.
*
* @todo pass the question context to the question view component,
* once we have a public-facing question context.
*/
renderDrillThroughQuestion?: () => ReactNode;
/**
* Height of a question component when drilled from the dashboard to a question level.
*/
drillThroughQuestionHeight?: number;
drillThroughQuestionProps?: Omit<InteractiveQuestionProps, "questionId"> &
/**
* Props of a question component when drilled from the dashboard to a question level.
*/
drillThroughQuestionProps?: Omit<BaseInteractiveQuestionProps, "questionId"> &
InteractiveQuestionDefaultViewProps;
} & SdkDashboardDisplayProps &
PublicOrEmbeddedDashboardEventHandlersProps;
@@ -184,6 +190,12 @@ const InteractiveDashboardInner = ({
);
};
/**
* A dashboard component with drill downs, click behaviors, and the ability to view and click into questions.
*
* @function
* @category InteractiveDashboard
*/
export const InteractiveDashboard = renderOnlyInSdkProvider(
InteractiveDashboardInner,
);

View File

@@ -1,4 +1,4 @@
import type { PropsWithChildren } from "react";
import type { ReactNode } from "react";
import {
BackButton,
@@ -31,23 +31,35 @@ import {
type InteractiveQuestionDefaultViewProps,
} from "embedding-sdk/components/private/InteractiveQuestionDefaultView";
import { withPublicComponentWrapper } from "embedding-sdk/components/private/PublicComponentWrapper";
import type { SdkQuestionId } from "embedding-sdk/types/question";
import type { InteractiveQuestionQuestionIdProps } from "embedding-sdk/components/public/InteractiveQuestion/types";
export type InteractiveQuestionProps = PropsWithChildren<{
questionId: SdkQuestionId;
plugins?: InteractiveQuestionProviderProps["componentPlugins"];
}> &
Pick<
InteractiveQuestionProviderProps,
| "questionId"
| "onBeforeSave"
| "onSave"
| "entityTypeFilter"
| "isSaveEnabled"
| "initialSqlParameters"
| "withDownloads"
| "targetCollection"
>;
/**
* @interface
*/
export type BaseInteractiveQuestionProps =
InteractiveQuestionQuestionIdProps & {
/**
* The children of the MetabaseProvider component.s
*/
children?: ReactNode;
plugins?: InteractiveQuestionProviderProps["componentPlugins"];
} & Pick<
InteractiveQuestionProviderProps,
| "onBeforeSave"
| "onSave"
| "entityTypeFilter"
| "isSaveEnabled"
| "initialSqlParameters"
| "withDownloads"
| "targetCollection"
>;
/**
* @interface
* @category InteractiveQuestion
*/
export type InteractiveQuestionProps = BaseInteractiveQuestionProps &
InteractiveQuestionDefaultViewProps;
export const _InteractiveQuestion = ({
questionId,
@@ -67,8 +79,7 @@ export const _InteractiveQuestion = ({
withChartTypeSelector = true,
withDownloads = false,
initialSqlParameters,
}: InteractiveQuestionProps &
InteractiveQuestionDefaultViewProps): JSX.Element | null => (
}: InteractiveQuestionProps): JSX.Element | null => (
<InteractiveQuestionProvider
questionId={questionId}
componentPlugins={plugins}
@@ -94,6 +105,13 @@ export const _InteractiveQuestion = ({
</InteractiveQuestionProvider>
);
/**
* A component that renders an interactive question.
*
* @function
* @category InteractiveQuestion
* @param props
*/
const InteractiveQuestion = withPublicComponentWrapper(
_InteractiveQuestion,
) as typeof _InteractiveQuestion & {
@@ -130,10 +148,8 @@ InteractiveQuestion.ResetButton = QuestionResetButton;
InteractiveQuestion.Title = Title;
InteractiveQuestion.Summarize = Summarize;
InteractiveQuestion.SummarizeDropdown = SummarizeDropdown;
/** @deprecated Use `InteractiveQuestion.Editor` instead */
InteractiveQuestion.Notebook = Editor;
InteractiveQuestion.Editor = Editor;
/** @deprecated Use `InteractiveQuestion.EditorButton` instead */
InteractiveQuestion.NotebookButton = EditorButton;
InteractiveQuestion.EditorButton = EditorButton;
InteractiveQuestion.QuestionVisualization = QuestionVisualization;
@@ -147,4 +163,5 @@ InteractiveQuestion.Breakout = Breakout;
InteractiveQuestion.ChartTypeDropdown = ChartTypeDropdown;
InteractiveQuestion.DownloadWidget = DownloadWidget;
InteractiveQuestion.DownloadWidgetDropdown = DownloadWidgetDropdown;
export { InteractiveQuestion };

View File

@@ -40,8 +40,8 @@ import {
import { useInteractiveQuestionContext } from "../../private/InteractiveQuestion/context";
import {
type BaseInteractiveQuestionProps,
InteractiveQuestion,
type InteractiveQuestionProps,
} from "./InteractiveQuestion";
const TEST_PARAM = createMockParameter({
@@ -99,7 +99,7 @@ const setup = ({
withChartTypeSelector = false,
initialSqlParameters,
}: Partial<
Pick<InteractiveQuestionProps, "initialSqlParameters"> &
Pick<BaseInteractiveQuestionProps, "initialSqlParameters"> &
Pick<
InteractiveQuestionDefaultViewProps,
"withChartTypeSelector" | "title"

View File

@@ -1,4 +1,26 @@
export {
type InteractiveQuestionBackButtonProps,
type InteractiveQuestionBreakoutDropdownProps,
type InteractiveQuestionChartTypeDropdownProps,
type InteractiveQuestionChartTypeSelectorProps,
type InteractiveQuestionDownloadWidgetProps,
type InteractiveQuestionDownloadWidgetDropdownProps,
type InteractiveQuestionEditorProps,
type InteractiveQuestionEditorButtonProps,
type InteractiveQuestionFilterProps,
type InteractiveQuestionFilterDropdownProps,
type InteractiveQuestionQuestionSettingsProps,
type InteractiveQuestionQuestionSettingsDropdownProps,
type InteractiveQuestionQuestionVisualizationProps,
type InteractiveQuestionResetButtonProps,
type InteractiveQuestionSaveButtonProps,
type InteractiveQuestionSaveQuestionFormProps,
type InteractiveQuestionSummarizeDropdownProps,
type InteractiveQuestionTitleProps,
} from "embedding-sdk/components/private/InteractiveQuestion/components";
export {
InteractiveQuestion,
type BaseInteractiveQuestionProps,
type InteractiveQuestionProps,
} from "./InteractiveQuestion";

View File

@@ -0,0 +1,12 @@
import type { SdkQuestionId } from "embedding-sdk/types/question";
export type InteractiveQuestionQuestionIdProps = {
/**
* The ID of the question.
* This is either:
* - The numerical ID when accessing a question link, e.g., `http://localhost:3000/question/1-my-question` where the ID is `1`
* - The `entity_id` key of the question object. You can find a question's Entity ID in the info panel when viewing a question
* - `new` to show the notebook editor for creating new questions. `isSaveEnabled` must be `true` to allow saving the question
*/
questionId: SdkQuestionId | null;
};

View File

@@ -18,6 +18,7 @@ import type {
} from "embedding-sdk/store/types";
import type { MetabaseAuthConfig } from "embedding-sdk/types";
import type { SdkEventHandlersConfig } from "embedding-sdk/types/events";
import type { CommonStylingProps } from "embedding-sdk/types/props";
import { EMBEDDING_SDK_ROOT_ELEMENT_ID } from "metabase/embedding-sdk/config";
import type { MetabaseTheme } from "metabase/embedding-sdk/theme";
import type { MetabasePluginsConfig } from "metabase/embedding-sdk/types/plugins";
@@ -39,13 +40,35 @@ import { SdkUsageProblemDisplay } from "../private/SdkUsageProblem";
import "metabase/css/index.module.css";
import "metabase/css/vendor.css";
export interface MetabaseProviderProps {
/**
* @category MetabaseProvider
*/
export interface MetabaseProviderProps
extends Omit<CommonStylingProps, "style"> {
/**
* The children of the MetabaseProvider component.
*/
children: ReactNode;
/**
* Defines how to authenticate with Metabase.
*/
authConfig: MetabaseAuthConfig;
pluginsConfig?: MetabasePluginsConfig;
eventHandlers?: SdkEventHandlersConfig;
/**
* See [Appearance](https://www.metabase.com/docs/latest/embedding/sdk/appearance).
*/
theme?: MetabaseTheme;
className?: string;
/**
* See [Plugins](https://www.metabase.com/docs/latest/embedding/sdk/plugins).
*/
pluginsConfig?: MetabasePluginsConfig;
/**
* See [Global event handlers](https://www.metabase.com/docs/latest/embedding/sdk/config#global-event-handlers).
*/
eventHandlers?: SdkEventHandlersConfig;
/**
* Defines the display language. Accepts an ISO language code such as `en` or `de`.
@@ -53,13 +76,19 @@ export interface MetabaseProviderProps {
**/
locale?: string;
/** A custom loader component to display while the SDK is loading. */
/**
* A custom loader component to display while the SDK is loading.
**/
loaderComponent?: () => JSX.Element;
/** A custom error component to display when the SDK encounters an error. */
/**
* A custom error component to display when the SDK encounters an error.
**/
errorComponent?: SdkErrorComponent;
/** Whether to allow logging to the DevTools console. Defaults to true. */
/**
* Whether to allow logging to the DevTools console. Defaults to true.
**/
allowConsoleLog?: boolean;
}
@@ -130,6 +159,11 @@ export const MetabaseProviderInternal = ({
);
};
/**
* A component that provides the Metabase SDK context and theme.
*
* @category MetabaseProvider
*/
export const MetabaseProvider = memo(function MetabaseProvider(
props: MetabaseProviderProps,
) {

View File

@@ -1,8 +1,14 @@
import { QuestionEditor } from "embedding-sdk/components/private/QuestionEditor";
import type { InteractiveQuestionProps } from "../InteractiveQuestion";
import type { BaseInteractiveQuestionProps } from "../InteractiveQuestion";
/** @deprecated Use `InteractiveQuestion` with `isSaveEnabled={true}` instead. */
/**
* @deprecated Use `InteractiveQuestion` with `isSaveEnabled={true}` instead.
*
* @function
* @category ModifyQuestion
* @param props
**/
export const ModifyQuestion = ({
questionId,
plugins,
@@ -11,7 +17,7 @@ export const ModifyQuestion = ({
entityTypeFilter,
isSaveEnabled,
targetCollection,
}: InteractiveQuestionProps) => (
}: BaseInteractiveQuestionProps) => (
<QuestionEditor
questionId={questionId}
plugins={plugins}

View File

@@ -22,6 +22,10 @@ import { setErrorPage } from "metabase/redux/app";
import { getErrorPage } from "metabase/selectors/app";
import { Box } from "metabase/ui";
/**
* @interface
* @category StaticDashboard
*/
export type StaticDashboardProps = SdkDashboardDisplayProps &
PublicOrEmbeddedDashboardEventHandlersProps;
@@ -90,6 +94,12 @@ export const StaticDashboardInner = ({
);
};
/**
* A lightweight dashboard component.
*
* @function
* @category StaticDashboard
*/
const StaticDashboard = withPublicComponentWrapper<StaticDashboardProps>(
({ dashboardId: initialDashboardId, ...rest }) => {
const { isLoading, id: resolvedDashboardId } = useValidatedEntityId({

View File

@@ -6,13 +6,15 @@ import {
import { withPublicComponentWrapper } from "embedding-sdk/components/private/PublicComponentWrapper";
import { InteractiveQuestion } from "../InteractiveQuestion";
import type { InteractiveQuestionQuestionIdProps } from "../InteractiveQuestion/types";
export type StaticQuestionProps = {
/**
* @interface
* @category StaticQuestion
*/
export type StaticQuestionProps = InteractiveQuestionQuestionIdProps & {
withChartTypeSelector?: boolean;
} & Pick<
InteractiveQuestionProviderProps,
"questionId" | "initialSqlParameters"
> &
} & Pick<InteractiveQuestionProviderProps, "initialSqlParameters"> &
FlexibleSizeProps;
const StaticQuestionInner = ({
@@ -39,4 +41,10 @@ const StaticQuestionInner = ({
</InteractiveQuestionProvider>
);
/**
* A component that renders a static question.
*
* @function
* @category StaticQuestion
*/
export const StaticQuestion = withPublicComponentWrapper(StaticQuestionInner);

View File

@@ -21,6 +21,24 @@ export {
export {
InteractiveQuestion,
type InteractiveQuestionProps,
type InteractiveQuestionBackButtonProps,
type InteractiveQuestionBreakoutDropdownProps,
type InteractiveQuestionChartTypeDropdownProps,
type InteractiveQuestionChartTypeSelectorProps,
type InteractiveQuestionDownloadWidgetProps,
type InteractiveQuestionDownloadWidgetDropdownProps,
type InteractiveQuestionEditorProps,
type InteractiveQuestionEditorButtonProps,
type InteractiveQuestionFilterProps,
type InteractiveQuestionFilterDropdownProps,
type InteractiveQuestionQuestionSettingsProps,
type InteractiveQuestionQuestionSettingsDropdownProps,
type InteractiveQuestionQuestionVisualizationProps,
type InteractiveQuestionResetButtonProps,
type InteractiveQuestionSaveButtonProps,
type InteractiveQuestionSaveQuestionFormProps,
type InteractiveQuestionSummarizeDropdownProps,
type InteractiveQuestionTitleProps,
} from "./InteractiveQuestion";
export {
MetabaseProvider,
@@ -36,7 +54,12 @@ export { StaticQuestion, type StaticQuestionProps } from "./StaticQuestion";
// declared. `const authConfig = defineMetabaseAuthConfig({})` will have the type of
// `MetabaseAuthConfig` and even provide autocompletion for js users depending on their
// IDE configuration.
/**
* Defines a Metabase auth config.
*
* @function
* @category MetabaseProvider
*/
export const defineMetabaseAuthConfig = (
config: MetabaseAuthConfig,
): MetabaseAuthConfig => config;

View File

@@ -1,8 +1,8 @@
import type { Query } from "history";
import type { CSSProperties } from "react";
import { pick } from "underscore";
import type { SdkDashboardId } from "embedding-sdk/types/dashboard";
import type { CommonStylingProps } from "embedding-sdk/types/props";
import { DEFAULT_DASHBOARD_DISPLAY_OPTIONS } from "metabase/dashboard/constants";
import {
useDashboardFullscreen,
@@ -14,16 +14,52 @@ import { useValidatedEntityId } from "metabase/lib/entity-id/hooks/use-validated
import { isNotNull } from "metabase/lib/types";
export type SdkDashboardDisplayProps = {
/**
* The ID of the dashboard.
* This is either:
* - the numerical ID when accessing a dashboard link, i.e. `http://localhost:3000/dashboard/1-my-dashboard` where the ID is `1`
* - the string ID found in the `entity_id` key of the dashboard object when using the API directly or using the SDK Collection Browser to return data
*/
dashboardId: SdkDashboardId;
/**
* Query parameters for the dashboard. For a single option, use a `string` value, and use a list of strings for multiple options.\
*
* @remarks
* * Combining {@link SdkDashboardDisplayProps.initialParameters | initialParameters} and {@link SdkDashboardDisplayProps.hiddenParameters | hiddenParameters} to filter data on the frontend is a [security risk](../../authentication.html#security-warning-each-end-user-must-have-their-own-metabase-account).
* * Combining {@link SdkDashboardDisplayProps.initialParameters | initialParameters} and {@link SdkDashboardDisplayProps.hiddenParameters | hiddenParameters} to declutter the user interface is fine.
*/
initialParameters?: Query;
/**
* Whether the dashboard should display a title.
*/
withTitle?: boolean;
/**
* Whether the dashboard cards should display a title.
*/
withCardTitle?: boolean;
/**
* Whether to hide the download button.
*/
withDownloads?: boolean;
/**
* Whether to display the footer.
*/
withFooter?: boolean;
/**
* A list of [parameters to hide](../../../public-links.html#appearance-parameters).
*
* @remarks
* * Combining {@link SdkDashboardDisplayProps.initialParameters | initialParameters} and {@link SdkDashboardDisplayProps.hiddenParameters | hiddenParameters} to filter data on the frontend is a [security risk](../../authentication.html#security-warning-each-end-user-must-have-their-own-metabase-account).
* * Combining {@link SdkDashboardDisplayProps.initialParameters | initialParameters} and {@link SdkDashboardDisplayProps.hiddenParameters | hiddenParameters} to declutter the user interface is fine.
**/
hiddenParameters?: string[];
className?: string;
style?: CSSProperties;
};
} & CommonStylingProps;
export const useSdkDashboardParams = ({
dashboardId: initialDashboardId,

View File

@@ -20,6 +20,9 @@ export type MetabaseAuthConfigWithApiKey = BaseMetabaseAuthConfig & {
authProviderUri?: never;
};
/**
* @category MetabaseProvider
*/
export type MetabaseAuthConfig =
| MetabaseAuthConfigWithProvider
| MetabaseAuthConfigWithApiKey;

View File

@@ -3,6 +3,13 @@ import type { Dashboard } from "metabase-types/api";
export type SdkDashboardLoadEvent = (dashboard: Dashboard | null) => void;
export type SdkEventHandlersConfig = {
onDashboardLoad?: SdkDashboardLoadEvent; // triggers when active tab loads with all cards in it
onDashboardLoadWithoutCards?: SdkDashboardLoadEvent; // triggers when dashboard structure loads (list of tabs, grid of active tab)
/**
* Triggers when a dashboard loads with all visible cards and their content
*/
onDashboardLoad?: SdkDashboardLoadEvent;
/**
* Triggers after a dashboard loads, but without its cards (at this stage only the dashboard title, tabs, and cards grid are rendered, but the contents of the cards have yet to load.
*/
onDashboardLoadWithoutCards?: SdkDashboardLoadEvent;
};

View File

@@ -0,0 +1,13 @@
import type { CSSProperties } from "react";
export type CommonStylingProps = {
/**
* A custom class name to be added to the root element.
*/
className?: string;
/**
* A custom style object to be added to the root element.
*/
style?: CSSProperties;
};

View File

@@ -18,10 +18,25 @@ export interface SdkQuestionState {
}
export interface LoadSdkQuestionParams {
options?: QueryParams;
deserializedCard?: Card;
questionId?: CardId | null;
/**
* For SQL questions only. A mapping of SQL parameter names to parameter values, such as `{ product_id: "42"}`
*/
initialSqlParameters?: ParameterValues;
/**
* @internal
*/
options?: QueryParams;
/**
* @internal
*/
deserializedCard?: Card;
/**
* @internal
*/
questionId?: CardId | null;
}
export interface NavigateToNewCardParams {

View File

@@ -35,8 +35,19 @@ const DASHBOARD_SCHEMA = Yup.object({
});
export interface CreateDashboardProperties {
/**
* Dashboard title
*/
name: string;
/**
* Dashboard description
*/
description: string | null;
/**
* @internal
*/
collection_id: CollectionId;
}

View File

@@ -5,6 +5,9 @@ import type { ClickAction, ClickObject } from "metabase/visualizations/types";
import type { MetabaseQuestion } from "./question";
/**
* @interface
*/
export type MetabaseDataPointObject = Pick<
ClickObject,
"value" | "column" | "data" | "event"

View File

@@ -1,6 +1,13 @@
import type { Dashboard } from "metabase-types/api";
export type PublicOrEmbeddedDashboardEventHandlersProps = {
/**
* Callback that is called when the dashboard is loaded.
*/
onLoad?: (dashboard: Dashboard | null) => void;
/**
* Callback that is called when the dashboard is loaded without cards.
*/
onLoadWithoutCards?: (dashboard: Dashboard | null) => void;
};

View File

@@ -10,6 +10,7 @@ export type ExtraButtonProps = {
highlightOnHover?: boolean;
type?: "button" | "submit";
};
export type ButtonProps = MantineButtonProps &
ExtraButtonProps &
HTMLAttributes<HTMLButtonElement>;

View File

@@ -333,6 +333,10 @@
"terser-webpack-plugin": "^5.3.9",
"ts-morph": "^25.0.0",
"tsx": "^4.19.3",
"typedoc": "0.28.2",
"typedoc-plugin-mdn-links": "^5.0.1",
"typedoc-plugin-merge-modules": "^7.0.0",
"typedoc-plugin-missing-exports": "^4.0.0",
"typescript-plugin-css-modules": "^5.1.0",
"webpack": "5.94.0",
"webpack-bundle-analyzer": "^4.10.1",
@@ -419,6 +423,8 @@
"embedding-sdk:generate-nextjs-compat": "tsx enterprise/frontend/src/embedding-sdk/bin/generate-nextjs-compat.ts",
"embedding-sdk:test-unit": "yarn test-unit enterprise/frontend/src/embedding-sdk/",
"embedding-sdk:tsc": "tsc --project tsconfig.sdk.json",
"embedding-sdk:docs:generate:pure": "typedoc --options ./.typedoc/typedoc.config.mjs",
"embedding-sdk:docs:generate": "yarn run build && yarn run build-embedding-sdk && yarn run embedding-sdk:docs:generate:pure",
"embedding-sdk:cli-snippets:type-check": "./bin/embedding-sdk/type-check-embedding-sdk-cli-snippets.sh",
"embedding-sdk:docs-snippets:type-check": "./bin/embedding-sdk/type-check-embedding-sdk-docs-snippets.sh",
"embedding-sdk:clean": "rm -rf resources/embedding-sdk/dist && rm -rf resources/embedding-sdk/.tsbuildinfo",

120
yarn.lock
View File

@@ -1917,6 +1917,17 @@
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62"
integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==
"@gerrit0/mini-shiki@^3.2.2":
version "3.2.2"
resolved "https://registry.yarnpkg.com/@gerrit0/mini-shiki/-/mini-shiki-3.2.2.tgz#86d968b15386f2396b8cb1b2de8da95c2fb9d4f2"
integrity sha512-vaZNGhGLKMY14HbF53xxHNgFO9Wz+t5lTlGNpl2N9xFiKQ0I5oIe0vKjU9dh7Nb3Dw6lZ7wqUE0ri+zcdpnK+Q==
dependencies:
"@shikijs/engine-oniguruma" "^3.2.1"
"@shikijs/langs" "^3.2.1"
"@shikijs/themes" "^3.2.1"
"@shikijs/types" "^3.2.1"
"@shikijs/vscode-textmate" "^10.0.2"
"@humanwhocodes/config-array@^0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
@@ -3224,6 +3235,41 @@
argparse "~1.0.9"
string-argv "~0.3.1"
"@shikijs/engine-oniguruma@^3.2.1":
version "3.2.1"
resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.2.1.tgz#be07d9f09a4b5e35af1896ff232001b9d4f5466e"
integrity sha512-wZZAkayEn6qu2+YjenEoFqj0OyQI64EWsNR6/71d1EkG4sxEOFooowKivsWPpaWNBu3sxAG+zPz5kzBL/SsreQ==
dependencies:
"@shikijs/types" "3.2.1"
"@shikijs/vscode-textmate" "^10.0.2"
"@shikijs/langs@^3.2.1":
version "3.2.1"
resolved "https://registry.yarnpkg.com/@shikijs/langs/-/langs-3.2.1.tgz#662390328675e0244f18559bade4aac0e60e5b64"
integrity sha512-If0iDHYRSGbihiA8+7uRsgb1er1Yj11pwpX1c6HLYnizDsKAw5iaT3JXj5ZpaimXSWky/IhxTm7C6nkiYVym+A==
dependencies:
"@shikijs/types" "3.2.1"
"@shikijs/themes@^3.2.1":
version "3.2.1"
resolved "https://registry.yarnpkg.com/@shikijs/themes/-/themes-3.2.1.tgz#93d9c5fcc16649ec1e017217c0a88209cc619731"
integrity sha512-k5DKJUT8IldBvAm8WcrDT5+7GA7se6lLksR+2E3SvyqGTyFMzU2F9Gb7rmD+t+Pga1MKrYFxDIeyWjMZWM6uBQ==
dependencies:
"@shikijs/types" "3.2.1"
"@shikijs/types@3.2.1", "@shikijs/types@^3.2.1":
version "3.2.1"
resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-3.2.1.tgz#8d289ad95d62833d7f9af536c073312d7afe8182"
integrity sha512-/NTWAk4KE2M8uac0RhOsIhYQf4pdU0OywQuYDGIGAJ6Mjunxl2cGiuLkvu4HLCMn+OTTLRWkjZITp+aYJv60yA==
dependencies:
"@shikijs/vscode-textmate" "^10.0.2"
"@types/hast" "^3.0.4"
"@shikijs/vscode-textmate@^10.0.2":
version "10.0.2"
resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz#a90ab31d0cc1dfb54c66a69e515bf624fa7b2224"
integrity sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==
"@sinclair/typebox@^0.24.1":
version "0.24.44"
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.44.tgz#0a0aa3bf4a155a678418527342a3ee84bd8caa5c"
@@ -4483,7 +4529,7 @@
dependencies:
"@types/unist" "*"
"@types/hast@^3.0.0", "@types/hast@^3.0.2":
"@types/hast@^3.0.0", "@types/hast@^3.0.2", "@types/hast@^3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa"
integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==
@@ -14026,6 +14072,13 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
linkify-it@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421"
integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==
dependencies:
uc.micro "^2.0.0"
lint-staged@^13.1.2:
version "13.1.2"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-13.1.2.tgz#443636a0cfd834d5518d57d228130dc04c83d6fb"
@@ -14368,6 +14421,11 @@ lru-cache@^8.0.0:
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-8.0.5.tgz#983fe337f3e176667f8e567cfcce7cb064ea214e"
integrity sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==
lunr@^2.3.9:
version "2.3.9"
resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1"
integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==
lz-string@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941"
@@ -14419,6 +14477,18 @@ map-or-similar@^1.5.0:
resolved "https://registry.yarnpkg.com/map-or-similar/-/map-or-similar-1.5.0.tgz#6de2653174adfb5d9edc33c69d3e92a1b76faf08"
integrity sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==
markdown-it@^14.1.0:
version "14.1.0"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45"
integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==
dependencies:
argparse "^2.0.1"
entities "^4.4.0"
linkify-it "^5.0.0"
mdurl "^2.0.0"
punycode.js "^2.3.1"
uc.micro "^2.1.0"
markdown-table@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b"
@@ -14619,6 +14689,11 @@ mdn-data@2.0.30:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc"
integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==
mdurl@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0"
integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -15121,7 +15196,7 @@ minimatch@^5.0.1, minimatch@^5.1.6:
dependencies:
brace-expansion "^2.0.1"
minimatch@^9.0.4:
minimatch@^9.0.4, minimatch@^9.0.5:
version "9.0.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
@@ -17240,6 +17315,11 @@ pump@^3.0.0:
end-of-stream "^1.1.0"
once "^1.3.1"
punycode.js@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
punycode@1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
@@ -20646,6 +20726,32 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typedoc-plugin-mdn-links@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/typedoc-plugin-mdn-links/-/typedoc-plugin-mdn-links-5.0.1.tgz#2ff23f997e71a2c77cd43590455754e29e4875f0"
integrity sha512-eofdcc2nZZpipz/ubjG+7UYMi6Xu95svUwnZ+ClJh6NJdrv7kAOerL9N3iDOpo5kwQeK86GqPWwnv6LUGo5Wrw==
typedoc-plugin-merge-modules@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/typedoc-plugin-merge-modules/-/typedoc-plugin-merge-modules-7.0.0.tgz#43b7d2b4020eff552d6e46b7d5cf2e5d306597fc"
integrity sha512-DQyfbbPNBElhpdpGrlkS+CrhGD3iooDjp/PHT8O1D/jumLCB+7XAY3jHiqob7d01o25EfwEOJWVwJK+9J6WlBg==
typedoc-plugin-missing-exports@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-4.0.0.tgz#01916ce83173cb2e7b2942edfbe484f3bfbb0c8c"
integrity sha512-Z4ei+853xppDEhcqzyeyRs4+R0kUuKQWnMK1EtSTEd5LFkgkdW5Bdn8vfo/rsCGbYVJxOWU99fxgM1mROw5Fug==
typedoc@0.28.2:
version "0.28.2"
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.28.2.tgz#342c42f2e4c0306c47a0a5e92592af9aa60e1a85"
integrity sha512-9Giuv+eppFKnJ0oi+vxqLM817b/IrIsEMYgy3jj6zdvppAfDqV3d6DXL2vXUg2TnlL62V48th25Zf/tcQKAJdg==
dependencies:
"@gerrit0/mini-shiki" "^3.2.2"
lunr "^2.3.9"
markdown-it "^14.1.0"
minimatch "^9.0.5"
yaml "^2.7.1"
typescript-plugin-css-modules@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/typescript-plugin-css-modules/-/typescript-plugin-css-modules-5.1.0.tgz#faa0ceffe8a8ffcbbc2f77ed637a64464195044a"
@@ -20683,6 +20789,11 @@ typescript@^5.4.4, typescript@^5.5.4:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e"
integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==
uc.micro@^2.0.0, uc.micro@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee"
integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==
uglify-js@^3.1.4:
version "3.14.1"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.1.tgz#e2cb9fe34db9cb4cf7e35d1d26dfea28e09a7d06"
@@ -21927,6 +22038,11 @@ yaml@^2.2.2:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.1.tgz#2e57e0b5e995292c25c75d2658f0664765210eed"
integrity sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==
yaml@^2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.1.tgz#44a247d1b88523855679ac7fa7cda6ed7e135cf6"
integrity sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==
yargs-parser@^18.1.2:
version "18.1.3"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"