diff --git a/bin/osx-release b/bin/osx-release index 868747e5ccc48f7c21adce59a7819fe7b1f42743..0165e4b915613d628c3e1b25a8c609e8d930525e 100755 --- a/bin/osx-release +++ b/bin/osx-release @@ -187,8 +187,10 @@ sub create_dmg_from_source_dir { '-fs', 'HFS+', '-fsargs', '-c c=64,a=16,e=16', '-format', 'UDRW', - '-size', '256MB', # it looks like this can be whatever size we want; compression slims it down + '-size', '512MB', # has to be big enough to hold everything uncompressed, but doesn't matter if there's extra space -- compression slims it down $dmg_filename) == 0 or die $!; + + announce "$dmg_filename created."; } # Mount the disk image, return the device name diff --git a/bin/version b/bin/version index fa20000a446b868c55f8ab8f6e49044d47f7c9a7..8f8564b7acdd9298a03c847856b3540774b54092 100755 --- a/bin/version +++ b/bin/version @@ -1,6 +1,6 @@ #!/usr/bin/env bash -VERSION="v0.24.0-snapshot" +VERSION="v0.25.0-snapshot" # dynamically pull more interesting stuff from latest git commit HASH=$(git show-ref --head --hash=7 head) # first 7 letters of hash should be enough; that's what GitHub uses diff --git a/docs/administration-guide/13-embedding.md b/docs/administration-guide/13-embedding.md index 8c853bd4128b15f8078c49d98010a374260f7f7d..950d545bfe4fe5f025463a146b2144371080f840 100644 --- a/docs/administration-guide/13-embedding.md +++ b/docs/administration-guide/13-embedding.md @@ -4,9 +4,9 @@ Metabase includes a powerful application embedding feature that allows you to em ### Key Concepts #### Applications -An important distinction to keep in mind is the difference between Metabase and the embedding application. The charts and dashboards you will be embedding live in the Metabase application, and will be embedded in your application (i.e. the embedding application). +An important distinction to keep in mind is the difference between Metabase and the embedding application. The charts and dashboards you will be embedding live in the Metabase application, and will be embedded in your application (i.e. the embedding application). -#### Parameters +#### Parameters Some dashboards and questions have the ability to accept parameters. In dashboards, these are synonymous with dashboard filters. For example, if you have a dashboard with a filter on Publisher ID, this can be specified as a parameter when embedding, so that you could insert the dashboard filtered down to a specific Publisher ID. SQL based questions with template variables can also accept parameters for each variable. So for a query like @@ -18,21 +18,21 @@ WHERE product_id = {{productID}} you could specify a specific productID when embedding the question. #### Signed parameters -In general, when embedding a chart or dashboard, the server of your embedding application will need to sign a request for that resource. +In general, when embedding a chart or dashboard, the server of your embedding application will need to sign a request for that resource. -If you choose to sign a specific parameter value, that means the user can't modify that, nor is a filter widget displayed for that parameter. For example, if the "Publisher ID" is assigned a value and the request signed, that means the front-end client that renders that dashboard on behalf of a given logged-in user can only see information for that publisher ID. +If you choose to sign a specific parameter value, that means the user can't modify that, nor is a filter widget displayed for that parameter. For example, if the "Publisher ID" is assigned a value and the request signed, that means the front-end client that renders that dashboard on behalf of a given logged-in user can only see information for that publisher ID. ### Enabling embedding -To enable embedding, go to the Admin Panel and under Settings, go to the "Embedding in other applications" tab. From there, click "Enable." Here you will see a secret signing key you can use later to sign requests. If you ever need to invalidate that key and generate a new one, just click on "Regenerate Key". +To enable embedding, go to the Admin Panel and under Settings, go to the "Embedding in other applications" tab. From there, click "Enable." Here you will see a secret signing key you can use later to sign requests. If you ever need to invalidate that key and generate a new one, just click on "Regenerate Key".  -You can also see all questions and dashboards that have been marked as "Embeddable" here, as well as revoke any questions or dashboards that should no longer be embeddable in other applications. +You can also see all questions and dashboards that have been marked as "Embeddable" here, as well as revoke any questions or dashboards that should no longer be embeddable in other applications. Once you've enabled the embedding feature on your Metabase instance, you should then go to the individual questions and dashboards you wish to embed to set them up for embedding. ### Embedding Charts and Dashboards -To mark a given question or dashboard, click on the sharing icon +To mark a given question or dashboard, click on the sharing icon  @@ -40,11 +40,11 @@ Then select "Embed this question in an application"  -Here you will see a preview of the question or dashboard as it will appear in your application, as well as a panel that shows you the code you will need to insert in your application. +Here you will see a preview of the question or dashboard as it will appear in your application, as well as a panel that shows you the code you will need to insert in your application.  -Importantly, you will need to hit "Publish" when you first set up a chart or dashboard for embedding and each time you change your embedding settings. Also, any changes you make to the resource might require you to update the code in your own application to the latest code sample in the "Code Pane". +Importantly, you will need to hit "Publish" when you first set up a chart or dashboard for embedding and each time you change your embedding settings. Also, any changes you make to the resource might require you to update the code in your own application to the latest code sample in the "Code Pane".  @@ -52,7 +52,7 @@ We provide code samples for common front end template languages as well as some ### Embedding Charts and Dashboards with locked parameters -If you wish to have a parameter locked down to prevent your embedding application's end users from seeing other users' data, you can mark parameters as "Locked."Once a parameter is marked as Locked, it is not displayed as a filter widget, and must be set by the embedding application's server code. +If you wish to have a parameter locked down to prevent your embedding application's end users from seeing other users' data, you can mark parameters as "Locked."Once a parameter is marked as Locked, it is not displayed as a filter widget, and must be set by the embedding application's server code.  @@ -65,6 +65,3 @@ Dashboards are a fixed aspect ratio, so if you'd like to ensure they're automati ### Reference applications To see concrete examples of how to embed Metabase in applications under a number of common frameworks, check out our [reference implementations](https://github.com/metabase/embedding-reference-apps) on Github. - -## That’s it! -If you still have questions, or want to share Metabase tips and tricks, head over to our [discussion board](http://discourse.metabase.com/). See you there! diff --git a/docs/administration-guide/14-caching.md b/docs/administration-guide/14-caching.md new file mode 100644 index 0000000000000000000000000000000000000000..963a0bd28bd036bdadac4457f2c47414b23693dc --- /dev/null +++ b/docs/administration-guide/14-caching.md @@ -0,0 +1,21 @@ +## Caching query results in Metabase +Metabase now gives you the ability to automatically cache the results of queries that take a long time to run. + +### Enabling caching +To start caching your queries, head to the Settings section of the Admin Panel, and click on the `Caching` tab at the bottom of the side navigation. Then turn the caching toggle to `Enabled`. + + + +End-users will see a timestamp on cached questions in the top right of the question detail page showing the time when that question was last updated (i.e., the time when the current result was cached). Clicking on the `Refresh` button on a question page will manually rerun the query and override the cached result with the new result. + +### Caching settings +In Metabase, rather than setting cache settings manually on a per-query basis, we give you two parameters to set to automatically cache the results of long queries: the minimum average query duration, and the cache TTL multiplier. + +#### Minimum query duration +Your Metabase instance keeps track of the average query execution times of your queries, and it will cache the results of all saved questions with an average query execution time longer than the number you put in this box (in seconds). + +#### Cache Time-to-live (TTL) +Instead of setting an absolute number of minutes or seconds for a cached result to persist, Metabase lets you put in a multiplier to determine the cache's TTL. Each query's cache TTL is computed by multiplying its average execution time by the number you put in this box. So if you put in `10`, a query that takes 5 seconds on average to execute will have its cache last for 50 seconds; and a query that takes 10 minutes will have a cached result lasting 100 minutes. This way, each query's cache is proportional to its execution time. + +#### Max cache entry size +Lastly, you can set the maximum size of each question's cache in kilobytes, to prevent them from taking up too much space on your server. diff --git a/docs/administration-guide/images/caching.png b/docs/administration-guide/images/caching.png new file mode 100644 index 0000000000000000000000000000000000000000..aa37220315bedebd7a78d69a93c80a0a5194bf01 Binary files /dev/null and b/docs/administration-guide/images/caching.png differ diff --git a/docs/administration-guide/start.md b/docs/administration-guide/start.md index 9466cc055dd76220810490b02ed6e7be918fbb85..9c8e21e123193cb85fc16fa916a1726159cf2054 100644 --- a/docs/administration-guide/start.md +++ b/docs/administration-guide/start.md @@ -17,6 +17,7 @@ Are you in charge of managing Metabase for your organization? Then you're in the * [Creating a Getting Started Guide for your team](11-getting-started-guide.md) * [Sharing dashboards and questions with public links](12-public-links.md) * [Embedding Metabase in other Applications](13-embedding.md) +* [Caching query results](14-caching.md) First things first, you'll need to install Metabase. If you haven’t done that yet, our [Installation Guide](../operations-guide/start.md#installing-and-running-metabase) will help you through the process. diff --git a/docs/operations-guide/running-the-metabase-jar-file.md b/docs/operations-guide/running-the-metabase-jar-file.md index 2c08ebbea287d2e5750f4abb727776a555fb3a9b..005d7b9d0c07ec7882ec46cb1c581e14a406a98c 100644 --- a/docs/operations-guide/running-the-metabase-jar-file.md +++ b/docs/operations-guide/running-the-metabase-jar-file.md @@ -1,6 +1,6 @@ # Running the Metabase Jar File -To run the Metabase jar file you need to have Java installed on your system. Currently Metabase requires Java 6 or higher and will work on either the OpenJDK or Oracle JDK. Note that the Metabase team prefers to stick with open source solutions where possible, so we use the OpenJDK for our Metabase instances. +To run the Metabase jar file you need to have Java installed on your system. Currently Metabase requires Java 7 or higher and will work on either the OpenJDK or Oracle JDK. Note that the Metabase team prefers to stick with open source solutions where possible, so we use the OpenJDK for our Metabase instances. ### Download Metabase @@ -15,11 +15,11 @@ Before you can launch the application you must verify that you have Java install You should see output such as: - java version "1.60_65" - Java (TM) SE Runtime Environment (build 1.6.0_65-b14-466.1-11M4716) - Java HotSpot (TM) 64-Bit Server VM (build 20.65-b04-466.1, mixed mode) + java version "1.8.0_31" + Java(TM) SE Runtime Environment (build 1.8.0_31-b13) + Java HotSpot(TM) 64-Bit Server VM (build 25.31-b07, mixed mode) -If you did not see the output above and instead saw either an error or your Java version is less than 1.6, then you need to install the Java Runtime. +If you did not see the output above and instead saw either an error or your Java version is less than 1.7, then you need to install the Java Runtime. [OpenJDK Downloads](http://openjdk.java.net/install/) [Oracle's Java Downloads](http://www.oracle.com/technetwork/java/javase/downloads/index.html) diff --git a/docs/users-guide/02-database-basics.md b/docs/users-guide/02-database-basics.md index 015087a6c33c2e0e7d5aa207ccec323fee17ff82..958cbc152bea916fe41ad8d0c879c63a7a7fe0fc 100644 --- a/docs/users-guide/02-database-basics.md +++ b/docs/users-guide/02-database-basics.md @@ -74,4 +74,4 @@ To do this, we’d open up the Reservation table, add a filter to only look at r --- ## Next: Asking questions -Now that we have a shared vocabulary and a basic understanding of databases, let's learn more about [asking questions](03-asking-questions.md) +Now that we have a shared vocabulary and a basic understanding of databases, let's learn more about [exploring in Metabase](03-basic-exploration.md) diff --git a/docs/users-guide/03-basic-exploration.md b/docs/users-guide/03-basic-exploration.md new file mode 100644 index 0000000000000000000000000000000000000000..98dbe34532d33ab8bfdf587433cac3651a518ada --- /dev/null +++ b/docs/users-guide/03-basic-exploration.md @@ -0,0 +1,47 @@ +### Exploring in Metabase +As long as you're not the very first user in your team's Metabase, the easiest way to get started is by exploring charts and dashboards that your teammates have already created. + +#### Exploring dashboards +Click on the `Dashboards` nav item to see all the dashboards your teammates have created. Dashboards are simply collections of charts and numbers that you want to be able to refer back to regularly. (You can learn more about dashboards [here](07-dashboards.md)) + +If you click on a part of a chart, such as a bar in a bar chart, or a dot on a line chart, you'll see a menu with actions you can take to dive deeper into that result, or to branch off from it in a different direction. + + + +In this example of pie orders by type over time, clicking on a dot on this line chart gives us the ability to: +- Zoom in — i.e., see just the banana cream pie orders in June 2017 over time +- View these Orders — which lets us see a list of banana cream pie orders in June 2017 +- Break out by a category — this lets us do things like see the banana cream pie orders in June 2017 broken out by the status of the customer (e.g., `new` or `VIP`, etc.) or other different aspects of the order. Different charts will have different break out options, such as Location and Time. + +Other charts as well as table cells will often also allow you to go to a filtered view of that chart or table. You can click on one of the inequality symbols to see that chart where, for example, the value of the Subtotal column is less than $100, or where the Purchased-at timestamp is greater than (i.e., after) April 1, 2017. + + + +Lastly, clicking on the ID of an item in table gives you the option to go to a detail view for that single record. (E.g., you can click on a customer's ID to see the profile view for that one customer.) + +**Note that charts created with SQL don't currently have these action options.** + +#### Exploring saved questions +In Metabase parlance, every chart on number on a dashboard is called a "question." Clicking on the title of a question on a dashboard will take you to a detail view of that question. You'll also end up at this detail view if you use one of the actions mentioned above. You can also browse all the questions your teammates have saved by clicking the `Questions` link in the main navigation. + +When you're viewing the detail view of a question, you can use all the same actions mentioned above. You can also click on the headings of tables to see more options, like viewing the sum of the values in a column, or finding the minimum or maximum value in it. + + + +Additionally, the question detail page has an Explore button in the bottom-right of the screen with options that change depending on the kind of question you're looking at. (Note that the Explore button disappears if your cursor stops moving.) + + + +Here's a list of all the actions: +* Table actions + - `Count of rows by time` lets you see how many rows there were in this table over time. + - `Summarize this segment` gives you options of various summarization functions (sum, average, maximum, etc.) you can use on this table to arrive at a number. +* Chart and pivot table actions + - `Break outs` will be listed depending on the question, and include the option to break out by a category, location, or time. For example, if you're looking at the count of total orders over time, you might be able to further break that out by customer gender, if that information is present. + - `View this as a table` does what it says. Every chart has a table behind it that is providing the data for the chart, and this action lets you see that table. + - `View the underlying records` shows you the un-summarized list of records underlying the chart or number you're currently viewing. + +--- + +## Next: Asking new questions +So what do you do if you can't find an existing dashboard or question that's exactly what you're looking for? Let's learn about [asking our own new questions](04-asking-questions.md) diff --git a/docs/users-guide/03-asking-questions.md b/docs/users-guide/04-asking-questions.md similarity index 99% rename from docs/users-guide/03-asking-questions.md rename to docs/users-guide/04-asking-questions.md index 26e61993e8268ba109d5b138ab6200c651f45e3a..767576f96741d1ab034ac8498c8ff93d5f55c024 100644 --- a/docs/users-guide/03-asking-questions.md +++ b/docs/users-guide/04-asking-questions.md @@ -159,4 +159,4 @@ Questions asked using SQL can be saved, downloaded, or added to a dashboard just --- ## Next: Creating charts -Once you have an answer to your question, you can now learn more about [visualizing answers](04-visualizing-results.md). +Once you have an answer to your question, you can now learn more about [visualizing answers](05-visualizing-results.md). diff --git a/docs/users-guide/04-visualizing-results.md b/docs/users-guide/05-visualizing-results.md similarity index 99% rename from docs/users-guide/04-visualizing-results.md rename to docs/users-guide/05-visualizing-results.md index 39722528142ef73a586b92cd58b7849c435c6ff1..6b60d2b120a748c62949328ec6659cb6aa3ce9d0 100644 --- a/docs/users-guide/04-visualizing-results.md +++ b/docs/users-guide/05-visualizing-results.md @@ -100,4 +100,4 @@ Metabase now also allows administrators to add custom region maps via GeoJSON fi --- ## Next: Sharing and organizing questions -Now let's learn about [sharing and organizing your saved questions](05-sharing-answers.md). +Now let's learn about [sharing and organizing your saved questions](06-sharing-answers.md). diff --git a/docs/users-guide/05-sharing-answers.md b/docs/users-guide/06-sharing-answers.md similarity index 99% rename from docs/users-guide/05-sharing-answers.md rename to docs/users-guide/06-sharing-answers.md index 21586407df429aee28bc451b7124a7c10f2167a4..ddb895afd22265b33987c2bd4d2ff78f29917d3c 100644 --- a/docs/users-guide/05-sharing-answers.md +++ b/docs/users-guide/06-sharing-answers.md @@ -51,4 +51,4 @@ Clicking on the icon to the left of questions let's you select several at once s --- ## Next: creating dashboards -Next, we'll learn about [creating dashboards and adding questions to them](06-dashboards.md). +Next, we'll learn about [creating dashboards and adding questions to them](07-dashboards.md). diff --git a/docs/users-guide/06-dashboards.md b/docs/users-guide/07-dashboards.md similarity index 82% rename from docs/users-guide/06-dashboards.md rename to docs/users-guide/07-dashboards.md index 970bb1f4ba5384a076b260991fa12a6490986e41..8a2b19e5cbaec3e7f58188c5326f036157164a9a 100644 --- a/docs/users-guide/06-dashboards.md +++ b/docs/users-guide/07-dashboards.md @@ -10,7 +10,7 @@ Have a few key performance indicators that you want to be able to easily check? You can make as many dashboards as you want. Go nuts. ### How to create a dashboard -Once you have a question saved, you can create a dashboard. Click the **Dashboards** dropdown at the top of the screen, then **Create a new dashboard**. Give your new dashboard a name and a description, then click **Create**, and you’ll be taken to your shiny new dashboard. You can always get to your dashboards from the dropdown at the very top of the screen. +Once you have a question saved, you can create a dashboard. Click the **Dashboards** link at the top of the screen, then click the plus icon in the top-right to create a new dashboard. Give your new dashboard a name and a description, then click **Create**, and you’ll be taken to your shiny new dashboard.  @@ -34,11 +34,17 @@ Once you're in edit mode you'll see a grid appear. You can move and resize the c Questions in your dashboard will automatically update their display based on the size you choose to make sure your data looks great at any size. +### Archiving a dashboard +Archiving a dashboard does not archive the individual saved questions on it — it just archives the dashboard. To archive a dashboard while viewing it, click the pencil icon to enter edit mode, then click the Archive button. -### Deleting a dashboard -Deleting a dashboard does not delete the individual saved questions on it — it just deletes the dashboard. Remember — dashboards are shared by everyone on your team, so think twice before you delete something that someone else might be using! +You can view all of your archived dashboards by clicking the box icon in the top-right of the Dashboards page. Archived dashboards in this list can be unarchived by clicking the icon of the box with the upward arrow next to that dashboard. -To delete a dashboard, click the pencil-looking **Edit** icon in the top right of the dashboard, then click **Delete**. +(Note: as of Metabase v0.24, dashboards can no longer be permanently deleted; only archived.) + +### Finding dashboards +After a while, your team might have a lot of dashboards. To make it a little easier to find dashboards that you look at often, you can mark a dashboard as a favorite by clicking the star icon on it from the dashboards list. You can use the filter dropdown in the top of the list to view only your favorite dashboards, or only the ones that you created yourself. + + ### Fullscreen dashboards @@ -90,4 +96,4 @@ Some tips: --- ## Next: Adding dashboard filters -Make your dashboards more flexible and powerful by [adding dashboard filters](07-dashboard-filters.md). +Make your dashboards more flexible and powerful by [adding dashboard filters](08-dashboard-filters.md). diff --git a/docs/users-guide/07-dashboard-filters.md b/docs/users-guide/08-dashboard-filters.md similarity index 99% rename from docs/users-guide/07-dashboard-filters.md rename to docs/users-guide/08-dashboard-filters.md index fde813da6856b156452c5daf647ebd9c2ec1a7ef..9b4d234e590ee5205a5267e755a9a2ee22b85ff4 100644 --- a/docs/users-guide/07-dashboard-filters.md +++ b/docs/users-guide/08-dashboard-filters.md @@ -74,4 +74,4 @@ Here are a few tips to get the most out of dashboard filters: --- ## Next: Charts with multiple series -We'll learn how to [create charts with multiple lines, bars, and more](08-multi-series-charting.md) next. +We'll learn how to [create charts with multiple lines, bars, and more](09-multi-series-charting.md) next. diff --git a/docs/users-guide/08-multi-series-charting.md b/docs/users-guide/09-multi-series-charting.md similarity index 98% rename from docs/users-guide/08-multi-series-charting.md rename to docs/users-guide/09-multi-series-charting.md index 2ca5cbee0192412b16c0961bfaa6a9cca34251f4..bec40797ed17296cd0682e75b142695c5b664429 100644 --- a/docs/users-guide/08-multi-series-charting.md +++ b/docs/users-guide/09-multi-series-charting.md @@ -78,4 +78,4 @@ Go forth and start letting your data get to know each other. ## Next: Getting reports with Pulses -Pulses let you send out a group of saved questions on a schedule via email or Slack. [Get started with Pulses](09-pulses.md). +Pulses let you send out a group of saved questions on a schedule via email or Slack. [Get started with Pulses](10-pulses.md). diff --git a/docs/users-guide/09-pulses.md b/docs/users-guide/10-pulses.md similarity index 98% rename from docs/users-guide/09-pulses.md rename to docs/users-guide/10-pulses.md index 96e68e3051af95267d184868ac9720ac40ca18c1..6786ee21b28e35d985fcbca2c4e1d9a6bf1b2be8 100644 --- a/docs/users-guide/09-pulses.md +++ b/docs/users-guide/10-pulses.md @@ -50,4 +50,4 @@ If you want to delete a pulse, you can find that option at the bottom of the edi ## Next: Connecting Metabase to Slack with Metabot 🤖 -If your team uses Slack to communicate, you can [use Metabot](10-metabot.md) to display your saved questions directly within Slack whenever you want. +If your team uses Slack to communicate, you can [use Metabot](11-metabot.md) to display your saved questions directly within Slack whenever you want. diff --git a/docs/users-guide/10-metabot.md b/docs/users-guide/11-metabot.md similarity index 97% rename from docs/users-guide/10-metabot.md rename to docs/users-guide/11-metabot.md index bf1b871eef447bd0c615e251409d5eaa608402e2..c7fd2b2608d427766668a05fa8776267d1171bb1 100644 --- a/docs/users-guide/10-metabot.md +++ b/docs/users-guide/11-metabot.md @@ -48,4 +48,4 @@ If you don’t have a sense of which questions you want to view in Slack, you c ## Next: -Sometimes you’ll need help understanding what data is available to you and what it means. Metabase provides a way for your administrators and data experts to build a [data model reference](11-data-model-reference.md) to help you make sense of your data. +Sometimes you’ll need help understanding what data is available to you and what it means. Metabase provides a way for your administrators and data experts to build a [data model reference](12-data-model-reference.md) to help you make sense of your data. diff --git a/docs/users-guide/11-data-model-reference.md b/docs/users-guide/12-data-model-reference.md similarity index 100% rename from docs/users-guide/11-data-model-reference.md rename to docs/users-guide/12-data-model-reference.md diff --git a/docs/users-guide/12-sql-parameters.md b/docs/users-guide/13-sql-parameters.md similarity index 100% rename from docs/users-guide/12-sql-parameters.md rename to docs/users-guide/13-sql-parameters.md diff --git a/docs/users-guide/images/dashboards/DashboardEdit.png b/docs/users-guide/images/dashboards/DashboardEdit.png index 827a25c6775a36aee0d20cebec36f16286806fdb..1c1601cac0c38f9553a6d1458d9fdc981b1bdb3a 100644 Binary files a/docs/users-guide/images/dashboards/DashboardEdit.png and b/docs/users-guide/images/dashboards/DashboardEdit.png differ diff --git a/docs/users-guide/images/dashboards/FilterDashboards.png b/docs/users-guide/images/dashboards/FilterDashboards.png new file mode 100644 index 0000000000000000000000000000000000000000..256a90cbd5b8016f6de73ec6aabbf7601f7c710e Binary files /dev/null and b/docs/users-guide/images/dashboards/FilterDashboards.png differ diff --git a/docs/users-guide/images/drill-through/actions.png b/docs/users-guide/images/drill-through/actions.png new file mode 100644 index 0000000000000000000000000000000000000000..feb04e38be96d9fcf4fb663ad149387a3141143e Binary files /dev/null and b/docs/users-guide/images/drill-through/actions.png differ diff --git a/docs/users-guide/images/drill-through/drill-through.png b/docs/users-guide/images/drill-through/drill-through.png new file mode 100644 index 0000000000000000000000000000000000000000..4df6668bcbea1d986c854391504419f95000108f Binary files /dev/null and b/docs/users-guide/images/drill-through/drill-through.png differ diff --git a/docs/users-guide/images/drill-through/heading-actions.png b/docs/users-guide/images/drill-through/heading-actions.png new file mode 100644 index 0000000000000000000000000000000000000000..f0817602da13e5ad8e1a943294764e3be0096e75 Binary files /dev/null and b/docs/users-guide/images/drill-through/heading-actions.png differ diff --git a/docs/users-guide/images/drill-through/inequality-filters.png b/docs/users-guide/images/drill-through/inequality-filters.png new file mode 100644 index 0000000000000000000000000000000000000000..6fffc3db838c85d299a0abba0e644adfe29b9c01 Binary files /dev/null and b/docs/users-guide/images/drill-through/inequality-filters.png differ diff --git a/docs/users-guide/start.md b/docs/users-guide/start.md index 164823359033c98ff04f6400a59305eec12ab761..402aa340b35e2c7320124a780ae495c0318c483b 100644 --- a/docs/users-guide/start.md +++ b/docs/users-guide/start.md @@ -4,15 +4,16 @@ * [What Metabase does](01-what-is-metabase.md) * [The basics of database terminology](02-database-basics.md) -* [Asking questions in Metabase](03-asking-questions.md) -* [How to visualize the answers to questions](04-visualizing-results.md) -* [Sharing and organizing your saved questions](05-sharing-answers.md) -* [Creating dashboards](06-dashboards.md) -* [Adding filters to dashboards](07-dashboard-filters.md) -* [Creating charts with multiple series](08-multi-series-charting.md) -* [Using Pulses for daily emails](09-pulses.md) -* [Get answers in Slack with Metabot](10-metabot.md) -* [Some helpful tips on building your data model](11-data-model-reference.md) -* [Creating SQL Templates](12-sql-parameters.md) +* [Basic exploration in Metabase](03-basic-exploration.md) +* [Asking questions in Metabase](04-asking-questions.md) +* [How to visualize the answers to questions](05-visualizing-results.md) +* [Sharing and organizing your saved questions](06-sharing-answers.md) +* [Creating dashboards](07-dashboards.md) +* [Adding filters to dashboards](08-dashboard-filters.md) +* [Creating charts with multiple series](09-multi-series-charting.md) +* [Using Pulses for daily emails](10-pulses.md) +* [Get answers in Slack with Metabot](11-metabot.md) +* [Some helpful tips on building your data model](12-data-model-reference.md) +* [Creating SQL Templates](13-sql-parameters.md) Let's get started with an overview of [What Metabase does](01-what-is-metabase.md). diff --git a/frontend/interfaces/underscore.js b/frontend/interfaces/underscore.js index b0bab2bfe69ec71ef7a1916e774d149862819cc9..3590308a6a62eb147b3db8fb5462487061caa8a7 100644 --- a/frontend/interfaces/underscore.js +++ b/frontend/interfaces/underscore.js @@ -62,4 +62,7 @@ declare module "underscore" { declare function chain<S>(obj: S): any; declare function constant<S>(obj: S): () => S; + + declare function isMatch(object: Object, properties: Object): boolean; + declare function identity<T>(o: T): T; } diff --git a/frontend/src/metabase/admin/permissions/components/PermissionsEditor.jsx b/frontend/src/metabase/admin/permissions/components/PermissionsEditor.jsx index a35dcf3310070bee770ce208458f2152e35fc334..04de3ec712f7b045e788e15ee4022355c2c9c70a 100644 --- a/frontend/src/metabase/admin/permissions/components/PermissionsEditor.jsx +++ b/frontend/src/metabase/admin/permissions/components/PermissionsEditor.jsx @@ -20,6 +20,7 @@ const PermissionsEditor = ({ title = "Permissions", modal, admin, grid, onUpdate action={onSave} content={<PermissionsConfirm diff={diff} />} triggerClasses={cx({ disabled: !isDirty })} + key="save" > <Button primary small={!modal}>Save Changes</Button> </Confirm>; @@ -29,11 +30,12 @@ const PermissionsEditor = ({ title = "Permissions", modal, admin, grid, onUpdate title="Discard changes?" action={onCancel} content="No changes to permissions will be made." + key="discard" > <Button small={!modal}>Cancel</Button> </Confirm> : - <Button small={!modal} onClick={onCancel}>Cancel</Button>; + <Button small={!modal} onClick={onCancel} key="cancel">Cancel</Button>; return ( <LoadingAndErrorWrapper loading={!grid} className="flex-full flex flex-column"> diff --git a/frontend/src/metabase/admin/permissions/components/PermissionsGrid.jsx b/frontend/src/metabase/admin/permissions/components/PermissionsGrid.jsx index 5e9dfe6bf15f38afaba310f621b71212412d480a..a2e242a2db27a695861ddef7c703c355d94defe3 100644 --- a/frontend/src/metabase/admin/permissions/components/PermissionsGrid.jsx +++ b/frontend/src/metabase/admin/permissions/components/PermissionsGrid.jsx @@ -219,7 +219,7 @@ const AccessOptionList = ({ value, options, onChange }) => )} </ul> -const EntityRowHeader = ({ entity, type }) => +const EntityRowHeader = ({ entity, icon }) => <div className="flex flex-column justify-center px1 pl4 ml2" style={{ @@ -227,7 +227,7 @@ const EntityRowHeader = ({ entity, type }) => }} > <div className="relative flex align-center"> - <Icon name={type} className="absolute" style={{ left: -28 }} /> + <Icon name={icon} className="absolute" style={{ left: -28 }} /> <h4>{entity.name}</h4> </div> { entity.subtitle && @@ -292,7 +292,7 @@ const PermissionsGrid = ({ className, grid, onUpdatePermission, entityId, groupI } renderRowHeader={({ rowIndex }) => <EntityRowHeader - type={grid.type} + icon={grid.icon} entity={grid.entities[rowIndex]} isFirstRow={rowIndex === 0} isLastRow={rowIndex === grid.entities.length - 1} diff --git a/frontend/src/metabase/admin/permissions/selectors.js b/frontend/src/metabase/admin/permissions/selectors.js index 4137cb6e44589da97127836358ad6c67f4865999..94c1584e9ba33cb8e88477736eca8eb84848d518 100644 --- a/frontend/src/metabase/admin/permissions/selectors.js +++ b/frontend/src/metabase/admin/permissions/selectors.js @@ -25,6 +25,7 @@ import { updateSchemasPermission, updateNativePermission, diffPermissions, + inferAndUpdateEntityPermissions } from "metabase/lib/permissions"; const getPermissions = (state) => state.admin.permissions.permissions; @@ -135,6 +136,36 @@ function getRawQueryWarningModal(permissions, groupId, entityId, value) { } } +// If the user is revoking an access to every single table of a database for a specific user group, +// warn the user that the access to raw queries will be revoked as well. +// This warning will only be shown if the user is editing the permissions of individual tables. +function getRevokingAccessToAllTablesWarningModal(database, permissions, groupId, entityId, value) { + if (value === "none" && + getSchemasPermission(permissions, groupId, entityId) === "controlled" && + getNativePermission(permissions, groupId, entityId) !== "none" + ) { + // allTableEntityIds contains tables from all schemas + const allTableEntityIds = database.tables().map((table) => ({ + databaseId: table.db_id, + schemaName: table.schema, + tableId: table.id + })); + + // Show the warning only if user tries to revoke access to the very last table of all schemas + const afterChangesNoAccessToAnyTable = _.every(allTableEntityIds, (id) => + getFieldsPermission(permissions, groupId, id) === "none" || _.isEqual(id, entityId) + ); + if (afterChangesNoAccessToAnyTable) { + return { + title: "Revoke access to all tables?", + message: "This will also revoke this group's access to raw queries for this database.", + confirmButtonText: "Revoke access", + cancelButtonText: "Cancel" + }; + } + } +} + const OPTION_GREEN = { icon: "check", iconColor: "#9CC177", @@ -217,6 +248,7 @@ export const getTablesPermissionsGrid = createSelector( return { type: "table", + icon: "table", crumbs: database.schemaNames().length > 1 ? [ ["Databases", "/admin/permissions/databases"], [database.name, "/admin/permissions/databases/"+database.id+"/schemas"], @@ -237,12 +269,14 @@ export const getTablesPermissionsGrid = createSelector( }, updater(groupId, entityId, value) { MetabaseAnalytics.trackEvent("Permissions", "fields", value); - return updateFieldsPermission(permissions, groupId, entityId, value, metadata); + let updatedPermissions = updateFieldsPermission(permissions, groupId, entityId, value, metadata); + return inferAndUpdateEntityPermissions(updatedPermissions, groupId, entityId, metadata); }, confirm(groupId, entityId, value) { return [ getPermissionWarningModal(getFieldsPermission, "fields", defaultGroup, permissions, groupId, entityId, value), - getControlledDatabaseWarningModal(permissions, groupId, entityId) + getControlledDatabaseWarningModal(permissions, groupId, entityId), + getRevokingAccessToAllTablesWarningModal(database, permissions, groupId, entityId, value) ]; }, warning(groupId, entityId) { @@ -277,6 +311,7 @@ export const getSchemasPermissionsGrid = createSelector( return { type: "schema", + icon: "folder", crumbs: [ ["Databases", "/admin/permissions/databases"], [database.name], @@ -293,7 +328,8 @@ export const getSchemasPermissionsGrid = createSelector( }, updater(groupId, entityId, value) { MetabaseAnalytics.trackEvent("Permissions", "tables", value); - return updateTablesPermission(permissions, groupId, entityId, value, metadata); + let updatedPermissions = updateTablesPermission(permissions, groupId, entityId, value, metadata); + return inferAndUpdateEntityPermissions(updatedPermissions, groupId, entityId, metadata); }, postAction(groupId, { databaseId, schemaName }, value) { if (value === "controlled") { @@ -335,6 +371,7 @@ export const getDatabasesPermissionsGrid = createSelector( return { type: "database", + icon: "database", groups, permissions: { "schemas": { @@ -364,7 +401,7 @@ export const getDatabasesPermissionsGrid = createSelector( }, confirm(groupId, entityId, value) { return [ - getPermissionWarningModal(getSchemasPermission, "schemas", defaultGroup, permissions, groupId, entityId, value) + getPermissionWarningModal(getSchemasPermission, "schemas", defaultGroup, permissions, groupId, entityId, value), ]; }, warning(groupId, entityId) { @@ -433,6 +470,7 @@ export const getCollectionsPermissionsGrid = createSelector( return { type: "collection", + icon: "collection", groups, permissions: { "access": { diff --git a/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx b/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx index 83e121977c4634a7a59b361963bfa73e21636f57..e00c797bb5f1250185bc79cccdfdda4b67800b0e 100644 --- a/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx +++ b/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx @@ -15,10 +15,11 @@ import SettingsSetupList from "../components/SettingsSetupList.jsx"; import SettingsUpdatesForm from "../components/SettingsUpdatesForm.jsx"; import SettingsSingleSignOnForm from "../components/SettingsSingleSignOnForm.jsx"; +import { prepareAnalyticsValue } from 'metabase/admin/settings/utils' + import _ from "underscore"; import cx from 'classnames'; - import { getSettings, getSettingValues, @@ -82,8 +83,16 @@ export default class SettingsEditorApp extends Component { } this.refs.layout.setSaved(); - let val = (setting.key === "report-timezone" || setting.type === "boolean") ? setting.value : "success"; - MetabaseAnalytics.trackEvent("General Settings", setting.display_name || setting.key, val); + + const value = prepareAnalyticsValue(setting); + + MetabaseAnalytics.trackEvent( + "General Settings", + setting.display_name || setting.key, + value, + // pass the actual value if it's a number + typeof(value) === 'number' && value + ); } catch (error) { let message = error && (error.message || (error.data && error.data.message)); this.refs.layout.setSaveError(message); diff --git a/frontend/src/metabase/admin/settings/selectors.js b/frontend/src/metabase/admin/settings/selectors.js index 1303d13df831c3ab32ca578b33f4285763d7cceb..0434679965a1356633557fbbc2c2da590b097b54 100644 --- a/frontend/src/metabase/admin/settings/selectors.js +++ b/frontend/src/metabase/admin/settings/selectors.js @@ -47,7 +47,8 @@ const SECTIONS = [ ...MetabaseSettings.get('timezones') ], placeholder: "Select a timezone", - note: "Not all databases support timezones, in which case this setting won't take effect." + note: "Not all databases support timezones, in which case this setting won't take effect.", + allowValueCollection: true }, { key: "anon-tracking-enabled", @@ -249,19 +250,22 @@ const SECTIONS = [ key: "query-caching-min-ttl", display_name: "Minimum Query Duration", type: "number", - getHidden: (settings) => !settings["enable-query-caching"] + getHidden: (settings) => !settings["enable-query-caching"], + allowValueCollection: true }, { key: "query-caching-ttl-ratio", - display_name: "Cache Time-To-Live (TTL)", + display_name: "Cache Time-To-Live (TTL) multiplier", type: "number", - getHidden: (settings) => !settings["enable-query-caching"] + getHidden: (settings) => !settings["enable-query-caching"], + allowValueCollection: true }, { key: "query-caching-max-kb", display_name: "Max Cache Entry Size", type: "number", - getHidden: (settings) => !settings["enable-query-caching"] + getHidden: (settings) => !settings["enable-query-caching"], + allowValueCollection: true } ] } diff --git a/frontend/src/metabase/admin/settings/utils.js b/frontend/src/metabase/admin/settings/utils.js new file mode 100644 index 0000000000000000000000000000000000000000..7d1979e37c258fd6ca9945b3b619dc5c73c9a530 --- /dev/null +++ b/frontend/src/metabase/admin/settings/utils.js @@ -0,0 +1,6 @@ +// in order to prevent collection of identifying information only fields +// that are explicitly marked as collectable or booleans should show the true value +export const prepareAnalyticsValue = (setting) => + (setting.allowValueCollection || setting.type === "boolean") + ? setting.value + : "success" diff --git a/frontend/src/metabase/admin/settings/utils.spec.js b/frontend/src/metabase/admin/settings/utils.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..a58c32653c8843eb17f356699977a486e9017c84 --- /dev/null +++ b/frontend/src/metabase/admin/settings/utils.spec.js @@ -0,0 +1,20 @@ +import { prepareAnalyticsValue } from './utils' + +describe('prepareAnalyticsValue', () => { + const defaultSetting = { value: 120, type: 'number' } + + const checkResult = (setting = defaultSetting, expected = "success") => + expect(prepareAnalyticsValue(setting)).toEqual(expected) + + it('should return a non identifying value by default ', () => { + checkResult() + }) + + it('should return the value of a setting marked collectable', () => { + checkResult({ ...defaultSetting, allowValueCollection: true }, defaultSetting.value) + }) + + it('should return the value of a setting with a type of "boolean" collectable', () => { + checkResult({ ...defaultSetting, type: 'boolean'}, defaultSetting.value) + }) +}) diff --git a/frontend/src/metabase/components/DatabaseDetailsForm.jsx b/frontend/src/metabase/components/DatabaseDetailsForm.jsx index c72bc0c2ec44840c164e09edb6458a2e46843165..3adc7254bd39e3781c24c8a4a222c92e28608f24 100644 --- a/frontend/src/metabase/components/DatabaseDetailsForm.jsx +++ b/frontend/src/metabase/components/DatabaseDetailsForm.jsx @@ -165,7 +165,7 @@ export default class DatabaseDetailsForm extends Component { <div style={{maxWidth: "40rem"}} className="pt1"> Some database installations can only be accessed by connecting through an SSH bastion host. This option also provides an extra layer of security when a VPN is not available. - Enabling this is usually slower than a dirrect connection. + Enabling this is usually slower than a direct connection. </div> </div> </div> diff --git a/frontend/src/metabase/containers/AddToDashSelectDashModal.jsx b/frontend/src/metabase/containers/AddToDashSelectDashModal.jsx index 182384687bfa8a4e624a1b48e6fe2c5353f2bd00..f8f1646a27c6dec88cee88eb28ccf0d2054f703a 100644 --- a/frontend/src/metabase/containers/AddToDashSelectDashModal.jsx +++ b/frontend/src/metabase/containers/AddToDashSelectDashModal.jsx @@ -45,7 +45,7 @@ export default class AddToDashSelectDashModal extends Component { addToDashboard = (dashboard: Dashboard) => { // we send the user over to the chosen dashboard in edit mode with the current card added - this.props.onChangeLocation(Urls.dashboard(dashboard.id)+"?add="+this.props.card.id); + this.props.onChangeLocation(Urls.dashboard(dashboard.id, {addCardWithId: this.props.card.id})); } createDashboard = async(newDashboard: Dashboard) => { diff --git a/frontend/src/metabase/dashboard/components/DashCardParameterMapper.jsx b/frontend/src/metabase/dashboard/components/DashCardParameterMapper.jsx index 70981453d08a0228dfa51720030df69bfa9e57c0..da2ed9a73a588d01603a021508435fd5f479b027 100644 --- a/frontend/src/metabase/dashboard/components/DashCardParameterMapper.jsx +++ b/frontend/src/metabase/dashboard/components/DashCardParameterMapper.jsx @@ -11,7 +11,7 @@ const DashCardParameterMapper = ({ dashcard }) => } <div className="flex mx4 z1" style={{ justifyContent: "space-around" }}> {[dashcard.card].concat(dashcard.series || []).map(card => - <DashCardCardParameterMapper dashcard={dashcard} card={card} /> + <DashCardCardParameterMapper key={`${dashcard.id},${card.id}`} dashcard={dashcard} card={card} /> )} </div> </div> diff --git a/frontend/src/metabase/dashboard/components/Dashboard.jsx b/frontend/src/metabase/dashboard/components/Dashboard.jsx index a73c9ffbfe162e8852b07778ab5d99ca2037c081..6a63a2900da1e8b501161e45c9e38c8dbf08611a 100644 --- a/frontend/src/metabase/dashboard/components/Dashboard.jsx +++ b/frontend/src/metabase/dashboard/components/Dashboard.jsx @@ -188,7 +188,7 @@ export default class Dashboard extends Component<*, Props, State> { } return ( - <LoadingAndErrorWrapper style={{ minHeight: "100%" }} className={cx("Dashboard flex-full", { "Dashboard--fullscreen": isFullscreen, "Dashboard--night": isNightMode})} loading={!dashboard} error={error}> + <LoadingAndErrorWrapper className={cx("Dashboard flex-full pb4", { "Dashboard--fullscreen": isFullscreen, "Dashboard--night": isNightMode})} loading={!dashboard} error={error}> {() => <div className="full" style={{ overflowX: "hidden" }}> <header className="DashboardHeader relative z2"> diff --git a/frontend/src/metabase/dashboard/containers/DashboardApp.jsx b/frontend/src/metabase/dashboard/containers/DashboardApp.jsx index 1475e94908f2fb8a2e4c6e040fc746f531acf379..9f53afad1f6e9efddab9261df2f891bc3cdbeac3 100644 --- a/frontend/src/metabase/dashboard/containers/DashboardApp.jsx +++ b/frontend/src/metabase/dashboard/containers/DashboardApp.jsx @@ -16,6 +16,7 @@ import { getUserIsAdmin } from "metabase/selectors/user"; import * as dashboardActions from "../dashboard"; import {archiveDashboard} from "metabase/dashboards/dashboards" +import {parseHashOptions} from "metabase/lib/browser"; const mapStateToProps = (state, props) => { return { @@ -34,8 +35,6 @@ const mapStateToProps = (state, props) => { editingParameter: getEditingParameter(state, props), parameters: getParameters(state, props), parameterValues: getParameterValues(state, props), - addCardOnLoad: props.location.query.add ? parseInt(props.location.query.add) : null, - metadata: getMetadata(state) } } @@ -48,10 +47,25 @@ const mapDispatchToProps = { onChangeLocation: push } +type DashboardAppState = { + addCardOnLoad: number|null +} + @connect(mapStateToProps, mapDispatchToProps) @title(({ dashboard }) => dashboard && dashboard.name) export default class DashboardApp extends Component { + state: DashboardAppState = { + addCardOnLoad: null + }; + + componentWillMount() { + let options = parseHashOptions(window.location.hash); + if (options.add) { + this.setState({addCardOnLoad: parseInt(options.add)}) + } + } + render() { - return <Dashboard {...this.props} />; + return <Dashboard addCardOnLoad={this.state.addCardOnLoad} {...this.props} />; } } diff --git a/frontend/src/metabase/dashboard/hoc/DashboardControls.jsx b/frontend/src/metabase/dashboard/hoc/DashboardControls.jsx index 3651bfac14ac23ad3a1687da17bf7e39218dfc7d..f291c2a50ad08acf4fd91e7ce0b095d367b7e2d6 100644 --- a/frontend/src/metabase/dashboard/hoc/DashboardControls.jsx +++ b/frontend/src/metabase/dashboard/hoc/DashboardControls.jsx @@ -97,8 +97,14 @@ export default (ComposedComponent: ReactClass<any>) => setValue("refresh", this.state.refreshPeriod); setValue("fullscreen", this.state.isFullscreen); setValue("theme", this.state.isNightMode ? "night" : null); + delete options.night; // DEPRECATED: options.night + // Delete the "add card to dashboard" parameter if it's present because we don't + // want to add the card again on page refresh. The `add` parameter is already handled in + // DashboardApp before this method is called. + delete options.add; + let hash = stringifyHashOptions(options); hash = hash ? "#" + hash : ""; @@ -106,7 +112,7 @@ export default (ComposedComponent: ReactClass<any>) => if (hash !== location.hash) { replace({ pathname: location.pathname, - earch: location.search, + search: location.search, hash }); } diff --git a/frontend/src/metabase/dashboard/selectors.js b/frontend/src/metabase/dashboard/selectors.js index 767f0646407b30ae7ac6e557e5b08765e18c7791..6073917ab6bf7bae0009d0713bc79d758293cfdd 100644 --- a/frontend/src/metabase/dashboard/selectors.js +++ b/frontend/src/metabase/dashboard/selectors.js @@ -157,6 +157,7 @@ export const getParameters = createSelector( .flatten() .map(m => m.field_id) .uniq() + .filter(fieldId => fieldId != null) .value(); return { ...parameter, diff --git a/frontend/src/metabase/dashboard/selectors.spec.js b/frontend/src/metabase/dashboard/selectors.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..cac0ee457575fb20af9d2ce3e01c3d508c7ac81a --- /dev/null +++ b/frontend/src/metabase/dashboard/selectors.spec.js @@ -0,0 +1,132 @@ +import { getParameters } from "./selectors"; + +import { chain } from "icepick"; + +const STATE = { + dashboard: { + dashboardId: 0, + dashboards: { + 0: { + ordered_cards: [0, 1], + parameters: [] + } + }, + dashcards: { + 0: { + card: { id: 0 }, + parameter_mappings: [] + }, + 1: { + card: { id: 1 }, + parameter_mappings: [] + } + } + }, + metadata: { + databases: {}, + tables: {}, + fields: {}, + } +} + +describe("dashboard/selectors", () => { + describe("getParameters", () => { + it("should work with no parameters", () => { + expect(getParameters(STATE)).toEqual([]); + }) + it("should not include field id with no mappings", () => { + const state = chain(STATE) + .assocIn(["dashboard", "dashboards", 0, "parameters", 0], { id: 1 }) + .value(); + expect(getParameters(state)).toEqual([{ + id: 1, + field_id: null + }]); + }) + it("should not include field id with one mapping, no field id", () => { + const state = chain(STATE) + .assocIn(["dashboard", "dashboards", 0, "parameters", 0], { id: 1 }) + .assocIn(["dashboard", "dashcards", 0, "parameter_mappings", 0], { + card_id: 0, + parameter_id: 1, + target: ["variable", ["template-tag", "foo"]] + }) + .value(); + expect(getParameters(state)).toEqual([{ + id: 1, + field_id: null + }]); + }) + it("should include field id with one mappings, with field id", () => { + const state = chain(STATE) + .assocIn(["dashboard", "dashboards", 0, "parameters", 0], { id: 1 }) + .assocIn(["dashboard", "dashcards", 0, "parameter_mappings", 0], { + card_id: 0, + parameter_id: 1, + target: ["dimension", ["field-id", 1]] + }) + .value(); + expect(getParameters(state)).toEqual([{ + id: 1, + field_id: 1 + }]); + }) + it("should include field id with two mappings, with same field id", () => { + const state = chain(STATE) + .assocIn(["dashboard", "dashboards", 0, "parameters", 0], { id: 1 }) + .assocIn(["dashboard", "dashcards", 0, "parameter_mappings", 0], { + card_id: 0, + parameter_id: 1, + target: ["dimension", ["field-id", 1]] + }) + .assocIn(["dashboard", "dashcards", 1, "parameter_mappings", 0], { + card_id: 1, + parameter_id: 1, + target: ["dimension", ["field-id", 1]] + }) + .value(); + expect(getParameters(state)).toEqual([{ + id: 1, + field_id: 1 + }]); + }) + it("should include field id with two mappings, one with field id, one without", () => { + const state = chain(STATE) + .assocIn(["dashboard", "dashboards", 0, "parameters", 0], { id: 1 }) + .assocIn(["dashboard", "dashcards", 0, "parameter_mappings", 0], { + card_id: 0, + parameter_id: 1, + target: ["dimension", ["field-id", 1]] + }) + .assocIn(["dashboard", "dashcards", 1, "parameter_mappings", 0], { + card_id: 1, + parameter_id: 1, + target: ["variable", ["template-tag", "foo"]] + }) + .value(); + expect(getParameters(state)).toEqual([{ + id: 1, + field_id: 1 + }]); + }) + it("should not include field id with two mappings, with different field ids", () => { + const state = chain(STATE) + .assocIn(["dashboard", "dashboards", 0, "parameters", 0], { id: 1 }) + .assocIn(["dashboard", "dashcards", 0, "parameter_mappings", 0], { + card_id: 0, + parameter_id: 1, + target: ["dimension", ["field-id", 1]] + }) + .assocIn(["dashboard", "dashcards", 1, "parameter_mappings", 0], { + card_id: 1, + parameter_id: 1, + target: ["dimension", ["field-id", 2]] + }) + .value(); + expect(getParameters(state)).toEqual([{ + id: 1, + field_id: null + }]); + }) + }) +}) diff --git a/frontend/src/metabase/icon_paths.js b/frontend/src/metabase/icon_paths.js index b790db3f0b6df606923d3ac7931d05cf6f2d1f27..6b55243deca7e0866774fdaae008857f769b3725 100644 --- a/frontend/src/metabase/icon_paths.js +++ b/frontend/src/metabase/icon_paths.js @@ -19,7 +19,7 @@ export var ICON_PATHS = { backArrow: 'M11.7416687,19.0096 L18.8461178,26.4181004 L14.2696969,30.568 L0.38960831,16.093881 L0,15.6875985 L0.49145276,15.241949 L14.6347557,1 L19.136,5.22693467 L11.3214393,13.096 L32,13.096 L32,19.0096 L11.7416687,19.0096 Z', bar: 'M2 23.467h6.4V32H2v-8.533zm10.667-12.8h6.4V32h-6.4V10.667zM23.333 0h6.4v32h-6.4V0z', beaker: 'M4.31736354,31.1631075 C3.93810558,30.6054137 3.89343681,29.6635358 4.20559962,29.0817181 L11.806982,14.9140486 L11.8069821,10.5816524 L10.7015144,10.4653256 C10.0309495,10.394763 9.48734928,9.78799739 9.48734928,9.12166999 L9.48734928,7.34972895 C9.48734928,6.67821106 10.0368737,6.13383825 10.7172248,6.13383825 L21.8462005,6.13383825 C22.525442,6.13383825 23.0760761,6.68340155 23.0760761,7.34972895 L23.0760761,9.12166999 C23.0760761,9.79318788 22.5250158,10.3375607 21.856025,10.3375607 L20.9787023,10.3375607 L20.9787024,14.9281806 L28.77277,29.0827118 C29.0983515,29.6739888 29.0709073,30.6193105 28.7174156,31.1846409 L28.852457,30.9686726 C28.4963041,31.538259 27.6541076,32 26.9865771,32 L6.10749779,32 C5.43315365,32 4.58248747,31.5529687 4.19978245,30.9902061 L4.31736354,31.1631075 Z M15.5771418,17.6040443 C16.5170398,17.6040443 17.2789777,16.8377777 17.2789777,15.89254 C17.2789777,14.9473023 16.5170398,14.1810358 15.5771418,14.1810358 C14.6372438,14.1810358 13.8753059,14.9473023 13.8753059,15.89254 C13.8753059,16.8377777 14.6372438,17.6040443 15.5771418,17.6040443 Z M16.5496195,12.8974079 C17.8587633,12.8974079 18.9200339,11.830108 18.9200339,10.5135268 C18.9200339,9.1969457 17.8587633,8.1296458 16.5496195,8.1296458 C15.2404758,8.1296458 14.1792052,9.1969457 14.1792052,10.5135268 C14.1792052,11.830108 15.2404758,12.8974079 16.5496195,12.8974079 Z M5.71098553,30.2209651 L10.9595331,20.5151267 C10.9595331,20.5151267 12.6834557,21.2672852 14.3734184,21.2672852 C16.0633811,21.2672852 16.8198616,19.2872624 17.588452,18.6901539 C18.3570425,18.0930453 19.9467191,17.1113296 19.9467191,17.1113296 L27.0506095,30.1110325 L5.71098553,30.2209651 Z M13.6608671,4.37817079 C14.4114211,4.37817079 15.0198654,3.78121712 15.0198654,3.04483745 C15.0198654,2.30845779 14.4114211,1.71150412 13.6608671,1.71150412 C12.9103132,1.71150412 12.3018689,2.30845779 12.3018689,3.04483745 C12.3018689,3.78121712 12.9103132,4.37817079 13.6608671,4.37817079 Z M17.9214578,2.45333328 C18.6119674,2.45333328 19.1717361,1.90413592 19.1717361,1.22666664 C19.1717361,0.549197362 18.6119674,0 17.9214578,0 C17.2309481,0 16.6711794,0.549197362 16.6711794,1.22666664 C16.6711794,1.90413592 17.2309481,2.45333328 17.9214578,2.45333328 Z', - breakout: 'M.025 12.864s7.682-.008 10.09-.008c2.407 0 6.815 1.072 9.87 3.155 3.055 2.084 6.63 6.03 6.63 6.03l3.22-3.146L32 32l-13.279-2.058 3.31-3.38c-1.263-1.328-4.24-4.964-7.857-6.441-3.617-1.478-10.762-.75-14.15-.98-.055-.003 0-6.277 0-6.277zM31.979 0l-2.123 13.164-3.754-3.62-4.74 4.491c-1.31-1.176-5.238-2.634-5.238-2.634 2.62-1.366 6.32-5.581 6.32-5.581l-3.7-3.824L31.98 0z', + breakout: 'M24.47 1H32v7.53h-7.53V1zm0 11.294H32v7.53h-7.53v-7.53zm0 11.294H32v7.53h-7.53v-7.53zM0 1h9.412v30.118H0V1zm11.731 13.714c.166-.183.452-.177.452-.177h6.475s-1.601-2.053-2.07-2.806c-.469-.753-.604-1.368 0-1.905.603-.536 1.226-.281 1.878.497.652.779 2.772 3.485 3.355 4.214.583.73.65 1.965 0 2.835-.65.87-2.65 4.043-3.163 4.65-.514.607-1.123.713-1.732.295-.609-.419-.838-1.187-.338-1.872.5-.684 2.07-3.073 2.07-3.073h-6.475s-.27 0-.46-.312-.151-.612-.151-.612l.007-1.246s-.014-.306.152-.488z', bubble: 'M18.155 20.882c-5.178-.638-9.187-5.051-9.187-10.402C8.968 4.692 13.66 0 19.448 0c5.789 0 10.48 4.692 10.48 10.48 0 3.05-1.302 5.797-3.383 7.712a7.127 7.127 0 1 1-8.39 2.69zm-6.392 10.14a2.795 2.795 0 1 1 0-5.59 2.795 2.795 0 0 1 0 5.59zm-6.079-6.288a4.541 4.541 0 1 1 0-9.083 4.541 4.541 0 0 1 0 9.083z', cards: 'M16.5,11 C16.1340991,11 15.7865579,10.9213927 15.4733425,10.7801443 L7.35245972,21.8211652 C7.7548404,22.264891 8,22.8538155 8,23.5 C8,24.8807119 6.88071187,26 5.5,26 C4.11928813,26 3,24.8807119 3,23.5 C3,22.1192881 4.11928813,21 5.5,21 C5.87370843,21 6.22826528,21.0819977 6.5466604,21.2289829 L14.6623495,10.1950233 C14.2511829,9.74948188 14,9.15407439 14,8.5 C14,7.11928813 15.1192881,6 16.5,6 C17.8807119,6 19,7.11928813 19,8.5 C19,8.96980737 18.8704088,9.4093471 18.6450228,9.78482291 L25.0405495,15.4699905 C25.4512188,15.1742245 25.9552632,15 26.5,15 C27.8807119,15 29,16.1192881 29,17.5 C29,18.8807119 27.8807119,20 26.5,20 C25.1192881,20 24,18.8807119 24,17.5 C24,17.0256697 24.1320984,16.5821926 24.3615134,16.2043506 L17.9697647,10.5225413 C17.5572341,10.8228405 17.0493059,11 16.5,11 Z M5.5,25 C6.32842712,25 7,24.3284271 7,23.5 C7,22.6715729 6.32842712,22 5.5,22 C4.67157288,22 4,22.6715729 4,23.5 C4,24.3284271 4.67157288,25 5.5,25 Z M26.5,19 C27.3284271,19 28,18.3284271 28,17.5 C28,16.6715729 27.3284271,16 26.5,16 C25.6715729,16 25,16.6715729 25,17.5 C25,18.3284271 25.6715729,19 26.5,19 Z M16.5,10 C17.3284271,10 18,9.32842712 18,8.5 C18,7.67157288 17.3284271,7 16.5,7 C15.6715729,7 15,7.67157288 15,8.5 C15,9.32842712 15.6715729,10 16.5,10 Z', calendar: { @@ -56,7 +56,7 @@ export var ICON_PATHS = { database: 'M1.18285296e-08,10.5127919 C-1.47856568e-08,7.95412848 1.18285298e-08,4.57337284 1.18285298e-08,4.57337284 C1.18285298e-08,4.57337284 1.58371041,5.75351864e-10 15.6571342,0 C29.730558,-5.7535027e-10 31.8900148,4.13849684 31.8900148,4.57337284 L31.8900148,10.4843058 C31.8900148,10.4843058 30.4448001,15.1365942 16.4659751,15.1365944 C2.48715012,15.1365947 2.14244494e-08,11.4353349 1.18285296e-08,10.5127919 Z M0.305419478,21.1290071 C0.305419478,21.1290071 0.0405133833,21.2033291 0.0405133833,21.8492606 L0.0405133833,27.3032816 C0.0405133833,27.3032816 1.46515486,31.941655 15.9641228,31.941655 C30.4630908,31.941655 32,27.3446712 32,27.3446712 C32,27.3446712 32,21.7986104 32,21.7986105 C32,21.2073557 31.6620557,21.0987647 31.6620557,21.0987647 C31.6620557,21.0987647 29.7146434,25.22314 16.0318829,25.22314 C2.34912233,25.22314 0.305419478,21.1290071 0.305419478,21.1290071 Z M0.305419478,12.656577 C0.305419478,12.656577 0.0405133833,12.730899 0.0405133833,13.3768305 L0.0405133833,18.8308514 C0.0405133833,18.8308514 1.46515486,23.4692249 15.9641228,23.4692249 C30.4630908,23.4692249 32,18.8722411 32,18.8722411 C32,18.8722411 32,13.3261803 32,13.3261803 C32,12.7349256 31.6620557,12.6263346 31.6620557,12.6263346 C31.6620557,12.6263346 29.7146434,16.7507099 16.0318829,16.7507099 C2.34912233,16.7507099 0.305419478,12.656577 0.305419478,12.656577 Z', dashboard: 'M32,29 L32,4 L32,0 L0,0 L0,8 L28,8 L28,28 L4,28 L4,8 L0,8 L0,29.5 L0,32 L32,32 L32,29 Z M7.27272727,18.9090909 L17.4545455,18.9090909 L17.4545455,23.2727273 L7.27272727,23.2727273 L7.27272727,18.9090909 Z M7.27272727,12.0909091 L24.7272727,12.0909091 L24.7272727,16.4545455 L7.27272727,16.4545455 L7.27272727,12.0909091 Z M20.3636364,18.9090909 L24.7272727,18.9090909 L24.7272727,23.2727273 L20.3636364,23.2727273 L20.3636364,18.9090909 Z', dashboards: 'M17,5.49100518 L17,10.5089948 C17,10.7801695 17.2276528,11 17.5096495,11 L26.4903505,11 C26.7718221,11 27,10.7721195 27,10.5089948 L27,5.49100518 C27,5.21983051 26.7723472,5 26.4903505,5 L17.5096495,5 C17.2281779,5 17,5.22788048 17,5.49100518 Z M18.5017326,14 C18.225722,14 18,13.77328 18,13.4982674 L18,26.5017326 C18,26.225722 18.22672,26 18.5017326,26 L5.49826741,26 C5.77427798,26 6,26.22672 6,26.5017326 L6,13.4982674 C6,13.774278 5.77327997,14 5.49826741,14 L18.5017326,14 Z M14.4903505,6 C14.2278953,6 14,5.78028538 14,5.49100518 L14,10.5089948 C14,10.2167107 14.2224208,10 14.4903505,10 L5.50964952,10 C5.77210473,10 6,10.2197146 6,10.5089948 L6,5.49100518 C6,5.78328929 5.77757924,6 5.50964952,6 L14.4903505,6 Z M26.5089948,22 C26.2251201,22 26,21.7774008 26,21.4910052 L26,26.5089948 C26,26.2251201 26.2225992,26 26.5089948,26 L21.4910052,26 C21.7748799,26 22,26.2225992 22,26.5089948 L22,21.4910052 C22,21.7748799 21.7774008,22 21.4910052,22 L26.5089948,22 Z M26.5089948,14 C26.2251201,14 26,13.7774008 26,13.4910052 L26,18.5089948 C26,18.2251201 26.2225992,18 26.5089948,18 L21.4910052,18 C21.7748799,18 22,18.2225992 22,18.5089948 L22,13.4910052 C22,13.7748799 21.7774008,14 21.4910052,14 L26.5089948,14 Z M26.4903505,6 C26.2278953,6 26,5.78028538 26,5.49100518 L26,10.5089948 C26,10.2167107 26.2224208,10 26.4903505,10 L17.5096495,10 C17.7721047,10 18,10.2197146 18,10.5089948 L18,5.49100518 C18,5.78328929 17.7775792,6 17.5096495,6 L26.4903505,6 Z M5,13.4982674 L5,26.5017326 C5,26.7769181 5.21990657,27 5.49826741,27 L18.5017326,27 C18.7769181,27 19,26.7800934 19,26.5017326 L19,13.4982674 C19,13.2230819 18.7800934,13 18.5017326,13 L5.49826741,13 C5.22308192,13 5,13.2199066 5,13.4982674 Z M5,5.49100518 L5,10.5089948 C5,10.7801695 5.22765279,11 5.50964952,11 L14.4903505,11 C14.7718221,11 15,10.7721195 15,10.5089948 L15,5.49100518 C15,5.21983051 14.7723472,5 14.4903505,5 L5.50964952,5 C5.22817786,5 5,5.22788048 5,5.49100518 Z M21,21.4910052 L21,26.5089948 C21,26.7801695 21.2278805,27 21.4910052,27 L26.5089948,27 C26.7801695,27 27,26.7721195 27,26.5089948 L27,21.4910052 C27,21.2198305 26.7721195,21 26.5089948,21 L21.4910052,21 C21.2198305,21 21,21.2278805 21,21.4910052 Z M21,13.4910052 L21,18.5089948 C21,18.7801695 21.2278805,19 21.4910052,19 L26.5089948,19 C26.7801695,19 27,18.7721195 27,18.5089948 L27,13.4910052 C27,13.2198305 26.7721195,13 26.5089948,13 L21.4910052,13 C21.2198305,13 21,13.2278805 21,13.4910052 Z', - distribution: 'M1.457 4.731v21.807h29.815a.73.73 0 0 1 .728.73.73.73 0 0 1-.728.732H0V4.731A.73.73 0 0 1 .728 4a.73.73 0 0 1 .729.731zM5.795 21.53a1.824 1.824 0 0 1-1.82-1.828c0-1.01.814-1.828 1.82-1.828 2.094 0 2.965-1.21 4.205-5.255l.147-.484c1.609-5.271 2.99-7.353 6.779-7.353 3.792 0 5.17 2.084 6.765 7.362l.145.48c1.227 4.043 2.093 5.25 4.181 5.25 1.006 0 1.821.819 1.821 1.828 0 1.01-.815 1.828-1.82 1.828-4.305 0-6.005-2.37-7.666-7.841l-.146-.483c-1.14-3.77-1.8-4.769-3.28-4.769-1.483 0-2.147 1-3.297 4.769l-.148.487c-1.675 5.469-3.382 7.837-7.686 7.837z', + curve: 'M3.033 3.791v22.211H31.09c.403 0 .882.872.882 1.59 0 .717-.48 1.408-.882 1.408H0V3.791c0-.403.875-.914 1.487-.914.612 0 1.546.511 1.546.914zm3.804 17.912C5.714 21.495 5 20.318 5 19.355c0-.963.831-2.296 1.837-2.296 2.093 0 2.965-1.207 4.204-5.242l.148-.482C12.798 6.077 14.18 3 17.968 3c3.792 0 5.17 3.08 6.765 8.343l.145.478c1.227 4.034 2.093 5.238 4.181 5.238 1.006 0 1.875 1.29 1.875 2.296 0 1.007-.898 2.184-1.875 2.348-3.656.612-6.004-2.364-7.665-7.821l-.146-.482c-1.14-3.76-1.8-6.754-3.28-6.754-1.483 0-2.147 2.995-3.297 6.754l-.148.486c-1.675 5.454-3.93 8.514-7.686 7.817z', document: 'M29,10.1052632 L29,28.8325291 C29,30.581875 27.5842615,32 25.8337327,32 L7.16626728,32 C5.41758615,32 4,30.5837102 4,28.8441405 L4,3.15585953 C4,1.41292644 5.42339685,9.39605581e-15 7.15970573,8.42009882e-15 L20.713352,8.01767853e-16 L20.713352,8.42105263 L22.3846872,8.42105263 L22.3846872,0.310375032 L28.7849894,8.42105263 L20.713352,8.42105263 L20.713352,10.1052632 L29,10.1052632 Z M7.3426704,12.8000006 L25.7273576,12.8000006 L25.7273576,14.4842112 L7.3426704,14.4842112 L7.3426704,12.8000006 Z M7.3426704,17.3473687 L25.7273576,17.3473687 L25.7273576,19.0315793 L7.3426704,19.0315793 L7.3426704,17.3473687 Z M7.3426704,21.8947352 L25.7273576,21.8947352 L25.7273576,23.5789458 L7.3426704,23.5789458 L7.3426704,21.8947352 Z M7.43137255,26.2736849 L16.535014,26.2736849 L16.535014,27.9578954 L7.43137255,27.9578954 L7.43137255,26.2736849 Z', downarrow: 'M12.2782161,19.3207547 L12.2782161,0 L19.5564322,0 L19.5564322,19.3207547 L26.8346484,19.3207547 L15.9173242,32 L5,19.3207547 L12.2782161,19.3207547 Z', download: { diff --git a/frontend/src/metabase/lib/analytics.js b/frontend/src/metabase/lib/analytics.js index 4c6028db7ba69d79bec89ea12f914e41245007d7..95d349604dbc93dec485b040c1ffc9614153251a 100644 --- a/frontend/src/metabase/lib/analytics.js +++ b/frontend/src/metabase/lib/analytics.js @@ -1,4 +1,5 @@ /*global ga*/ +/* @flow */ import MetabaseSettings from "metabase/lib/settings"; @@ -8,13 +9,14 @@ import { DEBUG } from "metabase/lib/debug"; // Simple module for in-app analytics. Currently sends data to GA but could be extended to anything else. const MetabaseAnalytics = { // track a pageview (a.k.a. route change) - trackPageView: function(url) { + trackPageView: function(url: string) { if (url) { // scrub query builder urls to remove serialized json queries from path url = (url.lastIndexOf('/q/', 0) === 0) ? '/q/' : url; const { tag } = MetabaseSettings.get('version'); + // $FlowFixMe ga('set', 'dimension1', tag); ga('set', 'page', url); ga('send', 'pageview', url); @@ -22,11 +24,12 @@ const MetabaseAnalytics = { }, // track an event - trackEvent: function(category, action, label, value) { + trackEvent: function(category: string, action?: string, label?: string, value?: number) { const { tag } = MetabaseSettings.get('version'); // category & action are required, rest are optional if (category && action) { + // $FlowFixMe ga('set', 'dimension1', tag); ga('send', 'event', category, action, label, value); } @@ -40,6 +43,7 @@ export default MetabaseAnalytics; export function registerAnalyticsClickListener() { + // $FlowFixMe document.body.addEventListener("click", function(e) { var node = e.target; diff --git a/frontend/src/metabase/lib/permissions.js b/frontend/src/metabase/lib/permissions.js index 8c1fd837bf3b998166a8f61bb5f9c9ef71eb5f85..cf2e925ee2debcebad380dc7173cca56ced9d61e 100644 --- a/frontend/src/metabase/lib/permissions.js +++ b/frontend/src/metabase/lib/permissions.js @@ -1,10 +1,11 @@ /* @flow */ import { getIn, setIn } from "icepick"; +import _ from "underscore"; import type Database from "metabase/meta/metadata/Database"; import type { DatabaseId } from "metabase/meta/types/Database"; -import type { SchemaName, TableId } from "metabase/meta/types/Table"; +import type { SchemaName, TableId, Table } from "metabase/meta/types/Table"; import Metadata from "metabase/meta/metadata/Metadata"; import type { Group, GroupId, GroupsPermissions } from "metabase/meta/types/Permissions"; @@ -12,6 +13,7 @@ import type { Group, GroupId, GroupsPermissions } from "metabase/meta/types/Perm type TableEntityId = { databaseId: DatabaseId, schemaName: SchemaName, tableId: TableId }; type SchemaEntityId = { databaseId: DatabaseId, schemaName: SchemaName }; type DatabaseEntityId = { databaseId: DatabaseId }; +type EntityId = TableEntityId | SchemaEntityId | DatabaseEntityId; export function getPermission( permissions: GroupsPermissions, @@ -92,7 +94,80 @@ export const getFieldsPermission = (permissions: GroupsPermissions, groupId: Gro } } -export function updateFieldsPermission(permissions: GroupsPermissions, groupId: GroupId, { databaseId, schemaName, tableId }: TableEntityId, value: string, metadata: Metadata): GroupsPermissions { +export function downgradeNativePermissionsIfNeeded(permissions: GroupsPermissions, groupId: GroupId, { databaseId }: DatabaseEntityId, value: string, metadata: Metadata): GroupsPermissions { + let currentSchemas = getSchemasPermission(permissions, groupId, { databaseId }); + let currentNative = getNativePermission(permissions, groupId, { databaseId }); + + if (value === "none") { + // if changing schemas to none, downgrade native to none + return updateNativePermission(permissions, groupId, { databaseId }, "none", metadata); + } else if (value === "controlled" && currentSchemas === "all" && currentNative === "write") { + // if changing schemas to controlled, downgrade native to read + return updateNativePermission(permissions, groupId, { databaseId }, "read", metadata); + } else { + return permissions; + } +} + +// $FlowFixMe +const metadataTableToTableEntityId = (table: Table): TableEntityId => ({ databaseId: table.db_id, schemaName: table.schema, tableId: table.id }); +const entityIdToMetadataTableFields = (entityId: EntityId) => ({ + ...(entityId.databaseId ? {db_id: entityId.databaseId} : {}), + ...(entityId.schemaName ? {schema: entityId.schemaName} : {}), + ...(entityId.tableId ? {tableId: entityId.tableId} : {}) +}) + +function inferEntityPermissionValueFromChildTables(permissions: GroupsPermissions, groupId: GroupId, entityId: DatabaseEntityId|SchemaEntityId, metadata: Metadata) { + const { databaseId } = entityId; + const database = metadata && metadata.database(databaseId); + + // $FlowFixMe + const entityIdsForDescendantTables: TableEntityId[] = _.chain(database.tables()) + .filter((t) => _.isMatch(t, entityIdToMetadataTableFields(entityId))) + .map(metadataTableToTableEntityId) + .value(); + + const entityIdsByPermValue = _.chain(entityIdsForDescendantTables) + .map((id) => getFieldsPermission(permissions, groupId, id)) + .groupBy(_.identity) + .value(); + + const keys = Object.keys(entityIdsByPermValue); + const allTablesHaveSamePermissions = keys.length === 1; + + if (allTablesHaveSamePermissions) { + // either "all" or "none" + return keys[0]; + } else { + return "controlled"; + } +} + +// Checks the child tables of a given entityId and updates the shared table and/or schema permission values according to table permissions +// This method was added for keeping the UI in sync when modifying child permissions +export function inferAndUpdateEntityPermissions(permissions: GroupsPermissions, groupId: GroupId, entityId: DatabaseEntityId|SchemaEntityId, metadata: Metadata) { + // $FlowFixMe + const { databaseId, schemaName } = entityId; + + if (schemaName) { + // Check all tables for current schema if their shared schema-level permission value should be updated + // $FlowFixMe + const tablesPermissionValue = inferEntityPermissionValueFromChildTables(permissions, groupId, { databaseId, schemaName }, metadata); + permissions = updateTablesPermission(permissions, groupId, { databaseId, schemaName }, tablesPermissionValue, metadata); + } + + if (databaseId) { + // Check all tables for current database if schemas' shared database-level permission value should be updated + const schemasPermissionValue = inferEntityPermissionValueFromChildTables(permissions, groupId, { databaseId }, metadata); + permissions = updateSchemasPermission(permissions, groupId, { databaseId }, schemasPermissionValue, metadata); + permissions = downgradeNativePermissionsIfNeeded(permissions, groupId, { databaseId }, schemasPermissionValue, metadata); + } + + return permissions; +} + +export function updateFieldsPermission(permissions: GroupsPermissions, groupId: GroupId, entityId: TableEntityId, value: string, metadata: Metadata): GroupsPermissions { + const { databaseId, schemaName, tableId } = entityId; permissions = updateTablesPermission(permissions, groupId, { databaseId, schemaName }, "controlled", metadata); permissions = updatePermission(permissions, groupId, [databaseId, "schemas", schemaName, tableId], value /* TODO: field ids, when enabled "controlled" fields */); @@ -102,7 +177,7 @@ export function updateFieldsPermission(permissions: GroupsPermissions, groupId: export function updateTablesPermission(permissions: GroupsPermissions, groupId: GroupId, { databaseId, schemaName }: SchemaEntityId, value: string, metadata: Metadata): GroupsPermissions { const database = metadata && metadata.database(databaseId); - const tableIds: ?number[] = database && database.tables().map(t => t.id); + const tableIds: ?number[] = database && database.tables().filter(t => t.schema === schemaName).map(t => t.id); permissions = updateSchemasPermission(permissions, groupId, { databaseId }, "controlled", metadata); permissions = updatePermission(permissions, groupId, [databaseId, "schemas", schemaName], value, tableIds); @@ -114,17 +189,7 @@ export function updateSchemasPermission(permissions: GroupsPermissions, groupId: let database = metadata.database(databaseId); let schemaNames = database && database.schemaNames(); - let currentSchemas = getSchemasPermission(permissions, groupId, { databaseId }); - let currentNative = getNativePermission(permissions, groupId, { databaseId }); - - if (value === "none") { - // if changing schemas to none, downgrade native to none - permissions = updateNativePermission(permissions, groupId, { databaseId }, "none", metadata); - } else if (value === "controlled" && currentSchemas === "all" && currentNative === "write") { - // if changing schemas to controlled, downgrade native to read - permissions = updateNativePermission(permissions, groupId, { databaseId }, "read", metadata); - } - + permissions = downgradeNativePermissionsIfNeeded(permissions, groupId, { databaseId }, value, metadata); return updatePermission(permissions, groupId, [databaseId, "schemas"], value, schemaNames); } diff --git a/frontend/src/metabase/lib/urls.js b/frontend/src/metabase/lib/urls.js index 9b3912b11d70daf4344fd1413c1d8be767265960..f63f47403069f448d5f6a7e1a4da1c7669420d5a 100644 --- a/frontend/src/metabase/lib/urls.js +++ b/frontend/src/metabase/lib/urls.js @@ -23,8 +23,10 @@ export function question(cardId, hash = "", query = "") { : `/question${query}${hash}`; } -export function dashboard(dashboardId) { - return `/dashboard/${dashboardId}`; +export function dashboard(dashboardId, {addCardWithId} = {}) { + return addCardWithId != null + ? `/dashboard/${dashboardId}#add=${addCardWithId}` + : `/dashboard/${dashboardId}`; } export function modelToUrl(model, modelId) { diff --git a/frontend/src/metabase/meta/types/Visualization.js b/frontend/src/metabase/meta/types/Visualization.js index b191504a37be136bf07ac649b06956610ca3a220..e172b2037c269d320e2fa536e560514346cb0744 100644 --- a/frontend/src/metabase/meta/types/Visualization.js +++ b/frontend/src/metabase/meta/types/Visualization.js @@ -40,7 +40,10 @@ export type ClickAction = { title: any, // React Element icon?: string, popover?: (props: ClickActionPopoverProps) => any, // React Element - card?: () => ?Card + card?: () => ?Card, + + section?: string, + name?: string, } export type ClickActionProps = { diff --git a/frontend/src/metabase/public/components/widgets/SharingPane.jsx b/frontend/src/metabase/public/components/widgets/SharingPane.jsx index 6d9a8aa8ad4775775f04cd79cd4301ba855c0150..783a46001e5756099bcd572225f91f5b5a60d2c6 100644 --- a/frontend/src/metabase/public/components/widgets/SharingPane.jsx +++ b/frontend/src/metabase/public/components/widgets/SharingPane.jsx @@ -6,6 +6,7 @@ import RetinaImage from "react-retina-image"; import Icon from "metabase/components/Icon"; import Toggle from "metabase/components/Toggle"; import CopyWidget from "metabase/components/CopyWidget"; +import Confirm from "metabase/components/Confirm"; import { getPublicEmbedHTML } from "metabase/public/lib/code"; @@ -67,15 +68,23 @@ export default class SharingPane extends Component<*, Props, State> { <div className="pb2 mb4 border-bottom flex align-center"> <h4>Enable sharing</h4> <div className="ml-auto"> - <Toggle value={!!resource.public_uuid} onChange={(value) => { - if (value) { + { resource.public_uuid ? + <Confirm + title="Disable this public link?" + content="This will cause the existing link to stop working. You can re-enable it, but when you do it will be a different link." + action={() => { + MetabaseAnalytics.trackEvent("Sharing Modal", "Public Link Disabled", resourceType); + onDisablePublicLink(); + }} + > + <Toggle value={true} /> + </Confirm> + : + <Toggle value={false} onChange={() => { MetabaseAnalytics.trackEvent("Sharing Modal", "Public Link Enabled", resourceType); onCreatePublicLink(); - } else { - MetabaseAnalytics.trackEvent("Sharing Modal", "Public Link Disabled", resourceType); - onDisablePublicLink(); - } - }}/> + }}/> + } </div> </div> } diff --git a/frontend/src/metabase/qb/components/actions/CommonMetricsAction.jsx b/frontend/src/metabase/qb/components/actions/CommonMetricsAction.jsx index 1a6db1e3dab125ebe0d79c277996e2c18cc405d7..63450c1325586739f3e1eb02bbb69407f2a9591f 100644 --- a/frontend/src/metabase/qb/components/actions/CommonMetricsAction.jsx +++ b/frontend/src/metabase/qb/components/actions/CommonMetricsAction.jsx @@ -11,6 +11,7 @@ import { summarize } from "metabase/qb/lib/actions"; export default ({ card, tableMetadata }: ClickActionProps): ClickAction[] => { return tableMetadata.metrics.slice(0, 5).map(metric => ({ + name: "common-metric", title: <span>View <strong>{metric.name}</strong></span>, card: () => summarize(card, ["METRIC", metric.id], tableMetadata) })); diff --git a/frontend/src/metabase/qb/components/actions/CountByTimeAction.jsx b/frontend/src/metabase/qb/components/actions/CountByTimeAction.jsx index 3c82110e2d3f56121ced7f507eac14fe7629ba39..fb74354a33ce3d3d11ff1d26b8237bbadd457f8b 100644 --- a/frontend/src/metabase/qb/components/actions/CountByTimeAction.jsx +++ b/frontend/src/metabase/qb/components/actions/CountByTimeAction.jsx @@ -18,9 +18,10 @@ export default ({ card, tableMetadata }: ClickActionProps): ClickAction[] => { return [ { - title: <span>Count of rows by time</span>, + name: "count-by-time", section: "sum", - icon: "sum", + title: <span>Count of rows by time</span>, + icon: "line", card: () => breakout( summarize(card, ["count"], tableMetadata), diff --git a/frontend/src/metabase/qb/components/actions/PivotByAction.jsx b/frontend/src/metabase/qb/components/actions/PivotByAction.jsx index 10cfd950168595c5333cabaa4c6b49104e9c53b5..a26f75a88c74bcb73115d693338025dbe63f935f 100644 --- a/frontend/src/metabase/qb/components/actions/PivotByAction.jsx +++ b/frontend/src/metabase/qb/components/actions/PivotByAction.jsx @@ -67,11 +67,12 @@ export default (name: string, icon: string, fieldFilter: FieldFilter) => return [ { + name: "pivot-by-" + name.toLowerCase(), section: "breakout", title: clicked ? name : <span> - Pivot by + Break out by {" "} <span className="text-dark"> {name.toLowerCase()} diff --git a/frontend/src/metabase/qb/components/actions/PlotSegmentField.jsx b/frontend/src/metabase/qb/components/actions/PlotSegmentField.jsx index ce060112ff419c2af51a103f89f5727f20ffd701..da28dcd54396d86db87d0a9080041bdf31eb1c7a 100644 --- a/frontend/src/metabase/qb/components/actions/PlotSegmentField.jsx +++ b/frontend/src/metabase/qb/components/actions/PlotSegmentField.jsx @@ -13,6 +13,7 @@ export default ({ card, tableMetadata }: ClickActionProps): ClickAction[] => { } return [ { + name: "plot", title: "Plot a field in this segment", icon: "bar", card: () => plotSegmentField(card) diff --git a/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx b/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx index 22e8f88eb4cde578bb377c6109781bf87a4b4bd0..851400885e1aa61b750b3a204911d6c1c8afa231 100644 --- a/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx +++ b/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx @@ -22,8 +22,9 @@ export default ({ card, tableMetadata }: ClickActionProps): ClickAction[] => { return [ { + name: "summarize", title: "Summarize this segment", - icon: "funnel", // FIXME: icon + icon: "sum", // eslint-disable-next-line react/display-name popover: ( { onChangeCardAndRun, onClose }: ClickActionPopoverProps diff --git a/frontend/src/metabase/qb/components/actions/UnderlyingDataAction.jsx b/frontend/src/metabase/qb/components/actions/UnderlyingDataAction.jsx index 7bdce877cca434ad10d8a7ee1b3a20c2b721908f..871d440cbc4883d28cb1130548188f89b8efcb8d 100644 --- a/frontend/src/metabase/qb/components/actions/UnderlyingDataAction.jsx +++ b/frontend/src/metabase/qb/components/actions/UnderlyingDataAction.jsx @@ -11,7 +11,8 @@ export default ({ card, tableMetadata }: ClickActionProps): ClickAction[] => { if (card.display !== "table" && card.display !== "scalar") { return [ { - title: "View the underlying data", + name: "underlying-data", + title: "View this as a table", icon: "table", card: () => toUnderlyingData(card) } diff --git a/frontend/src/metabase/qb/components/actions/UnderlyingRecordsAction.jsx b/frontend/src/metabase/qb/components/actions/UnderlyingRecordsAction.jsx index 7d251e63017ea8f1ab6071534125d692f71a7fe0..95fb1c00aac1833b993fb5f9c37a08dbc2f69694 100644 --- a/frontend/src/metabase/qb/components/actions/UnderlyingRecordsAction.jsx +++ b/frontend/src/metabase/qb/components/actions/UnderlyingRecordsAction.jsx @@ -16,6 +16,7 @@ export default ({ card, tableMetadata }: ClickActionProps): ClickAction[] => { if (query && !Query.isBareRows(query)) { return [ { + name: "underlying-records", title: ( <span> View the underlying @@ -27,7 +28,7 @@ export default ({ card, tableMetadata }: ClickActionProps): ClickAction[] => { records </span> ), - icon: "table", + icon: "table2", card: () => toUnderlyingRecords(card) } ]; diff --git a/frontend/src/metabase/qb/components/drill/CountByColumnDrill.js b/frontend/src/metabase/qb/components/drill/CountByColumnDrill.js index 7ddd2410ab9c5902b9da047a078b307f5c309d22..f755db6470fbc9d17232b1956f1d40b54dc51233 100644 --- a/frontend/src/metabase/qb/components/drill/CountByColumnDrill.js +++ b/frontend/src/metabase/qb/components/drill/CountByColumnDrill.js @@ -34,7 +34,9 @@ export default ( return [ { - title: <span>Count of rows by {column.display_name}</span>, + name: "count-by-column", + section: "distribution", + title: <span>Distribution</span>, card: () => pivot( summarize(card, ["count"], tableMetadata), diff --git a/frontend/src/metabase/qb/components/drill/ObjectDetailDrill.jsx b/frontend/src/metabase/qb/components/drill/ObjectDetailDrill.jsx index dee0baaa58707dd28172aa569522368da98528a8..2759d9c4ba410b5a0dd43c810a6e241ed49a4bc2 100644 --- a/frontend/src/metabase/qb/components/drill/ObjectDetailDrill.jsx +++ b/frontend/src/metabase/qb/components/drill/ObjectDetailDrill.jsx @@ -39,6 +39,7 @@ export default ( return [ { + name: "object-detail", section: "details", title: "View details", default: true, diff --git a/frontend/src/metabase/qb/components/drill/QuickFilterDrill.jsx b/frontend/src/metabase/qb/components/drill/QuickFilterDrill.jsx index b2b092623836a2053fcb319ebffd1c7bad9efda9..299a986e40779f0219059abb53fe8a24522b0447 100644 --- a/frontend/src/metabase/qb/components/drill/QuickFilterDrill.jsx +++ b/frontend/src/metabase/qb/components/drill/QuickFilterDrill.jsx @@ -47,6 +47,7 @@ export default ( } else if (isFK(column.special_type)) { return [ { + name: "view-fks", section: "filter", title: ( <span> @@ -65,6 +66,7 @@ export default ( let operators = getFiltersForColumn(column) || []; return operators.map(({ name, operator }) => ({ + name: operator, section: "filter", title: <span className="h2">{name}</span>, card: () => filter(card, operator, column, value) diff --git a/frontend/src/metabase/qb/components/drill/SortAction.jsx b/frontend/src/metabase/qb/components/drill/SortAction.jsx index 19c6277be18717ced1001725311824eada77aa43..9af247862e702c945949bfd503907f195fcda570 100644 --- a/frontend/src/metabase/qb/components/drill/SortAction.jsx +++ b/frontend/src/metabase/qb/components/drill/SortAction.jsx @@ -38,8 +38,9 @@ export default ( sortDirection === "desc" ) { actions.push({ - title: "Ascending", + name: "sort-ascending", section: "sort", + title: "Ascending", card: () => assocIn( card, @@ -54,8 +55,9 @@ export default ( sortDirection === "asc" ) { actions.push({ - title: "Descending", + name: "sort-descending", section: "sort", + title: "Descending", card: () => assocIn( card, diff --git a/frontend/src/metabase/qb/components/drill/SummarizeColumnByTimeDrill.js b/frontend/src/metabase/qb/components/drill/SummarizeColumnByTimeDrill.js index 915024ca04be43706ac0d82f764a2bdd0d026d14..2cd4003007a2a03470ad081440ec4bd08cba6933 100644 --- a/frontend/src/metabase/qb/components/drill/SummarizeColumnByTimeDrill.js +++ b/frontend/src/metabase/qb/components/drill/SummarizeColumnByTimeDrill.js @@ -36,8 +36,9 @@ export default ( const { column } = clicked; return ["sum", "count"].map(aggregation => ({ - title: <span>{capitalize(aggregation)} by time</span>, + name: "summarize-by-time", section: "sum", + title: <span>{capitalize(aggregation)} by time</span>, card: () => pivot( summarize( diff --git a/frontend/src/metabase/qb/components/drill/SummarizeColumnDrill.js b/frontend/src/metabase/qb/components/drill/SummarizeColumnDrill.js index 0a6d31773097d6a0f58802eb3cd3f18ee989a707..352b6bb47448d6de96533e6beb1744c9e8d6a2c7 100644 --- a/frontend/src/metabase/qb/components/drill/SummarizeColumnDrill.js +++ b/frontend/src/metabase/qb/components/drill/SummarizeColumnDrill.js @@ -15,19 +15,19 @@ const AGGREGATIONS = { title: "Sum" }, avg: { - section: "distribution", + section: "averages", title: "Avg" }, min: { - section: "distribution", + section: "averages", title: "Min" }, max: { - section: "distribution", + section: "averages", title: "Max" }, distinct: { - section: "distribution", + section: "averages", title: "Distincts" } }; @@ -50,7 +50,11 @@ export default ( const { column } = clicked; // $FlowFixMe - return Object.entries(AGGREGATIONS).map(([aggregation, action]) => ({ + return Object.entries(AGGREGATIONS).map(([aggregation, action]: [string, { + section: string, + title: string + }]) => ({ + name: action.title.toLowerCase(), ...action, card: () => summarize( diff --git a/frontend/src/metabase/qb/components/drill/TimeseriesPivotDrill.jsx b/frontend/src/metabase/qb/components/drill/TimeseriesPivotDrill.jsx index bee8949f2d05fb354e83e60e0dacc08f301564e6..9f614a9dcf7131d62cf738afcaee6a3c7d8181f3 100644 --- a/frontend/src/metabase/qb/components/drill/TimeseriesPivotDrill.jsx +++ b/frontend/src/metabase/qb/components/drill/TimeseriesPivotDrill.jsx @@ -18,6 +18,7 @@ export default ( return [ { + name: "timeseries-zoom", section: "zoom", title: "Zoom in", card: () => diff --git a/frontend/src/metabase/qb/components/drill/UnderlyingRecordsDrill.jsx b/frontend/src/metabase/qb/components/drill/UnderlyingRecordsDrill.jsx index ece7068f8b93193b12185f780017a03b00ae75eb..a160a080d05660be9a0a3d9c3922f23003d7d560 100644 --- a/frontend/src/metabase/qb/components/drill/UnderlyingRecordsDrill.jsx +++ b/frontend/src/metabase/qb/components/drill/UnderlyingRecordsDrill.jsx @@ -22,7 +22,8 @@ export default ( return [ { - section: "zoom", + name: "underlying-records", + section: "records", title: "View " + inflect("these", count, "this", "these") + " " + diff --git a/frontend/src/metabase/query_builder/components/ActionsWidget.jsx b/frontend/src/metabase/query_builder/components/ActionsWidget.jsx index b7559ebb500d603982cf260df4e7508b428d8a17..981c66149e17f7b953854e00fd9d54ff83196ce8 100644 --- a/frontend/src/metabase/query_builder/components/ActionsWidget.jsx +++ b/frontend/src/metabase/query_builder/components/ActionsWidget.jsx @@ -7,6 +7,8 @@ import OnClickOutsideWrapper from "metabase/components/OnClickOutsideWrapper"; import { getModeActions } from "metabase/qb/lib/modes"; +import MetabaseAnalytics from "metabase/lib/analytics"; + import cx from "classnames"; import _ from "underscore"; @@ -60,6 +62,9 @@ export default class ActionsWidget extends Component<*, Props, *> { }; toggle = () => { + if (!this.state.isOpen) { + MetabaseAnalytics.trackEvent("Actions", "Opened Action Menu"); + } this.setState({ isOpen: !this.state.isOpen, selectedActionIndex: null @@ -88,6 +93,7 @@ export default class ActionsWidget extends Component<*, Props, *> { } else if (action && action.card) { const nextCard = action.card(); if (nextCard) { + MetabaseAnalytics.trackEvent("Actions", "Executed Action", `${action.section||""}:${action.name||""}`); this.handleOnChangeCardAndRun(nextCard); } this.close(); @@ -132,7 +138,10 @@ export default class ActionsWidget extends Component<*, Props, *> { /> </div> {isOpen && - <OnClickOutsideWrapper handleDismissal={this.close}> + <OnClickOutsideWrapper handleDismissal={() => { + MetabaseAnalytics.trackEvent("Actions", "Dismissed Action Menu"); + this.close(); + }}> <div className="absolute bg-white rounded bordered shadowed py1" style={{ @@ -163,6 +172,9 @@ export default class ActionsWidget extends Component<*, Props, *> { <PopoverComponent onChangeCardAndRun={(card) => { if (card) { + if (selectedAction) { + MetabaseAnalytics.trackEvent("Actions", "Executed Action", `${selectedAction.section||""}:${selectedAction.name||""}`); + } this.handleOnChangeCardAndRun(card) } }} diff --git a/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx b/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx index 346b08be2b47f5f40374b039263e8016f9f93d43..d0a637dcb108746d81f88279a5188131d8cfd869 100644 --- a/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx +++ b/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx @@ -285,7 +285,7 @@ export default class ReferenceGettingStartedGuide extends Component { collapsedTitle="Do you have any commonly referenced metrics?" collapsedIcon="ruler" linkMessage="Learn how to define a metric" - link="http://www.metabase.com/docs/latest/administration-guide/05-segments-and-metrics#creating-a-metric" + link="http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html#creating-a-metric" expand={() => important_metrics.addField({id: null, caveats: null, points_of_interest: null, important_fields: null})} > <div className="my2"> @@ -338,7 +338,7 @@ export default class ReferenceGettingStartedGuide extends Component { collapsedTitle="Do you have any commonly referenced segments or tables?" collapsedIcon="table2" linkMessage="Learn how to create a segment" - link="http://www.metabase.com/docs/latest/administration-guide/05-segments-and-metrics#creating-a-segment" + link="http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html#creating-a-segment" expand={() => important_segments_and_tables.addField({id: null, type: null, caveats: null, points_of_interest: null})} > <div> diff --git a/frontend/src/metabase/visualizations/components/ChartClickActions.jsx b/frontend/src/metabase/visualizations/components/ChartClickActions.jsx index e740d906516f3f82b3bc59a87bfdf57c25906b24..13182daf6a5ece303c9310479606c5a9592959e6 100644 --- a/frontend/src/metabase/visualizations/components/ChartClickActions.jsx +++ b/frontend/src/metabase/visualizations/components/ChartClickActions.jsx @@ -6,6 +6,8 @@ import cx from 'classnames' import Icon from "metabase/components/Icon"; import Popover from "metabase/components/Popover"; +import MetabaseAnalytics from "metabase/lib/analytics"; + import type { ClickObject, ClickAction } from "metabase/meta/types/Visualization"; import type { Card } from "metabase/meta/types/Card"; @@ -15,6 +17,9 @@ const SECTIONS = { zoom: { icon: "zoom" }, + records: { + icon: "table2" + }, details: { icon: "document" }, @@ -27,14 +32,17 @@ const SECTIONS = { sum: { icon: "sum" }, - distribution: { - icon: "distribution" + averages: { + icon: "curve" }, filter: { icon: "funneloutline" }, dashboard: { icon: "dashboard" + }, + distribution: { + icon: "bar" } } // give them indexes so we can sort the sections by the above ordering (JS objects are ordered) @@ -71,7 +79,9 @@ export default class ChartClickActions extends Component<*, Props, State> { if (action.popover) { this.setState({ popoverAction: action }); } else if (action.card) { - onChangeCardAndRun(action.card()); + const card = action.card(); + MetabaseAnalytics.trackEvent("Actions", "Executed Click Action", `${action.section||""}:${action.name||""}`); + onChangeCardAndRun(card); this.close(); } } @@ -89,8 +99,16 @@ export default class ChartClickActions extends Component<*, Props, State> { const PopoverContent = popoverAction.popover; popover = ( <PopoverContent - onChangeCardAndRun={onChangeCardAndRun} - onClose={this.close} + onChangeCardAndRun={(card) => { + if (popoverAction) { + MetabaseAnalytics.trackEvent("Action", "Executed Click Action", `${popoverAction.section||""}:${popoverAction.name||""}`); + } + onChangeCardAndRun(card); + }} + onClose={() => { + MetabaseAnalytics.trackEvent("Action", "Dismissed Click Action Menu"); + this.close(); + }} /> ); } @@ -105,7 +123,10 @@ export default class ChartClickActions extends Component<*, Props, State> { <Popover target={clicked.element} targetEvent={clicked.event} - onClose={this.close} + onClose={() => { + MetabaseAnalytics.trackEvent("Action", "Dismissed Click Action Menu"); + this.close(); + }} verticalAttachments={["top", "bottom"]} horizontalAttachments={["left", "center", "right"]} sizeToFit diff --git a/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx b/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx index ba39b956cc4d6ba6702336a50eba4b2d9ac2a41b..17a6b75f3aa7260907b4b564b50b75d5ee7dd2f6 100644 --- a/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx +++ b/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx @@ -308,6 +308,7 @@ function transformSingleSeries(s, series, seriesIndex) { cols: rowColumnIndexes.map(i => cols[i]), _rawCols: cols }, + // for when the legend header for the breakout is clicked clicked: { dimensions: [{ value: breakoutValue, @@ -330,6 +331,7 @@ function transformSingleSeries(s, series, seriesIndex) { metricColumnIndexes.length > 1 && getFriendlyName(col) ].filter(n => n).join(": "), _transformed: true, + _seriesIndex: seriesIndex, }, data: { rows: rows.map((row, rowIndex) => { diff --git a/frontend/src/metabase/visualizations/components/Visualization.jsx b/frontend/src/metabase/visualizations/components/Visualization.jsx index 7ed3b1f386d2d7380ab03e6b5e408ebe8ccb95c9..c740ea684b457633a6dfa912e29a1abdc9985c49 100644 --- a/frontend/src/metabase/visualizations/components/Visualization.jsx +++ b/frontend/src/metabase/visualizations/components/Visualization.jsx @@ -11,6 +11,7 @@ import Icon from "metabase/components/Icon.jsx"; import Tooltip from "metabase/components/Tooltip.jsx"; import { duration, formatNumber } from "metabase/lib/formatting"; +import MetabaseAnalytics from "metabase/lib/analytics"; import { getVisualizationTransformed } from "metabase/visualizations"; import { getSettings } from "metabase/visualizations/lib/settings"; @@ -208,38 +209,39 @@ export default class Visualization extends Component<*, Props, State> { } handleVisualizationClick = (clicked: ClickObject) => { + if (clicked) { + MetabaseAnalytics.trackEvent( + "Actions", + "Clicked", + `${clicked.column ? "column" : ""} ${clicked.value ? "value" : ""} ${clicked.dimensions ? "dimensions=" + clicked.dimensions.length : ""}` + ); + } + // needs to be delayed so we don't clear it when switching from one drill through to another setTimeout(() => { - // const { onChangeCardAndRun } = this.props; - // let clickActions = this.getClickActions(clicked); - // if there's a single drill action (without a popover) execute it immediately - // if (clickActions.length === 1 && clickActions[0].default && clickActions[0].card) { - // onChangeCardAndRun(clickActions[0].card()); - // } else { - this.setState({ clicked }); - // } - }, 100) + this.setState({ clicked }); + }, 100); } handleOnChangeCardAndRun = (card: UnsavedCard) => { const { series, clicked } = this.state; - // If the current card is saved or is based on a saved question, - // carry that information to the new card for showing lineage const index = (clicked && clicked.seriesIndex) || 0; - // $FlowFixMe - const hasOriginalCard = series[index] && series[index].card && (series[index].card.id || series[index].card.original_card_id); - if (hasOriginalCard) { - const cardWithOriginalId: UnsavedCard = { - ...card, - // $FlowFixMe - original_card_id: series[index].card.id || series[index].card.original_card_id - }; + const originalCard = series && series[index] && series[index].card; - this.props.onChangeCardAndRun(cardWithOriginalId) - } else { - this.props.onChangeCardAndRun(card) + let cardId = card.id || card.original_card_id; + // if the supplied card doesn't have an id, get it from the original card + if (cardId == null && originalCard) { + // $FlowFixMe + cardId = originalCard.id || originalCard.original_card_id; } + + this.props.onChangeCardAndRun({ + ...card, + id: cardId, + // $FlowFixMe + original_card_id: cardId + }); } onRender = ({ yAxisSplit, warnings = [] } = {}) => { diff --git a/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js b/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js index f5d4977325164a7058f41360ad88f549b473a13e..0752437e6581edd62b2ea3fa3169b2b85c2bc1d9 100644 --- a/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js +++ b/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js @@ -434,11 +434,11 @@ function applyChartTooltips(chart, series, isStacked, isScalarSeries, onHoverCha column: card._breakoutColumn }); } - // series was not transformed - else if (!series._raw) { - // $FlowFixMe - clicked.seriesIndex = seriesIndex; - } + } + + if (card._seriesIndex != null) { + // $FlowFixMe + clicked.seriesIndex = card._seriesIndex; } if (clicked) { diff --git a/frontend/src/metabase/visualizations/visualizations/Scalar.jsx b/frontend/src/metabase/visualizations/visualizations/Scalar.jsx index c3b09b1bd7eeed6e947defbe81471c5f273f5fbf..a973509d374442caa81a4fb5942033d0378b5d20 100644 --- a/frontend/src/metabase/visualizations/visualizations/Scalar.jsx +++ b/frontend/src/metabase/visualizations/visualizations/Scalar.jsx @@ -45,14 +45,15 @@ export default class Scalar extends Component<*, VisualizationProps, *> { static transformSeries(series) { if (series.length > 1) { - return series.map(s => ({ + return series.map((s, seriesIndex) => ({ card: { ...s.card, display: "funnel", visualization_settings: { ...s.card.visualization_settings, "graph.x_axis.labels_enabled": false - } + }, + _seriesIndex: seriesIndex, }, data: { cols: [ diff --git a/frontend/test/e2e/dashboards/dashboards.utils.js b/frontend/test/e2e/dashboards/dashboards.utils.js index ab790367caad83a66024b817cc7dbcc075d39e05..f5e6a1020258bdaf018b8d2473700d367ed5baad 100644 --- a/frontend/test/e2e/dashboards/dashboards.utils.js +++ b/frontend/test/e2e/dashboards/dashboards.utils.js @@ -3,7 +3,6 @@ export const incrementDashboardCount = () => { dashboardCount += 1; } export const getLatestDashboardUrl = () => { - console.log(`/dashboard/${dashboardCount}`) return `/dashboard/${dashboardCount}` } export const getPreviousDashboardUrl = (nFromLatest) => { diff --git a/frontend/test/e2e/parameters/questions.spec.js b/frontend/test/e2e/parameters/questions.spec.js index 9a66f5a2d2cec4ca89157c6cc782e957c6fe13ae..969ff8f1625a2335e33a72f2af74ee3a8184893a 100644 --- a/frontend/test/e2e/parameters/questions.spec.js +++ b/frontend/test/e2e/parameters/questions.spec.js @@ -106,22 +106,27 @@ describeE2E("parameters", () => { // public url await d.get(publicUrl); await d::checkScalar(COUNT_ALL); + await d.sleep(1000); // making sure that the previous api call has finished // manually click parameter await d::setCategoryParameter("Doohickey"); await d::checkScalar(COUNT_DOOHICKEY); + await d.sleep(1000); // set parameter via url await d.get(publicUrl + "?category=Gadget"); await d::checkScalar(COUNT_GADGET); + await d.sleep(1000); // embed await d.get(embedUrl); await d::checkScalar(COUNT_ALL); + await d.sleep(1000); // manually click parameter await d::setCategoryParameter("Doohickey"); await d::checkScalar(COUNT_DOOHICKEY); + await d.sleep(1000); // set parameter via url await d.get(embedUrl + "?category=Gadget"); diff --git a/frontend/test/e2e/query_builder/query_builder.spec.js b/frontend/test/e2e/query_builder/query_builder.spec.js index 616170c39c10eb1777c9a3d996b14fb014c67db6..6209e3686b24b30b13a278f7a2fbe40658121869 100644 --- a/frontend/test/e2e/query_builder/query_builder.spec.js +++ b/frontend/test/e2e/query_builder/query_builder.spec.js @@ -56,14 +56,11 @@ describeE2E("query_builder", () => { } catch (e) { } - // get the card ID from the URL - const cardId = (await d.wd().getCurrentUrl()).match(/\/question\/(\d+)/)[1]; - await d.select("#CreateDashboardModal input[name='name']").wait().sendKeys("Main Dashboard"); await d.select("#CreateDashboardModal .Button.Button--primary").wait().click().waitRemoved(); // wait for the modal to be removed incrementDashboardCount(); - await d.waitUrl(getLatestDashboardUrl() + "?add=" + cardId); + await d.waitUrl(getLatestDashboardUrl()); // save dashboard await d.select(".EditHeader .Button.Button--primary").wait().click(); diff --git a/src/metabase/config.clj b/src/metabase/config.clj index ac7ff3fd221372021a4a8776af0136b0b9ad5fa1..5a3d3b4c660abcd681074ccf004633ca681d5c26 100644 --- a/src/metabase/config.clj +++ b/src/metabase/config.clj @@ -5,32 +5,28 @@ [environ.core :as environ]) (:import clojure.lang.Keyword)) +(def ^Boolean is-windows? + "Are we running on a Windows machine?" + (s/includes? (s/lower-case (System/getProperty "os.name")) "win")) + (def ^:private ^:const app-defaults "Global application defaults" - {;; Database Configuration (general options? dburl?) - :mb-run-mode "prod" - :mb-db-type "h2" - ;:mb-db-dbname "postgres" - ;:mb-db-host "localhost" - ;:mb-db-port "5432" - ;:mb-db-user "metabase" - ;:mb-db-pass "metabase" - :mb-db-file "metabase.db" - :mb-db-automigrate "true" - :mb-db-logging "true" - ;; Embedded Jetty Webserver - ;; check here for all available options: - ;; https://github.com/ring-clojure/ring/blob/master/ring-jetty-adapter/src/ring/adapter/jetty.clj - :mb-jetty-port "3000" - :mb-jetty-join "true" - ;; Other Application Settings + {:mb-run-mode "prod" + ;; DB Settings + :mb-db-type "h2" + :mb-db-file "metabase.db" + :mb-db-automigrate "true" + :mb-db-logging "true" + ;; Jetty Settings. Full list of options is available here: https://github.com/ring-clojure/ring/blob/master/ring-jetty-adapter/src/ring/adapter/jetty.clj + :mb-jetty-port "3000" + :mb-jetty-join "true" + ;; other application settings :mb-password-complexity "normal" - ;:mb-password-length "8" - :mb-version-info-url "http://static.metabase.com/version-info.json" - :max-session-age "20160" ; session length in minutes (14 days) - :mb-colorize-logs "true" - :mb-emoji-in-logs "true" - :mb-qp-cache-backend "db"}) + :mb-version-info-url "http://static.metabase.com/version-info.json" + :max-session-age "20160" ; session length in minutes (14 days) + :mb-colorize-logs (str (not is-windows?)) ; since PowerShell and cmd.exe don't support ANSI color escape codes or emoji, + :mb-emoji-in-logs (str (not is-windows?)) ; disable them by default when running on Windows. Otherwise they're enabled + :mb-qp-cache-backend "db"}) (defn config-str diff --git a/src/metabase/driver/druid.clj b/src/metabase/driver/druid.clj index bae048ff55b4e2b89bf0dbd2ee739f8f094aa59f..26380b68295a4e401ed6319f944b3fac1c0b466c 100644 --- a/src/metabase/driver/druid.clj +++ b/src/metabase/driver/druid.clj @@ -47,7 +47,8 @@ ;;; ### Misc. Driver Fns (defn- can-connect? [details] - (= 200 (:status (http/get (details->url details "/status"))))) + (ssh/with-ssh-tunnel [details-with-tunnel details] + (= 200 (:status (http/get (details->url details-with-tunnel "/status")))))) ;;; ### Query Processing diff --git a/src/metabase/driver/generic_sql.clj b/src/metabase/driver/generic_sql.clj index 5145d3f804b3a5cee3f962347b99a39194e33a7d..4d97cf543544b5688e00f50391b8d984e642c061 100644 --- a/src/metabase/driver/generic_sql.clj +++ b/src/metabase/driver/generic_sql.clj @@ -202,9 +202,9 @@ ^clojure.lang.Keyword [k] (keyword (hx/escape-dots (name k)))) - (defn- can-connect? [driver details] - (let [connection (connection-details->spec driver details)] + (let [details-with-tunnel (ssh/include-ssh-tunnel details) + connection (connection-details->spec driver details-with-tunnel)] (= 1 (first (vals (first (jdbc/query connection ["SELECT 1"]))))))) (defn pattern-based-column->base-type diff --git a/src/metabase/driver/mongo/query_processor.clj b/src/metabase/driver/mongo/query_processor.clj index 6b3c2d1c2975b14b313142b7e0b23e325dbfb2e5..4cc67a2dd9fc0f5b7ed3880e00c6de51799cac6b 100644 --- a/src/metabase/driver/mongo/query_processor.clj +++ b/src/metabase/driver/mongo/query_processor.clj @@ -422,9 +422,11 @@ (encode-fncalls-for-fn \"ObjectId\" \"{\\\"$match\\\":ObjectId(\\\"583327789137b2700a1621fb\\\")}\") ;; -> \"{\\\"$match\\\":[\\\"___ObjectId\\\", \\\"583327789137b2700a1621fb\\\"]}\"" [fn-name query-string] - (s/replace query-string - (re-pattern (format "%s\\(([^)]*)\\)" (name fn-name))) - (format "[\"___%s\", $1]" (name fn-name)))) + (-> query-string + ;; replace any forms WITH NO args like ISODate() with ones like ["___ISODate"] + (s/replace (re-pattern (format "%s\\(\\)" (name fn-name))) (format "[\"___%s\"]" (name fn-name))) + ;; now replace any forms WITH args like ISODate("2016-01-01") with ones like ["___ISODate", "2016-01-01"] + (s/replace (re-pattern (format "%s\\(([^)]*)\\)" (name fn-name))) (format "[\"___%s\", $1]" (name fn-name))))) (defn- encode-fncalls "Replace occurances of `ISODate(...)` and similary function calls (invalid JSON, but legal in Mongo) diff --git a/src/metabase/driver/oracle.clj b/src/metabase/driver/oracle.clj index ad731c1a310459f4ea2e8e9d3be6b46e9b962ed2..8b1bb29f3d5a4780bbcac8cc5acd01f5602f21ee 100644 --- a/src/metabase/driver/oracle.clj +++ b/src/metabase/driver/oracle.clj @@ -59,7 +59,7 @@ (dissoc details :host :port :sid :service-name))) (defn- can-connect? [details] - (let [connection (connection-details->spec details)] + (let [connection (connection-details->spec (ssh/include-ssh-tunnel details))] (= 1M (first (vals (first (jdbc/query connection ["SELECT 1 FROM dual"]))))))) diff --git a/src/metabase/models/query.clj b/src/metabase/models/query.clj index 35eb4b74e5ddc5d6618e0968ecca7763566fce56..9ca6036813eb2d98eba309d4761286054c363b94 100644 --- a/src/metabase/models/query.clj +++ b/src/metabase/models/query.clj @@ -25,17 +25,34 @@ :unsigned :integer)) +(defn- update-rolling-average-execution-time! + "Update the rolling average execution time for query with QUERY-HASH. Returns `true` if a record was updated, + or `false` if no matching records were found." + ^Boolean [^bytes query-hash, ^Integer execution-time-ms] + (db/update-where! Query {:query_hash query-hash} + :average_execution_time (hx/cast (int-casting-type) (hx/round (hx/+ (hx/* 0.9 :average_execution_time) + (* 0.1 execution-time-ms)) + 0)))) + +(defn- record-new-execution-time! + "Record the execution time for a query with QUERY-HASH that's not already present in the DB. + EXECUTION-TIME-MS is used as a starting point." + [^bytes query-hash, ^Integer execution-time-ms] + (db/insert! Query + :query_hash query-hash + :average_execution_time execution-time-ms)) + (defn update-average-execution-time! "Update the recorded average execution time for query with QUERY-HASH." - ^Integer [^bytes query-hash, ^Integer execution-time-ms] + [^bytes query-hash, ^Integer execution-time-ms] {:pre [(instance? (Class/forName "[B") query-hash)]} (or ;; if there's already a matching Query update the rolling average - (db/update-where! Query {:query_hash query-hash} - :average_execution_time (hx/cast (int-casting-type) (hx/round (hx/+ (hx/* 0.9 :average_execution_time) - (* 0.1 execution-time-ms)) - 0))) - ;; otherwise add a new entry, using the value of EXECUTION-TIME-MS as a starting point - (db/insert! Query - :query_hash query-hash - :average_execution_time execution-time-ms))) + (update-rolling-average-execution-time! query-hash execution-time-ms) + ;; otherwise try adding a new entry. If for some reason there was a race condition and a Query entry was added in the meantime + ;; we'll try updating that existing record + (try (record-new-execution-time! query-hash execution-time-ms) + (catch Throwable e + (or (update-rolling-average-execution-time! query-hash execution-time-ms) + ;; rethrow e if updating an existing average execution time failed + (throw e)))))) diff --git a/src/metabase/public_settings.clj b/src/metabase/public_settings.clj index 98e5d3ea17a3b5f5b1ed856f0e25039569956973..e12e00b76333902185c79931085edb385a5f3290 100644 --- a/src/metabase/public_settings.clj +++ b/src/metabase/public_settings.clj @@ -62,7 +62,7 @@ :default false) (defsetting query-caching-max-kb - "The maximum size of the cache per card, in kilobytes:" + "The maximum size of the cache, per saved question, in kilobytes:" ;; (This size is a measurement of the length of *uncompressed* serialized result *rows*. The actual size of ;; the results as stored will vary somewhat, since this measurement doesn't include metadata returned with the ;; results, and doesn't consider whether the results are compressed, as the `:db` backend does.) diff --git a/src/metabase/util/stats.clj b/src/metabase/util/stats.clj index 5d747e6788bb509dbdf24f837853f86a53189535..061f6706a905a8782841d838298a171a0b429f07 100644 --- a/src/metabase/util/stats.clj +++ b/src/metabase/util/stats.clj @@ -26,6 +26,7 @@ [pulse :refer [Pulse]] [pulse-card :refer [PulseCard]] [pulse-channel :refer [PulseChannel]] + [query-cache :refer [QueryCache]] [query-execution :refer [QueryExecution]] [segment :refer [Segment]] [table :refer [Table]] @@ -62,7 +63,7 @@ 2 "2" "3+")) -#_(defn- bin-small-number +(defn- bin-small-number "Return small bin number. Assumes positive inputs." [x] (cond @@ -337,24 +338,37 @@ (update :num_per_user summarize-executions-per-user))) +;;; Cache Metrics + +(defn- cache-metrics + "Metrics based on use of the QueryCache." + [] + (let [{:keys [length count]} (db/select-one [QueryCache [:%avg.%length.results :length] [:%count.* :count]])] + {:average_entry_size (int (or length 0)) + :num_queries_cached (bin-small-number count)})) + + +;;; Combined Stats & Logic for sending them in + (defn anonymous-usage-stats "generate a map of the usage stats for this instance" [] (merge (instance-settings) {:uuid anonymous-id, :timestamp (Date.)} - {:stats {:user (user-metrics) - :question (question-metrics) + {:stats {:cache (cache-metrics) + :collection (collection-metrics) :dashboard (dashboard-metrics) :database (database-metrics) - :table (table-metrics) + :execution (execution-metrics) :field (field-metrics) - :pulse (pulse-metrics) - :segment (segment-metrics) - :metric (metric-metrics) :group (group-metrics) :label (label-metrics) - :collection (collection-metrics) - :execution (execution-metrics)}})) + :metric (metric-metrics) + :pulse (pulse-metrics) + :question (question-metrics) + :segment (segment-metrics) + :table (table-metrics) + :user (user-metrics)}})) (defn- send-stats! diff --git a/test/metabase/driver/druid_test.clj b/test/metabase/driver/druid_test.clj index 46295750ab60f3d5b051066329a82d1a3b483928..4d1dd9db8d4377f75ac29238a0e1044b9efd78ed 100644 --- a/test/metabase/driver/druid_test.clj +++ b/test/metabase/driver/druid_test.clj @@ -1,6 +1,8 @@ (ns metabase.driver.druid-test (:require [cheshire.core :as json] + [expectations :refer [expect]] [metabase + [driver :as driver] [query-processor :as qp] [query-processor-test :refer [rows rows+column-names]] [timeseries-query-processor-test :as timeseries-qp-test] @@ -246,3 +248,21 @@ :query {:source-table (data/id :checkins) :aggregation [:+ ["METRIC" (u/get-id metric)] 1] :breakout [(ql/breakout (ql/field-id (data/id :checkins :venue_price)))]}}))))) + +(expect + #"com.jcraft.jsch.JSchException:" + (try + (let [engine :druid + details {:ssl false, + :password "changeme", + :tunnel-host "localhost", + :tunnel-pass "BOGUS-BOGUS", + :port 5432, + :dbname "test", + :host "http://localhost", + :tunnel-enabled true, + :tunnel-port 22, + :tunnel-user "bogus"}] + (driver/can-connect-with-details? engine details :rethrow-exceptions)) + (catch Exception e + (.getMessage e)))) diff --git a/test/metabase/driver/generic_sql_test.clj b/test/metabase/driver/generic_sql_test.clj index 43beff8b14b5b6190fdc42bdc71ef255e3e120cb..b78b80cc98f1e3c4dafc2cc35e4503bd776d0af7 100644 --- a/test/metabase/driver/generic_sql_test.clj +++ b/test/metabase/driver/generic_sql_test.clj @@ -123,3 +123,23 @@ 0.5) (dataset half-valid-urls (field-percent-urls datasets/*driver* (db/select-one 'Field :id (id :urls :url))))) + +;;; Make sure invalid ssh credentials are detected if a direct connection is possible +(expect + #"com.jcraft.jsch.JSchException:" + (try (let [engine :postgres + details {:ssl false, + :password "changeme", + :tunnel-host "localhost", ;; this test works if sshd is running or not + :tunnel-pass "BOGUS-BOGUS-BOGUS", + :port 5432, + :dbname "test", + :host "localhost", + :tunnel-enabled true, + :tunnel-port 22, + :engine :postgres, + :user "postgres", + :tunnel-user "example"}] + (driver/can-connect-with-details? engine details :rethrow-exceptions)) + (catch Exception e + (.getMessage e)))) diff --git a/test/metabase/driver/mongo/util_test.clj b/test/metabase/driver/mongo/util_test.clj index 52a4a4f516577491b9ab25f95089e90fac6cdfac..e8134f1174dda49eb660aed96f7ce663d4b4d6bc 100644 --- a/test/metabase/driver/mongo/util_test.clj +++ b/test/metabase/driver/mongo/util_test.clj @@ -1,6 +1,6 @@ (ns metabase.driver.mongo.util-test (:require [expectations :refer :all] - metabase.driver.mongo.util + [metabase.driver :as driver] [metabase.test.util :as tu]) (:import com.mongodb.ReadPreference)) @@ -28,3 +28,21 @@ (expect IllegalArgumentException (build-connection-options :additional-options "readPreference=ternary")) + +(expect + #"We couldn't connect to the ssh tunnel host" + (try + (let [engine :mongo + details {:ssl false, + :password "changeme", + :tunnel-host "localhost", + :tunnel-pass "BOGUS-BOGUS", + :port 5432, + :dbname "test", + :host "localhost", + :tunnel-enabled true, + :tunnel-port 22, + :tunnel-user "bogus"}] + (driver/can-connect-with-details? engine details :rethrow-exceptions)) + (catch Exception e + (.getMessage e)))) diff --git a/test/metabase/driver/mongo_test.clj b/test/metabase/driver/mongo_test.clj index 5a741a4d5093de05768b6e7dc83a6b6ed2abccea..5495ead63d547d9679b3a841ef2510f5b2e9c874 100644 --- a/test/metabase/driver/mongo_test.clj +++ b/test/metabase/driver/mongo_test.clj @@ -189,6 +189,11 @@ "[{\"$match\":{\"entityId\":{\"$eq\":[\"___ObjectId\", \"583327789137b2700a1621fb\"]}}}]" (encode-fncalls "[{\"$match\":{\"entityId\":{\"$eq\":ObjectId(\"583327789137b2700a1621fb\")}}}]")) +;; make sure fn calls with no arguments work as well (#4996) +(expect + "[{\"$match\":{\"date\":{\"$eq\":[\"___ISODate\"]}}}]" + (encode-fncalls "[{\"$match\":{\"date\":{\"$eq\":ISODate()}}}]")) + (expect (DateTime. "2012-01-01") (maybe-decode-fncall ["___ISODate" "2012-01-01"])) diff --git a/test/metabase/driver/oracle_test.clj b/test/metabase/driver/oracle_test.clj index e6178bab05a32fc8c36b8e2dcf225aae726615d0..dcba09ad9940c05103d3d79b0dd7ecfc86d75ca4 100644 --- a/test/metabase/driver/oracle_test.clj +++ b/test/metabase/driver/oracle_test.clj @@ -4,7 +4,7 @@ [metabase.driver :as driver] [metabase.driver [generic-sql :as sql] - oracle]) + [oracle :as oracle]]) (:import metabase.driver.oracle.OracleDriver)) ;; make sure we can connect with an SID @@ -44,3 +44,21 @@ :port 1521 :service-name "MyCoolService" :sid "ORCL"})) + + +(expect + com.jcraft.jsch.JSchException + (let [engine :oracle + details {:ssl false, + :password "changeme", + :tunnel-host "localhost", + :tunnel-pass "BOGUS-BOGUS-BOGUS", + :port 12345, + :service-name "test", + :sid "asdf", + :host "localhost", + :tunnel-enabled true, + :tunnel-port 22, + :user "postgres", + :tunnel-user "example"}] + (#'oracle/can-connect? details))) diff --git a/test/metabase/driver/presto_test.clj b/test/metabase/driver/presto_test.clj index 190b56836899f57810ab99da0b235ff44a49be8b..ffeca83d0e4424e0863a85f4b8c840bdaadff06f 100644 --- a/test/metabase/driver/presto_test.clj +++ b/test/metabase/driver/presto_test.clj @@ -142,3 +142,20 @@ :order-by [[:default.categories.id :asc]]} {:page {:page 2 :items 5}})) + +(expect + #"com.jcraft.jsch.JSchException:" + (try + (let [engine :presto + details {:ssl false, + :password "changeme", + :tunnel-host "localhost", + :tunnel-pass "BOGUS-BOGUS", + :catalog "BOGUS" + :host "localhost", + :tunnel-enabled true, + :tunnel-port 22, + :tunnel-user "bogus"}] + (driver/can-connect-with-details? engine details :rethrow-exceptions)) + (catch Exception e + (.getMessage e)))) diff --git a/test/metabase/util/stats_test.clj b/test/metabase/util/stats_test.clj index 8ad29b6f9c4e66b4960d48704991b321ca20299a..a327bfea7c2523e1648519f6723a7a9681d8988e 100644 --- a/test/metabase/util/stats_test.clj +++ b/test/metabase/util/stats_test.clj @@ -6,7 +6,7 @@ [toucan.db :as db])) (tu/resolve-private-vars metabase.util.stats - bin-micro-number bin-medium-number bin-large-number) + bin-micro-number bin-small-number bin-medium-number bin-large-number) (expect "0" (bin-micro-number 0)) @@ -15,15 +15,15 @@ (expect "3+" (bin-micro-number 3)) (expect "3+" (bin-micro-number 100)) -#_(expect "0" (bin-small-number 0)) -#_(expect "1-5" (bin-small-number 1)) -#_(expect "1-5" (bin-small-number 5)) -#_(expect "6-10" (bin-small-number 6)) -#_(expect "6-10" (bin-small-number 10)) -#_(expect "11-25" (bin-small-number 11)) -#_(expect "11-25" (bin-small-number 25)) -#_(expect "25+" (bin-small-number 26)) -#_(expect "25+" (bin-small-number 500)) +(expect "0" (bin-small-number 0)) +(expect "1-5" (bin-small-number 1)) +(expect "1-5" (bin-small-number 5)) +(expect "6-10" (bin-small-number 6)) +(expect "6-10" (bin-small-number 10)) +(expect "11-25" (bin-small-number 11)) +(expect "11-25" (bin-small-number 25)) +(expect "25+" (bin-small-number 26)) +(expect "25+" (bin-small-number 500)) (expect "0" (bin-medium-number 0)) (expect "1-5" (bin-medium-number 1))