Skip to content

Commit

Permalink
Merge branch 'master' into caching
Browse files Browse the repository at this point in the history
  • Loading branch information
camsaul committed Apr 3, 2017
2 parents d1119f8 + c4bb7cc commit 2925b3d
Show file tree
Hide file tree
Showing 74 changed files with 1,123 additions and 561 deletions.
24 changes: 21 additions & 3 deletions docs/operations-guide/start.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,33 @@ Step-by-step instructions on how to upgrade Metabase running on Heroku.

# Troubleshooting Common Problems

### Metabase fails to startup
### Metabase fails to start due to database locks

Sometimes Metabase will fail to complete its startup due to a database lock that was not cleared properly.
Sometimes Metabase will fail to complete its startup due to a database lock that was not cleared properly. The error message will look something like:

liquibase.exception.DatabaseException: liquibase.exception.LockException: Could not acquire change log lock.

When this happens, go to a terminal where Metabase is installed and run:

java -jar metabase.jar migrate release-locks

in the command line to manually clear the locks. Then restart your Metabase instance.
in the command line to manually clear the locks. Then restart your Metabase instance.

### Metabase fails to start due to OutOfMemoryErrors

On Java 7, Metabase may fail to launch with a message like

java.lang.OutOfMemoryError: PermGen space

or one like

Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler

If this happens, setting a few JVM options should fix your issue:

java -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC -XX:MaxPermSize=256m -jar target/uberjar/metabase.jar

Alternatively, you can upgrade to Java 8 instead, which will fix the issue as well.


# Configuring the Metabase Application Database
Expand Down
33 changes: 31 additions & 2 deletions docs/users-guide/04-visualizing-results.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ In Metabase, an answer to a question can be visualized in a number of ways:
* Area chart
* Scatterplot or bubble chart
* Pie/donut chart
* Funnel
* Map

To change how the answer to your question is displayed, click on the Visualization dropdown menu beneath the question builder bar.
Expand All @@ -29,14 +30,26 @@ Each visualization type has its own advanced options you can tweak. Just click t
#### Numbers
This option is for displaying a single number, nice and big. The options for numbers include adding character prefixes or suffixes to it (so you can do things like put a currency symbol in front or a percent at the end), setting the number of decimal places you want to include, and multiplying your result by a number (like if you want to multiply a decimal by 100 to make it look like a percent).

![Number](images/visualizations/number.png)

#### 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 bar](images/visualizations/progress.png)

#### Tables
The Table option is good for looking at tabular data (duh), or for lists of things like users. The options allow you to hide and 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. The options allow you to hide and rearrange fields in the table you're looking at. 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.

![Pivot table](images/visualizations/pivot.png)

#### Line, bar, and area charts
Line charts are best for displaying the trend of a number over time, especially when you have lots of x-axis values. Bar charts are great for displaying a metric grouped by a category (e.g., the number of users you have by country), and they can also be useful for showing a number over time if you have a smaller number of x-axis values (like orders per month this year). Area charts are useful when comparing the the proportions between two metrics over time. Both bar and area charts can be stacked.
Line charts are best for displaying the trend of a number over time, especially when you have lots of x-axis values. Bar charts are great for displaying a metric grouped by a category (e.g., the number of users you have by country), and they can also be useful for showing a number over time if you have a smaller number of x-axis values (like orders per month this year).

![Bar chart](images/visualizations/bar.png)

Area charts are useful when comparing the the proportions between two metrics over time. Both bar and area charts can be stacked.

![Stacked area chart](images/visualizations/area.png)

These three charting types have very similar options, which are broken up into the following:

Expand All @@ -52,18 +65,34 @@ If you have a third numeric field, you can also create a bubble chart. Select th

Scatterplots and bubble charts also have similar chart options as line, bar, and area charts.

![Scatter](images/visualizations/scatter.png)

#### 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.

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.

![Donut](images/visualizations/donut.png)

