diff --git a/bin/i18n/update-translation-template b/bin/i18n/update-translation-template index 15eba26961d183e6544630c77ba3d5933f57f39a..fd0873cc0ab6dd6575ab9c8f8fbe1df25c63912d 100755 --- a/bin/i18n/update-translation-template +++ b/bin/i18n/update-translation-template @@ -15,11 +15,16 @@ fi POT_NAME="locales/metabase.pot" POT_BACKEND_NAME="locales/metabase-backend.pot" +# NOTE: hardcoded in .babelrc POT_FRONTEND_NAME="locales/metabase-frontend.pot" +# NOTE: hardcoded in src/metabase/automagic_dashboards/rules.clj +POT_AUTODASH_NAME="locales/metabase-automatic-dashboards.pot" mkdir -p "locales" -# update frontend pot +####################### +# update frontend pot # +####################### # NOTE: about twice as fast to call babel directly rather than a full webpack build BABEL_ENV=extract ./node_modules/.bin/babel -q -x .js,.jsx -o /dev/null frontend/src @@ -29,7 +34,9 @@ BABEL_ENV=extract ./node_modules/.bin/babel -q -x .js,.jsx -o /dev/null frontend sed -i".bak" -E 's/\$\{ *([0-9]+) *\}/{\1}/g' "$POT_FRONTEND_NAME" rm "$POT_FRONTEND_NAME.bak" -# update backend pot +###################### +# update backend pot # +###################### # xgettext before 0.19 does not understand --add-location=file. Even CentOS # 7 ships with an older gettext. We will therefore generate full location @@ -55,5 +62,14 @@ find src -name "*.clj" | xgettext \ sed -i".bak" 's/charset=CHARSET/charset=UTF-8/' "$POT_BACKEND_NAME" rm "$POT_BACKEND_NAME.bak" -# merge frontend and backend pots -msgcat "$POT_FRONTEND_NAME" "$POT_BACKEND_NAME" > "$POT_NAME" +######################## +# update auto dash pot # +######################## + +lein generate-automagic-dashboards-pot + +################## +# merge all pots # +################## + +msgcat "$POT_FRONTEND_NAME" "$POT_BACKEND_NAME" "$POT_AUTODASH_NAME" > "$POT_NAME" diff --git a/docs/administration-guide/03-metadata-editing.md b/docs/administration-guide/03-metadata-editing.md index bff10187eaf22507b111f8aa8a9360c4694d63a6..21ee64b10fa1d479cc9f47c9e6acfbf6a41dd958 100644 --- a/docs/administration-guide/03-metadata-editing.md +++ b/docs/administration-guide/03-metadata-editing.md @@ -64,11 +64,12 @@ Common detailed types include: * Longitude * Entity Name * Number +* Currency * State * URL * Zip Code -This is also where you set mark special fields in a table: +This is also where you set special fields in a table: * Entity Key — the field in this table that uniquely identifies each row. Could be a product ID, serial number, etc. * Entity Name — different from the entity key, this is the field whose heading represents what each row in the table *is*. For example, in a Users table, the User column might be the entity name. diff --git a/docs/administration-guide/08-configuration-settings.md b/docs/administration-guide/08-configuration-settings.md index 0bad0e6362fbcf1be26fa767e9f53975f3197302..f916ecc4a51d8bba89ad1c7378cc1bdb5507bc88 100644 --- a/docs/administration-guide/08-configuration-settings.md +++ b/docs/administration-guide/08-configuration-settings.md @@ -13,6 +13,9 @@ The **report timezone** sets the default time zone for displaying times. The tim *Setting the default timezone will not change the timezone of any data in your database*. If the underlying times in your database aren't assigned to a timezone, Metabase will use the report timezone as the default timezone. +### Enable X-rays +[X-rays](../users-guide/14-x-rays.md) are a great way to allow your users to quickly explore your data or interesting parts of charts, or to see a comparison of different things. But if you're dealing with data sources where allowing users to run x-rays on them would incur burdonsome performance or monetary costs, you can turn them off here. + ### Anonymous Tracking This option turns determines whether or not you allow anonymous data about your usage of Metabase to be sent back to us to help us improve the product. *Your database’s data is never tracked or sent*. @@ -23,5 +26,5 @@ To manually fix field or table names if they still look wrong, you can go to the --- -## Next: caching query results -Metabase makes it easy to [automatically cache results](14-caching.md) for queries that take a long time to run. +## Next: setting formatting defaults for dates and numbers +Easily customize how numbers, dates, times, and currencies should be displayed in Metabase with [formatting settings](19-formatting-settings.md). diff --git a/docs/administration-guide/17-data-sandboxes.md b/docs/administration-guide/17-data-sandboxes.md index b4d1188c57bf8c2b3a903156be2baa950bc83040..b69e195d0de038a915acff27e091e64816e0821b 100644 --- a/docs/administration-guide/17-data-sandboxes.md +++ b/docs/administration-guide/17-data-sandboxes.md @@ -4,7 +4,6 @@ Say you have users who you want to be able to log into your Metabase instance, but who should only be able to view data that pertains to them. For example, you might have some customers or partners who you want to let view your Orders table, but you only want them to see their orders. Metabase has a feature called sandboxing that lets you do just that. - The way it works is that you pick a table that you want to sandbox for users in a certain group, then customize how exactly you want to filter that table for those users. For this to work in most cases you’ll first need to add attributes to your users so that Metabase will know how to filter things for them specifically. ### Getting user attributes @@ -105,6 +104,9 @@ Now, when I log in as this user and look at the `Orders` table, I only see the c  +#### Current limitations +Currently, a user can only have one sandbox per table. I.e., if a user belongs to two user groups, both of which have been given sandboxed access to the same table, that user will not be able to access data from that table. You will either need to remove that user from one of those groups, or remove the sandboxed access from one of those groups. + --- ## Next: sharing and embedding with public links diff --git a/docs/administration-guide/19-formatting-settings.md b/docs/administration-guide/19-formatting-settings.md new file mode 100644 index 0000000000000000000000000000000000000000..7d7cf187e7ce0033d26dd654199aa275f5cd2f51 --- /dev/null +++ b/docs/administration-guide/19-formatting-settings.md @@ -0,0 +1,51 @@ +## Setting default formatting + +There are lots of Metabase users around the world, each with different preferences for how dates, times, numbers, and currencies should be formatted and displayed. Metabase allows you to customize these formatting options at three different levels: + +1. Global formatting defaults in the Admin Panel +2. Field-level formatting overrides in the Data Model section +3. Question-level overrides in visualization settings + +### Global formatting defaults +Here are the formatting options available to you from the `Formatting` tab of the `Settings` section in the Admin Panel: + +**Dates and Times** +* `Date style:` the way dates should be displayed in tables, axis labels, and tooltips. +* `Date separators:` you can choose between slashes, dashes, and dots here. +* `Abbreviate names of days and months:` whenever a date is displayed with the day of the week and/or the month written out, turning this setting on will display e.g. `January` as `Jan` or `Monday` as `Mon`. +* `Time style:` this lets you choose between a 12-hour or 24-hour clock to display the time by default where applicable. + +**Numbers** +* `Separator style:` some folks use commas to separate thousands places, and others use periods. Here's where you can indicate which camp you belong to. + +**Currency** +* `Unit of currency:` if you do most of your business in a particular currency, you can specify that here. +* `Currency label style:` whether you want to have your currencies labeled with a symbol, a code (like `USD`), or its full name. +* `Where to display the unit of currency:` this pertains specifically to tables, and lets you choose if you want the currency labels to appear only in the column heading, or next to each value in the column. + +### Field-level formatting +You can override the global defaults for a specific field by going to the `Data Model` section of the Admin Panel, selecting the database and table of the field in question, and clicking the gear icon on the far right of the screen next to that field to go to its options page, then clicking on the `Formatting` tab. + +The options you'll see here will depend on the field's type. They're generally the same options as in the global formatting settings, with a few additions: + +**Dates and Times** +* `Show the time:` this lets you choose if this time field should be displayed by default without the time; with hours and minutes; with hours, minutes, and seconds; or additionally with milliseconds. + +**Numbers** +* `Show a mini bar chart:` this only applies to situations where this number is displayed in a table, and if it's on it will show a bar next to each value in this column to show how large or small it is relative to the other values in the column. +* `Style:` lets you choose to display the number as a plain number, a percent, in scientific notation, or as a currency. +* `Separator style:` this gives you various options for how commas and periods are used to separate the number. +* `Minimum number of decimal places:` forces the number to be displayed with exactly this many decimal places. +* `Multiply by a number:` multiplies this number by whatever you type here. +* `Add a prefix/suffix:` lets you put a symbol, word, etc. before or after this number. + +**Currency** +Currency field formatting settings include all the same options as in the global formatting section, as well as all the options that Number fields have. + +### Question-level formatting +Lastly, you can override all formatting settings in any specific saved question or dashboard card by clicking on the gear to open up the visualization options. To reset any overridden setting to the default, just click on the rotating arrow icon next to the setting's label. This will reset the setting to the field-level setting if there is one; otherwise it will be reset to the global default. + +--- + +## Next: caching query results +Metabase makes it easy to [automatically cache results](14-caching.md) for queries that take a long time to run. diff --git a/docs/administration-guide/start.md b/docs/administration-guide/start.md index 72db64ccd7672ecf2456ec14f35a5f5824c780aa..717834b68b1d1642d3ba7da39e0fe72a75b64dfb 100644 --- a/docs/administration-guide/start.md +++ b/docs/administration-guide/start.md @@ -7,6 +7,7 @@ Are you in charge of managing Metabase for your organization? Then you're in the * [Enabling features that send email (SMTP)](02-setting-up-email.md) * [Setting up Slack integration](09-setting-up-slack.md) * [Configuring settings](08-configuration-settings.md) +* [Setting formatting defaults for dates and numbers](19-formatting-settings.md) * [Caching query results](14-caching.md) * [Customizing how Metabase looks with white labeling<sup>*</sup>](15-whitelabeling.md) diff --git a/docs/users-guide/05-visualizing-results.md b/docs/users-guide/05-visualizing-results.md index ffaac59c495404d59f8967bca093e08ce5cd5f2a..52c8d35015c54b6b1d103eae6dc4d9c155f6caa5 100644 --- a/docs/users-guide/05-visualizing-results.md +++ b/docs/users-guide/05-visualizing-results.md @@ -5,10 +5,13 @@ While tables are useful for looking up information or finding specific numbers, In Metabase, an answer to a question can be visualized in a number of ways: * Number +* Smart number * Progress bar +* Gauge * Table * Line chart * Bar chart +* Line + bar chart * Row chart * Area chart * Scatterplot or bubble chart @@ -33,38 +36,93 @@ This option is for displaying a single number, nice and big. The options for num  +#### Smart numbers +The Smart Number visualization is great for displaying how a single number has changed over time. To use this visualization, you'll need to have a single number grouped by a Time field, like the Count of Orders by Created At. The Smart Number will show you the value of the number during the most recent period, and below that you'll see how much the number has increased or decreased compared to its value in the period before that. The period is determined by your group-by field: if you're grouping by Day, the Smart Number will show you the most recent day compared to the day before that. + + + +By default, Smart Numbers will display increases as green (i.e. "good") and decreases as red ("bad"). If your number is something where an increase is bad and a decrease is good (such as Bounce Rate, or Costs), you can reverse this behavior in the visualization settings: + + + #### Progress bars -Progress bars are for comparing a single number result to a goal value that you input. Open up the chart options for your progress bar to choose a goal for it, and Metabase will show you how far away your question's current result is from the goal. +Progress bars are for comparing a single number to a goal value that you set. Open up the chart options for your progress bar to choose a value for your goal, and Metabase will show you how far away your question's current result is from the goal.  +#### Gauges +Ah, gauges: you either love 'em or you hate 'em. …Or you feel "meh" about them, I guess. Whatever the case, gauges allow you to show a single number and where its value falls within a set of colored ranges that you can specify. By default, when you choose the Gauge visualization, Metabase will create red, yellow, and green ranges for you. + + + +Open up the visualization settings to define your own ranges, choose colors for them, and optionally add labels to some or all of your ranges: + + + #### Tables -The Table option is good for looking at tabular data (duh), or for lists of things like users or orders. The visualization options for tables allow you to add, hide, or rearrange fields in the table you're looking at. +The Table option is good for looking at tabular data (duh), or for lists of things like users or orders. The visualization options for tables allow you to add, hide, or rearrange fields in the table you're looking at, as well as modify their formatting. -##### Adding or hiding fields +##### Rearranging, adding, and removing columns  -Open up the visualization options and you'll see the Data tab, which displays all the fields currently being shown in the table, as well as more fields from linked tables that you can add to the current table view. +Open up the visualization options for a table and you'll see the Columns tab, which displays all the columns currently being shown in the table. Below that you'll see a list of more columns from linked tables that you can add to the current table view. -To hide a field, click the X icon on it; that'll send it down to the "More fields" area in case you want to bring it back. To add a linked field, just click the + icon on it, which will bring it to the "Visible fields" section. Click and drag any of the fields listed there to rearrange the order in which they appear. +To hide a column, click the X icon on it; that'll send it down to the "More columns" area in case you want to bring it back. To add a linked column, just click the + icon on it, which will bring it to the "Visible columns" section. Click and drag any of the columns listed there to rearrange the order in which they appear. Another super easy way to rearrange columns without having to open up the visualization settings is to simply click and drag on a column's heading to move it where you'd like it to go. **Note:** changing these options doesn't change the actual table itself; it just creates a custom view of it that you can save as a "question" in Metabase and refer back to later, share with others, or add to a dashboard. -##### Conditional formatting + +##### Column formatting options + +To format the display of any column in a table, click on the column heading and choose the `Formatting` option (you can also get there by clicking on the gear on any column when in the `Columns` tab of the visualization settings). + + + +The options you see will be different depending on the type of column you're viewing: + +**Dates** +* `Date style` gives you a bunch of different choices for how to display the date. +* `Abbreviate names of days and months`, when turned on, will turn things like `January` to `Jan`, and `Monday` to `Mon`. +* `Show the time` lets you decide whether or not to display the time, and if so, how. You can include hours and minutes, and additionally seconds and milliseconds. + +**Numbers** +* `Show a mini bar chart` will display a small horizontal bar next to each number in this column to show its size relative to the other values in the column. +* `Style` lets you choose to display the number as a plain number, a percent, in scientific notation, or as a currency. +* `Separator style` gives you various options for how commas and periods are used to separate the number. +* `Minimum number of decimal places` forces the number to be displayed with exactly this many decimal places. +* `Multiply by a number` multiplies each number in this column by whatever you type here. Just don't type an emoji here; it almost always causes a temporal vortex to manifest. +* `Add a prefix/suffix` lets you put a symbol, word, or whatever before or after each cell's value. + +**Currency** +Currency columns have all the same options as numbers, plus the following: +* `Unit of Currency` lets you change the unit of currency from whatever the system default is. +* `Currency label style` allows you to switch between displaying the currency label as a symbol, a code like (USD), or the full name of the currency. +* `Where to display the unit of currency` lets you toggle between showing the currency label in the column heading or in every cell in the column. + +##### Formatting data in charts +While we're talking about formatting, we thought you should also know that you can access formatting options for the columns used in a chart. Just open the visualization options, select the `Data` tab: + + + +Then click on the gear icon next to the column that you want to format. Dates, numbers, and currencies tend to have the most useful formatting options. + + + +##### Conditional table formatting Sometimes is helpful to highlight certain rows or columns in your tables when they meet a specific condition. You can set up conditional formatting rules by going to the visualization settings while looking at any table, then clicking on the `Formatting` tab.  -When you add a new rule, you'll first need to pick which column(s) should be affected. For now, you can only pick numeric columns. Your columns can be formatted in one of two ways: +When you add a new rule, you'll first need to pick which column(s) should be affected. Your columns can be formatted in one of two ways: -1. **Single color:** pick this if you want to highlight cells in the column if they're greater, less than, or equal to a specific number. You can optionally highlight the whole row of a cell that matches the condition you pick so that it's easier to spot as you scroll down your table. -2. **Color range:** choose this option if you want to tint all the cells in the column from smallest to largest or vice a versa. +1. **Single color:** pick this if you want to highlight cells in the column if they're greater, less than, or equal to a specific number, or if they match or contain a certain word or phrase. You can optionally highlight the whole row of a cell that matches the condition you pick so that it's easier to spot as you scroll down your table. +2. **Color range:** choose this option if you want to tint all the cells in the column from smallest to largest or vice a versa. This option is only available for numeric columns. You can set as many rules on a table as you want. If two or more rules disagree with each other, the rule that's on the top of your list of rules will win. You can click and drag your rules to reorder them, and click on a rule to edit it. ##### Pivoted tables -If your table is a result that contains one metric and two dimensions, Metabase will also automatically "pivot" your table, like in the example below (the example shows the count of orders grouped by the review rating for that order and the category of the product that was purchased; you can tell it's pivoted because the grouping field values are all in the first column and first row). You can turn this behavior off in the chart settings. +If your table is a result that contains one numeric column and two grouping columns, Metabase will also automatically "pivot" your table, like in the example below. What this does is it takes one of your columns and rotates it 90 degrees ("pivots" it) so that each of its values becomes a column heading. If you open up the visualization settings by clicking the gear icon, you can choose which column to pivot in case Metabase got it wrong; or you can also turn the pivoting behavior off entirely.  @@ -77,8 +135,30 @@ Area charts are useful when comparing the proportions of two metrics over time.  +**Trend lines** +Another useful option for line, area, bar, and scatter charts is trend lines. If you have a question where you're grouping by a time field, open up the visualization options by clicking the gear icon, and turn the `Show trend line` toggle on to display a trend line. Metabase will choose the best type of line to fit to the trend of your series. This will even work if you have multiple numbers selected in the View section of your question. It won't work, however, if you have any groupings beyond the one time field. + + + +#### Line + Bar charts +Also called Combo Charts, the Line + Bar chart lets you combine bars and lines (or areas) on the same chart. + + + +Metabase will pick one of your series to display as a line, and another to display as a bar by default. Open up the visualization settings to change which series are lines, bars, or areas, and to change other per-series settings like colors. Click the down arrow icon on the right of a series to see additional options: + + + +To use a Line + Bar chart, you'll either need to have two or more numbers selected in the View section of your question, with one or two grouping columns, like this… + + + +…or you'll need a question with a single item in the View section, with two grouping columns, like this: + + + #### Row charts -If you're trying to group a number by a field that has a lot of possible values, like a Vendor or Product Title field, try visualizing it as a row chart. Metabase will show you the bars in descending order of size, with a final bar at the bottom for items that didn't fit. +If you're trying to group a number by a column that has a lot of possible values, like a Vendor or Product Title field, try visualizing it as a row chart. Metabase will show you the bars in descending order of size, with a final bar at the bottom for items that didn't fit.  @@ -88,7 +168,7 @@ If you have a bar chart like Count of Users by Age, where the x-axis is a number  -By default, Metabase will automatically choose a good way to bin your results. But you can change how many bins your result has, or turn the binning off entirely, by clicking on the number field you're grouping by in the Question Builder, then clicking on the area to the right of the field name: +By default, Metabase will automatically choose a good way to bin your results. But you can change how many bins your result has, or turn the binning off entirely, by clicking on the number field you're grouping by, then clicking on the area to the right of the field name:  @@ -96,24 +176,24 @@ By default, Metabase will automatically choose a good way to bin your results. B These three charting types have very similar options, which are broken up into the following: -* **Data** — choose the fields you want to plot on your x and y axes. This is mostly useful if your table or result set contains more than two columns, like if you're trying to graph fields from an unaggregated table. You can also add additional metric fields by clicking the `Add another series` link below the y-axis dropdown, or break your current metric out by an additional dimension by clicking the `Add a series breakout` link below the x-axis dropdown (note that you can't add an additional series breakout if you have more than one metric/series). -* **Display** — here's where you can make some cosmetic changes, like setting colors, and stacking bar or area charts. With line and area charts, you can also change the line style (line, curve, or step). We've also recently added the ability to create a goal line for your chart, and to configure how your chart deals with x-axis points that have missing y-axis values. +* **Data** — choose the fields you want to plot on your x and y axes. This is mostly useful if your table or result set contains more than two columns, like if you're trying to graph fields from an unaggregated table. You can also add additional metrics to your chart by clicking the `Add another series` link below the y-axis dropdown, or break your current metric out by an additional dimension by clicking the `Add a series breakout` link below the x-axis dropdown (note that you can't add an additional series breakout if you have more than one metric/series). +* **Display** — here's where you can make some cosmetic changes, like setting colors, and stacking bar or area charts. With line and area charts, you can also change the line style (line, curve, or step). You can also set a goal line for your chart, display a trend line, or configure how your chart deals with x-axis points that have missing y-axis values. * **Axes** — this is where you can hide axis markers or change their ranges, and turn split axes on or off. You can also configure the way your axes are scaled, if you're into that kind of thing. * **Labels** — if you want to hide axis labels or customize them, here's where to go. #### Scatterplots and bubble charts Scatterplots are useful for visualizing the correlation between two variables, like comparing the age of your users vs. how many dollars they've spent on your products. To use a scatterplot, you'll need to ask a question that results in two numeric columns, like `Count of Orders grouped by Customer Age`. Alternatively, you can use a raw data table and select the two numeric fields you want to use in the chart options. -If you have a third numeric field, you can also create a bubble chart. Select the Scatter visualization, then open up the chart options and select a field in the bubble size dropdown. This field will be used to determine the size of each bubble on your chart. For example, you could use a field that contains the number or count of items for the given x-y pair — i.e., larger bubbles for larger total dollar amounts spent on orders. +If you have a third numeric field, you can also create a bubble chart. Select the Scatter visualization, then open up the chart options and select a field in the `bubble size` dropdown. This field will be used to determine the size of each bubble on your chart. For example, you could use a field that contains the total dollar amount for each x-y pair — i.e., larger bubbles for larger total dollar amounts spent on orders. -Scatterplots and bubble charts also have similar chart options as line, bar, and area charts. +Scatterplots and bubble charts also have similar chart options as line, bar, and area charts, including the option to display trend or goal lines.  #### Pie or donut charts -A pie or donut chart can be used when breaking out a metric by a single dimension, especially when the number of possible breakouts is small, like users by gender. If you have more than a few breakouts, like users by country, it's usually better to use a bar chart so that your users can more easily compare the relative sizes of each bar. +A pie or donut chart can be used when breaking out a metric by a single dimension, especially when the number of possible breakouts is small, like users by gender. If you have more than a few breakouts, like users per country, it's usually better to use a bar chart so that your users can more easily compare the relative sizes of each bar. -The options for pie charts let you choose which field to use as your measurement, and which one to use for the dimension (i.e., the pie slices). You can also customize the pie chart's legend, whether or not to show each slice's percent of the whole in the legend, and the minimum size a slice needs to be in order for it to be displayed. +The options for pie charts let you choose which field to use as your measurement, and which one to use for the dimension (i.e., the pie slices). You can also customize the color of each piece slice, the pie chart's legend, whether or not to show each slice's percent of the whole in the legend, and the minimum size a slice needs to be in order for it to be displayed.  @@ -127,7 +207,7 @@ For example, I might have an Opportunities table, and I could create a question #### Maps When you select the Map visualization setting, Metabase will automatically try and pick the best kind of map to use based on the table or result set you're currently looking at. Here are the maps that Metabase uses: -* **United States Map** — Creating a map of the United States from your data requires your results to contain a column field with states. This lets you do things like visualize the count of your users broken out by state, with darker states representing more users. +* **United States Map** — Creating a map of the United States from your data requires your results to contain a column that contains states. This lets you do things like visualize the count of your users broken out by state, with darker states representing more users. * **Country Map** — To visualize your results in the format of a map of the world broken out by country, your result must contain a field with countries. (E.g., count of users by country.)  @@ -138,7 +218,7 @@ When you select the Map visualization setting, Metabase will automatically try a When you open up the Map options, you can manually switch between a region map (i.e., United States or world) and a pin map. If you're using a region map, you can also choose which field to use as the measurement, and which to use as the region (i.e. State or Country). -Metabase now also allows administrators to add custom region maps via GeoJSON files through the Metabase Admin Panel. +Metabase also allows administrators to add custom region maps via GeoJSON files through the Metabase Admin Panel. --- diff --git a/docs/users-guide/08-dashboard-filters.md b/docs/users-guide/08-dashboard-filters.md index 89f302bb448c91e6a0e06bb92bd3e11f209cd3b1..11b908748ae9b9822156e7cb601663610e008167 100644 --- a/docs/users-guide/08-dashboard-filters.md +++ b/docs/users-guide/08-dashboard-filters.md @@ -41,7 +41,7 @@ You can add multiple filters to your dashboard following the same steps. We just ### Editing a filter -To edit a filter, enter dashboard editing mode, then click the `Edit` button on the filter you want to change. You an also click `Remove` to get rid of a filter. If you do this by accident, just click `Cancel` in the top-right to exit dashboard editing mode without saving your changes. +To edit a filter, enter dashboard editing mode, then click the `Edit` button on the filter you want to change. You an also click `Remove` to get rid of a filter. If you do this by accident, just click `Cancel` in the top-right to exit dashboard editing mode without saving your changes. To reorder your filters, just click on the grabber handle on the left side of a filter and drag it where you'd like it to go.  diff --git a/docs/users-guide/13-sql-parameters.md b/docs/users-guide/13-sql-parameters.md index 2794f2512d8f7490679672c6dde6b4ba9f5e4ea0..7441cda33247347db19fb113d52ad5cd30c107a7 100644 --- a/docs/users-guide/13-sql-parameters.md +++ b/docs/users-guide/13-sql-parameters.md @@ -10,7 +10,7 @@ Options and settings for your variables will appear in the `Variables` side pane  ### Defining Variables -Typing `{% raw %}{{variable_name}}{% endraw %}` in your native query creates a variable called `variable_name`. Variables can be given types in the side panel, which changes their behavior. All variable types other than `field filter` will cause a filter widget to be placed on this question corresponding to the chosen variable type. When a value is selected via a filter widget, that value replaces the corresponding variable in the SQL template, wherever it appears. +Typing `{% raw %}{{variable_name}}{% endraw %}` in your native query creates a variable called `variable_name`. Variables can be given types in the side panel, which changes their behavior. All variable types other than `field filter` will cause a filter widget to be placed on this question corresponding to the chosen variable type. When a value is selected via a filter widget, that value replaces the corresponding variable in the SQL template, wherever it appears. If you have multiple filter widgets, you can click and drag on any of them to move and reorder them. This example defines a variable called `cat`, allowing you to dynamically change the `WHERE` clause in this query: @@ -64,6 +64,16 @@ Filter widgets **can't** be displayed if the variable is mapped to a field marke ##### Setting a default value If you input a default value for your field filter, this value will be selected in the filter whenever you come back to this question. If you clear out the filter, though, no value will be passed (i.e., not even the default value). The default value has no effect on the behavior of your SQL question when viewed in a dashboard. +###### Default value in the query +You can also define default value directly in your query, useful for complex default value. + +Current date example: +``` +SELECT p.* +FROM products p +WHERE p.createdAt = [[ {{dateOfCreation}} #]]CURRENT_DATE() +``` + ##### Connecting a SQL question to a dashboard filter In order for a saved SQL question to be usable with a dashboard filter, it must contain at least one field filter. The kind of dashboard filter that can be used with the SQL question depends on the field that you map to the question's field filter(s). For example, if you have a field filter called `{% raw %}{{var}}{% endraw %}` and you map it to a State field, you can map a Location dashboard filter to your SQL question. In this example, you'd create a new dashboard or go to an existing one, click the Edit button, and the SQL question that contains your State field filter, add a new dashboard filter or edit an existing Location filter, then click the dropdown on the SQL question card to see the State field filter. [Learn more about dashboard filters here](08-dashboard-filters.md). diff --git a/docs/users-guide/14-x-rays.md b/docs/users-guide/14-x-rays.md index 153994c1790acbfc3fafb2de6ac038a6c214e6a0..598a34227b17ede9693a5f3a4db3e703d1e6a0b1 100644 --- a/docs/users-guide/14-x-rays.md +++ b/docs/users-guide/14-x-rays.md @@ -63,6 +63,10 @@ You can see more suggested x-rays over on the right-hand side of the screen. Bro If you come across an x-ray that's particularly interesting, you can save it as a dashboard by clicking the green Save button. Metabase will create a new dashboard and put it and all of its charts in a new collection, and will save this new collection wherever you choose. +### Disabling x-rays + +If for some reason x-rays aren't a good fit for your team or your data, administrators can turn them off completely in the general settings area of the Admin Panel. + ### Where did the old x-rays go? We're reworking the way we do things like time series growth analysis, which was present in past versions of x-rays. In the meantime, we've removed those previous x-rays, and will bring those features back in a more elegant and streamlined way in a future version of Metabase. diff --git a/docs/users-guide/images/dashboard-filters/06-edit-and-remove.png b/docs/users-guide/images/dashboard-filters/06-edit-and-remove.png index 0e3d9188e8ca841ebee8080dbddd600ec237f7a3..69de25e8f334dee415f82156a9ac430ac3ce4846 100644 Binary files a/docs/users-guide/images/dashboard-filters/06-edit-and-remove.png and b/docs/users-guide/images/dashboard-filters/06-edit-and-remove.png differ diff --git a/docs/users-guide/images/visualizations/chart-formatting-options.png b/docs/users-guide/images/visualizations/chart-formatting-options.png new file mode 100644 index 0000000000000000000000000000000000000000..0c4f8c6f8d7bbc823bb88d2086678ffdf3cb070c Binary files /dev/null and b/docs/users-guide/images/visualizations/chart-formatting-options.png differ diff --git a/docs/users-guide/images/visualizations/chart-formatting.png b/docs/users-guide/images/visualizations/chart-formatting.png new file mode 100644 index 0000000000000000000000000000000000000000..23e465f5b0c30207a627c013baef4b83678bc23a Binary files /dev/null and b/docs/users-guide/images/visualizations/chart-formatting.png differ diff --git a/docs/users-guide/images/visualizations/column-header-formatting.png b/docs/users-guide/images/visualizations/column-header-formatting.png new file mode 100644 index 0000000000000000000000000000000000000000..be896806e4e49a6c228ab6547c377160ade5e9f7 Binary files /dev/null and b/docs/users-guide/images/visualizations/column-header-formatting.png differ diff --git a/docs/users-guide/images/visualizations/combo-chart-data-1.png b/docs/users-guide/images/visualizations/combo-chart-data-1.png new file mode 100644 index 0000000000000000000000000000000000000000..008d259edb3463894ccc0cc206e0ae7bec454e38 Binary files /dev/null and b/docs/users-guide/images/visualizations/combo-chart-data-1.png differ diff --git a/docs/users-guide/images/visualizations/combo-chart-data-2.png b/docs/users-guide/images/visualizations/combo-chart-data-2.png new file mode 100644 index 0000000000000000000000000000000000000000..1f926ee4378b9665bc0cc044d8bc5fc2eb91cf60 Binary files /dev/null and b/docs/users-guide/images/visualizations/combo-chart-data-2.png differ diff --git a/docs/users-guide/images/visualizations/combo-chart-settings.png b/docs/users-guide/images/visualizations/combo-chart-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..29e76994c1610bc12d4a37f9f5ccb579112143ff Binary files /dev/null and b/docs/users-guide/images/visualizations/combo-chart-settings.png differ diff --git a/docs/users-guide/images/visualizations/combo-chart.png b/docs/users-guide/images/visualizations/combo-chart.png new file mode 100644 index 0000000000000000000000000000000000000000..4fa379cef1081b614cfbc6e854e283103a66fd98 Binary files /dev/null and b/docs/users-guide/images/visualizations/combo-chart.png differ diff --git a/docs/users-guide/images/visualizations/gauge-settings.png b/docs/users-guide/images/visualizations/gauge-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..fee18c7d33071c3bd3ae4aa9f6e981c13dc76d71 Binary files /dev/null and b/docs/users-guide/images/visualizations/gauge-settings.png differ diff --git a/docs/users-guide/images/visualizations/gauge.png b/docs/users-guide/images/visualizations/gauge.png new file mode 100644 index 0000000000000000000000000000000000000000..85f7b07deadaebad767b378d521be687886e983c Binary files /dev/null and b/docs/users-guide/images/visualizations/gauge.png differ diff --git a/docs/users-guide/images/visualizations/smart-number.png b/docs/users-guide/images/visualizations/smart-number.png new file mode 100644 index 0000000000000000000000000000000000000000..34932e818e70b44f43fa53a34faa6d08b37ffbf9 Binary files /dev/null and b/docs/users-guide/images/visualizations/smart-number.png differ diff --git a/docs/users-guide/images/visualizations/smart-scalar-settings.png b/docs/users-guide/images/visualizations/smart-scalar-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..0b33f973d049503fd540860c809c6334f21d8ca2 Binary files /dev/null and b/docs/users-guide/images/visualizations/smart-scalar-settings.png differ diff --git a/docs/users-guide/images/visualizations/trend-lines.png b/docs/users-guide/images/visualizations/trend-lines.png new file mode 100644 index 0000000000000000000000000000000000000000..15bcf87248f4124d6033d65caa9cb7ecb53a0cc0 Binary files /dev/null and b/docs/users-guide/images/visualizations/trend-lines.png differ diff --git a/frontend/src/metabase/css/core/base.css b/frontend/src/metabase/css/core/base.css index e6dc72edd37d5d55f386631376d5f905faee68b6..5174d4149c1f56d89faefe2a161f1c480e41e2fd 100644 --- a/frontend/src/metabase/css/core/base.css +++ b/frontend/src/metabase/css/core/base.css @@ -46,6 +46,7 @@ button { padding: 0; margin: 0; outline: none; + background-color: transparent; } a { diff --git a/frontend/src/metabase/lib/query.js b/frontend/src/metabase/lib/query.js index 01fb2809033acc76316d8aa4d70328cdee41e596..d31158877221e59a935aaed4ad8dd538e5a2f280 100644 --- a/frontend/src/metabase/lib/query.js +++ b/frontend/src/metabase/lib/query.js @@ -350,6 +350,7 @@ const Query = { return this.cleanQuery(removeExpressionReferences(query)); }, + // DEPRECATED isRegularField(field) { return typeof field === "number"; }, @@ -394,8 +395,8 @@ const Query = { Query.isRegularField(field) || Query.isLocalField(field) || (Query.isForeignKeyField(field) && - Query.isRegularField(field[1]) && - Query.isRegularField(field[2])) || + (Query.isLocalField(field[1]) || Query.isRegularField(field[1])) && + (Query.isLocalField(field[2]) || Query.isRegularField(field[2]))) || // datetime field can be either 4-item (deprecated): ["datetime-field", <field>, "as", <unit>] // or 3 item (preferred style): ["datetime-field", <field>, <unit>] (Query.isDatetimeField(field) && @@ -442,8 +443,9 @@ const Query = { } else if (Query.isLocalField(field)) { return Query.getFieldTarget(field[1], tableDef, path); } else if (Query.isForeignKeyField(field)) { - let fkFieldDef = Table.getField(tableDef, field[1]); - let targetTableDef = fkFieldDef && fkFieldDef.target.table; + const fkFieldId = Query.getFieldTargetId(field[1]); + const fkFieldDef = Table.getField(tableDef, fkFieldId); + const targetTableDef = fkFieldDef && fkFieldDef.target.table; return Query.getFieldTarget( field[2], targetTableDef, diff --git a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx index 0020002e53aa148da969aa6c1d55bbcf4d6a97b5..2a20f7169e98003ce0ce62b6d7f4bcec22a61526 100644 --- a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx +++ b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx @@ -250,7 +250,7 @@ export default class QueryVisualization extends Component { <VisualizationResult lastRunDatasetQuery={this.state.lastRunDatasetQuery} onUpdateWarnings={warnings => this.setState({ warnings })} - onOpenChartSettings={() => this.refs.settings.open()} + onOpenChartSettings={initial => this.refs.settings.open(initial)} {...this.props} className="spread" /> diff --git a/frontend/src/metabase/query_builder/components/VisualizationSettings.jsx b/frontend/src/metabase/query_builder/components/VisualizationSettings.jsx index 0b87abba291e1435eb6e55960ba974c5e48fde8f..412f2270643d357baf6dfd19d8604bef94dfa994 100644 --- a/frontend/src/metabase/query_builder/components/VisualizationSettings.jsx +++ b/frontend/src/metabase/query_builder/components/VisualizationSettings.jsx @@ -90,8 +90,8 @@ export default class VisualizationSettings extends React.Component { ); } - open = () => { - this.props.showChartSettings({}); + open = initial => { + this.props.showChartSettings(initial || {}); }; close = () => { @@ -123,7 +123,7 @@ export default class VisualizationSettings extends React.Component { ]} onChange={this.props.onReplaceAllVisualizationSettings} onClose={this.close} - initialWidget={chartSettings && chartSettings.widget} + initial={chartSettings} /> </Modal> </div> diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx index 7dc3ab8f172c64a8a415675a51197825cf23a927..4cee77c3b6bce09ea6e630545d532ae157047fe4 100644 --- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx +++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx @@ -197,15 +197,13 @@ export default class TagEditorParam extends Component { </div> )} - {tag.type !== "dimension" && ( - <div className="flex align-center pb1"> - <h5 className="text-normal mr1">{t`Required?`}</h5> - <Toggle - value={tag.required} - onChange={value => this.setRequired(value)} - /> - </div> - )} + <div className="flex align-center pb1"> + <h5 className="text-normal mr1">{t`Required?`}</h5> + <Toggle + value={tag.required} + onChange={value => this.setRequired(value)} + /> + </div> {((tag.type !== "dimension" && tag.required) || (tag.type === "dimension" || tag["widget-type"])) && ( diff --git a/frontend/src/metabase/visualizations/components/ChartSettings.jsx b/frontend/src/metabase/visualizations/components/ChartSettings.jsx index 6c64ddaa2877641830342cf1849a1c0b28dce05a..821b4d7f321bd67182862a7315c4398cc1a3e6fd 100644 --- a/frontend/src/metabase/visualizations/components/ChartSettings.jsx +++ b/frontend/src/metabase/visualizations/components/ChartSettings.jsx @@ -19,14 +19,15 @@ import { } from "metabase/visualizations"; import { updateSettings } from "metabase/visualizations/lib/settings"; -const DEFAULT_TAB_PRIORITY = ["Display"]; +// section names are localized +const DEFAULT_TAB_PRIORITY = [t`Display`]; class ChartSettings extends Component { constructor(props) { super(props); this.state = { - currentTab: null, - showWidget: props.initialWidget, + currentSection: (props.initial && props.initial.section) || null, + currentWidget: (props.initial && props.initial.widget) || null, ...this._getState( props.series, props.series[0].card.visualization_settings, @@ -56,8 +57,18 @@ class ChartSettings extends Component { }; } - handleSelectTab = tab => { - this.setState({ currentTab: tab, showWidget: null }); + handleShowSection = section => { + this.setState({ currentSection: section, currentWidget: null }); + }; + + // allows a widget to temporarily replace itself with a different widget + handleShowWidget = widget => { + this.setState({ currentWidget: widget }); + }; + + // go back to previously selected section + handleEndShowWidget = () => { + this.setState({ currentWidget: null }); }; handleResetSettings = () => { @@ -79,21 +90,13 @@ class ChartSettings extends Component { this.props.onClose(); }; - // allows a widget to temporarily replace itself with a different widget - handleShowWidget = widget => { - this.setState({ showWidget: widget }); - }; - handleEndShowWidget = () => { - this.setState({ showWidget: null }); - }; - render() { const { isDashboard, question, addField } = this.props; - const { rawSeries, transformedSeries, showWidget } = this.state; + const { rawSeries, transformedSeries, currentWidget } = this.state; const widgetsById = {}; - const tabs = {}; + const sections = {}; for (const widget of getSettingsWidgetsForSeries( transformedSeries, this.handleChangeSettings, @@ -101,38 +104,38 @@ class ChartSettings extends Component { )) { widgetsById[widget.id] = widget; if (widget.widget && !widget.hidden) { - tabs[widget.section] = tabs[widget.section] || []; - tabs[widget.section].push(widget); + sections[widget.section] = sections[widget.section] || []; + sections[widget.section].push(widget); } } // Move settings from the "undefined" section in the first tab - if (tabs["undefined"] && Object.values(tabs).length > 1) { - let extra = tabs["undefined"]; - delete tabs["undefined"]; - Object.values(tabs)[0].unshift(...extra); + if (sections["undefined"] && Object.values(sections).length > 1) { + let extra = sections["undefined"]; + delete sections["undefined"]; + Object.values(sections)[0].unshift(...extra); } - const tabNames = Object.keys(tabs); - const currentTab = - this.state.currentTab || - _.find(DEFAULT_TAB_PRIORITY, name => name in tabs) || - tabNames[0]; + const sectionNames = Object.keys(sections); + const currentSection = + this.state.currentSection || + _.find(DEFAULT_TAB_PRIORITY, name => name in sections) || + sectionNames[0]; let widgets; - let widget = showWidget && widgetsById[showWidget.id]; + let widget = currentWidget && widgetsById[currentWidget.id]; if (widget) { widget = { ...widget, hidden: false, props: { ...(widget.props || {}), - ...(showWidget.props || {}), + ...(currentWidget.props || {}), }, }; widgets = [widget]; } else { - widgets = tabs[currentTab]; + widgets = sections[currentSection]; } const extraWidgetProps = { @@ -145,12 +148,12 @@ class ChartSettings extends Component { return ( <div className="flex flex-column spread"> - {tabNames.length > 1 && ( + {sectionNames.length > 1 && ( <div className="border-bottom flex flex-no-shrink pl4"> <Radio - value={currentTab} - onChange={this.handleSelectTab} - options={tabNames} + value={currentSection} + onChange={this.handleShowSection} + options={sectionNames} optionNameFn={v => v} optionValueFn={v => v} underlined diff --git a/frontend/src/metabase/visualizations/components/LineAreaBarChart.css b/frontend/src/metabase/visualizations/components/LineAreaBarChart.css index 6330938ec0ad32345430e7a312f47868fa8698cd..565ac9af0c6865a92f54682e14b2b268c3936528 100644 --- a/frontend/src/metabase/visualizations/components/LineAreaBarChart.css +++ b/frontend/src/metabase/visualizations/components/LineAreaBarChart.css @@ -31,7 +31,8 @@ } .LineAreaBarChart .dc-chart g.row text.outside { - fill: var(--color-text-light); + fill: var(--color-text-medium); + font-weight: 900; } .LineAreaBarChart .dc-chart g.row text.inside { fill: white; diff --git a/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx b/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx index 4ae705928611c8917465b12e49f1901f935e3deb..dfa529e0707e87cf85e549a28709681beab67b81 100644 --- a/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx +++ b/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx @@ -99,7 +99,7 @@ export default class LineAreaBarChart extends Component { if (dimensions.length < 1 || metrics.length < 1) { throw new ChartSettingsError( t`Which fields do you want to use for the X and Y axes?`, - t`Data`, + { section: t`Data` }, t`Choose fields`, ); } diff --git a/frontend/src/metabase/visualizations/components/Visualization.jsx b/frontend/src/metabase/visualizations/components/Visualization.jsx index 9002d937b0dcfacbe0dae2022ae1fb7097dc8260..0a156a60f301ba240496ced51bf36d25cd5fe039 100644 --- a/frontend/src/metabase/visualizations/components/Visualization.jsx +++ b/frontend/src/metabase/visualizations/components/Visualization.jsx @@ -85,7 +85,7 @@ type Props = { // misc onUpdateWarnings: (string[]) => void, - onOpenChartSettings: () => void, + onOpenChartSettings: ({ section?: ?string, widget?: ?any }) => void, // number of grid cells wide and tall gridSize?: { width: number, height: number }, @@ -367,7 +367,7 @@ export default class Visualization extends Component { <div className="mt2"> <button className="Button Button--primary Button--medium" - onClick={this.props.onOpenChartSettings} + onClick={() => this.props.onOpenChartSettings(e.initial)} > {e.buttonText} </button> diff --git a/frontend/src/metabase/visualizations/lib/errors.js b/frontend/src/metabase/visualizations/lib/errors.js index d826d117c3382d21bb7bb778e1ff6a89a334d32c..2d69ae85f2c454928c8fb8634ad2ab2b8b8cee4e 100644 --- a/frontend/src/metabase/visualizations/lib/errors.js +++ b/frontend/src/metabase/visualizations/lib/errors.js @@ -3,6 +3,8 @@ import { t, ngettext, msgid } from "c-3po"; // NOTE: extending Error with Babel requires babel-plugin-transform-builtin-extend +type ChartSettingsInitial = { section?: ?string, widget?: ?any }; + export class MinColumnsError extends Error { constructor(minColumns: number, actualColumns: number) { super( @@ -42,11 +44,16 @@ export class NoBreakoutError extends Error { } export class ChartSettingsError extends Error { - section: ?string; + initial: ?ChartSettingsInitial; buttonText: ?string; - constructor(message: string, section?: string, buttonText?: string) { + + constructor( + message: string, + initial?: ChartSettingsInitial, + buttonText?: string, + ) { super(message || t`Please configure this chart in the chart settings`); - this.section = section; + this.initial = initial; this.buttonText = buttonText || t`Edit Settings`; } } diff --git a/frontend/src/metabase/visualizations/visualizations/Funnel.jsx b/frontend/src/metabase/visualizations/visualizations/Funnel.jsx index e7ae26b78d45584feb4ae657e9c3508931847eb0..fd3cf68357a999cd2057da2dcf46a9e55d129902 100644 --- a/frontend/src/metabase/visualizations/visualizations/Funnel.jsx +++ b/frontend/src/metabase/visualizations/visualizations/Funnel.jsx @@ -56,7 +56,7 @@ export default class Funnel extends Component { if (!settings["funnel.dimension"] || !settings["funnel.metric"]) { throw new ChartSettingsError( t`Which fields do you want to use?`, - t`Data`, + { section: t`Data` }, t`Choose fields`, ); } diff --git a/frontend/src/metabase/visualizations/visualizations/Map.jsx b/frontend/src/metabase/visualizations/visualizations/Map.jsx index 11860a58b8abca680a6dc62711b89545235a27c1..039468968fd073c2878a87796f298d0c9ec41b4f 100644 --- a/frontend/src/metabase/visualizations/visualizations/Map.jsx +++ b/frontend/src/metabase/visualizations/visualizations/Map.jsx @@ -223,17 +223,19 @@ export default class Map extends Component { ) { throw new ChartSettingsError( t`Please select longitude and latitude columns in the chart settings.`, - "Data", + { section: t`Data` }, ); } } else if (settings["map.type"] === "region") { if (!settings["map.region"]) { - throw new ChartSettingsError(t`Please select a region map.`, "Data"); + throw new ChartSettingsError(t`Please select a region map.`, { + section: t`Data`, + }); } if (!settings["map.dimension"] || !settings["map.metric"]) { throw new ChartSettingsError( t`Please select region and metric columns in the chart settings.`, - "Data", + { section: t`Data` }, ); } } diff --git a/frontend/src/metabase/visualizations/visualizations/PieChart.jsx b/frontend/src/metabase/visualizations/visualizations/PieChart.jsx index d9ad29d7978c0676c606fbc17ad897795c9ff608..b6d64b727eda02c807d3ca01d9e31081a272a30d 100644 --- a/frontend/src/metabase/visualizations/visualizations/PieChart.jsx +++ b/frontend/src/metabase/visualizations/visualizations/PieChart.jsx @@ -50,10 +50,9 @@ export default class PieChart extends Component { static checkRenderable([{ data: { cols, rows } }], settings) { if (!settings["pie.dimension"] || !settings["pie.metric"]) { - throw new ChartSettingsError( - t`Which columns do you want to use?`, - t`Data`, - ); + throw new ChartSettingsError(t`Which columns do you want to use?`, { + section: `Data`, + }); } } @@ -91,10 +90,13 @@ export default class PieChart extends Component { title: t`Colors`, widget: "colors", getDefault: (series, settings) => - getColorsForValues(settings["pie._dimensionValues"]), + settings["pie._dimensionValues"] + ? getColorsForValues(settings["pie._dimensionValues"]) + : [], getProps: (series, settings) => ({ - seriesTitles: settings["pie._dimensionValues"], + seriesTitles: settings["pie._dimensionValues"] || [], }), + getDisabled: (series, settings) => !settings["pie._dimensionValues"], readDependencies: ["pie._dimensionValues"], }, // this setting recomputes color assignment using pie.colors as the existing @@ -122,7 +124,10 @@ export default class PieChart extends Component { "pie._dimensionValues": { getValue: ([{ data: { rows } }], settings) => { const dimensionIndex = settings["pie._dimensionIndex"]; - return rows.map(row => row[dimensionIndex]); + return dimensionIndex >= 0 + ? // cast to string because getColorsForValues expects strings + rows.map(row => String(row[dimensionIndex])) + : null; }, readDependencies: ["pie._dimensionIndex"], }, diff --git a/frontend/test/lib/query.unit.spec.js b/frontend/test/lib/query.unit.spec.js index fa7c4012b0137c899d34c040790de5354b170e39..97a5c63e87e8d2217fa84a99b6e103b01fbdd71a 100644 --- a/frontend/test/lib/query.unit.spec.js +++ b/frontend/test/lib/query.unit.spec.js @@ -279,7 +279,7 @@ describe("Legacy Query library", () => { expect(target.unit).toEqual("day"); }); - it("should return field object and table for fk field", () => { + it("should return field object and table for old-style fk field", () => { let target = Query.getFieldTarget(["fk->", 1, 2], table1); expect(target.table).toEqual(table2); expect(target.field).toEqual(field2); @@ -287,6 +287,17 @@ describe("Legacy Query library", () => { expect(target.unit).toEqual(undefined); }); + it("should return field object and table for new-style fk field", () => { + let target = Query.getFieldTarget( + ["fk->", ["field-id", 1], ["field-id", 2]], + table1, + ); + expect(target.table).toEqual(table2); + expect(target.field).toEqual(field2); + expect(target.path).toEqual([field1]); + expect(target.unit).toEqual(undefined); + }); + it("should return field object and table and unit for fk + datetime field", () => { let target = Query.getFieldTarget( ["datetime-field", ["fk->", 1, 2], "day"], @@ -308,6 +319,17 @@ describe("Legacy Query library", () => { }); }); +describe("isValidField", () => { + it("should return true for old-style fk", () => { + expect(Query.isValidField(["fk->", 1, 2])).toBe(true); + }); + it("should return true for new-style fk", () => { + expect(Query.isValidField(["fk->", ["field-id", 1], ["field-id", 2]])).toBe( + true, + ); + }); +}); + describe("generateQueryDescription", () => { it("should work with multiple aggregations", () => { expect( diff --git a/frontend/test/services/__snapshots__/MetabaseApi.integ.spec.js.snap b/frontend/test/services/__snapshots__/MetabaseApi.integ.spec.js.snap index e9a271504ba6e1e5ed070278721969851af395c9..50962449edd1c91a6181b325a3e0620123973ddc 100644 --- a/frontend/test/services/__snapshots__/MetabaseApi.integ.spec.js.snap +++ b/frontend/test/services/__snapshots__/MetabaseApi.integ.spec.js.snap @@ -426,6 +426,7 @@ Object { "fingerprint": Object { "global": Object { "distinct-count": 10001, + "nil%": 0, }, "type": Object { "type/DateTime": Object { @@ -1575,6 +1576,7 @@ Object { "fingerprint": Object { "global": Object { "distinct-count": 2308, + "nil%": 0, }, "type": Object { "type/DateTime": Object { @@ -1797,6 +1799,7 @@ Object { "fingerprint": Object { "global": Object { "distinct-count": 2500, + "nil%": 0, }, "type": Object { "type/DateTime": Object { @@ -2784,6 +2787,7 @@ Object { "fingerprint": Object { "global": Object { "distinct-count": 200, + "nil%": 0, }, "type": Object { "type/DateTime": Object { @@ -3631,6 +3635,7 @@ Object { "fingerprint": Object { "global": Object { "distinct-count": 1112, + "nil%": 0, }, "type": Object { "type/DateTime": Object { diff --git a/project.clj b/project.clj index f82ab4e7cb9a7822363a2d9f51000caf195a631d..9374b0a266f78d47d0437783029287aa8dea36fe 100644 --- a/project.clj +++ b/project.clj @@ -10,7 +10,8 @@ "test" ["with-profile" "+expectations" "expectations"] "generate-sample-dataset" ["with-profile" "+generate-sample-dataset" "run"] "profile" ["with-profile" "+profile" "run" "profile"] - "h2" ["with-profile" "+h2-shell" "run" "-url" "jdbc:h2:./metabase.db" "-user" "" "-password" "" "-driver" "org.h2.Driver"]} + "h2" ["with-profile" "+h2-shell" "run" "-url" "jdbc:h2:./metabase.db" "-user" "" "-password" "" "-driver" "org.h2.Driver"] + "generate-automagic-dashboards-pot" ["with-profile" "+generate-automagic-dashboards-pot" "run"]} :dependencies [[org.clojure/clojure "1.9.0"] [org.clojure/core.async "0.3.442"] [org.clojure/core.match "0.3.0-alpha4"] ; optimized pattern matching library for Clojure @@ -28,6 +29,7 @@ [amalloy/ring-gzip-middleware "0.1.3"] ; Ring middleware to GZIP responses if client can handle it [aleph "0.4.5-alpha2" ; Async HTTP library; WebSockets :exclusions [org.clojure/tools.logging]] + [bigml/histogram "4.1.3"] ; Histogram data structure [buddy/buddy-core "1.2.0"] ; various cryptograhpic functions [buddy/buddy-sign "1.5.0"] ; JSON Web Tokens; High-Level message signing library [cheshire "5.7.0"] ; fast JSON encoding (used by Ring JSON middleware) @@ -177,4 +179,5 @@ :profile {:jvm-opts ["-XX:+CITime" ; print time spent in JIT compiler "-XX:+PrintGC"]} ; print a message when garbage collection takes place ;; get the H2 shell with 'lein h2' - :h2-shell {:main org.h2.tools.Shell}}) + :h2-shell {:main org.h2.tools.Shell} + :generate-automagic-dashboards-pot {:main metabase.automagic-dashboards.rules}}) diff --git a/src/metabase/automagic_dashboards/comparison.clj b/src/metabase/automagic_dashboards/comparison.clj index 46e91afedc7c6c728140c187e164e8d52ced8eef..4181c9d9dd12384801016351c3b8de5ed037b052 100644 --- a/src/metabase/automagic_dashboards/comparison.clj +++ b/src/metabase/automagic_dashboards/comparison.clj @@ -12,7 +12,7 @@ [metabase.mbql.normalize :as normalize] [metabase.models.table :refer [Table]] [metabase.query-processor.util :as qp.util] - [puppetlabs.i18n.core :as i18n :refer [tru]])) + [metabase.util.i18n :refer [tru]])) (def ^:private ^{:arglists '([root])} comparison-name (comp capitalize-first (some-fn :comparison-name :full-name))) diff --git a/src/metabase/automagic_dashboards/core.clj b/src/metabase/automagic_dashboards/core.clj index faf50e4c341554f7cae1163897cd2ebd367b6a26..f1cd5af9410c63fcbe072edbfd94422698f6ab8e 100644 --- a/src/metabase/automagic_dashboards/core.clj +++ b/src/metabase/automagic_dashboards/core.clj @@ -38,7 +38,7 @@ [metabase.query-processor.util :as qp.util] [metabase.sync.analyze.classify :as classify] [metabase.util.date :as date] - [puppetlabs.i18n.core :as i18n :refer [trs tru]] + [metabase.util.i18n :refer [trs tru] :as ui18n] [ring.util.codec :as codec] [schema.core :as s] [toucan.db :as db]) @@ -417,9 +417,9 @@ (fn [[_ identifier attribute]] (let [entity (bindings identifier) attribute (some-> attribute qp.util/normalize-token)] - (or (and (ifn? entity) (entity attribute)) - (root attribute) - (->reference template-type entity))))))) + (str (or (and (ifn? entity) (entity attribute)) + (root attribute) + (->reference template-type entity)))))))) (defn- field-candidates [context {:keys [field_type links_to named max_cardinality] :as constraints}] @@ -564,7 +564,8 @@ (defn capitalize-first "Capitalize only the first letter in a given string." [s] - (str (str/upper-case (subs s 0 1)) (subs s 1))) + (let [s (str s)] + (str (str/upper-case (subs s 0 1)) (subs s 1)))) (defn- instantiate-metadata [x context bindings] @@ -982,15 +983,15 @@ ;; (no chunking). first))] (let [show (or show max-cards)] - (log/infof (trs "Applying heuristic %s to %s.") (:rule rule) full-name) - (log/infof (trs "Dimensions bindings:\n%s") - (->> context - :dimensions - (m/map-vals #(update % :matches (partial map :name))) - u/pprint-to-str)) - (log/infof (trs "Using definitions:\nMetrics:\n%s\nFilters:\n%s") - (-> context :metrics u/pprint-to-str) - (-> context :filters u/pprint-to-str)) + (log/infof (str (trs "Applying heuristic {0} to {1}." (:rule rule) full-name))) + (log/infof (str (trs "Dimensions bindings:\n{0}" + (->> context + :dimensions + (m/map-vals #(update % :matches (partial map :name))) + u/pprint-to-str)))) + (log/infof (str (trs "Using definitions:\nMetrics:\n{0}\nFilters:\n{1}" + (->> context :metrics (m/map-vals :metric) u/pprint-to-str) + (-> context :filters u/pprint-to-str)))) (-> dashboard (populate/create-dashboard show) (assoc :related (related context rule) @@ -999,7 +1000,7 @@ (format "%s#show=all" (:url root))) :transient_filters (:query-filter context) :param_fields (->> context :query-filter (filter-referenced-fields root))))) - (throw (ex-info (trs "Can''t create dashboard for {0}" full-name) + (throw (ui18n/ex-info (trs "Can''t create dashboard for {0}" full-name) {:root root :available-rules (map :rule (or (some-> rule rules/get-rule vector) (rules/get-rules rules-prefix)))})))) @@ -1105,14 +1106,14 @@ humanize-filter-value (fn [_ [op & args]] (qp.util/normalize-token op))) -(def ^:private unit-name (comp {:minute-of-hour "minute" - :hour-of-day "hour" - :day-of-week "day of week" - :day-of-month "day of month" - :day-of-year "day of year" - :week-of-year "week" - :month-of-year "month" - :quarter-of-year "quarter"} +(def ^:private unit-name (comp {:minute-of-hour (tru "minute") + :hour-of-day (tru "hour") + :day-of-week (tru "day of week") + :day-of-month (tru "day of month") + :day-of-year (tru "day of year") + :week-of-year (tru "week") + :month-of-year (tru "month") + :quarter-of-year (tru "quarter")} qp.util/normalize-token)) (defn- field-name @@ -1120,7 +1121,7 @@ (->> field-reference (field-reference->field root) field-name)) ([{:keys [display_name unit] :as field}] (cond->> display_name - (and (filters/periodic-datetime? field) unit) (format "%s of %s" (unit-name unit))))) + (and (filters/periodic-datetime? field) unit) (tru "{0} of {1}" (unit-name unit))))) (defmethod humanize-filter-value := [root [_ field-reference value]] diff --git a/src/metabase/automagic_dashboards/rules.clj b/src/metabase/automagic_dashboards/rules.clj index 22b7d3987fc59f598e5d4ee830b731c8edf9f705..2f7d009f9053c693cbd11fa9f87ce1c20f903d1f 100644 --- a/src/metabase/automagic_dashboards/rules.clj +++ b/src/metabase/automagic_dashboards/rules.clj @@ -7,11 +7,12 @@ [metabase.query-processor.util :as qp.util] [metabase.util :as u] [metabase.util - [i18n :refer [trs]] + [i18n :refer [trs tru LocalizedString]] [schema :as su]] [schema [coerce :as sc] [core :as s]] + [schema.spec.core :as spec] [yaml.core :as yaml]) (:import [java.nio.file Files FileSystem FileSystems Path])) @@ -28,7 +29,7 @@ (def ^:private Metric {Identifier {(s/required-key :metric) MBQL (s/required-key :score) Score - (s/optional-key :name) s/Str}}) + (s/optional-key :name) LocalizedString}}) (def ^:private Filter {Identifier {(s/required-key :filter) MBQL (s/required-key :score) Score}}) @@ -87,28 +88,28 @@ (def ^:private CardDimension {Identifier {(s/optional-key :aggregation) s/Str}}) (def ^:private Card - {Identifier {(s/required-key :title) s/Str + {Identifier {(s/required-key :title) LocalizedString (s/required-key :score) Score (s/optional-key :visualization) Visualization - (s/optional-key :text) s/Str + (s/optional-key :text) LocalizedString (s/optional-key :dimensions) [CardDimension] (s/optional-key :filters) [s/Str] (s/optional-key :metrics) [s/Str] (s/optional-key :limit) su/IntGreaterThanZero (s/optional-key :order_by) [OrderByPair] - (s/optional-key :description) s/Str + (s/optional-key :description) LocalizedString (s/optional-key :query) s/Str (s/optional-key :width) Width (s/optional-key :height) Height (s/optional-key :group) s/Str - (s/optional-key :y_label) s/Str - (s/optional-key :x_label) s/Str - (s/optional-key :series_labels) [s/Str]}}) + (s/optional-key :y_label) LocalizedString + (s/optional-key :x_label) LocalizedString + (s/optional-key :series_labels) [LocalizedString]}}) (def ^:private Groups - {Identifier {(s/required-key :title) s/Str - (s/optional-key :comparison_title) s/Str - (s/optional-key :description) s/Str}}) + {Identifier {(s/required-key :title) LocalizedString + (s/optional-key :comparison_title) LocalizedString + (s/optional-key :description) LocalizedString}}) (def ^{:arglists '([definition])} identifier "Return `key` in `{key {}}`." @@ -141,8 +142,7 @@ (dimension-form? subform) [(second subform)] (string? subform) (->> subform (re-seq #"\[\[(\w+)\]\]") - (map second)) - :else nil))) + (map second))))) distinct)) (defn- valid-metrics-references? @@ -189,15 +189,14 @@ (def Rule "Rules defining an automagic dashboard." (constrained-all - {(s/required-key :title) s/Str + {(s/required-key :title) LocalizedString (s/required-key :rule) s/Str (s/required-key :specificity) s/Int (s/optional-key :cards) [Card] (s/optional-key :dimensions) [Dimension] (s/optional-key :applies_to) AppliesTo - (s/optional-key :transient_title) s/Str - (s/optional-key :short_title) s/Str - (s/optional-key :description) s/Str + (s/optional-key :transient_title) LocalizedString + (s/optional-key :description) LocalizedString (s/optional-key :metrics) [Metric] (s/optional-key :filters) [Filter] (s/optional-key :groups) Groups @@ -274,7 +273,8 @@ [(->entity table-type) (->type field-type)] [(if (-> table-type ->entity table-type?) (->entity table-type) - (->type table-type))])))})) + (->type table-type))]))) + LocalizedString #(tru %)})) (def ^:private rules-dir "automagic_dashboards/") @@ -360,3 +360,48 @@ "Get rule at path `path`." [path] (get-in @rules (concat path [::leaf]))) + +(defn- extract-localized-strings + [[path rule]] + (let [strings (atom [])] + ((spec/run-checker + (fn [s params] + (let [walk (spec/checker (s/spec s) params)] + (fn [x] + (when (= LocalizedString s) + (swap! strings conj x)) + (walk x)))) + false + Rule) + rule) + (map vector (distinct @strings) (repeat path)))) + +(defn- make-pot + [strings] + (->> strings + (group-by first) + (mapcat (fn [[s ctxs]] + (concat (for [[_ ctx] ctxs] + (format "#: resources/%s%s.yaml" rules-dir (str/join "/" ctx))) + [(format "msgid \"%s\"\nmsgstr \"\"\n" s)]))) + (str/join "\n"))) + +(defn- all-rules + ([] + (all-rules [] @rules)) + ([path rules] + (when (map? rules) + (mapcat (fn [[k v]] + (if (= k ::leaf) + [[path v]] + (all-rules (conj path k) v))) + rules)))) + +(defn -main + "Entry point for lein task `generate-automagic-dashboards-pot`" + [& _] + (->> (all-rules) + (mapcat extract-localized-strings) + make-pot + (spit "locales/metabase-automatic-dashboards.pot")) + (System/exit 0)) diff --git a/src/metabase/driver/googleanalytics/query_processor.clj b/src/metabase/driver/googleanalytics/query_processor.clj index 4f77ce00835434792b5e97956d244294214b4a9f..500513ee16aa922ea812ba3cf9553f19b090406d 100644 --- a/src/metabase/driver/googleanalytics/query_processor.clj +++ b/src/metabase/driver/googleanalytics/query_processor.clj @@ -5,7 +5,6 @@ [clojure.tools.reader.edn :as edn] [metabase.mbql.util :as mbql.u] [metabase.query-processor.store :as qp.store] - [metabase.util :as u] [metabase.util [date :as du] [i18n :as ui18n :refer [tru]] @@ -312,11 +311,10 @@ (defn- header->column [^GaData$ColumnHeaders header] (let [date-parser (ga-dimension->date-format-fn (.getName header))] (if date-parser - {:name (keyword "ga:date") - :base-type :type/DateTime} - {:name (keyword (.getName header)) - :base-type (ga-type->base-type (.getDataType header)) - :field-display-name "COOL"}))) + {:name "ga:date" + :base_type :type/DateTime} + {:name (.getName header) + :base_type (ga-type->base-type (.getDataType header))}))) (defn- header->getter-fn [^GaData$ColumnHeaders header] (let [date-parser (ga-dimension->date-format-fn (.getName header)) @@ -333,7 +331,7 @@ columns (map header->column (.getColumnHeaders response)) getters (map header->getter-fn (.getColumnHeaders response))] {:cols columns - :columns (map (comp u/keyword->qualified-name :name) columns) + :columns (map :name columns) :rows (for [row (.getRows response)] (for [[data getter] (map vector row getters)] (getter data)))})) diff --git a/src/metabase/mbql/normalize.clj b/src/metabase/mbql/normalize.clj index 46a221166164863aec91743c363b05cabe438f05..fd44ee2c21f481f1d1e3b66aa093ecd5b5fbcadd 100644 --- a/src/metabase/mbql/normalize.clj +++ b/src/metabase/mbql/normalize.clj @@ -31,7 +31,9 @@ (:require [clojure.tools.logging :as log] [clojure.walk :as walk] [medley.core :as m] - [metabase.mbql.util :as mbql.u] + [metabase.mbql + [predicates :as mbql.pred] + [util :as mbql.u]] [metabase.util :as u] [metabase.util.i18n :refer [tru]])) @@ -506,11 +508,27 @@ (defn- remove-breakout-fields-from-fields "Remove any Fields specified in both `:breakout` and `:fields` from `:fields`; it is implied that any breakout Field will be returned, specifying it in both would imply it is to be returned twice, which tends to cause confusion for - the QP and drivers." + the QP and drivers. (This is done to work around historic bugs with the way queries were generated on the frontend; + I'm not sure this behavior makes sense, but removing it would break existing queries.) + + We will remove either exact matches: + + {:breakout [[:field-id 10]], :fields [[:field-id 10]]} ; -> {:breakout [[:field-id 10]]} + + or unbucketed matches: + + {:breakout [[:datetime-field [:field-id 10] :month]], :fields [[:field-id 10]]} + ;; -> {:breakout [[:field-id 10]]}" [{{:keys [breakout fields]} :query, :as query}] (if-not (and (seq breakout) (seq fields)) query - (update-in query [:query :fields] (comp vec (partial remove (set breakout)))))) + ;; get a set of all Field clauses (of any type) in the breakout. For `datetime-field` clauses, we'll include both + ;; the bucketed `[:datetime-field <field> ...]` clause and the `<field>` clause it wraps + (let [breakout-fields (set (reduce concat (mbql.u/match breakout + [:datetime-field field-clause _] [&match field-clause] + mbql.pred/Field? [&match])))] + ;; now remove all the Fields in `:fields` that match the ones in the set + (update-in query [:query :fields] (comp vec (partial remove breakout-fields)))))) (defn- perform-whole-query-transformations "Perform transformations that operate on the query as a whole, making sure the structure as a whole is logical and diff --git a/src/metabase/mbql/predicates.clj b/src/metabase/mbql/predicates.clj index b5b3d0dad5bb9ac6c30c0722afda2931359f9491..88ccf76d0380f0e8173cfbcc4475f769c2dc5dd0 100644 --- a/src/metabase/mbql/predicates.clj +++ b/src/metabase/mbql/predicates.clj @@ -17,3 +17,7 @@ (def ^{:arglists '([ag-clause])} Aggregation? "Is this a valid Aggregation clause?" (complement (s/checker mbql.s/Aggregation))) + +(def ^{:arglists '([field-clause])} Field? + "Is this a valid Field clause?" + (complement (s/checker mbql.s/Field))) diff --git a/src/metabase/mbql/schema.clj b/src/metabase/mbql/schema.clj index d2d28fa1de50f05ab433fb7ee14c8079733f348e..13732aae57643ac674921d180e6e2189d193cd8c 100644 --- a/src/metabase/mbql/schema.clj +++ b/src/metabase/mbql/schema.clj @@ -181,16 +181,16 @@ ;; TODO - binning strategy param is disallowed for `:default` and required for the others. For `num-bins` it must also ;; be an integer. (defclause ^{:requires-features #{:binning}} binning-strategy - field BinnableField - strategy-name BinningStrategyName - strategy-param (optional (s/constrained s/Num (complement neg?) "strategy param must be >= 0.")) + field BinnableField + strategy-name BinningStrategyName + strategy-param (optional (s/constrained s/Num (complement neg?) "strategy param must be >= 0.")) ;; These are added in automatically by the `binning` middleware. Don't add them yourself, as they're just be ;; replaced. Driver implementations can rely on this being populated resolved-options (optional ResolvedBinningStrategyOptions)) (def Field "Schema for anything that refers to a Field, from the common `[:field-id <id>]` to variants like `:datetime-field` or - `:fk->`." + `:fk->` or an expression reference `[:expression <name>]`." (one-of field-id field-literal fk-> datetime-field expression binning-strategy)) ;; aggregate field reference refers to an aggregation, e.g. diff --git a/src/metabase/models/dashboard.clj b/src/metabase/models/dashboard.clj index 6de3818456dbdc4c7a8e3948471965bd73e09ebc..39937027bbd8db628795311ae057acb9f92ef854 100644 --- a/src/metabase/models/dashboard.clj +++ b/src/metabase/models/dashboard.clj @@ -21,6 +21,7 @@ [revision :as revision]] [metabase.models.revision.diff :refer [build-sentence]] [metabase.query-processor.interface :as qpi] + [metabase.util.i18n :as ui18n] [toucan [db :as db] [hydrate :refer [hydrate]] @@ -247,8 +248,8 @@ [collection-name parent-collection-id] (let [c (db/count 'Collection :name [:like (format "%s%%" collection-name)] - :location (collection/children-location (db/select-one ['Collection :location :id] - :id parent-collection-id)))] + :location (collection/children-location (db/select-one ['Collection :location :id] + :id parent-collection-id)))] (if (zero? c) collection-name (format "%s %s" collection-name (inc c))))) @@ -256,7 +257,8 @@ (defn save-transient-dashboard! "Save a denormalized description of `dashboard`." [dashboard parent-collection-id] - (let [dashcards (:ordered_cards dashboard) + (let [dashboard (ui18n/localized-strings->strings dashboard) + dashcards (:ordered_cards dashboard) collection (magic.populate/create-collection! (ensure-unique-collection-name (:name dashboard) parent-collection-id) (rand-nth magic.populate/colors) diff --git a/src/metabase/query_processor/middleware/parameters/sql.clj b/src/metabase/query_processor/middleware/parameters/sql.clj index 413db06ff28d4618da2940e4bdb0b2f848dcc5a8..3763d0b6b07da3c07e74976803854c65fc0fc612 100644 --- a/src/metabase/query_processor/middleware/parameters/sql.clj +++ b/src/metabase/query_processor/middleware/parameters/sql.clj @@ -154,6 +154,8 @@ (s/defn ^:private default-value-for-dimension :- (s/maybe DimensionValue) "Return the default value for a Dimension (Field Filter) param defined by the map TAG, if one is set." [tag :- TagParam] + (when (and (:required tag) (not (:default tag))) + (throw (Exception. (str (tru "''{0}'' is a required param." (:display-name tag)))))) (when-let [default (:default tag)] {:type (:widget-type tag :dimension) ; widget-type is the actual type of the default value if set :target [:dimension [:template-tag (:name tag)]] diff --git a/src/metabase/sync/analyze/fingerprint/fingerprinters.clj b/src/metabase/sync/analyze/fingerprint/fingerprinters.clj index 0442c123d0821ed5417820e33b34dedc669bbbf3..680957e64119f654e2a1ca2afe10d32181ce12ac 100644 --- a/src/metabase/sync/analyze/fingerprint/fingerprinters.clj +++ b/src/metabase/sync/analyze/fingerprint/fingerprinters.clj @@ -1,8 +1,11 @@ (ns metabase.sync.analyze.fingerprint.fingerprinters "Non-identifying fingerprinters for various field types." - (:require [cheshire.core :as json] + (:require [bigml.histogram.core :as hist] + [cheshire.core :as json] [clj-time.coerce :as t.coerce] - [kixi.stats.core :as stats] + [kixi.stats + [core :as stats] + [math :as math]] [metabase.models.field :as field] [metabase.sync.analyze.classifiers.name :as classify.name] [metabase.sync.util :as sync-util] @@ -11,7 +14,8 @@ [date :as du] [i18n :refer [trs]]] [redux.core :as redux]) - (:import com.clearspring.analytics.stream.cardinality.HyperLogLogPlus + (:import com.bigml.histogram.Histogram + com.clearspring.analytics.stream.cardinality.HyperLogLogPlus org.joda.time.DateTime)) (defn col-wise @@ -61,7 +65,8 @@ (def ^:private global-fingerprinter (redux/post-complete - (redux/fuse {:distinct-count cardinality}) + (redux/fuse {:distinct-count cardinality + :nil% (stats/share nil?)}) (partial hash-map :global))) (defmethod fingerprinter :default @@ -167,10 +172,23 @@ (redux/fuse {:earliest earliest :latest latest}))) +(defn- histogram + "Transducer that summarizes numerical data with a histogram." + ([] (hist/create)) + ([^Histogram histogram] histogram) + ([^Histogram histogram x] (hist/insert-simple! histogram x))) + (deffingerprinter :type/Number - (redux/fuse {:min stats/min - :max stats/max - :avg stats/mean})) + (redux/post-complete + histogram + (fn [h] + (let [{q1 0.25 q3 0.75} (hist/percentiles h 0.25 0.75)] + {:min (hist/minimum h) + :max (hist/maximum h) + :avg (hist/mean h) + :sd (some-> h hist/variance math/sqrt) + :q1 q1 + :q3 q3})))) (defn- valid-serialized-json? "Is x a serialized JSON dictionary or array." diff --git a/src/metabase/sync/interface.clj b/src/metabase/sync/interface.clj index 2b9cfcaa995f4ea92e1164c648071502b94f9c15..c0f87e55e2db604bb6f46fd6199a609fc4bf67f5 100644 --- a/src/metabase/sync/interface.clj +++ b/src/metabase/sync/interface.clj @@ -89,19 +89,23 @@ [[s/Any]]) -(def GlobalFingerprint - "Fingerprint values that Fields of all types should have." - {(s/optional-key :distinct-count) s/Int}) - (def Percent "Schema for something represting a percentage. A floating-point value between (inclusive) 0 and 1." (s/constrained s/Num #(<= 0 % 1) "Valid percentage between (inclusive) 0 and 1.")) +(def GlobalFingerprint + "Fingerprint values that Fields of all types should have." + {(s/optional-key :distinct-count) s/Int + (s/optional-key :nil%) (s/maybe Percent)}) + (def NumberFingerprint "Schema for fingerprint information for Fields deriving from `:type/Number`." {(s/optional-key :min) (s/maybe s/Num) (s/optional-key :max) (s/maybe s/Num) - (s/optional-key :avg) (s/maybe s/Num)}) + (s/optional-key :avg) (s/maybe s/Num) + (s/optional-key :q1) (s/maybe s/Num) + (s/optional-key :q3) (s/maybe s/Num) + (s/optional-key :sd) (s/maybe s/Num)}) (def TextFingerprint "Schema for fingerprint information for Fields deriving from `:type/Text`." @@ -160,6 +164,7 @@ "Map of fingerprint version to the set of Field base types that need to be upgraded to this version the next time we do analysis. The highest-numbered entry is considered the latest version of fingerprints." {1 #{:type/*} + 2 #{:type/Number} 3 #{:type/DateTime}}) (def latest-fingerprint-version diff --git a/src/metabase/util/i18n.clj b/src/metabase/util/i18n.clj index 17c32e25e248009653e1eb14b89b16efc068dc32..755e9d0fd575dfb21c7a9bb1109d57a50c51bcfa 100644 --- a/src/metabase/util/i18n.clj +++ b/src/metabase/util/i18n.clj @@ -67,7 +67,7 @@ localized string needs to be 'late bound' and only occur when the user's locale is in scope. Calling `str` on the results of this invocation will lookup the translated version of the string." [msg & args] - `(UserLocalizedString. (namespace-munge *ns*) ~msg ~(vec args))) + `(UserLocalizedString. ~(namespace-munge *ns*) ~msg ~(vec args))) (defmacro trs "Similar to `puppetlabs.i18n.core/trs` but creates a `SystemLocalizedString` instance so that conversion to the @@ -75,7 +75,7 @@ overridden/changed by a setting. Calling `str` on the results of this invocation will lookup the translated version of the string." [msg & args] - `(SystemLocalizedString. (namespace-munge *ns*) ~msg ~(vec args))) + `(SystemLocalizedString. ~(namespace-munge *ns*) ~msg ~(vec args))) (def ^:private localized-string-checker "Compiled checker for `LocalizedString`s which is more efficient when used repeatedly like in `localized-string?` diff --git a/test/metabase/api/card_test.clj b/test/metabase/api/card_test.clj index 86881368be18314d4d396ab02a555e5469e4c736..f62b5d2bb55006049b3a22b87096cc9885183f94 100644 --- a/test/metabase/api/card_test.clj +++ b/test/metabase/api/card_test.clj @@ -330,8 +330,9 @@ :display_name "count" :name "count" :special_type "type/Quantity" - :fingerprint {:global {:distinct-count 1}, - :type {:type/Number {:min 100.0, :max 100.0, :avg 100.0}}}}] + :fingerprint {:global {:distinct-count 1 + :nil% 0.0}, + :type {:type/Number {:min 100.0, :max 100.0, :avg 100.0, :q1 100.0, :q3 100.0 :sd nil}}}}] (tu/with-non-admin-groups-no-root-collection-perms (let [metadata [{:base_type :type/Integer :display_name "Count Chocula" @@ -540,8 +541,9 @@ :display_name "count" :name "count" :special_type "type/Quantity" - :fingerprint {:global {:distinct-count 1}, - :type {:type/Number {:min 100.0, :max 100.0, :avg 100.0}}}}] + :fingerprint {:global {:distinct-count 1 + :nil% 0.0}, + :type {:type/Number {:min 100.0, :max 100.0, :avg 100.0, :q1 100.0, :q3 100.0 :sd nil}}}}] (let [metadata [{:base_type :type/Integer :display_name "Count Chocula" :name "count_chocula" diff --git a/test/metabase/api/table_test.clj b/test/metabase/api/table_test.clj index 56247c59240c48f365bab159433b5b63756d6248..fce422c37eddb291f8fc6b6d1a8be2f8eca19398 100644 --- a/test/metabase/api/table_test.clj +++ b/test/metabase/api/table_test.clj @@ -492,7 +492,12 @@ ;; run the Card which will populate its result_metadata column ((user->client :crowberto) :post 200 (format "card/%d/query" (u/get-id card))) ;; Now fetch the metadata for this "table" - (tu/round-all-decimals 2 ((user->client :crowberto) :get 200 (format "table/card__%d/query_metadata" (u/get-id card)))))) + (->> card + u/get-id + (format "table/card__%d/query_metadata") + ((user->client :crowberto) :get 200) + (tu/round-fingerprint-cols [:fields]) + (tu/round-all-decimals 2)))) ;; Test date dimensions being included with a nested query (tt/expect-with-temp [Card [card {:name "Users" @@ -515,7 +520,8 @@ :special_type "type/Name" :default_dimension_option nil :dimension_options [] - :fingerprint {:global {:distinct-count 15}, + :fingerprint {:global {:distinct-count 15 + :nil% 0.0}, :type {:type/Text {:percent-json 0.0, :percent-url 0.0, :percent-email 0.0, :average-length 13.27}}}} {:name "LAST_LOGIN" @@ -526,14 +532,20 @@ :special_type nil :default_dimension_option (var-get #'table-api/date-default-index) :dimension_options (var-get #'table-api/datetime-dimension-indexes) - :fingerprint {:global {:distinct-count 15}, + :fingerprint {:global {:distinct-count 15 + :nil% 0.0}, :type {:type/DateTime {:earliest "2014-01-01T08:30:00.000Z", :latest "2014-12-05T15:15:00.000Z"}}}}]}) (do ;; run the Card which will populate its result_metadata column ((user->client :crowberto) :post 200 (format "card/%d/query" (u/get-id card))) ;; Now fetch the metadata for this "table" - (tu/round-all-decimals 2 ((user->client :crowberto) :get 200 (format "table/card__%d/query_metadata" (u/get-id card)))))) + (->> card + u/get-id + (format "table/card__%d/query_metadata") + ((user->client :crowberto) :get 200) + (tu/round-fingerprint-cols [:fields]) + (tu/round-all-decimals 2)))) ;; make sure GET /api/table/:id/fks just returns nothing for 'virtual' tables diff --git a/test/metabase/automagic_dashboards/core_test.clj b/test/metabase/automagic_dashboards/core_test.clj index 7aef64c5e674654e688ebe7b89d8c5884aedc9e7..6f414fbc0b7f5be099512287c5add13f920d77ba 100644 --- a/test/metabase/automagic_dashboards/core_test.clj +++ b/test/metabase/automagic_dashboards/core_test.clj @@ -476,47 +476,47 @@ (t.format/unparse (t.format/formatter formatter (t/time-zone-for-id tz)) dt))] (expect - [(tru "at {0}" (unparse-with-formatter "h:mm a, MMMM d, YYYY" dt)) - (tru "at {0}" (unparse-with-formatter "h a, MMMM d, YYYY" dt)) - (tru "on {0}" (unparse-with-formatter "MMMM d, YYYY" dt)) - (tru "in {0} week - {1}" - (#'magic/pluralize (date/date-extract :week-of-year dt tz)) - (str (date/date-extract :year dt tz))) - (tru "in {0}" (unparse-with-formatter "MMMM YYYY" dt)) - (tru "in Q{0} - {1}" - (date/date-extract :quarter-of-year dt tz) - (str (date/date-extract :year dt tz))) - (unparse-with-formatter "YYYY" dt) - (unparse-with-formatter "EEEE" dt) - (tru "at {0}" (unparse-with-formatter "h a" dt)) - (unparse-with-formatter "MMMM" dt) - (tru "Q{0}" (date/date-extract :quarter-of-year dt tz)) - (date/date-extract :minute-of-hour dt tz) - (date/date-extract :day-of-month dt tz) - (date/date-extract :week-of-year dt tz)] + (map str [(tru "at {0}" (unparse-with-formatter "h:mm a, MMMM d, YYYY" dt)) + (tru "at {0}" (unparse-with-formatter "h a, MMMM d, YYYY" dt)) + (tru "on {0}" (unparse-with-formatter "MMMM d, YYYY" dt)) + (tru "in {0} week - {1}" + (#'magic/pluralize (date/date-extract :week-of-year dt tz)) + (str (date/date-extract :year dt tz))) + (tru "in {0}" (unparse-with-formatter "MMMM YYYY" dt)) + (tru "in Q{0} - {1}" + (date/date-extract :quarter-of-year dt tz) + (str (date/date-extract :year dt tz))) + (unparse-with-formatter "YYYY" dt) + (unparse-with-formatter "EEEE" dt) + (tru "at {0}" (unparse-with-formatter "h a" dt)) + (unparse-with-formatter "MMMM" dt) + (tru "Q{0}" (date/date-extract :quarter-of-year dt tz)) + (date/date-extract :minute-of-hour dt tz) + (date/date-extract :day-of-month dt tz) + (date/date-extract :week-of-year dt tz)]) (let [dt (t.format/unparse (t.format/formatters :date-hour-minute-second) dt)] - [(#'magic/humanize-datetime dt :minute) - (#'magic/humanize-datetime dt :hour) - (#'magic/humanize-datetime dt :day) - (#'magic/humanize-datetime dt :week) - (#'magic/humanize-datetime dt :month) - (#'magic/humanize-datetime dt :quarter) - (#'magic/humanize-datetime dt :year) - (#'magic/humanize-datetime dt :day-of-week) - (#'magic/humanize-datetime dt :hour-of-day) - (#'magic/humanize-datetime dt :month-of-year) - (#'magic/humanize-datetime dt :quarter-of-year) - (#'magic/humanize-datetime dt :minute-of-hour) - (#'magic/humanize-datetime dt :day-of-month) - (#'magic/humanize-datetime dt :week-of-year)]))) - -(expect - [(tru "{0}st" 1) - (tru "{0}nd" 22) - (tru "{0}rd" 303) - (tru "{0}th" 0) - (tru "{0}th" 8)] - (map #'magic/pluralize [1 22 303 0 8])) + (map (comp str (partial #'magic/humanize-datetime dt)) [:minute + :hour + :day + :week + :month + :quarter + :year + :day-of-week + :hour-of-day + :month-of-year + :quarter-of-year + :minute-of-hour + :day-of-month + :week-of-year])))) + +(expect + (map str [(tru "{0}st" 1) + (tru "{0}nd" 22) + (tru "{0}rd" 303) + (tru "{0}th" 0) + (tru "{0}th" 8)]) + (map (comp str #'magic/pluralize) [1 22 303 0 8])) ;; Make sure we have handlers for all the units available (expect diff --git a/test/metabase/driver/postgres_test.clj b/test/metabase/driver/postgres_test.clj index 5e8fc238bfb6b963553a8ab5b4791be330cefaee..504eacfc94b20ad3dc279f300fbda2d23b956d2b 100644 --- a/test/metabase/driver/postgres_test.clj +++ b/test/metabase/driver/postgres_test.clj @@ -298,11 +298,14 @@ ;; Make sure we're able to fingerprint TIME fields (#5911) (expect-with-engine :postgres - #{#metabase.models.field.FieldInstance{:name "start_time", :fingerprint {:global {:distinct-count 1} + #{#metabase.models.field.FieldInstance{:name "start_time", :fingerprint {:global {:distinct-count 1 + :nil% 0.0} :type {:type/DateTime {:earliest "1970-01-01T22:00:00.000Z", :latest "1970-01-01T22:00:00.000Z"}}}} - #metabase.models.field.FieldInstance{:name "end_time", :fingerprint {:global {:distinct-count 1} + #metabase.models.field.FieldInstance{:name "end_time", :fingerprint {:global {:distinct-count 1 + :nil% 0.0} :type {:type/DateTime {:earliest "1970-01-01T09:00:00.000Z", :latest "1970-01-01T09:00:00.000Z"}}}} - #metabase.models.field.FieldInstance{:name "reason", :fingerprint {:global {:distinct-count 1} + #metabase.models.field.FieldInstance{:name "reason", :fingerprint {:global {:distinct-count 1 +:nil% 0.0} :type {:type/Text {:percent-json 0.0 :percent-url 0.0 :percent-email 0.0 diff --git a/test/metabase/mbql/normalize_test.clj b/test/metabase/mbql/normalize_test.clj index 2ee6eaf08f8a96580e37da4f15ed1c193822c37d..dbcac1e44291ce27942eeed81f26cd231b999c12 100644 --- a/test/metabase/mbql/normalize_test.clj +++ b/test/metabase/mbql/normalize_test.clj @@ -724,6 +724,45 @@ :query {:breakout [[:field-id 1] [:field-id 2]] :fields [[:field-id 2] [:field-id 3]]}})) +;; should work with FKs +(expect + {:type :query + :query {:breakout [[:field-id 1] + [:fk-> [:field-id 2] [:field-id 4]]] + :fields [[:field-id 3]]}} + (#'normalize/perform-whole-query-transformations + {:type :query + :query {:breakout [[:field-id 1] + [:fk-> [:field-id 2] [:field-id 4]]] + :fields [[:fk-> [:field-id 2] [:field-id 4]] + [:field-id 3]]}})) + +;; should work if the Field is bucketed in the breakout & in fields +(expect + {:type :query + :query {:breakout [[:field-id 1] + [:datetime-field [:fk-> [:field-id 2] [:field-id 4]] :month]] + :fields [[:field-id 3]]}} + (#'normalize/perform-whole-query-transformations + {:type :query + :query {:breakout [[:field-id 1] + [:datetime-field [:fk-> [:field-id 2] [:field-id 4]] :month]] + :fields [[:datetime-field [:fk-> [:field-id 2] [:field-id 4]] :month] + [:field-id 3]]}})) + +;; should work if the Field is bucketed in the breakout but not in fields +(expect + {:type :query + :query {:breakout [[:field-id 1] + [:datetime-field [:fk-> [:field-id 2] [:field-id 4]] :month]] + :fields [[:field-id 3]]}} + (#'normalize/perform-whole-query-transformations + {:type :query + :query {:breakout [[:field-id 1] + [:datetime-field [:fk-> [:field-id 2] [:field-id 4]] :month]] + :fields [[:fk-> [:field-id 2] [:field-id 4]] + [:field-id 3]]}})) + ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | REMOVE EMPTY CLAUSES | diff --git a/test/metabase/models/dashboard_test.clj b/test/metabase/models/dashboard_test.clj index 443c5601aeaca9e21fd59c6531e356578eb8ecf3..f23c01044c6666f83d730790588516f4d205bedb 100644 --- a/test/metabase/models/dashboard_test.clj +++ b/test/metabase/models/dashboard_test.clj @@ -236,7 +236,7 @@ users/user->id user/permissions-set atom)] - (let [dashboard (magic/automagic-analysis (Table (id :venues)) {}) + (let [dashboard (magic/automagic-analysis (Table (id :venues)) {}) rastas-personal-collection (db/select-one-field :id 'Collection :personal_owner_id api/*current-user-id*)] (->> (save-transient-dashboard! dashboard rastas-personal-collection) diff --git a/test/metabase/query_processor/middleware/parameters/sql_test.clj b/test/metabase/query_processor/middleware/parameters/sql_test.clj index 2cd581dbd72e0b4358567f3ee4402a6d1a994970..ef550dae4c825c78e2be3dae298dcc8c09df149a 100644 --- a/test/metabase/query_processor/middleware/parameters/sql_test.clj +++ b/test/metabase/query_processor/middleware/parameters/sql_test.clj @@ -319,6 +319,26 @@ (into {} (#'sql/value-for-tag {:name "checkin_date", :display-name "Checkin Date", :type :dimension, :dimension [:field-id (data/id :checkins :date)]} nil))) +;; dimension -- required but unspecified +(expect Exception + (into {} (#'sql/value-for-tag {:name "checkin_date", :display-name "Checkin Date", :type "dimension", :required true, + :dimension ["field-id" (data/id :checkins :date)]} + nil))) + +;; dimension -- required and default specified +(expect + {:field {:name "DATE" + :parent_id nil + :table_id (data/id :checkins) + :base_type :type/Date} + :param {:type :dimension + :target [:dimension [:template-tag "checkin_date"]] + :value "2015-04-01~2015-05-01"}} + (into {} (#'sql/value-for-tag {:name "checkin_date", :display-name "Checkin Date", :type :dimension, :required true, :default "2015-04-01~2015-05-01", + :dimension [:field-id (data/id :checkins :date)]} + nil))) + + ;; multiple values for the same tag should return a vector with multiple params instead of a single param (expect {:field {:name "DATE" diff --git a/test/metabase/query_processor/middleware/results_metadata_test.clj b/test/metabase/query_processor/middleware/results_metadata_test.clj index 9c23cdc4fefa62f3d1a83ed49949eb500a6a4b3e..a2eb0578f5a8023017dbed26b63417e4d684b787 100644 --- a/test/metabase/query_processor/middleware/results_metadata_test.clj +++ b/test/metabase/query_processor/middleware/results_metadata_test.clj @@ -47,7 +47,7 @@ :special_type "type/Longitude", :fingerprint (:longitude mutil/venue-fingerprints)}]) (def ^:private default-card-results-native - (update-in default-card-results [3 :fingerprint] assoc :type {:type/Number {:min 2.0, :max 74.0, :avg 29.98}})) + (update-in default-card-results [3 :fingerprint] assoc :type {:type/Number {:min 2.0, :max 74.0, :avg 29.98, :q1 7.0, :q3 49.0 :sd 23.06}})) ;; test that Card result metadata is saved after running a Card (expect @@ -58,7 +58,10 @@ :info {:card-id (u/get-id card) :query-hash (qputil/query-hash {})})) (assert (= (:status <>) :completed))) - (round-to-2-decimals (card-metadata card)))) + (-> card + card-metadata + round-to-2-decimals + tu/round-fingerprint-cols))) ;; check that using a Card as your source doesn't overwrite the results metadata... (expect @@ -108,7 +111,8 @@ :native {:query "SELECT ID, NAME, PRICE, CATEGORY_ID, LATITUDE, LONGITUDE FROM VENUES"}}) (get-in [:data :results_metadata]) (update :checksum class) - round-to-2-decimals)) + round-to-2-decimals + (->> (tu/round-fingerprint-cols [:columns])))) ;; make sure that a Card where a DateTime column is broken out by year advertises that column as Text, since you can't ;; do datetime breakouts on years @@ -118,14 +122,15 @@ :name "DATE" :unit nil :special_type nil - :fingerprint {:global {:distinct-count 618}, :type {:type/DateTime {:earliest "2013-01-03T00:00:00.000Z" + :fingerprint {:global {:distinct-count 618 :nil% 0.0}, :type {:type/DateTime {:earliest "2013-01-03T00:00:00.000Z" :latest "2015-12-29T00:00:00.000Z"}}}} {:base_type "type/Integer" :display_name "count" :name "count" :special_type "type/Quantity" - :fingerprint {:global {:distinct-count 3} - :type {:type/Number {:min 235.0, :max 498.0, :avg 333.33}}}}] + :fingerprint {:global {:distinct-count 3 + :nil% 0.0}, + :type {:type/Number {:min 235.0, :max 498.0, :avg 333.33 :q1 243.0, :q3 440.0 :sd 143.5}}}}] (tt/with-temp Card [card] (qp/process-query {:database (data/id) :type :query @@ -134,4 +139,7 @@ :breakout [[:datetime-field [:field-id (data/id :checkins :date)] :year]]} :info {:card-id (u/get-id card) :query-hash (qputil/query-hash {})}}) - (round-to-2-decimals (card-metadata card)))) + (-> card + card-metadata + round-to-2-decimals + tu/round-fingerprint-cols))) diff --git a/test/metabase/query_processor_test.clj b/test/metabase/query_processor_test.clj index 91c327d4fdbb56be72d8cc209e2b7e55d153ed22..469eb3ddd34ebceed2f402c9c4733371f0a8ff79 100644 --- a/test/metabase/query_processor_test.clj +++ b/test/metabase/query_processor_test.clj @@ -120,11 +120,12 @@ :base_type (data/expected-base-type->actual :type/Text) :name (data/format-name "name") :display_name "Name" - :fingerprint {:global {:distinct-count 75} + :fingerprint {:global {:distinct-count 75 + :nil% 0.0} :type {:type/Text {:percent-json 0.0 :percent-url 0.0 :percent-email 0.0 - :average-length 8.333}}}}))) + :average-length 8.33}}}}))) ;; #### users (defn users-col @@ -144,17 +145,19 @@ :base_type (data/expected-base-type->actual :type/Text) :name (data/format-name "name") :display_name "Name" - :fingerprint {:global {:distinct-count 15} + :fingerprint {:global {:distinct-count 15 + :nil% 0.0} :type {:type/Text {:percent-json 0.0 :percent-url 0.0 :percent-email 0.0 - :average-length 13.267}}}} + :average-length 13.27}}}} :last_login {:special_type nil :base_type (data/expected-base-type->actual :type/DateTime) :name (data/format-name "last_login") :display_name "Last Login" :unit :default - :fingerprint {:global {:distinct-count 15} + :fingerprint {:global {:distinct-count 15 + :nil% 0.0} :type {:type/DateTime {:earliest "2014-01-01T08:30:00.000Z" :latest "2014-12-05T15:15:00.000Z"}}}}))) @@ -184,28 +187,39 @@ :name (data/format-name "category_id") :display_name "Category ID" :fingerprint (if (data/fks-supported?) - {:global {:distinct-count 28}} - {:global {:distinct-count 28}, :type {:type/Number {:min 2.0, :max 74.0, :avg 29.98}}})} + {:global {:distinct-count 28 + :nil% 0.0}} + {:global {:distinct-count 28 + :nil% 0.0}, + :type {:type/Number {:min 2.0, :max 74.0, :avg 29.98, :q1 7.0, :q3 49.0 :sd 23.06}}})} :price {:special_type :type/Category :base_type (data/expected-base-type->actual :type/Integer) :name (data/format-name "price") :display_name "Price" - :fingerprint {:global {:distinct-count 4}, :type {:type/Number {:min 1.0, :max 4.0, :avg 2.03}}}} + :fingerprint {:global {:distinct-count 4 + :nil% 0.0}, + :type {:type/Number {:min 1.0, :max 4.0, :avg 2.03, :q1 1.0, :q3 2.0 :sd 0.77}}}} :longitude {:special_type :type/Longitude :base_type (data/expected-base-type->actual :type/Float) :name (data/format-name "longitude") - :fingerprint {:global {:distinct-count 84}, :type {:type/Number {:min -165.374, :max -73.953, :avg -115.998}}} + :fingerprint {:global {:distinct-count 84 + :nil% 0.0}, + :type {:type/Number {:min -165.37, :max -73.95, :avg -116.0 :q1 -122.0, :q3 -118.0 :sd 14.16}}} :display_name "Longitude"} :latitude {:special_type :type/Latitude :base_type (data/expected-base-type->actual :type/Float) :name (data/format-name "latitude") :display_name "Latitude" - :fingerprint {:global {:distinct-count 94}, :type {:type/Number {:min 10.065, :max 40.779, :avg 35.506}}}} + :fingerprint {:global {:distinct-count 94 + :nil% 0.0}, + :type {:type/Number {:min 10.06, :max 40.78, :avg 35.51, :q1 34.0, :q3 38.0 :sd 3.43}}}} :name {:special_type :type/Name :base_type (data/expected-base-type->actual :type/Text) :name (data/format-name "name") :display_name "Name" - :fingerprint {:global {:distinct-count 100}, :type {:type/Text {:percent-json 0.0, :percent-url 0.0, :percent-email 0.0, :average-length 15.63}}}}))) + :fingerprint {:global {:distinct-count 100 + :nil% 0.0}, + :type {:type/Text {:percent-json 0.0, :percent-url 0.0, :percent-email 0.0, :average-length 15.63}}}}))) (defn venues-cols "`cols` information for all the columns in `venues`." @@ -231,8 +245,11 @@ :name (data/format-name "venue_id") :display_name "Venue ID" :fingerprint (if (data/fks-supported?) - {:global {:distinct-count 100}} - {:global {:distinct-count 100}, :type {:type/Number {:min 1.0, :max 100.0, :avg 51.965}}})} + {:global {:distinct-count 100 + :nil% 0.0}} + {:global {:distinct-count 100 + :nil% 0.0}, + :type {:type/Number {:min 1.0, :max 100.0, :avg 51.97, :q1 28.0, :q3 76.0 :sd 28.51}}})} :user_id {:special_type (if (data/fks-supported?) :type/FK :type/Category) @@ -240,8 +257,11 @@ :name (data/format-name "user_id") :display_name "User ID" :fingerprint (if (data/fks-supported?) - {:global {:distinct-count 15}} - {:global {:distinct-count 15}, :type {:type/Number {:min 1.0, :max 15.0, :avg 7.929}}})}))) + {:global {:distinct-count 15 + :nil% 0.0}} + {:global {:distinct-count 15 + :nil% 0.0}, + :type {:type/Number {:min 1.0, :max 15.0, :avg 7.93 :q1 4.0, :q3 11.0 :sd 3.99}}})}))) ;;; #### aggregate columns diff --git a/test/metabase/query_processor_test/aggregation_test.clj b/test/metabase/query_processor_test/aggregation_test.clj index 45729eecb8b6ea97523274fd28096a9f7d6d403f..258ca94ff0c32aef429062e976b068ab81b0cf76 100644 --- a/test/metabase/query_processor_test/aggregation_test.clj +++ b/test/metabase/query_processor_test/aggregation_test.clj @@ -258,7 +258,8 @@ {:aggregation [[:cum-sum $id]] :breakout [$price]}) booleanize-native-form - (format-rows-by [int int]))) + (format-rows-by [int int]) + tu/round-fingerprint-cols)) ;;; ------------------------------------------------ CUMULATIVE COUNT ------------------------------------------------ @@ -324,8 +325,8 @@ {:aggregation [[:cum-count $id]] :breakout [$price]}) booleanize-native-form - (format-rows-by [int int]))) - + (format-rows-by [int int]) + tu/round-fingerprint-cols)) ;; Does Field.settings show up for aggregate Fields? (expect diff --git a/test/metabase/query_processor_test/breakout_test.clj b/test/metabase/query_processor_test/breakout_test.clj index cec20fa963fe7130736cb9019032482c0827d27f..a1143bfacdcfd2de3f34fbfb8cce7da64c37b899 100644 --- a/test/metabase/query_processor_test/breakout_test.clj +++ b/test/metabase/query_processor_test/breakout_test.clj @@ -34,7 +34,8 @@ :breakout [$user_id] :order-by [[:asc $user_id]]}) booleanize-native-form - (format-rows-by [int int]))) + (format-rows-by [int int]) + tu/round-fingerprint-cols)) ;;; BREAKOUT w/o AGGREGATION ;; This should act as a "distinct values" query and return ordered results @@ -47,7 +48,8 @@ {:breakout [$user_id] :limit 10}) booleanize-native-form - (format-rows-by [int]))) + (format-rows-by [int]) + tu/round-fingerprint-cols)) ;;; "BREAKOUT" - MULTIPLE COLUMNS W/ IMPLICT "ORDER_BY" @@ -66,7 +68,8 @@ :breakout [$user_id $venue_id] :limit 10}) booleanize-native-form - (format-rows-by [int int int]))) + (format-rows-by [int int int]) + tu/round-fingerprint-cols)) ;;; "BREAKOUT" - MULTIPLE COLUMNS W/ EXPLICIT "ORDER_BY" ;; `breakout` should not implicitly order by any fields specified in `order-by` @@ -85,7 +88,8 @@ :order-by [[:desc $user_id]] :limit 10}) booleanize-native-form - (format-rows-by [int int int]))) + (format-rows-by [int int int]) + tu/round-fingerprint-cols)) (qp-expect-with-all-engines {:rows [[2 8 "Artisan"] @@ -115,7 +119,8 @@ :breakout [$category_id] :limit 5}) booleanize-native-form - (format-rows-by [int int str])))) + (format-rows-by [int int str]) + tu/round-fingerprint-cols))) (datasets/expect-with-engines (non-timeseries-engines-with-feature :foreign-keys) [["Wine Bar" "Thai" "Thai" "Thai" "Thai" "Steakhouse" "Steakhouse" "Steakhouse" "Steakhouse" "Southern"] diff --git a/test/metabase/query_processor_test/order_by_test.clj b/test/metabase/query_processor_test/order_by_test.clj index 9dcbca0ccfe73ae3e4233465219ce9baae77aa1a..d3edcadb6a15eca87cb98704d83991e83a947bf4 100644 --- a/test/metabase/query_processor_test/order_by_test.clj +++ b/test/metabase/query_processor_test/order_by_test.clj @@ -4,7 +4,8 @@ [metabase.models.field :refer [Field]] [metabase.query-processor-test :refer :all] [metabase.test.data :as data] - [metabase.test.data.datasets :as datasets :refer [*engine*]])) + [metabase.test.data.datasets :as datasets :refer [*engine*]] + [metabase.test.util :as tu])) (expect-with-non-timeseries-dbs [[1 12 375] @@ -44,7 +45,8 @@ :breakout [$price] :order-by [[:asc [:aggregation 0]]]}) booleanize-native-form - (format-rows-by [int int]))) + (format-rows-by [int int]) + tu/round-fingerprint-cols)) ;;; order-by aggregate ["sum" field-id] @@ -63,7 +65,8 @@ :breakout [$price] :order-by [[:desc [:aggregation 0]]]}) booleanize-native-form - (format-rows-by [int int]))) + (format-rows-by [int int]) + tu/round-fingerprint-cols)) ;;; order-by aggregate ["distinct" field-id] @@ -82,7 +85,8 @@ :breakout [$price] :order-by [[:asc [:aggregation 0]]]}) booleanize-native-form - (format-rows-by [int int]))) + (format-rows-by [int int]) + tu/round-fingerprint-cols)) ;;; order-by aggregate ["avg" field-id] @@ -102,7 +106,8 @@ :order-by [[:asc [:aggregation 0]]]}) booleanize-native-form data - (format-rows-by [int int]))) + (format-rows-by [int int]) + tu/round-fingerprint-cols)) ;;; ### order-by aggregate ["stddev" field-id] ;; SQRT calculations are always NOT EXACT (normal behavior) so round everything to the nearest int. @@ -123,4 +128,5 @@ :order-by [[:desc [:aggregation 0]]]}) booleanize-native-form data - (format-rows-by [int (comp int math/round)]))) + (format-rows-by [int (comp int math/round)]) + tu/round-fingerprint-cols)) \ No newline at end of file diff --git a/test/metabase/query_processor_test/remapping_test.clj b/test/metabase/query_processor_test/remapping_test.clj index ec2810fd4cfa11d31a496abe6235bbb9294dde46..aae49ff298885b2ae05532c1ecb45187327e15f4 100644 --- a/test/metabase/query_processor_test/remapping_test.clj +++ b/test/metabase/query_processor_test/remapping_test.clj @@ -34,7 +34,8 @@ :order-by [[:asc $name]] :limit 4}) booleanize-native-form - (format-rows-by [str int str])))) + (format-rows-by [str int str]) + tu/round-fingerprint-cols))) (defn- select-columns "Focuses the given resultset to columns that return true when passed to `columns-pred`. Typically this would be done diff --git a/test/metabase/sample_dataset_test.clj b/test/metabase/sample_dataset_test.clj index 4edb2a20eb0b1f56d98bb07ab2a682b61e84829e..271063a9269f06a55ddb54a622a49586e0ad2b7c 100644 --- a/test/metabase/sample_dataset_test.clj +++ b/test/metabase/sample_dataset_test.clj @@ -57,7 +57,8 @@ :visibility_type :normal :preview_display true :display_name "Name" - :fingerprint {:global {:distinct-count 2499} + :fingerprint {:global {:distinct-count 2499 + :nil% 0.0} :type {:type/Text {:percent-json 0.0 :percent-url 0.0 :percent-email 0.0 diff --git a/test/metabase/sync/analyze/fingerprint/fingerprinters_test.clj b/test/metabase/sync/analyze/fingerprint/fingerprinters_test.clj index 9214e52df4ad16ed9d1de4615c2275c6ea8e96ee..01aebb842e14b8413bed8697c746c26476a243ad 100644 --- a/test/metabase/sync/analyze/fingerprint/fingerprinters_test.clj +++ b/test/metabase/sync/analyze/fingerprint/fingerprinters_test.clj @@ -5,7 +5,8 @@ [metabase.util.date :as du])) (expect - {:global {:distinct-count 3} + {:global {:distinct-count 3 + :nil% 0.0} :type {:type/DateTime {:earliest (du/date->iso-8601 #inst "2013") :latest (du/date->iso-8601 #inst "2018")}}} (transduce identity @@ -13,7 +14,8 @@ [#inst "2013" #inst "2018" #inst "2015"])) (expect - {:global {:distinct-count 1} + {:global {:distinct-count 1 + :nil% 1.0} :type {:type/DateTime {:earliest nil :latest nil}}} (transduce identity @@ -21,16 +23,21 @@ (repeat 10 nil))) (expect - {:global {:distinct-count 3} + {:global {:distinct-count 3 + :nil% 0.0} :type {:type/Number {:avg 2.0 :min 1.0 - :max 3.0}}} + :max 3.0 + :q1 1.25 + :q3 2.75 + :sd 1.0}}} (transduce identity (fingerprinter (field/map->FieldInstance {:base_type :type/Number})) [1.0 2.0 3.0])) (expect - {:global {:distinct-count 5} + {:global {:distinct-count 5 + :nil% 0.0} :type {:type/Text {:percent-json 0.2, :percent-url 0.0, :percent-email 0.0, diff --git a/test/metabase/sync/analyze/query_results_test.clj b/test/metabase/sync/analyze/query_results_test.clj index a9cf399b86af3a6883a9a642ead58052a4c3ece2..727d50d01b3e0fc6162641f68d3bd64fe325870b 100644 --- a/test/metabase/sync/analyze/query_results_test.clj +++ b/test/metabase/sync/analyze/query_results_test.clj @@ -23,7 +23,7 @@ (defn- name->fingerprints [field-or-metadata] (zipmap (map column->name-keyword field-or-metadata) - (map :fingerprint field-or-metadata))) + (map :fingerprint (tu/round-fingerprint-cols field-or-metadata)))) (defn- name->special-type [field-or-metadata] (zipmap (map column->name-keyword field-or-metadata) @@ -74,10 +74,11 @@ ;; Native queries don't know what the associated Fields are for the results, we need to compute the fingerprints, but ;; they should sill be the same except for some of the optimizations we do when we have all the information. (expect - (update mutil/venue-fingerprints :category_id assoc :type {:type/Number {:min 2.0, :max 74.0, :avg 29.98}}) + (update mutil/venue-fingerprints :category_id assoc :type {:type/Number {:min 2.0, :max 74.0, :avg 29.98, :q1 7.0, :q3 49.0 :sd 23.06}}) (tt/with-temp Card [card {:dataset_query {:database (data/id) :type :native :native {:query "select * from venues"}}}] + (name->fingerprints (query->result-metadata (query-for-card card))))) diff --git a/test/metabase/test/mock/util.clj b/test/metabase/test/mock/util.clj index bfc745de1a59d8071ed7db1e0294eea5892a1509..f3c0fa45b8bc826858b7c7fdb59c62ecf5da6d44 100644 --- a/test/metabase/test/mock/util.clj +++ b/test/metabase/test/mock/util.clj @@ -43,17 +43,22 @@ (def venue-fingerprints "Fingerprints for the full venues table" - {:name {:global {:distinct-count 100}, + {:name {:global {:distinct-count 100 + :nil% 0.0}, :type {:type/Text {:percent-json 0.0, :percent-url 0.0, :percent-email 0.0, :average-length 15.63}}} :id nil - :price {:global {:distinct-count 4}, - :type {:type/Number {:min 1.0, :max 4.0, :avg 2.03}}} - :latitude {:global {:distinct-count 94}, - :type {:type/Number {:min 10.06, :max 40.78, :avg 35.51}}} - :category_id {:global {:distinct-count 28}} - :longitude {:global {:distinct-count 84}, - :type {:type/Number {:min -165.37, :max -73.95, :avg -116.0}}}}) + :price {:global {:distinct-count 4 + :nil% 0.0}, + :type {:type/Number {:min 1.0, :max 4.0, :avg 2.03, :q1 1.0, :q3 2.0 :sd 0.77}}} + :latitude {:global {:distinct-count 94 + :nil% 0.0}, + :type {:type/Number {:min 10.06, :max 40.78, :avg 35.51, :q1 34.0, :q3 38.0 :sd 3.43}}} + :category_id {:global {:distinct-count 28 + :nil% 0.0}} + :longitude {:global {:distinct-count 84 + :nil% 0.0}, + :type {:type/Number {:min -165.37, :max -73.95, :avg -116.0 :q1 -122.0, :q3 -118.0 :sd 14.16}}}}) ;; This is just a fake implementation that just swoops in and returns somewhat-correct looking results for different ;; queries we know will get ran as part of sync diff --git a/test/metabase/test/util.clj b/test/metabase/test/util.clj index 8966a6b4003667910ee336b8b5c52069ecd4331b..705fe1a975a5e206d0be6abab7124e327077a22d 100644 --- a/test/metabase/test/util.clj +++ b/test/metabase/test/util.clj @@ -406,26 +406,33 @@ m (apply update-in m ks f args))) -(defn- round-fingerprint-fields [fprint-type-map fields] +(defn- round-fingerprint-fields [fprint-type-map decimal-places fields] (reduce (fn [fprint field] (update-in-if-present fprint [field] (fn [num] (if (integer? num) num - (u/round-to-decimals 3 num))))) + (u/round-to-decimals decimal-places num))))) fprint-type-map fields)) (defn round-fingerprint - "Rounds the numerical fields of a fingerprint to 4 decimal places" + "Rounds the numerical fields of a fingerprint to 2 decimal places" [field] (-> field - (update-in-if-present [:fingerprint :type :type/Number] round-fingerprint-fields [:min :max :avg]) - (update-in-if-present [:fingerprint :type :type/Text] round-fingerprint-fields [:percent-json :percent-url :percent-email :average-length]))) - -(defn round-fingerprint-cols [query-results] - (let [maybe-data-cols (if (contains? query-results :data) - [:data :cols] - [:cols])] - (update-in query-results maybe-data-cols #(map round-fingerprint %)))) + (update-in-if-present [:fingerprint :type :type/Number] round-fingerprint-fields 2 [:min :max :avg :sd]) + ;; quartal estimation is order dependent and the ordering is not stable across different DB engines, hence more aggressive trimming + (update-in-if-present [:fingerprint :type :type/Number] round-fingerprint-fields 0 [:q1 :q3]) + (update-in-if-present [:fingerprint :type :type/Text] round-fingerprint-fields 2 [:percent-json :percent-url :percent-email :average-length]))) + +(defn round-fingerprint-cols + ([query-results] + (if (map? query-results) + (let [maybe-data-cols (if (contains? query-results :data) + [:data :cols] + [:cols])] + (round-fingerprint-cols maybe-data-cols query-results)) + (map round-fingerprint query-results))) + ([k query-results] + (update-in query-results k #(map round-fingerprint %)))) (defn postwalk-pred "Transform `form` by applying `f` to each node where `pred` returns true" diff --git a/yarn.lock b/yarn.lock index 6132985205478ea3100e74a9293fa2e4dd5a5e97..f9ead6746ad6e7820ab1cec06894c2081e6c723f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3713,6 +3713,11 @@ dom-serializer@0, dom-serializer@~0.1.0: domelementtype "~1.1.1" entities "~1.1.1" +dom-walk@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018" + integrity sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg= + domain-browser@^1.1.1: version "1.1.7" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" @@ -5192,6 +5197,14 @@ global-dirs@^0.1.0: dependencies: ini "^1.3.4" +global@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/global/-/global-4.3.2.tgz#e76989268a6c74c38908b1305b10fc0e394e9d0f" + integrity sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8= + dependencies: + min-document "^2.19.0" + process "~0.5.1" + globals-docs@^2.3.0: version "2.4.0" resolved "https://registry.yarnpkg.com/globals-docs/-/globals-docs-2.4.0.tgz#f2c647544eb6161c7c38452808e16e693c2dafbb" @@ -8075,6 +8088,13 @@ mimic-fn@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" integrity sha1-5md4PZLonb00KBi1IwudYqZyrRg= +min-document@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" + integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU= + dependencies: + dom-walk "^0.1.0" + minimalistic-assert@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" @@ -10141,6 +10161,11 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= +process@~0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf" + integrity sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8= + progress@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" @@ -13773,6 +13798,14 @@ xdg-basedir@^3.0.0: resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ= +xhr-mock@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/xhr-mock/-/xhr-mock-2.4.1.tgz#cb502e3d50b8b2ec31bd61766ce516bfc1dd072f" + integrity sha1-y1AuPVC4suwxvWF2bOUWv8HdBy8= + dependencies: + global "^4.3.0" + url "^0.11.0" + xml-char-classes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/xml-char-classes/-/xml-char-classes-1.0.0.tgz#64657848a20ffc5df583a42ad8a277b4512bbc4d"