diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_No_Background_Default.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_No_Background_Default.png index 2a95a2a19ddd1db108b05b2c89a1119aa190fb0c..5583b8869580cc51549b93621b08e369bc5f6cda 100644 Binary files a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_No_Background_Default.png and b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_No_Background_Default.png differ diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Light_Theme_Date_Filter_Quarter_Year_Dropdown.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Light_Theme_Date_Filter_Quarter_Year_Dropdown.png index 4ae0fa3108ca52e068ffbc4c8a0e53861035d890..bb6659b6e95d139d1e2aeb429418944a67b55264 100644 Binary files a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Light_Theme_Date_Filter_Quarter_Year_Dropdown.png and b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Light_Theme_Date_Filter_Quarter_Year_Dropdown.png differ diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Light_Theme_Parameter_Search_With_Value.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Light_Theme_Parameter_Search_With_Value.png index a76c1d89bcec7ac49718bf01c5f0d91290d8a1cd..d73f3f82406320c4472b67cef05353a121907097 100644 Binary files a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Light_Theme_Parameter_Search_With_Value.png and b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Light_Theme_Parameter_Search_With_Value.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_PieChart_Sunburst_Other_Label.png b/.loki/reference/chrome_laptop_static_viz_PieChart_Sunburst_Other_Label.png new file mode 100644 index 0000000000000000000000000000000000000000..b2069d1ccdd0b333bcc6a8fbdea59a2a561a2db6 Binary files /dev/null and b/.loki/reference/chrome_laptop_static_viz_PieChart_Sunburst_Other_Label.png differ diff --git a/docs/dashboards/filters.md b/docs/dashboards/filters.md index b9464de48bfa5fc66798eef036b34badc4671994..59b5e0c803447f06b57ee6ae383179ff821dbdc8 100644 --- a/docs/dashboards/filters.md +++ b/docs/dashboards/filters.md @@ -60,6 +60,8 @@ Single Date and Date Range will provide a calendar widget, while the other optio You can add a time grouping widget to a dashboard to change how charts are grouped by time. For example, you may want to look at the time series charts grouped by month by default, but give people the option to view the results by other groupings: by week, by quarter, and so on. +> Time grouping parameter widget can only be connected to questions built with the [graphical query builder](../questions/query-builder/introduction.md). + This widget doesn't filter data in the charts; the widget just changes the time granularity for any cards that have a datetime field connected to the widget. You can group by: @@ -128,6 +130,8 @@ To undo this auto-connecting of cards, click on the toast that pops up when Meta If you're trying to connect a filter to a card with a native/SQL questions, you'll need to [add a variable or field filter to your query](../questions/native-editor/sql-parameters.md). For an in-depth article, check out [Adding filters to dashboards with SQL questions](https://www.metabase.com/learn/dashboards/filters). +You can't connect a time grouping parameter widget to a card with a SQL question. + ## Wiring up dashboard filters to text cards You can even wire up filters to text cards, but only if [the text card includes a variable](./introduction.md#including-variables-in-text-cards). diff --git a/docs/dashboards/interactive.md b/docs/dashboards/interactive.md index fae02452eefdf461a5f97a114bfaf9879252d65a..6f970a0eef039dd9a1afded8b0217b92bb8d29c7 100644 --- a/docs/dashboards/interactive.md +++ b/docs/dashboards/interactive.md @@ -1,7 +1,7 @@ --- title: Interactive dashboards redirect_from: -- /docs/latest/users-guide/interactive-dashboards + - /docs/latest/users-guide/interactive-dashboards --- # Interactive dashboards @@ -42,7 +42,7 @@ For questions composed using the query builder, you can select from three option - Go to a custom destination. - Update a dashboard filter (if the dashboard has a filter). -SQL questions will only have the option to **Go to a custom destination**, and **Update a dashboard filter**, as the drill-through menu is only available to questions composed with the query builder. +SQL questions will only have the option to **Go to a custom destination**, and **Update a dashboard filter**. If your dashboard has a filter, you'll also see an option to [update the filter](#use-a-chart-to-filter-a-dashboard). @@ -94,11 +94,11 @@ What we need to do here is to type in the full URL of where a user should go whe For example, we could type a URL like this: - ``` - https://www.metabase.com/search.html?query={% raw %}{{Category}}{% endraw %} - ``` +``` +https://www.metabase.com/search.html?query={% raw %}{{Category}}{% endraw %} +``` - The important part is the `{% raw %}{{Category}}{% endraw %}` bit. What we’re doing here is referring to the `Category` that the user clicked on. So if a user clicks on the `Widget` bar in our chart, the value of the `Category` column for that bar (`Widget`) would be inserted into our URL: `https://www.metabase.com/search.html?query=Widget`. Your URL can use as many column variables as you want - you can even refer to the same column multiple times in different parts of the URL. Click on the dropdown menu **Values you can reference** to see your options for which variables you can include in the URL. +The important part is the `{% raw %}{{Category}}{% endraw %}` bit. What we’re doing here is referring to the `Category` that the user clicked on. So if someone clicks on the `Widget` bar in our chart, the value of the `Category` column for that bar (`Widget`) would be inserted into our URL: `https://www.metabase.com/search.html?query=Widget`. Your URL can use as many column variables as you want - you can even refer to the same column multiple times in different parts of the URL. To see which variables you can include in the URL, click on the dropdown menu **Values you can reference**. Next we’ll click **Done**, then **Save** our dashboard. Now when we click our chart, we’ll be taken to the URL that we entered above, with the value of the clicked bar inserted into the URL. diff --git a/docs/dashboards/multiple-series.md b/docs/dashboards/multiple-series.md index a7b5666c769fad4c7bdf85a41042c3ace28e3133..da798f150fa7e432454524f662c7c9fc04950315 100644 --- a/docs/dashboards/multiple-series.md +++ b/docs/dashboards/multiple-series.md @@ -41,7 +41,7 @@ Note: you won’t be able to add another saved question to multi-series visualiz ## Combining two saved questions -If you already have two or more saved questions you’d like to compare, and they share a dimension, they can be combined onto a single dashboard card. You can even compare questions that pull data from different databases. Here’s how: +If you already have two or more saved questions you’d like to compare, and they have the same first dimension, they can be combined onto a single dashboard card. You can even compare questions that pull data from different databases. Here’s how: 1. Add a question with a dimension like time or category to a dashboard. In practice, these will usually be line charts or bar charts. diff --git a/docs/data-modeling/field-types.md b/docs/data-modeling/field-types.md index ded6b2bf85f082ac7f71a3ff5ba3a6399632178b..536c8df499741ca51af93c391524d482f15544a8 100644 --- a/docs/data-modeling/field-types.md +++ b/docs/data-modeling/field-types.md @@ -144,6 +144,10 @@ While data types themselves can't be edited in Metabase, admins can manually [ca See [Working with JSON](./json-unfolding.md). +### Arrays + +Metabase currently does not support array types with any database. You'll only be able to use **Is empty** or **Is not empty** filters on columns containing arrays. + ## Further Reading - [Exploring data with Metabase's data browser](https://www.metabase.com/learn/getting-started/data-browser.html). diff --git a/docs/data-modeling/json-unfolding.md b/docs/data-modeling/json-unfolding.md index 72bde4b9604bd14d73c957c756f824ff3c9c8646..856928c9a26f516ce497452818b684d3f2933c03 100644 --- a/docs/data-modeling/json-unfolding.md +++ b/docs/data-modeling/json-unfolding.md @@ -68,3 +68,4 @@ For example, if you upload a CSV with JSON in it, you might need to update the d - [PostgreSQL](../databases/connections/postgresql.md) - [MySQL](../databases/connections/mysql.md) - [Druid (JDBC)](../databases/connections/druid.md) +- [BigQuery](../databases/connections/bigquery.md) (always enabled) diff --git a/docs/data-modeling/models.md b/docs/data-modeling/models.md index 1951af22e54557309bbeb4e812617a94af6ab92a..aba7f8696fe8aa3e34f5ed2b242b199d8b9a819a 100644 --- a/docs/data-modeling/models.md +++ b/docs/data-modeling/models.md @@ -23,7 +23,7 @@ For a deep dive on why and how to use models, check out our [Learn article on mo You can use models to: - Create, uh, models, with model here meaning an intuitive description of some concept in your business that you codify as a set of columns. An example model could be a "customer", which is a table that pulls together customer information from multiple tables and adds computed columns, like adding a lifetime value (LTV) column. This model represents the [measures and dimensions][measures-dimensions] that you think are relevant to your understanding of your customers. -- Let people explore the results of SQL queries with the drill-through menu and query builder (provided you [set the column types](#column-type)). +- Let people explore the results of SQL queries with the query builder (provided you [set the column types](#column-type)). - Create summary tables that pull in or aggregate data from multiple tables. - Clean up tables with unnecessary columns and rows filtered out. @@ -82,7 +82,7 @@ You can also edit the model's metadata. ## Add metadata to columns in a model -Metadata is the secret sauce of models. When you write a SQL query, Metabase can display the results, but it can't "know" what kind of data it's returning (like it can with questions built using the query builder). What this means in practice is that people won't be able to drill-through the results, or explore the results with the query builder, because Metabase doesn't understand what the results are. With models, however, you can tell Metabase what kind of data is in each returned column so that Metabase can still do its drill-through magic. Metadata will also make filtering nicer by showing the correct filter widget, and it will help Metabase to pick the right visualization for the results. +Metadata is the secret sauce of models. When you write a SQL query, Metabase can display the results, but it can't "know" what kind of data it's returning (like it can with questions built using the query builder). What this means in practice is that people won't be able explore the results with the query builder, because Metabase doesn't understand what the results are. With models, however, you can tell Metabase what kind of data is in each returned column so that Metabase can still do its query magic. Metadata will also make filtering nicer by showing the correct filter widget, and it will help Metabase to pick the right visualization for the results. If you only set one kind of metadata, set the **Column type** to let Metabase know what kind of data it's working with. @@ -102,7 +102,7 @@ For models based on SQL queries, you can tell Metabase if the column has the sam You can set the [column type][column-type]. The default is "No special type". -If your model is based on a SQL query and you want people to be able to explore the results with the query builder and drill-through menu, you'll need to set the [column type](./field-types.md) for each column in your model. +If your model is based on a SQL query and you want people to be able to explore the results with the query builder, you'll need to set the [column type](./field-types.md) for each column in your model. ### This column should appear in... diff --git a/docs/databases/connections/postgresql.md b/docs/databases/connections/postgresql.md index 093009073355adf95cdfe53e82f8d399421f0681..ad512c2663a318fdfff0d1377f52854a580b1077 100644 --- a/docs/databases/connections/postgresql.md +++ b/docs/databases/connections/postgresql.md @@ -83,11 +83,7 @@ See the PostgreSQL docs for a table about the different [SSL Modes](https://jdbc If you set the SSL Mode to either "verify-ca" or "verify-full", you'll need to specify a root certificate (PEM). You have the option of using a **Local file path** or an **Uploaded file path**. If you're on Metabase Cloud, you'll need to select **Uploaded file path** and upload your certificate. -### Use an SSH tunnel - -See our [guide to SSH tunneling](../ssh-tunnel.md). - -### Authenticate client certificate +#### Authenticate client certificate Toggle on to bring up client certificate options. @@ -109,6 +105,12 @@ openssl pkcs8 -topk8 -inform PEM -outform DER -in client-key.pem -out client-key Note: if you're using GCP and you managed to issue client certificates, everything will be given in PEM format, you only need to transform the client-key.pem into a client-key.der for the "SSL Client Key" +### Use an SSH tunnel + +See our [guide to SSH tunneling](../ssh-tunnel.md). + +## Advanced settings + ### Unfold JSON Columns For PostgreSQL databases, Metabase can unfold JSON columns into component fields to yield a table where each JSON key becomes a column. JSON unfolding is on by default, but you can turn off JSON unfolding if performance is slow. diff --git a/docs/embedding/sdk/authentication.md b/docs/embedding/sdk/authentication.md index e663ecd647cfa2f82b5aac0ce7a8ab1628e125c2..a303313703c9438eef7bf854e43eef4de2a01179 100644 --- a/docs/embedding/sdk/authentication.md +++ b/docs/embedding/sdk/authentication.md @@ -32,7 +32,7 @@ if (auth.status === "success") { You can customize how the SDK fetches the refresh token by specifying the `fetchRefreshToken` function in the `config` prop: -```typescript jsx +```typescript /** * This is the default implementation used in the SDK. * You can customize this function to fit your needs, such as adding headers or excluding cookies. @@ -59,7 +59,7 @@ const config = { fetchRequestToken }; In case you need to reload a Metabase component, for example, your users modify your application data and that data is used to render a question in Metabase. If you embed this question and want to force Metabase to reload the question to show the latest data, you can do so by using the `key` prop to force a component to reload. -```typescript jsx +```typescript // Inside your application component const [data, setData] = useState({}); // This is used to force reloading Metabase components diff --git a/docs/embedding/sdk/collections.md b/docs/embedding/sdk/collections.md index 9ba286f889a5f80b19911041c1c457807ce33efc..5d8701b7fb99ed7761414c0ddf45d9e09e0d1ac0 100644 --- a/docs/embedding/sdk/collections.md +++ b/docs/embedding/sdk/collections.md @@ -12,17 +12,18 @@ You can embed Metabase's collection browser so that people can explore items in ## `CollectionBrowser` props -| Prop | Type | Description | -| ------------------ | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| collectionId | `number` | The numerical ID of the 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`. If no ID is provided, the collection browser will start at the root `Our analytics` collection, which is ID = 0. | -| 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. | +| Prop | Type | Description | +| ------------------ | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| collectionId | `number` | The numerical ID of the 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`. If no ID is provided, the collection browser will start at the root `Our analytics` collection, which is ID = 0. | +| 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. | + ## Example embedding code with `CollectionBrowser` ```tsx import React from "react"; -import { CollectionBrowser } from "@metabase/embedding-sdk-react" +import { CollectionBrowser } from "@metabase/embedding-sdk-react"; export default function App() { const collectionId = 123; // This is the collection ID you want to browse diff --git a/docs/embedding/sdk/dashboards.md b/docs/embedding/sdk/dashboards.md index 1878c7cd4d160de52ccd9ab151aba5414d7cb0f9..d140f81b9d9f9a2968b5979df6b5e0cc96d65c92 100644 --- a/docs/embedding/sdk/dashboards.md +++ b/docs/embedding/sdk/dashboards.md @@ -37,7 +37,7 @@ _\* Not available for `StaticDashboard`._ ## Example embedded dashboard with `InteractiveDashboard` component -```typescript jsx +```typescript import React from "react"; import {MetabaseProvider, InteractiveDashboard} from "@metabase/embedding-sdk-react"; @@ -72,7 +72,7 @@ This plugin allows you to add, remove, and modify the custom actions on the over The plugin's default configuration looks like this: -```typescript jsx +```typescript const plugins = { dashboard: { dashcardMenu: { @@ -86,7 +86,7 @@ const plugins = { `dashcardMenu`: can be used in the InteractiveDashboard like this: -```typescript jsx +```typescript {% raw %} <InteractiveDashboard questionId={1} @@ -103,7 +103,7 @@ const plugins = { To remove the download button from the dashcard menu, set `withDownloads` to `false`. To remove the edit link from the dashcard menu, set `withEditLink` to `false`. -```typescript jsx +```typescript const plugins = { dashboard: { dashcardMenu: { @@ -119,7 +119,7 @@ const plugins = { You can add custom actions to the dashcard menu by adding an object to the `customItems` array. Each element can either be an object or a function that takes in the dashcard's question, and outputs a list of custom items in the form of: -```typescript jsx +```typescript { iconName: string; label: string; @@ -130,7 +130,7 @@ You can add custom actions to the dashcard menu by adding an object to the `cust Here's an example: -```typescript jsx +```typescript const plugins: SdkPluginsConfig = { dashboard: { dashcardMenu: { @@ -161,7 +161,7 @@ const plugins: SdkPluginsConfig = { If you want to replace the existing menu with your own component, you can do so by providing a function that returns a React component. This function also can receive the question as an argument. -```typescript jsx +```typescript const plugins: SdkPluginsConfig = { dashboard: { dashcardMenu: ({ question }) => ( @@ -177,7 +177,7 @@ Creating a dashboard could be done with `useCreateDashboardApi` hook or `CreateD ### Hook -```typescript jsx +```typescript const { createDashboard } = useCreateDashboardApi(); const handleDashboardCreate = async () => { @@ -199,7 +199,7 @@ Props: ### Component -```typescript jsx +```typescript const [dashboard, setDashboard] = useState<Dashboard | null>(null); if (dashboard) { diff --git a/docs/embedding/sdk/introduction.md b/docs/embedding/sdk/introduction.md index f93662a8d400e7c85e5dd5bb3250116b2cdde5e5..7eac22ff126264701f71f26064b977bc249f6a63 100644 --- a/docs/embedding/sdk/introduction.md +++ b/docs/embedding/sdk/introduction.md @@ -18,16 +18,16 @@ To give you and idea of what's possible with the SDK, we've put together example  -# Embedding SDK prerequisites +# Embedded analytics SDK prerequisites - React application. The SDK is tested to work with React 18 or higher, though it may work with earlier versions. - [Metabase Pro or Enterprise subscription or free trial](https://www.metabase.com/pricing/). - Metabase version 1.50 or higher. - [Node.js 18.x LTS](https://nodejs.org/en) or higher. -## Embedding SDK on NPM +## Embedded analytics SDK on NPM -Check out the [Metabase embedding SDK on NPM: [metaba.se/sdk](https://metaba.se/sdk). +Check out the Metabase Embedded analytics SDK on NPM: [metaba.se/sdk](https://metaba.se/sdk). ## Installation diff --git a/docs/embedding/sdk/plugins.md b/docs/embedding/sdk/plugins.md index ae162cc54529b3cb03a6b6ab745cba3194c81423..73cc6560cb0caf34b9aca13f58eb91f8bc0c2998 100644 --- a/docs/embedding/sdk/plugins.md +++ b/docs/embedding/sdk/plugins.md @@ -14,7 +14,7 @@ The Metabase Embedding SDK supports plugins to customize the behavior of compone To use a plugin globally, add the plugin to the `MetabaseProvider`'s `pluginsConfig` prop: -```typescript jsx +```typescript {% raw %} <MetabaseProvider config={config} @@ -32,7 +32,7 @@ To use a plugin globally, add the plugin to the `MetabaseProvider`'s `pluginsCon To use a plugin on a per-component basis, pass the plugin as a prop to the component: -```typescript jsx +```typescript {% raw %} <InteractiveQuestion questionId={1} diff --git a/docs/embedding/sdk/questions.md b/docs/embedding/sdk/questions.md index fd79ffb7c59d839cb1a0459fc170ab1243619dd8..8d0a5c02bde93155e7f00a2498c68e540c2e93cd 100644 --- a/docs/embedding/sdk/questions.md +++ b/docs/embedding/sdk/questions.md @@ -21,7 +21,7 @@ You can embed a static question using the `StaticQuestion` component. 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 jsx +```typescript import React from "react"; import {MetabaseProvider, StaticQuestion} from "@metabase/embedding-sdk-react"; @@ -50,7 +50,7 @@ You can pass parameter values to questions defined with SQL via `parameterValues You can embed an interactive question using the `InteractiveQuestion` component. -```typescript jsx +```typescript import React from "react"; import {MetabaseProvider, InteractiveQuestion} from "@metabase/embedding-sdk-react"; @@ -89,13 +89,13 @@ By default, the Embedded Analytics SDK provides a default layout for interactive Here's an example of using the `InteractiveQuestion` component with its default layout: -```typescript jsx +```typescript <InteractiveQuestion questionId={95} /> ``` To customize the layout, use namespaced components within the `InteractiveQuestion` component. For example: -```typescript jsx +```typescript {% raw %} <InteractiveQuestion questionId={95}> <div @@ -160,7 +160,7 @@ This plugin allows you to add custom actions to the click-through menu of an interactive question. You can add and customize the appearance and behavior of the custom actions. -```typescript jsx +```typescript // You can provide a custom action with your own `onClick` logic. const createCustomAction = clicked => ({ buttonType: "horizontal", diff --git a/docs/exploration-and-organization/exploration.md b/docs/exploration-and-organization/exploration.md index cdd8381396487caf13863915be143803e8b36025..ba1579940ad5b7fe9669a5f3c2dcc1ac1554e50b 100644 --- a/docs/exploration-and-organization/exploration.md +++ b/docs/exploration-and-organization/exploration.md @@ -88,7 +88,7 @@ In this example of orders by product category per month, clicking on a data poin - **Automatic insights**: See orders for a particular category over a shorter time range. - **Filter by this value**: update the chart based on the value you clicked: equal to, less than, greater than, or not equal to. -> Note that while charts created with SQL don't currently have the drill-through menu, you can add SQL questions to a dashboard and customize their click behavior. You can send people to a [custom destination](https://www.metabase.com/learn/building-analytics/dashboards/custom-destinations.html) (like another dashboard or an external URL), or have the clicked value [update a dashboard filter](https://www.metabase.com/learn/building-analytics/dashboards/cross-filtering.html). +> Note that while charts created with SQL currently only have [limited drill-through menu](../questions/native-editor/writing-sql.md#drill-though-in-sql-questions), you can add SQL questions to a dashboard and customize their click behavior. You can send people to a [custom destination](https://www.metabase.com/learn/building-analytics/dashboards/custom-destinations.html) (like another dashboard or an external URL), or have the clicked value [update a dashboard filter](https://www.metabase.com/learn/building-analytics/dashboards/cross-filtering.html). Clicking on a table cell will often allow you to filter the results using a comparison operator, like =, >, or <. For example, you can click on a table cell, and select the less than operator `<` to filter for values that are less than the selected value. diff --git a/docs/installation-and-operation/observability-with-prometheus.md b/docs/installation-and-operation/observability-with-prometheus.md index 79416920062cad193e62df975d8d4666749b10e5..7ab0bdaaafb69617c7a09b4ba3fcfbcf71a39feb 100644 --- a/docs/installation-and-operation/observability-with-prometheus.md +++ b/docs/installation-and-operation/observability-with-prometheus.md @@ -1,5 +1,5 @@ --- -title: observability-with-prometheus +title: Observability with Prometheus --- # Observability with Prometheus @@ -47,24 +47,24 @@ Change into the Prometheus directory, add the following YAML file to configure y ```yaml global: - scrape_interval: 15s # By default, scrape targets every 15 seconds. + scrape_interval: 15s # By default, scrape targets every 15 seconds. # Attach these labels to any time series or alerts when communicating with # external systems (federation, remote storage, Alertmanager). external_labels: - monitor: 'codelab-monitor' + monitor: "codelab-monitor" # A scrape configuration containing exactly one endpoint to scrape: # Here it's Prometheus itself. scrape_configs: # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config. - - job_name: 'prometheus' + - job_name: "prometheus" # Override the global default and scrape targets from this job every 5 seconds. scrape_interval: 5s # use whatever port here that you set for MB_PROMETHEUS_SERVER_PORT static_configs: - - targets: ['localhost:9191'] + - targets: ["localhost:9191"] ``` You need to change the "target" to where Metabase is, for this particular example, Metabase resides in the same host where Prometheus is running ("localhost"). diff --git a/docs/questions/images/bulk-filter-modal.png b/docs/questions/images/bulk-filter-modal.png index b1202f48e68ee020b59bde82d58a3014b057f29e..c8c0597019e762b9e14bf1c58881739b59cc90f7 100644 Binary files a/docs/questions/images/bulk-filter-modal.png and b/docs/questions/images/bulk-filter-modal.png differ diff --git a/docs/questions/images/data-picker.png b/docs/questions/images/data-picker.png new file mode 100644 index 0000000000000000000000000000000000000000..72e246f8a2ca85d1df6150ba101693bc9ffa576a Binary files /dev/null and b/docs/questions/images/data-picker.png differ diff --git a/docs/questions/images/legend.png b/docs/questions/images/legend.png new file mode 100644 index 0000000000000000000000000000000000000000..5ddf1b86dbcb56bcdb2b6cd27c038bdf6366ea74 Binary files /dev/null and b/docs/questions/images/legend.png differ diff --git a/docs/questions/native-editor/writing-sql.md b/docs/questions/native-editor/writing-sql.md index f49d4108bac52462b9a91ebc10ef3d8428874db0..a15d29d75b56968bc4d845839773c925e8db1998 100644 --- a/docs/questions/native-editor/writing-sql.md +++ b/docs/questions/native-editor/writing-sql.md @@ -88,13 +88,12 @@ On saved SQL questions without [parameters](./sql-parameters.md), you'll get the  -## To enable drill-through, turn a SQL question into a model and set the data types +## Drill-though in SQL questions -Visualizations created with SQL do not have [drill-through][drill-through] capability. To enable drill-through on a SQL question, you can turn it into a model: +Visualizations created with SQL have limited [drill-through][drill-through] capabilities: -1. Save the SQL question and [turn it into a model](../../data-modeling/models.md#create-a-model-from-a-saved-question). -2. [Edit the column metadata](../../data-modeling/metadata-editing.md#column-field-settings) in the model's settings. Make sure to set the data types for all the columns. -3. [Create a Query Builder question](../query-builder/introduction.md#creating-a-new-question-with-the-query-builder) based on the model. You should be able to use drill-through on this question, if you configured the metadata correctly. +- You can filter results of SQL queries by clicking on data points, zoom in on time series or maps, and use some [column header actions](../sharing/visualizations/table.md#column-heading-options-for-filtering-and-summarizing). +- You won't be able to drill down to unaggregated records, change time granularity, or break out by categories or locations. ## Caching results diff --git a/docs/questions/query-builder/introduction.md b/docs/questions/query-builder/introduction.md index 3f0b61e151cbabdc01ae0e7f92563fe91027e2c1..b12dfdf57c990fbe44b804238db5af49cff29c61 100644 --- a/docs/questions/query-builder/introduction.md +++ b/docs/questions/query-builder/introduction.md @@ -20,6 +20,7 @@ From the **+ New** dropdown, select **Question**, then pick your starting data: You can start a question from: - **A model**. A [model](../../data-modeling/models.md) is a special kind of saved question meant to be used as a good starting point for questions. Sometimes these are called derived tables, as they usually pull together data from multiple raw tables. +- **A metric**. [Metrics](../../data-modeling/metrics.md) are pre-defined calculations. If you pick a metric as a starting point for a question, Metabase will create a question with the same data source as the selected metric, and apply the metric. You'll be able to add more joins, filter, and summaries. - **Tables**. You'll need to specify the database and the table in that database as the starting point for your question. - A **saved question**. You can use the results of any question as the starting point for a new question. @@ -42,13 +43,21 @@ This is the query builder's editor. It has three default steps. - [Filtering](#filtering) - [Summarizing and grouping by](#summarizing-and-grouping-by) +You can also add steps for [joining data](#joining-data), [custom columns](#creating-custom-columns), and [sorting results](#sorting-results). + To the right of each completed step is a **Preview** button (looks like a Play button - a triangle pointing to the right) that shows you the first 10 rows of the results of your question up to that step.  ## Picking data -The data section is where you select the data you want to work with. Here you'll pick a [model](../../data-modeling/models.md), a table from a database, or a saved question. You can click on a table to select which columns you want to include in your results. See also [adding or removing columns in a table](#adding-or-removing-columns-in-a-table). +The data section is where you select the data you want to work with. Here you'll pick a [model](../../data-modeling/models.md), a [metric](../../data-modeling/metrics.md), a table from a database, or a saved question. + + + +You can see the data source in a new browser tab by Cmd/Ctrl+Clicking on the data source's name in the query builder. + +To choose which columns to include in your query, click on the arrow next to the data source . You'll also be able [hide columns ](#adding-or-removing-columns-in-a-table) from the table view once you visualize your results. ## Joining data @@ -68,7 +77,7 @@ Once you're happy with your filter, click **Add filter**, and visualize your res If you want to edit your filter, just click the little purple filter at the top of the screen. If you click on the X, you'll remove your filter. You can add as many filters as you need. -## Filter types +### Filter types Depending on the data type of the column, Metabase will present different filtering options. @@ -77,13 +86,13 @@ Depending on the data type of the column, Metabase will present different filter - **Date columns** give you a lot of options to filter by specific date ranges, relative date ranges, and more. - **Structured data columns**, typically JSON or XML, can only be filtered by "Is empty" or "Not empty". Some databases, however, support [JSON unfolding](../../data-modeling/json-unfolding.md), which allows you to split up JSON data into separate columns, which you can then filter on. -## Filter multiple columns +### Filter multiple columns When viewing a table or chart, clicking on the **Filter** will bring up the filter modal.  -Here you can add multiple filters to your question in one go (which can save you a lot of loading time). Filter options will differ depending on the [field type](../../data-modeling/field-types.md). Any tables linked by foreign keys will be displayed in the left tab of the modal. +Here you can add multiple filters to your question in one go (which can save you a lot of loading time). Filter options will differ depending on the [field type](../../data-modeling/field-types.md). Any tables linked by foreign keys will be displayed in the left tab of the modal. You can also filter your summaries. When you're done adding filters, hit **Apply filters** to rerun the query and update its results. To remove all the filters you've applied, click on **Clear all filters** in the bottom left of the filter modal. Any filters you apply here will show up in the editor, and vice versa. @@ -159,9 +168,14 @@ When you click on a different grouping column than the one you currently have se  -Some grouping columns will give you the option of choosing how big or small to make the groupings. So for example, if you've picked a Date column to group by, you can click on the words `by month` to change the grouping to day, week, hour, quarter, year, etc. If you're grouping by a numeric column, like age, Metabase will automatically "bin" the results, so you'll see your metric grouped in age brackets, like 0–10, 11–20, 21–30, etc. Just like with dates, you can click on the current binning option to change it to a specific number of bins. It's not currently possible to choose your own ranges for bins, though. +Some grouping columns will give you the option of choosing how big or small to make the groupings: + +- For datetime columns, you can click on the words `by month` to change the grouping to day, week, hour, quarter, year, etc. You'll also be able to add multiple breakouts by the same datetime column with different time granularities (for example, group by week and day of the week). +- For numeric columns like age, Metabase will automatically "bin" the results, so you'll see your metric grouped in age brackets, like 0–10, 11–20, 21–30, etc. Just like with dates, you can click on the current binning option to change it to a specific number of bins. Currently, you can't choose your own ranges for bins. + +  - + If you select a fixed number of bins, Metabase will break the range of the data into that number of equal size intervals. Some intervals might end up having no data, and Metabase will not display them. Once you're done setting your metrics and groupings, click **Visualize** to see your results in all their glory. @@ -175,7 +189,7 @@ When viewing a chart, you can also click through questions to explore the data i  -The drill-through menu will present different options depending on what you click on. You can then optionally save any exploration as a new question. The drill-through menu is only available for questions built using the query builder. For more on how drill-through works, check out [Creating interactive charts](https://www.metabase.com/learn/questions/drill-through). +The drill-through menu will present different options depending on what you click on. You can then optionally save any exploration as a new question. Full drill-through menu is only available for questions built using the query builder. Questions build with SQL/native queries will have only have [limited drill-through actions](../native-editor/writing-sql.md#drill-though-in-sql-questions). For more on how drill-through works, check out [Creating interactive charts](https://www.metabase.com/learn/questions/drill-through). ## Column heading drill-through @@ -193,7 +207,7 @@ Custom expressions allow you to use spreadsheet-like functions and simple arithm  - For example, you could do `Average(sqrt[FieldX]) + Sum([FieldY])` or `Max(floor([FieldX] - [FieldY]))`, where `FieldX` and `FieldY` are fields in the currently selected table. [Learn more about writing expressions](./expressions.md). +For example, you could do `Average(sqrt[FieldX]) + Sum([FieldY])` or `Max(floor([FieldX] - [FieldY]))`, where `FieldX` and `FieldY` are fields in the currently selected table. [Learn more about writing expressions](./expressions.md). ### Creating custom columns diff --git a/docs/questions/sharing/visualizations/line-bar-and-area-charts.md b/docs/questions/sharing/visualizations/line-bar-and-area-charts.md index a7a24778f6d7600a6e7f42575079b4e7e821fa39..0576b4d8a77294ebd58d4bd01956f757a1c70016 100644 --- a/docs/questions/sharing/visualizations/line-bar-and-area-charts.md +++ b/docs/questions/sharing/visualizations/line-bar-and-area-charts.md @@ -125,21 +125,40 @@ Here you'll find additional settings for configuring your x and y axes (as in ax ### X-axis -- Show label (the legend label for the axis). +- Show label (the label for the axis). - Rename the axis - Show line and marks - Scale: Timeseries or Ordinal. ### Y-axis -- Show label (the legend label for the axis). +- Show label (the label for the axis). - Rename the axis - Split y-axis when necessary - Auto y-axis range. When not toggled on, you can set the y-axis range (it's min and max values). -- Unpin from zero. Allows you to "Zoom in" on charts with values well above zero. Here's an example (note the y-axis starts at 20,000): - - Scale: Linear, power, or log. - Show lines and marks +- Unpin from zero. Allows you to "Zoom in" on charts with values well above zero. Here's an example (note the y-axis starts at 20,000): +  + +## Chart legend + +For charts with multiple series or breakouts, chart legend displays the label and color of each series. + + + +You can change the color and label for each series and reorder them in [data settings](#data-settings). + +You can use the legend to: + +- Highlight a series, by hovering over the name of the series in the legend. +- Hide the series, by clicking on the color circle for the series. + +To permanently hide the series from the chart, use the [data settings](#data-settings). + +- Drill down to individual records for aggregated series, by clicking on the series name. + +Currently, you can't hide the legend or change its position on the chart. ## Further reading diff --git a/docs/troubleshooting-guide/diagnostic-info.md b/docs/troubleshooting-guide/diagnostic-info.md index 92dde556146544d48491daeaf7b2e6fed028cdd7..7dd0597ad0de0ad089b591eb6f50bc3940723f9d 100644 --- a/docs/troubleshooting-guide/diagnostic-info.md +++ b/docs/troubleshooting-guide/diagnostic-info.md @@ -4,7 +4,7 @@ title: Diagnostic information for troubleshooting # Diagnostic information for troubleshooting -To download diagnostic information, hit `Cmd + F1` on Macs, `Ctrl + F1` on PCs. +To download diagnostic information, hit `Cmd + F1` on Macs, `Ctrl + F1` on PCs. Hit Cmd/Ctrl + K to bring up the command palette, search for "Diagnostic", and select "Open diagnostic error modal" Select the info you want to include in the diagnostic JSON file. Options include: @@ -15,7 +15,7 @@ Select the info you want to include in the diagnostic JSON file. Options include - Server logs from the current user only - Metabase instance version information -What data Metabase captures depends on the page you're on when you hit `Cmd/Ctrl + F1`. +What data Metabase captures depends on the page you're on when you request diagnostic information. > Review the downloaded file before sharing it, as the diagnostic info may contain sensitive data. diff --git a/e2e/support/helpers/api/createDashboardWithQuestions.ts b/e2e/support/helpers/api/createDashboardWithQuestions.ts index d2a269cd292b3ed416b3e574037058282ff4d5f2..99b8d6d1955debce11d304844d530764937b84da 100644 --- a/e2e/support/helpers/api/createDashboardWithQuestions.ts +++ b/e2e/support/helpers/api/createDashboardWithQuestions.ts @@ -1,4 +1,4 @@ -import type { Card, Dashboard } from "metabase-types/api"; +import type { Card, Dashboard, DashboardCard } from "metabase-types/api"; import { cypressWaitAll } from "../e2e-misc-helpers"; @@ -16,14 +16,11 @@ export const createDashboardWithQuestions = ({ dashboardName?: string; dashboardDetails?: DashboardDetails; questions: (NativeQuestionDetails | StructuredQuestionDetails)[]; - cards?: Partial<Card>[]; -}): Cypress.Chainable< - Cypress.Response<{ - dashboard: Dashboard; - questions: Card; - }> -> => { - // @ts-expect-error - Cypress typings don't account for what happens in then() here + cards?: Partial<DashboardCard>[]; +}): Cypress.Chainable<{ + dashboard: Dashboard; + questions: Card[]; +}> => { return createDashboard({ name: dashboardName, ...dashboardDetails }).then( ({ body: dashboard }) => { return cypressWaitAll( diff --git a/e2e/test/scenarios/embedding/embed-resource-downloads.cy.spec.ts b/e2e/test/scenarios/embedding/embed-resource-downloads.cy.spec.ts index 534b746e04f423b244e5c2d47b7d2014e16e53d1..cd5258f9e6f4d1e172cb5c85a59bce4aea0df4c4 100644 --- a/e2e/test/scenarios/embedding/embed-resource-downloads.cy.spec.ts +++ b/e2e/test/scenarios/embedding/embed-resource-downloads.cy.spec.ts @@ -1,8 +1,12 @@ +import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database"; import { ORDERS_BY_YEAR_QUESTION_ID, ORDERS_DASHBOARD_ID, } from "e2e/support/cypress_sample_instance_data"; import { + addOrUpdateDashboardCard, + createDashboardWithQuestions, + createNativeQuestion, describeWithSnowplowEE, expectGoodSnowplowEvent, expectNoBadSnowplowEvents, @@ -16,6 +20,9 @@ import { showDashboardCardActions, visitEmbeddedPage, } from "e2e/support/helpers"; +import { createMockParameter } from "metabase-types/api/mocks"; + +const { PRODUCTS, PRODUCTS_ID } = SAMPLE_DATABASE; /** These tests are about the `downloads` flag for static embeds, both dashboards and questions. * Unless the product changes, these should test the same things as `public-resource-downloads.cy.spec.ts` @@ -120,6 +127,89 @@ describeWithSnowplowEE( export_type: "csv", }); }); + + describe("with dashboard parameters", () => { + beforeEach(() => { + cy.signInAsAdmin(); + + setTokenFeatures("all"); + + // Test parameter with accentuation (metabase#49118) + const CATEGORY_FILTER = createMockParameter({ + id: "5aefc725", + name: "usuário", + slug: "usu%C3%A1rio", + type: "string/=", + }); + const questionDetails = { + name: "Products", + query: { + "source-table": PRODUCTS_ID, + }, + }; + createDashboardWithQuestions({ + // Can't figure out the type if I extracted `dashboardDetails` to a variable. + dashboardDetails: { + name: "Dashboard with a parameter", + parameters: [CATEGORY_FILTER], + enable_embedding: true, + embedding_params: { + [CATEGORY_FILTER.slug]: "enabled", + }, + }, + questions: [questionDetails], + }).then(({ dashboard, questions }) => { + const dashboardId = dashboard.id; + cy.wrap(dashboardId).as("dashboardId"); + const questionId = questions[0].id; + addOrUpdateDashboardCard({ + dashboard_id: dashboardId, + card_id: questionId, + card: { + parameter_mappings: [ + { + card_id: questionId, + parameter_id: CATEGORY_FILTER.id, + target: ["dimension", ["field", PRODUCTS.CATEGORY, null]], + }, + ], + }, + }); + }); + + cy.signOut(); + }); + + it("should be able to download a static embedded dashcard as CSV", () => { + cy.get("@dashboardId").then(dashboardId => { + visitEmbeddedPage( + { + resource: { dashboard: Number(dashboardId) }, + params: {}, + }, + { + pageStyle: { + downloads: true, + }, + }, + ); + }); + + waitLoading(); + + showDashboardCardActions(); + getDashboardCardMenu().click(); + exportFromDashcard(".csv"); + cy.verifyDownload(".csv", { contains: true }); + + expectGoodSnowplowEvent({ + event: "download_results_clicked", + resource_type: "dashcard", + accessed_via: "static-embed", + export_type: "csv", + }); + }); + }); }); describe("Static embed questions", () => { @@ -216,6 +306,92 @@ describeWithSnowplowEE( export_type: "csv", }); }); + + describe("with native question parameters", () => { + beforeEach(() => { + cy.signInAsAdmin(); + + setTokenFeatures("all"); + + // Can't figure out the type if I extracted `questionDetails` to a variable. + createNativeQuestion( + { + name: "Native question with a parameter", + native: { + "template-tags": { + num: { + id: "f7672b4d-1e84-1fa8-bf02-b5e584cd4535", + name: "num", + "display-name": "Num", + type: "number", + default: null, + }, + }, + query: "select {{num}}", + }, + parameters: [ + { + id: "f7672b4d-1e84-1fa8-bf02-b5e584cd4535", + type: "number/=", + target: ["variable", ["template-tag", "num"]], + name: "Num", + slug: "num", + default: null, + }, + ], + enable_embedding: true, + embedding_params: { + num: "enabled", + }, + }, + { + idAlias: "questionId", + wrapId: true, + }, + ); + + cy.signOut(); + }); + + it("should be able to download a static embedded dashcard as CSV", () => { + const value = 9999; + cy.get("@questionId").then(questionId => { + visitEmbeddedPage( + { + resource: { question: Number(questionId) }, + params: { + num: value, + }, + }, + { + pageStyle: { + downloads: true, + }, + }, + ); + }); + + waitLoading(); + + main().findByText(value).should("exist"); + + cy.findByTestId("download-button").click(); + + popover().within(() => { + cy.findByText(".csv").click(); + cy.findByTestId("download-results-button").click(); + }); + + cy.verifyDownload(".csv", { contains: true }); + + expectGoodSnowplowEvent({ + event: "download_results_clicked", + resource_type: "question", + accessed_via: "static-embed", + export_type: "csv", + }); + }); + }); }); }, ); diff --git a/e2e/test/scenarios/embedding/embedding-reproductions.cy.spec.js b/e2e/test/scenarios/embedding/embedding-reproductions.cy.spec.js index 5f01b261a7737ca869cc7a6a9668491c2a45a962..e3e96598bdcf493418f260577b445bad5b474bf9 100644 --- a/e2e/test/scenarios/embedding/embedding-reproductions.cy.spec.js +++ b/e2e/test/scenarios/embedding/embedding-reproductions.cy.spec.js @@ -2,6 +2,7 @@ import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database"; import { addOrUpdateDashboardCard, createNativeQuestion, + createQuestionAndDashboard, describeEE, filterWidget, getDashboardCard, @@ -983,3 +984,39 @@ describe("issue 40660", () => { }); }); }); + +// Skipped since it does not make sense when CSP is disabled +describe.skip("issue 49142", () => { + const questionDetails = { + name: "Products", + query: { "source-table": PRODUCTS_ID, limit: 2 }, + }; + + const dashboardDetails = { + name: "Embeddable dashboard", + enable_embedding: true, + }; + + beforeEach(() => { + restore(); + cy.signInAsAdmin(); + + createQuestionAndDashboard({ + questionDetails, + dashboardDetails, + }).then(({ body: { dashboard_id } }) => { + visitDashboard(dashboard_id); + }); + }); + + it("embedding preview should be always working", () => { + openStaticEmbeddingModal({ + activeTab: "lookAndFeel", + previewMode: "preview", + }); + cy.findByTestId("embed-preview-iframe") + .its("0.contentDocument.body") + .should("be.visible") + .and("contain", "Embeddable dashboard"); + }); +}); diff --git a/e2e/test/scenarios/filters-reproductions/dashboard-filters-reproductions.cy.spec.js b/e2e/test/scenarios/filters-reproductions/dashboard-filters-reproductions.cy.spec.js index 8ca110b681be17d558674da72c711e4776d08481..7e087ea86ab8ea715ec69d488d7c9282f68fb9df 100644 --- a/e2e/test/scenarios/filters-reproductions/dashboard-filters-reproductions.cy.spec.js +++ b/e2e/test/scenarios/filters-reproductions/dashboard-filters-reproductions.cy.spec.js @@ -1,5 +1,6 @@ import moment from "moment-timezone"; // eslint-disable-line no-restricted-imports -- deprecated usage +import { WRITABLE_DB_ID } from "e2e/support/cypress_data"; import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database"; import { ADMIN_PERSONAL_COLLECTION_ID, @@ -29,7 +30,9 @@ import { openNavigationSidebar, openQuestionsSidebar, popover, + resetTestTable, restore, + resyncDatabase, saveDashboard, selectDashboardFilter, setFilter, @@ -3541,20 +3544,13 @@ describe("issue 44790", () => { }); }); - cy.log( - "wrong value for id filter should be handled and card should not hang", - ); - + cy.log("wrong value for id filter should be ignored"); visitDashboard("@dashboardId", { params: { [idFilter.slug]: "{{test}}", }, }); - - getDashboardCard().should( - "contain", - "There was a problem displaying this chart.", - ); + getDashboardCard().should("contain", "borer-hudson@yahoo.com"); cy.log("wrong value for number filter should be ignored"); visitDashboard("@dashboardId", { @@ -3563,7 +3559,6 @@ describe("issue 44790", () => { [idFilter.slug]: "1", }, }); - getDashboardCard().should("contain", "borer-hudson@yahoo.com"); }); }); @@ -3885,3 +3880,98 @@ describe("issue 32573", () => { }); }); }); + +describe("issue 45670", { tags: ["@external"] }, () => { + const dialect = "postgres"; + const tableName = "many_data_types"; + + const parameterDetails = { + id: "92eb69ea", + name: "boolean", + type: "string/=", + slug: "boolean", + sectionId: "string", + }; + + const dashboardDetails = { + parameters: [parameterDetails], + }; + + function getField() { + return cy.request("GET", "/api/table").then(({ body: tables }) => { + const table = tables.find(table => table.name === tableName); + return cy + .request("GET", `/api/table/${table.id}/query_metadata`) + .then(({ body: metadata }) => { + const { fields } = metadata; + return fields.find(field => field.name === "boolean"); + }); + }); + } + + function getQuestionDetails(fieldId) { + return { + database: WRITABLE_DB_ID, + native: { + query: "SELECT id, boolean FROM many_data_types WHERE {{boolean}}", + "template-tags": { + boolean: { + id: "4b77cc1f-ea70-4ef6-84db-58432fce6928", + name: "boolean", + type: "dimension", + "display-name": "Boolean", + dimension: ["field", fieldId, null], + "widget-type": "string/=", + }, + }, + }, + }; + } + + function getParameterMapping(cardId) { + return { + card_id: cardId, + parameter_id: parameterDetails.id, + target: ["dimension", ["template-tag", parameterDetails.name]], + }; + } + + beforeEach(() => { + resetTestTable({ type: dialect, table: tableName }); + restore(`${dialect}-writable`); + cy.signInAsAdmin(); + resyncDatabase({ dbId: WRITABLE_DB_ID, tableName }); + cy.intercept("PUT", "/api/card/*").as("updateCard"); + }); + + it("should be able to pass query string parameters for boolean parameters in dashboards (metabase#45670)", () => { + getField().then(field => { + createNativeQuestion(getQuestionDetails(field.id)).then( + ({ body: card }) => { + createDashboard(dashboardDetails).then(({ body: dashboard }) => { + cy.request("PUT", `/api/dashboard/${dashboard.id}`, { + dashcards: [ + createMockDashboardCard({ + card_id: card.id, + parameter_mappings: [getParameterMapping(card.id, field.id)], + size_x: 8, + size_y: 8, + }), + ], + }); + visitDashboard(dashboard.id, { + params: { + [parameterDetails.slug]: "true", + }, + }); + }); + }, + ); + }); + filterWidget().should("contain.text", "true"); + getDashboardCard().within(() => { + cy.findByText("true").should("be.visible"); + cy.findByText("false").should("not.exist"); + }); + }); +}); diff --git a/e2e/test/scenarios/visualizations-charts/pie_chart.cy.spec.js b/e2e/test/scenarios/visualizations-charts/pie_chart.cy.spec.js index e37984ae2d1b4b4a2e9e7c5352662a3e7dbb158b..c35c849636f09d18a087ce830671d7afd9ab1a17 100644 --- a/e2e/test/scenarios/visualizations-charts/pie_chart.cy.spec.js +++ b/e2e/test/scenarios/visualizations-charts/pie_chart.cy.spec.js @@ -534,6 +534,60 @@ describe("scenarios > visualizations > pie chart", () => { confirmSliceClickBehavior("Organic", 1180, 1); confirmSliceClickBehavior("Gizmo", 354, 8); }); + + it("should handle min percentage setting correctly", () => { + createQuestionAndDashboard({ + questionDetails: { + query: threeRingQuery.query, + display: "pie", + visualization_settings: { + "pie.slice_threshold": 20.6, + "pie.percent_visibility": "inside", + "pie.show_labels": false, + }, + }, + cardDetails: { + size_x: 30, + size_y: 15, + }, + }).then(({ body: { dashboard_id } }) => { + visitDashboard(dashboard_id); + }); + + // Other slice percentage + echartsContainer().findByText("79%").realHover(); + + assertEChartsTooltip({ + header: "2024", + rows: [ + { + name: "Affiliate", + value: "1,046", + secondaryValue: "22.68 %", + }, + { + name: "Google", + value: "1,195", + secondaryValue: "25.92 %", + }, + { + name: "Organic", + value: "1,180", + secondaryValue: "25.59 %", + }, + { + name: "Twitter", + value: "1,190", + secondaryValue: "25.81 %", + }, + { + name: "Total", + value: "4,611", + secondaryValue: "100 %", + }, + ], + }); + }); }); function ensurePieChartRendered(rows, middleRows, outerRows, totalValue) { diff --git a/frontend/src/metabase-lib/v1/parameters/utils/parameter-parsing.ts b/frontend/src/metabase-lib/v1/parameters/utils/parameter-parsing.ts index ba53aa958846d7a8e1a3bdb0ebb5f60b34c5e25c..3498ccc9ef5eb854d0fbf7d36b227b1254dc82d1 100644 --- a/frontend/src/metabase-lib/v1/parameters/utils/parameter-parsing.ts +++ b/frontend/src/metabase-lib/v1/parameters/utils/parameter-parsing.ts @@ -93,7 +93,8 @@ function parseParameterValueForFields( // unix dates fields are numeric but query params shouldn't be parsed as numbers if (fields.every(f => f.isNumeric() && !f.isDate())) { - return parseFloat(value); + const number = parseFloat(value); + return isNaN(number) ? [] : number; } if (fields.every(f => f.isBoolean())) { diff --git a/frontend/src/metabase-lib/v1/parameters/utils/parameter-parsing.unit.spec.js b/frontend/src/metabase-lib/v1/parameters/utils/parameter-parsing.unit.spec.js index 287c47da11e465213130d50d49b4a8c4cadbaa98..8456066522932359d4fb7d170a9d5d0fc59a38c2 100644 --- a/frontend/src/metabase-lib/v1/parameters/utils/parameter-parsing.unit.spec.js +++ b/frontend/src/metabase-lib/v1/parameters/utils/parameter-parsing.unit.spec.js @@ -287,7 +287,7 @@ describe("parameters/utils/parameter-values", () => { getParameterValueFromQueryParams(parameter2, { [parameter2.slug]: "parameter2 default value", }), - ).toEqual([NaN]); + ).toEqual([]); expect(getParameterValueFromQueryParams(parameter2, {})).toBe( "parameter2 default value", diff --git a/frontend/src/metabase/dashboard/actions/data-fetching.ts b/frontend/src/metabase/dashboard/actions/data-fetching.ts index 765fb9a22369652e0d6e9da09a1152a7b774cdf7..d78829bc36e8ee160ec94aa0c89a9708c915da8b 100644 --- a/frontend/src/metabase/dashboard/actions/data-fetching.ts +++ b/frontend/src/metabase/dashboard/actions/data-fetching.ts @@ -32,7 +32,12 @@ import { defer } from "metabase/lib/promise"; import { createAsyncThunk, createThunkAction } from "metabase/lib/redux"; import { equals } from "metabase/lib/utils"; import { uuid } from "metabase/lib/uuid"; +import { + getDashboardQuestions, + getDashboardUiParameters, +} from "metabase/parameters/utils/dashboards"; import { addFields, addParamValues } from "metabase/redux/metadata"; +import { getMetadata } from "metabase/selectors/metadata"; import { AutoApi, CardApi, @@ -726,10 +731,18 @@ export const fetchDashboard = createAsyncThunk( const lastUsedParametersValues = result["last_used_param_values"] ?? {}; + const metadata = getMetadata(getState()); + const questions = getDashboardQuestions(result.dashcards, metadata); + const parameters = getDashboardUiParameters( + result.dashcards, + result.parameters ?? [], + metadata, + questions, + ); const parameterValuesById = preserveParameters ? getParameterValues(getState()) : getParameterValuesByIdFromQueryParams( - result.parameters ?? [], + parameters, queryParams, lastUsedParametersValues, ); diff --git a/frontend/src/metabase/dashboard/selectors.ts b/frontend/src/metabase/dashboard/selectors.ts index 666b93f5655066d29da64a23ae88a5f8d768d1c5..0c73299191e328a421f7e36f648eb6b5cd87a3cf 100644 --- a/frontend/src/metabase/dashboard/selectors.ts +++ b/frontend/src/metabase/dashboard/selectors.ts @@ -1,6 +1,5 @@ import { createSelector } from "@reduxjs/toolkit"; import { createCachedSelector } from "re-reselect"; -import { createSelectorCreator, lruMemoize } from "reselect"; import _ from "underscore"; import { @@ -9,7 +8,10 @@ import { } from "metabase/dashboard/constants"; import { LOAD_COMPLETE_FAVICON } from "metabase/hoc/Favicon"; import * as Urls from "metabase/lib/urls"; -import { getDashboardUiParameters } from "metabase/parameters/utils/dashboards"; +import { + getDashboardQuestions, + getDashboardUiParameters, +} from "metabase/parameters/utils/dashboards"; import { getParameterMappingOptions as _getParameterMappingOptions } from "metabase/parameters/utils/mapping-options"; import { getVisibleParameters } from "metabase/parameters/utils/ui"; import type { EmbeddingParameterVisibility } from "metabase/public/lib/types"; @@ -25,7 +27,6 @@ import { } from "metabase-lib/v1/parameters/utils/parameter-values"; import type { Card, - CardId, DashCardId, Dashboard, DashboardCard, @@ -63,8 +64,6 @@ function isEditParameterSidebar( return sidebar.name === SIDEBAR_NAME.editParameter; } -const createDeepEqualSelector = createSelectorCreator(lruMemoize, _.isEqual); - export const getDashboardBeforeEditing = (state: State) => state.dashboard.editingDashboard; @@ -374,50 +373,18 @@ export const getParameterTarget = createSelector( }, ); -export const getQuestions = (state: State) => { - const dashboard = getDashboard(state); - - if (!dashboard) { - return []; - } - - const dashcardIds = dashboard.dashcards; - - const questionsById = dashcardIds.reduce<Record<CardId, Question>>( - (acc, dashcardId) => { - const dashcard = getDashCardById(state, dashcardId); - - if (isQuestionDashCard(dashcard)) { - const cards = [dashcard.card, ...(dashcard.series ?? [])]; - - for (const card of cards) { - const question = getQuestionByCard(state, { card }); - if (question) { - acc[card.id] = question; - } - } - } - - return acc; - }, - {}, - ); - - return questionsById; -}; - -// TODO: remove it as we added cache to MLv2 and it should be fast now -// getQuestions selector returns an array with stable references to the questions -// but array itself is always new, so it may cause troubles in re-renderings -const getQuestionsMemoized = createDeepEqualSelector( - [getQuestions], - questions => { - return questions; +export const getQuestions = createSelector( + [getDashboardComplete, getMetadata], + (dashboard, metadata) => { + if (!dashboard) { + return {}; + } + return getDashboardQuestions(dashboard.dashcards, metadata); }, ); export const getParameters = createSelector( - [getDashboardComplete, getMetadata, getQuestionsMemoized], + [getDashboardComplete, getMetadata, getQuestions], (dashboard, metadata, questions) => { if (!dashboard || !metadata) { return []; diff --git a/frontend/src/metabase/parameters/utils/dashboards.ts b/frontend/src/metabase/parameters/utils/dashboards.ts index c4fe9632599703000e5ebc58b8b5b936694804e5..b95b6412ded07e95bec5ac7ee20b94df5c631040 100644 --- a/frontend/src/metabase/parameters/utils/dashboards.ts +++ b/frontend/src/metabase/parameters/utils/dashboards.ts @@ -20,6 +20,7 @@ import type { CardId, DashCardId, Dashboard, + DashboardCard, DashboardParameterMapping, Parameter, ParameterMappingOptions, @@ -142,6 +143,28 @@ export function getDashboardUiParameters( return uiParameters; } +export function getDashboardQuestions( + dashcards: DashboardCard[], + metadata: Metadata, +) { + return dashcards.reduce<Record<CardId, Question>>((acc, dashcard) => { + if (isQuestionDashCard(dashcard)) { + const cards = [dashcard.card, ...(dashcard.series ?? [])]; + + for (const card of cards) { + const question = isQuestionCard(card) + ? new Question(card, metadata) + : undefined; + if (question) { + acc[card.id] = question; + } + } + } + + return acc; + }, {}); +} + function buildFieldFilterUiParameter( parameter: Parameter, mappings: ExtendedMapping[], diff --git a/frontend/src/metabase/redux/downloads.ts b/frontend/src/metabase/redux/downloads.ts index 76046612aafd8e34a88928b6cb183fd186790ebe..a41539744396af38dfbb592bd82b9063888c6754 100644 --- a/frontend/src/metabase/redux/downloads.ts +++ b/frontend/src/metabase/redux/downloads.ts @@ -206,20 +206,23 @@ const getDatasetParams = ({ return { method: "GET", url: `/api/embed/dashboard/${token}/dashcard/${dashcardId}/card/${cardId}/${type}`, - params: Urls.getEncodedUrlSearchParams({ ...params, ...exportParams }), + params: new URLSearchParams({ + parameters: JSON.stringify(params), + ..._.mapObject(exportParams, value => String(value)), + }), }; } if (resource === "question" && token) { - // For whatever wacky reason the /api/embed endpoint expect params like ?key=value instead - // of like ?params=<json-encoded-params-array> like the other endpoints do. const params = new URLSearchParams(window.location.search); - params.set("format_rows", String(enableFormatting)); - params.set("pivot_results", String(enablePivot)); + return { method: "GET", url: Urls.embedCard(token, type), - params, + params: new URLSearchParams({ + parameters: JSON.stringify(Object.fromEntries(params)), + ..._.mapObject(exportParams, value => String(value)), + }), }; } } diff --git a/frontend/src/metabase/static-viz/components/PieChart/PieChart.stories.tsx b/frontend/src/metabase/static-viz/components/PieChart/PieChart.stories.tsx index 8615400f019f6058fac1d8154ef71948492c835d..a2f7cc4804f7b817f7f0035759806603c2e30b55 100644 --- a/frontend/src/metabase/static-viz/components/PieChart/PieChart.stories.tsx +++ b/frontend/src/metabase/static-viz/components/PieChart/PieChart.stories.tsx @@ -412,3 +412,9 @@ LabelsOnChart.args = { rawSeries: data.labelsOnChart as any, renderingContext, }; + +export const SunburstOtherLabel = Template.bind({}); +SunburstOtherLabel.args = { + rawSeries: data.sunburstOtherLabel as any, + renderingContext, +}; diff --git a/frontend/src/metabase/static-viz/components/PieChart/stories-data/index.ts b/frontend/src/metabase/static-viz/components/PieChart/stories-data/index.ts index d94786064832220c7baf39aa9d13aee2d071cab9..4448ea8023b2e65f5228fe486aeee5e25cef82b4 100644 --- a/frontend/src/metabase/static-viz/components/PieChart/stories-data/index.ts +++ b/frontend/src/metabase/static-viz/components/PieChart/stories-data/index.ts @@ -37,6 +37,7 @@ import showPercentagesOnChartDense from "./show-percentages-on-chart-dense.json" import showPercentagesOnChart from "./show-percentages-on-chart.json"; import singleDimension from "./single-dimension.json"; import smallMinimumSlicePercentage from "./small-min-slice-percentage.json"; +import sunburstOtherLabel from "./sunburst-other-label.json"; import threeRingsNoLabels from "./three-rings-no-labels.json"; import threeRingsOtherSlices from "./three-rings-other-slices.json"; import threeRingsPercentagesAndLabels from "./three-rings-percentages-and-labels.json"; @@ -98,4 +99,5 @@ export const data = { threeRingsPercentagesOnChart, labelsWithPercent, labelsOnChart, + sunburstOtherLabel, }; diff --git a/frontend/src/metabase/static-viz/components/PieChart/stories-data/sunburst-other-label.json b/frontend/src/metabase/static-viz/components/PieChart/stories-data/sunburst-other-label.json new file mode 100644 index 0000000000000000000000000000000000000000..0d95f42e6c2c6a3e4a08574cc438240c727083b6 --- /dev/null +++ b/frontend/src/metabase/static-viz/components/PieChart/stories-data/sunburst-other-label.json @@ -0,0 +1,551 @@ +[ + { + "data": { + "rows": [ + ["2022-01-01T00:00:00-05:00", 0, 6], + ["2022-01-01T00:00:00-05:00", 20, 212], + ["2022-01-01T00:00:00-05:00", 40, 242], + ["2022-01-01T00:00:00-05:00", 60, 95], + ["2022-01-01T00:00:00-05:00", 80, 184], + ["2022-01-01T00:00:00-05:00", 100, 5], + ["2023-01-01T00:00:00-05:00", -60, 1], + ["2023-01-01T00:00:00-05:00", 0, 37], + ["2023-01-01T00:00:00-05:00", 20, 1014], + ["2023-01-01T00:00:00-05:00", 40, 1104], + ["2023-01-01T00:00:00-05:00", 60, 649], + ["2023-01-01T00:00:00-05:00", 80, 751], + ["2023-01-01T00:00:00-05:00", 100, 54], + ["2024-01-01T00:00:00-05:00", 0, 5], + ["2024-01-01T00:00:00-05:00", 20, 377], + ["2024-01-01T00:00:00-05:00", 40, 1155], + ["2024-01-01T00:00:00-05:00", 60, 1331], + ["2024-01-01T00:00:00-05:00", 80, 662], + ["2024-01-01T00:00:00-05:00", 100, 963], + ["2024-01-01T00:00:00-05:00", 120, 992], + ["2024-01-01T00:00:00-05:00", 140, 349], + ["2025-01-01T00:00:00-05:00", 0, 3], + ["2025-01-01T00:00:00-05:00", 20, 449], + ["2025-01-01T00:00:00-05:00", 40, 1239], + ["2025-01-01T00:00:00-05:00", 60, 1495], + ["2025-01-01T00:00:00-05:00", 80, 805], + ["2025-01-01T00:00:00-05:00", 100, 1062], + ["2025-01-01T00:00:00-05:00", 120, 1121], + ["2025-01-01T00:00:00-05:00", 140, 404], + ["2026-01-01T00:00:00-05:00", 20, 138], + ["2026-01-01T00:00:00-05:00", 40, 367], + ["2026-01-01T00:00:00-05:00", 60, 437], + ["2026-01-01T00:00:00-05:00", 80, 247], + ["2026-01-01T00:00:00-05:00", 100, 345], + ["2026-01-01T00:00:00-05:00", 120, 343], + ["2026-01-01T00:00:00-05:00", 140, 117] + ], + "cols": [ + { + "description": "The date and time an order was submitted.", + "database_type": "TIMESTAMP", + "semantic_type": "type/CreationTimestamp", + "table_id": 5, + "coercion_strategy": null, + "unit": "year", + "name": "CREATED_AT", + "settings": null, + "source": "breakout", + "fk_target_field_id": null, + "field_ref": [ + "field", + 41, + { + "base-type": "type/DateTime", + "temporal-unit": "year" + } + ], + "effective_type": "type/DateTime", + "active": true, + "nfc_path": null, + "parent_id": null, + "id": 41, + "position": 7, + "visibility_type": "normal", + "display_name": "Created At", + "fingerprint": { + "global": { + "distinct-count": 10001, + "nil%": 0 + }, + "type": { + "type/DateTime": { + "earliest": "2022-04-30T18:56:13.352Z", + "latest": "2026-04-19T14:07:15.657Z" + } + } + }, + "base_type": "type/DateTime" + }, + { + "description": "The total billed amount.", + "database_type": "DECFLOAT", + "semantic_type": "type/Currency", + "table_id": 5, + "coercion_strategy": null, + "binning_info": { + "min_value": 0, + "max_value": 160, + "num_bins": 8, + "bin_width": 20, + "binning_strategy": "num-bins" + }, + "name": "TOTAL", + "settings": { + "currency": "BTC" + }, + "source": "breakout", + "fk_target_field_id": null, + "field_ref": [ + "field", + 42, + { + "base-type": "type/Float", + "binning": { + "strategy": "num-bins", + "min-value": 0, + "max-value": 160, + "num-bins": 8, + "bin-width": 20 + } + } + ], + "effective_type": "type/Float", + "active": true, + "nfc_path": null, + "parent_id": null, + "id": 42, + "position": 5, + "visibility_type": "normal", + "display_name": "Total", + "fingerprint": { + "global": { + "distinct-count": 4426, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 8.93914247937167, + "q1": 51.34535490743823, + "q3": 110.29428389265787, + "max": 159.34900526552292, + "sd": 34.26469575709948, + "avg": 80.35871658771228 + } + } + }, + "base_type": "type/Float" + }, + { + "database_type": "BIGINT", + "semantic_type": "type/Quantity", + "name": "count", + "source": "aggregation", + "field_ref": ["aggregation", 0], + "effective_type": "type/BigInteger", + "aggregation_index": 0, + "display_name": "Count", + "base_type": "type/BigInteger" + } + ], + "native_form": { + "query": "SELECT DATE_TRUNC('year', \"PUBLIC\".\"ORDERS\".\"CREATED_AT\") AS \"CREATED_AT\", FLOOR((\"PUBLIC\".\"ORDERS\".\"TOTAL\" / 20.0)) * 20.0 AS \"TOTAL\", COUNT(*) AS \"count\" FROM \"PUBLIC\".\"ORDERS\" GROUP BY DATE_TRUNC('year', \"PUBLIC\".\"ORDERS\".\"CREATED_AT\"), FLOOR((\"PUBLIC\".\"ORDERS\".\"TOTAL\" / 20.0)) * 20.0 ORDER BY DATE_TRUNC('year', \"PUBLIC\".\"ORDERS\".\"CREATED_AT\") ASC, FLOOR((\"PUBLIC\".\"ORDERS\".\"TOTAL\" / 20.0)) * 20.0 ASC", + "params": null + }, + "format-rows?": true, + "results_timezone": "America/Toronto", + "requested_timezone": "UTC", + "results_metadata": { + "columns": [ + { + "description": "The date and time an order was submitted.", + "semantic_type": "type/CreationTimestamp", + "coercion_strategy": null, + "unit": "year", + "name": "CREATED_AT", + "settings": null, + "fk_target_field_id": null, + "field_ref": [ + "field", + 41, + { + "base-type": "type/DateTime", + "temporal-unit": "year" + } + ], + "effective_type": "type/DateTime", + "id": 41, + "visibility_type": "normal", + "display_name": "Created At", + "fingerprint": { + "global": { + "distinct-count": 10001, + "nil%": 0 + }, + "type": { + "type/DateTime": { + "earliest": "2022-04-30T18:56:13.352Z", + "latest": "2026-04-19T14:07:15.657Z" + } + } + }, + "base_type": "type/DateTime" + }, + { + "description": "The total billed amount.", + "semantic_type": "type/Currency", + "coercion_strategy": null, + "name": "TOTAL", + "settings": { + "currency": "BTC" + }, + "fk_target_field_id": null, + "field_ref": [ + "field", + 42, + { + "base-type": "type/Float", + "binning": { + "strategy": "num-bins", + "min-value": 0, + "max-value": 160, + "num-bins": 8, + "bin-width": 20 + } + } + ], + "effective_type": "type/Float", + "id": 42, + "visibility_type": "normal", + "display_name": "Total", + "fingerprint": { + "global": { + "distinct-count": 4426, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 8.93914247937167, + "q1": 51.34535490743823, + "q3": 110.29428389265787, + "max": 159.34900526552292, + "sd": 34.26469575709948, + "avg": 80.35871658771228 + } + } + }, + "base_type": "type/Float" + }, + { + "display_name": "Count", + "semantic_type": "type/Quantity", + "field_ref": ["aggregation", 0], + "base_type": "type/BigInteger", + "effective_type": "type/BigInteger", + "name": "count", + "fingerprint": { + "global": { + "distinct-count": 35, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 1, + "q1": 127.5, + "q3": 977.5, + "max": 1495, + "sd": 451.9696435936364, + "avg": 521.1111111111111 + } + } + } + } + ] + }, + "insights": [ + { + "previous-value": null, + "unit": "year", + "offset": -468.93384350639036, + "last-change": null, + "col": "TOTAL", + "slope": 0.026860351877681935, + "last-value": 140, + "best-fit": [ + "+", + -5172.348585061598, + ["*", 529.1804023001592, ["log", "x"]] + ] + }, + { + "previous-value": null, + "unit": "year", + "offset": -2641.067164556649, + "last-change": null, + "col": "count", + "slope": 0.16007753395618673, + "last-value": 117, + "best-fit": [ + "*", + 6.273101848957077e-94, + ["pow", "x", 22.241709211614104] + ] + } + ] + }, + "cached": null, + "database_id": 1, + "started_at": "2024-10-23T20:47:42.708547-04:00", + "json_query": { + "constraints": { + "max-results": 10000, + "max-results-bare-rows": 2000 + }, + "type": "query", + "middleware": { + "js-int-to-string?": true, + "ignore-cached-results?": false, + "process-viz-settings?": false, + "userland-query?": true + }, + "cache-strategy": { + "multiplier": 10000, + "min_duration_ms": 1000, + "type": "ttl", + "avg-execution-ms": 87 + }, + "viz-settings": { + "pie.dimension": ["CREATED_AT", "TOTAL"], + "pie.slice_threshold": 20, + "pie.percent_visibility": "inside", + "pie.show_labels": false + }, + "database": 1, + "query": { + "source-table": 5, + "aggregation": [["count"]], + "breakout": [ + [ + "field", + 41, + { + "base-type": "type/DateTime", + "temporal-unit": "year" + } + ], + [ + "field", + 42, + { + "base-type": "type/Float", + "binning": { + "strategy": "default" + } + } + ] + ] + } + }, + "average_execution_time": null, + "status": "completed", + "context": "question", + "row_count": 36, + "running_time": 51, + "card": { + "original_card_id": 562, + "can_delete": false, + "public_uuid": null, + "parameter_usage_count": 0, + "created_at": "2024-10-23T20:44:49.067728Z", + "parameters": [], + "metabase_version": "v0.1.39-SNAPSHOT (49cc16b)", + "collection": { + "metabase.models.collection.root/is-root?": true, + "authority_level": null, + "name": "Our analytics", + "is_personal": false, + "id": "root", + "can_write": true + }, + "visualization_settings": { + "pie.dimension": ["CREATED_AT", "TOTAL"], + "pie.slice_threshold": 20, + "pie.percent_visibility": "inside", + "pie.show_labels": false + }, + "collection_preview": true, + "entity_id": "YBf5VfcgUJtH69_V3WNW5", + "archived_directly": false, + "display": "pie", + "can_manage_db": true, + "parameter_mappings": [], + "id": 562, + "dataset_query": { + "database": 1, + "type": "query", + "query": { + "source-table": 5, + "aggregation": [["count"]], + "breakout": [ + [ + "field", + 41, + { + "base-type": "type/DateTime", + "temporal-unit": "year" + } + ], + [ + "field", + 42, + { + "base-type": "type/Float", + "binning": { + "strategy": "default" + } + } + ] + ] + } + }, + "cache_ttl": null, + "embedding_params": null, + "made_public_by_id": null, + "updated_at": "2024-10-23T20:44:49.067728Z", + "moderation_reviews": [], + "can_restore": false, + "creator_id": 1, + "average_query_time": 76.3125, + "type": "question", + "last_used_at": "2024-10-24T00:47:03.391993Z", + "dashboard_count": 0, + "last_query_start": "2024-10-24T00:47:03.352203Z", + "name": "sunburst other slice percentage 2", + "query_type": "query", + "collection_id": null, + "enable_embedding": false, + "database_id": 1, + "trashed_from_collection_id": null, + "can_write": true, + "initially_published_at": null, + "result_metadata": [ + { + "description": "The date and time an order was submitted.", + "semantic_type": "type/CreationTimestamp", + "coercion_strategy": null, + "unit": "year", + "name": "CREATED_AT", + "settings": null, + "fk_target_field_id": null, + "field_ref": [ + "field", + 41, + { + "base-type": "type/DateTime", + "temporal-unit": "year" + } + ], + "effective_type": "type/DateTime", + "id": 41, + "visibility_type": "normal", + "display_name": "Created At", + "fingerprint": { + "global": { + "distinct-count": 10001, + "nil%": 0 + }, + "type": { + "type/DateTime": { + "earliest": "2022-04-30T18:56:13.352Z", + "latest": "2026-04-19T14:07:15.657Z" + } + } + }, + "base_type": "type/DateTime" + }, + { + "description": "The total billed amount.", + "semantic_type": "type/Currency", + "coercion_strategy": null, + "name": "TOTAL", + "settings": { + "currency": "BTC" + }, + "fk_target_field_id": null, + "field_ref": [ + "field", + 42, + { + "base-type": "type/Float", + "binning": { + "strategy": "num-bins", + "min-value": 0, + "max-value": 160, + "num-bins": 8, + "bin-width": 20 + } + } + ], + "effective_type": "type/Float", + "id": 42, + "visibility_type": "normal", + "display_name": "Total", + "fingerprint": { + "global": { + "distinct-count": 4426, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 8.93914247937167, + "q1": 51.34535490743823, + "q3": 110.29428389265787, + "max": 159.34900526552292, + "sd": 34.26469575709948, + "avg": 80.35871658771228 + } + } + }, + "base_type": "type/Float" + }, + { + "display_name": "Count", + "semantic_type": "type/Quantity", + "field_ref": ["aggregation", 0], + "base_type": "type/BigInteger", + "effective_type": "type/BigInteger", + "name": "count", + "fingerprint": { + "global": { + "distinct-count": 35, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 1, + "q1": 127.5, + "q3": 977.5, + "max": 1495, + "sd": 451.9696435936364, + "avg": 521.1111111111111 + } + } + } + } + ], + "can_run_adhoc_query": true, + "table_id": 5, + "source_card_id": null, + "collection_position": null, + "view_count": 17, + "archived": false, + "description": null, + "cache_invalidated_at": null, + "displayIsLocked": true + } + } +] diff --git a/frontend/src/metabase/visualizations/echarts/pie/model/index.ts b/frontend/src/metabase/visualizations/echarts/pie/model/index.ts index 1ac3eeec6656b54a8cf1ef573dc13b20c05c9e06..c6f20af21d3704efecccc1219a239d863c0c4df4 100644 --- a/frontend/src/metabase/visualizations/echarts/pie/model/index.ts +++ b/frontend/src/metabase/visualizations/echarts/pie/model/index.ts @@ -167,9 +167,8 @@ function calculatePercentageAndIsOther( ); } -function aggregateSlices( +function aggregateChildrenSlices( node: SliceTreeNode, - total: number, renderingContext: RenderingContext, ) { const children = getArrayFromMapValues(node.children); @@ -178,9 +177,13 @@ function aggregateSlices( if (others.length > 1 && otherTotal > 0) { const otherSliceChildren: SliceTree = new Map(); - others.forEach(o => { - otherSliceChildren.set(String(o.key), { ...o, color: "" }); - node.children.delete(String(o.key)); + others.forEach(otherChildSlice => { + otherSliceChildren.set(String(otherChildSlice.key), { + ...otherChildSlice, + normalizedPercentage: otherChildSlice.value / otherTotal, + color: "", + }); + node.children.delete(String(otherChildSlice.key)); }); node.children.set(OTHER_SLICE_KEY, { @@ -188,7 +191,7 @@ function aggregateSlices( name: OTHER_SLICE_NAME, value: otherTotal, displayValue: otherTotal, - normalizedPercentage: otherTotal / total, + normalizedPercentage: otherTotal / node.value, color: renderingContext.getColor("text-light"), children: otherSliceChildren, visible: true, @@ -200,7 +203,7 @@ function aggregateSlices( others[0].isOther = false; } - children.forEach(child => aggregateSlices(child, total, renderingContext)); + children.forEach(child => aggregateChildrenSlices(child, renderingContext)); } function computeSliceAngles( @@ -457,10 +460,11 @@ export function getPieChartModel( const otherTotal = others.reduce((currTotal, o) => currTotal + o.value, 0); if (otherTotal > 0) { const children: SliceTree = new Map(); - others.forEach(node => { - children.set(String(node.key), { - ...node, + others.forEach(otherChildSlice => { + children.set(String(otherChildSlice.key), { + ...otherChildSlice, color: "", + normalizedPercentage: otherChildSlice.value / otherTotal, }); }); const visible = !hiddenSlices.includes(OTHER_SLICE_KEY); @@ -485,7 +489,7 @@ export function getPieChartModel( // Aggregate slices in middle and outer ring into "other" slices sliceTreeNodes.forEach(node => - aggregateSlices(node, total, renderingContext), + aggregateChildrenSlices(node, renderingContext), ); // We increase the size of small slices, but only for the first ring, because diff --git a/src/metabase/server/middleware/security.clj b/src/metabase/server/middleware/security.clj index 0ca4576a8a1cc4faef77dcad12dceb99f71f8673..a25752168aede7beaab386aed328c77770ae9d5d 100644 --- a/src/metabase/server/middleware/security.clj +++ b/src/metabase/server/middleware/security.clj @@ -104,7 +104,7 @@ (->> (str/split hosts-string #"[ ,\s\r\n]+") (remove str/blank?) (mapcat add-wildcard-entries) - vec)) + (into ["'self'"]))) (def ^{:doc "Parse the string of allowed iframe hosts, adding wildcard prefixes as needed."} parse-allowed-iframe-hosts diff --git a/test/metabase/server/middleware/security_test.clj b/test/metabase/server/middleware/security_test.clj index 132b63bed0768ff0c5586b369b29acdcfd6a48ec..f537825845b17836cccd1350a966aa9bb572bb6e 100644 --- a/test/metabase/server/middleware/security_test.clj +++ b/test/metabase/server/middleware/security_test.clj @@ -60,10 +60,13 @@ (deftest csp-header-iframe-hosts-tests (testing "Allowed iframe hosts setting is used in the CSP frame-src directive." (tu/with-temporary-setting-values [public-settings/allowed-iframe-hosts "https://www.wikipedia.org, https://www.metabase.com https://clojure.org"] - (is (= (str "frame-src https://wikipedia.org https://*.wikipedia.org https://www.wikipedia.org " + (is (= (str "frame-src 'self' https://wikipedia.org https://*.wikipedia.org https://www.wikipedia.org " "https://metabase.com https://*.metabase.com https://www.metabase.com " "https://clojure.org https://*.clojure.org") - (csp-directive "frame-src")))))) + (csp-directive "frame-src"))))) + (testing "Includes 'self' so embed previews work (#49142)" + (let [hosts (-> (csp-directive "frame-src") (str/split #"\s+") set)] + (is (contains? hosts "'self'") "frame-src hosts does not include 'self'")))) (deftest xframeoptions-header-tests (mt/with-premium-features #{:embedding} @@ -232,7 +235,8 @@ (testing "The allowed iframe hosts parse in the expected way." (let [default-hosts @#'public-settings/default-allowed-iframe-hosts] (testing "The defaults hosts parse correctly" - (is (= ["youtube.com" + (is (= ["'self'" + "youtube.com" "*.youtube.com" "youtu.be" "*.youtu.be" @@ -275,7 +279,7 @@ "*.x.com"] (mw.security/parse-allowed-iframe-hosts default-hosts)))) (testing "Additional hosts a user may configure will parse correctly as well" - (is (= ["localhost" + (is (= ["'self'" "localhost" "http://localhost:8000" "my.domain.local:9876" "*" @@ -286,5 +290,5 @@ "www.mysite.cool.com"] (mw.security/parse-allowed-iframe-hosts "localhost, http://localhost:8000, my.domain.local:9876, *, www.mysite.com/, www.mysite.cool.com")))) (testing "invalid hosts are not included" - (is (= [] + (is (= ["'self'"] (mw.security/parse-allowed-iframe-hosts "asdf/wasd/:8000 */localhost:*")))))))