#### Funnel
Funnels are commonly used in e-commerce or sales to visualize how many customers are present within each step of a checkout flow or sales cycle. At their most general, funnels show you values broken out by steps, and the percent decrease between each successive step. To create a funnel in Metabase, you'll need to have a table with at least two columns: one column that contains the metric you're interested in, and another that contains the funnel's steps.

For example, I might have an Opportunities table, and I could create a question that gives me the number of sales leads broken out by a field that contains stages such as `Prospecting`, `Qualification`, `Proposal`, `Negotiation`, and `Closed`. In this example, the percentages shown along the x-axis tell you what percent of the total starting opportunities are still present at each subsequent step; so 18.89% of our total opportunities have made it all the way to being closed deals. The number below each percent is the actual value of the count at that step — in our example, the actual number of opportunities that are currently at each step. Together, these numbers help you figure out where you're losing your customers or users.

![Funnel](images/visualizations/funnel.png)

#### 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.
* **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.)

![Region map](images/visualizations/map.png)

* **Pin Map** — If your table contains a latitude and longitude field, Metabase will try to display it as a pin map of the world. This will put one pin on the map for each row in your table, based on the latitude and longitude fields. You can try this with the Sample Dataset that's included in Metabase: start a new question and select the People table, use `raw data` for your view, and choose the Map option for your visualization. you'll see a map of the world, with each dot representing the latitude and longitude coordinates of a single person from the People table.

![Pin map](images/visualizations/pin-map.png)

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.
Expand Down
Binary file added docs/users-guide/images/visualizations/area.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/users-guide/images/visualizations/bar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/users-guide/images/visualizations/donut.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/users-guide/images/visualizations/map.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/users-guide/images/visualizations/number.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/users-guide/images/visualizations/pivot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions frontend/src/metabase/css/core/text.css
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@
line-height: 1.5em;
}

.text-small {
font-size: 0.875em;
}

.text-current {
color: currentColor;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import DateRangeWidget from "./widgets/DateRangeWidget.jsx";
import DateRelativeWidget from "./widgets/DateRelativeWidget.jsx";
import DateMonthYearWidget from "./widgets/DateMonthYearWidget.jsx";
import DateQuarterYearWidget from "./widgets/DateQuarterYearWidget.jsx";
import DateAllOptionsWidget from "./widgets/DateAllOptionsWidget.jsx";
import CategoryWidget from "./widgets/CategoryWidget.jsx";
import TextWidget from "./widgets/TextWidget.jsx";

Expand All @@ -22,6 +23,7 @@ const WIDGETS = {
"date/relative": DateRelativeWidget,
"date/month-year": DateMonthYearWidget,
"date/quarter-year": DateQuarterYearWidget,
"date/all-options": DateAllOptionsWidget
}

export default class ParameterValueWidget extends Component {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const ParameterOptionsSectionsPane = ({ sections, onSelectSection }) =>

const ParameterOptionItem = ({ option, onClick }) =>
<li onClick={onClick} className="p1 px2 cursor-pointer brand-hover">
<div className="text-brand text-bold">{option.name}</div>
<div className="text-brand text-bold">{option.menuName || option.name}</div>
<div>{option.description}</div>
</li>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/* @flow */

import React, {Component, PropTypes} from "react";
import cx from "classnames";

import DatePicker, {DATE_OPERATORS} from "metabase/query_builder/components/filters/pickers/DatePicker.jsx";
import {generateTimeFilterValuesDescriptions} from "metabase/lib/query_time";

import type {OperatorName} from "metabase/query_builder/components/filters/pickers/DatePicker.jsx";
import type {FieldFilter, LocalFieldReference} from "metabase/meta/types/Query";

type UrlEncoded = string;
// $FlowFixMe
type RegexMatches = [string];
type Deserializer = (RegexMatches) => FieldFilter;

// Use a placeholder value as field references are not used in dashboard filters
// $FlowFixMe
const noopRef: LocalFieldReference = null;

function getFilterValueSerializer(func: ((val1: string, val2: string) => UrlEncoded)) {
// $FlowFixMe
return filter => func(filter[2], filter[3])
}

const serializersByOperatorName: { [id: OperatorName]: (FieldFilter) => UrlEncoded } = {
// $FlowFixMe
"Previous": getFilterValueSerializer((value, unit) => `past${-value}${unit}s`),
"Next": getFilterValueSerializer((value, unit) => `next${value}${unit}s`),
"Current": getFilterValueSerializer((_, unit) => `this${unit}`),
"Before": getFilterValueSerializer((value) => `~${value}`),
"After": getFilterValueSerializer((value) => `${value}~`),
"On": getFilterValueSerializer((value) => `${value}`),
"Between": getFilterValueSerializer((from, to) => `${from}~${to}`)
};

function getFilterOperator(filter) {
return DATE_OPERATORS.find((op) => op.test(filter));
}
function filterToUrlEncoded(filter: FieldFilter): ?UrlEncoded {
const operator = getFilterOperator(filter)

if (operator) {
return serializersByOperatorName[operator.name](filter);
} else {
return null;
}
}

const deserializersWithTestRegex: [{ testRegex: RegExp, deserialize: Deserializer}] = [
{testRegex: /^past([0-9]+)([a-z]+)s$/, deserialize: (matches) => {
return ["time-interval", noopRef, -parseInt(matches[0]), matches[1]]
}},
{testRegex: /^next([0-9]+)([a-z]+)s$/, deserialize: (matches) => {
return ["time-interval", noopRef, parseInt(matches[0]), matches[1]]
}},
{testRegex: /^this([a-z]+)$/, deserialize: (matches) => ["time-interval", noopRef, "current", matches[0]] },
{testRegex: /^~([0-9-T:]+)$/, deserialize: (matches) => ["<", noopRef, matches[0]]},
{testRegex: /^([0-9-T:]+)~$/, deserialize: (matches) => [">", noopRef, matches[0]]},
{testRegex: /^([0-9-T:]+)$/, deserialize: (matches) => ["=", noopRef, matches[0]]},
// TODO 3/27/17 Atte Keinänen
// Unify BETWEEN -> between, IS_NULL -> is-null, NOT_NULL -> not-null throughout the codebase
// $FlowFixMe
{testRegex: /^([0-9-T:]+)~([0-9-T:]+)$/, deserialize: (matches) => ["BETWEEN", noopRef, matches[0], matches[1]]},
];

function urlEncodedToFilter(urlEncoded: UrlEncoded): ?FieldFilter {
const deserializer =
deserializersWithTestRegex.find((des) => urlEncoded.search(des.testRegex) !== -1);

if (deserializer) {
const substringMatches = deserializer.testRegex.exec(urlEncoded).splice(1);
return deserializer.deserialize(substringMatches);
} else {
return null;
}
}

const prefixedOperators: [OperatorName] = ["Before", "After", "On", "Is Empty", "Not Empty"];
function getFilterTitle(filter) {
const desc = generateTimeFilterValuesDescriptions(filter).join(" - ")
const op = getFilterOperator(filter);
const prefix = op && prefixedOperators.indexOf(op.name) !== -1 ? `${op.name} ` : "";
return prefix + desc;
}

type Props = {
setValue: (value: ?string) => void,
onClose: () => void
};

type State = { filter: FieldFilter };

export default class DateAllOptionsWidget extends Component<*, Props, State> {
state: State;

constructor(props: Props) {
super(props);

this.state = {
// $FlowFixMe
filter: props.value != null ? urlEncodedToFilter(props.value) || [] : []
}
}

static propTypes = {};
static defaultProps = {};

static format = (urlEncoded: ?string) => {
if (urlEncoded == null) return null;
const filter = urlEncodedToFilter(urlEncoded);

return filter ? getFilterTitle(filter) : null;
};

commitAndClose = () => {
this.props.setValue(filterToUrlEncoded(this.state.filter));
this.props.onClose()
}

setFilter = (filter: FieldFilter) => {
this.setState({filter});
}

isValid() {
const filterValues = this.state.filter.slice(2);
return filterValues.every((value) => value != null);
}

render() {
return (<div style={{minWidth: "300px"}}>
<DatePicker
filter={this.state.filter}
onFilterChange={this.setFilter}
hideEmptinessOperators
hideTimeSelectors
/>
<div className="FilterPopover-footer p1">
<button
className={cx("Button Button--purple full", {"disabled": !this.isValid()})}
onClick={this.commitAndClose}
>
Update filter
</button>
</div>
</div>)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
position: relative;
height: 0;
line-height: 0;
margin-left: 0.5em;
margin-left: -0.45em;
padding: 0 0.5em;
}

:local(.container.deemphasized) {
Expand Down
58 changes: 27 additions & 31 deletions frontend/src/metabase/home/components/RecentViews.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,9 @@ import Icon from "metabase/components/Icon.jsx";
import SidebarSection from "./SidebarSection.jsx";
import Urls from "metabase/lib/urls";

export default class RecentViews extends Component {
constructor(props, context) {
super(props, context);

this.state = { error : null };
}
import { normal } from "metabase/lib/colors";

export default class RecentViews extends Component {
static propTypes = {
fetchRecentViews: PropTypes.func.isRequired,
recentViews: PropTypes.array.isRequired
Expand All @@ -22,46 +18,46 @@ export default class RecentViews extends Component {
}

async componentDidMount() {
try {
await this.props.fetchRecentViews();
} catch (error) {
this.setState({ error });
}
this.props.fetchRecentViews();
}

renderIllustration(item) {
if (item.model === 'card' && 'display' in item.model_object) {
return (
<Icon name={'illustration-'+item.model_object.display} size={22} />
);

} else if(item.model === 'dashboard') {
return (
<Icon name={'illustration-dashboard'} size={22} />
);

getIconName({ model, model_object}) {
if (model === 'card' && 'display' in model_object) {
return model_object.display
} else if(model === 'dashboard') {
return 'dashboard'
} else {
return null;
}
}

render() {
let { recentViews } = this.props;

const { recentViews } = this.props;
return (
<SidebarSection title="Recently Viewed" icon="clock">
{recentViews.length > 0 ?
<ul className="p2">
{recentViews.map((item, index) =>
<li key={index} className="py1 ml1 flex align-center clearfix">
{this.renderIllustration(item)}
<Link to={Urls.modelToUrl(item.model, item.model_id)} data-metabase-event={"Recent Views;"+item.model+";"+item.cnt} className="ml1 flex-full link">{item.model_object.name}</Link>
</li>
)}
{recentViews.map((item, index) => {
const iconName = this.getIconName(item);
return (
<li key={index} className="py1 ml1 flex align-center clearfix">
<Icon
name={iconName}
size={18}
style={{ color: iconName === 'dashboard' ? normal.purple : normal.blue }}
/>
<Link to={Urls.modelToUrl(item.model, item.model_id)} data-metabase-event={"Recent Views;"+item.model+";"+item.cnt} className="ml1 flex-full link">
{item.model_object.name}
</Link>
</li>
);
})}
</ul>
:
<div className="flex flex-column layout-centered text-normal text-grey-2">
<p className="p3 text-centered text-grey-2" style={{ "maxWidth": "100%" }}>You haven't looked at any dashboards or questions recently</p>
<p className="p3 text-centered text-grey-2" style={{ "maxWidth": "100%" }}>
You haven't looked at any dashboards or questions recently
</p>
</div>
}
</SidebarSection>
Expand Down
Loading

0 comments on commit 2925b3d

Please sign in to comment.