Skip to content
Snippets Groups Projects
Unverified Commit 5dc87dad authored by Tom Robinson's avatar Tom Robinson
Browse files

Merge branch 'master' of github.com:metabase/metabase into funnel-visualization-single-channel

parents e5d3078a 985e1c59
No related merge requests found
Showing
with 175 additions and 41 deletions
......@@ -4,8 +4,7 @@
"add-react-displayname",
"transform-decorators-legacy",
["transform-builtin-extend", {
"globals": ["Error"],
"approximate": true
"globals": ["Error", "Array"]
}]
],
"presets": ["es2015", "stage-0", "react"],
......
......@@ -45,7 +45,8 @@
"env": {
"browser": true,
"es6": true,
"commonjs": true
"commonjs": true,
"jest": true
},
"parser": "babel-eslint",
"plugins": [
......
......@@ -16,7 +16,7 @@ Metabase is the easy, open source way for everyone in your company to ask questi
- Let anyone on your team [ask questions](http://www.metabase.com/docs/latest/users-guide/03-asking-questions) without knowing SQL
- Rich beautiful [dashboards](http://www.metabase.com/docs/latest/users-guide/05-sharing-answers) with auto refresh and fullscreen
- SQL Mode for analysts and data pros
- Create canonical [segments and metrics](http://www.metabase.com/docs/latest/administration-guide/06-segments-and-metrics) for your team to use
- Create canonical [segments and metrics](http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics) for your team to use
- Send data to Slack or email on a schedule with [Pulses](http://www.metabase.com/docs/latest/users-guide/09-pulses)
- View data in Slack anytime with [Metabot](http://www.metabase.com/docs/latest/users-guide/10-metabot)
- [Humanize data](http://www.metabase.com/docs/latest/administration-guide/03-metadata-editing) for your team by renaming, annotating and hiding fields
......
......@@ -23,7 +23,7 @@ node-2() {
if is_engine_enabled "crate"; then
run_step install-crate
fi
MB_DB_TYPE=mysql MB_DB_DBNAME=circle_test MB_DB_PORT=3306 MB_DB_USER=ubuntu MB_DB_HOST=localhost \
MB_ENCRYPTION_SECRET_KEY='Orw0AAyzkO/kPTLJRxiyKoBHXa/d6ZcO+p+gpZO/wSQ=' MB_DB_TYPE=mysql MB_DB_DBNAME=circle_test MB_DB_PORT=3306 MB_DB_USER=ubuntu MB_DB_HOST=localhost \
run_step lein-test
}
node-3() {
......@@ -45,6 +45,7 @@ node-5() {
run_step lein eastwood
run_step yarn run lint
run_step yarn run test
run_step yarn run test-jest
run_step yarn run flow
}
node-6() {
......
......@@ -7,15 +7,23 @@ Starting in v0.15.0 Metabase provides a driver for connecting to BigQuery direct
1. make sure you have a [Google Cloud Platform](https://cloud.google.com/) account with a Project you would like to use in Metabase.
* Start by giving this connection a __Name__ and providing your Google Cloud Platform __Project ID__ along with your desired BigQuery __Dataset ID__. If you don't have a dataset and want to play around with something we recommend copying one of the [sample tables](https://cloud.google.com/bigquery/sample-tables)
![basicfields](../images/bigquery_basic.png)
![Basic Fields](../images/bigquery_basic.png)
* Follow the `Click here` link provided below the __Client ID__ field which will open a new browser tab and guide you through the process of generating OAuth 2.0 credentials for Metabase. Make sure to choose `Other` for your application type.
![clientid](../images/bigquery_clientid.png)
![Client ID](../images/bigquery_clientid.png)
* take the resulting client ID and client secret and copy them over to Metabase.
![clientid](../images/bigquery_clientdetails.png)
![Client Details](../images/bigquery_clientdetails.png)
* Now follow the link below the __Auth Code__ field for `Click here to get an auth code` which will open a new browser window and authorize your credentials for a BigQuery access token to use the api. Simply click the `Allow` button.
![clientid](../images/bigquery_authcode.png)
![Generating an Auth Code](../images/bigquery_authcode.png)
* Copy the resulting code provided into the __Auth Code__ field in Metabase.
![clientid](../images/bigquery_copycode.png)
![Copying the Auth Code](../images/bigquery_copycode.png)
* Click the `Save` button!
Metabase will now begin inspecting your BigQuery Dataset and finding any tables and fields to build up a sense for the schema. Give it a little bit of time to do its work and then you're all set to start querying.
## Using Standard SQL
By default, Metabase tells BigQuery to interpret queries as [Legacy SQL](https://cloud.google.com/bigquery/docs/reference/legacy-sql). If you prefer using
[Standard SQL](https://cloud.google.com/bigquery/docs/reference/standard-sql/) instead, you can tell Metabase to do so by including a `#standardSQL` directive at the beginning of your query:
![Enabling Standard SQL](../images/bigquery_standard_sql.png)
docs/administration-guide/images/bigquery_standard_sql.png

12.4 KiB

......@@ -8,6 +8,7 @@
* [Migrating from using the H2 database to MySQL or Postgres](#migrating-from-using-the-h2-database-to-mysql-or-postgres)
* [Running database migrations manually](#running-metabase-database-migrations-manually)
* [Backing up Metabase Application Data](#backing-up-metabase-application-data)
* [Encrypting your database connection details at rest](#encrypting-your-database-connection-details-at-rest)
* [Customizing the Metabase Jetty Webserver](#customizing-the-metabase-jetty-webserver)
* [Changing password complexity](#changing-metabase-password-complexity)
* [Handling Timezones](#handling-timezones-in-metabase)
......@@ -221,6 +222,27 @@ Instructions can be found in the [Amazon RDS User Guide](http://docs.aws.amazon.
Simply follow the same instructions you would use for making any normal database backup. It's a large topic more fit for a DBA to answer, but as long as you have a dump of the Metabase database you'll be good to go.
# Encrypting your database connection details at rest
Metabase stores connection information for the various databases you add in the Metabase application database. To prevent bad actors from being able to access these details if they were to gain access to
the application DB, Metabase can automatically encrypt them when they are saved, and decrypt them on-the-fly whenever they are needed. The only thing you need to do is set the environment variable
`MB_ENCRYPTION_SECRET_KEY`.
Your secret key must be at least 16 characters (longer is even better!), and we recommend using a secure random key generator to generate it. `openssl` is a good choice:
openssl rand -base64 32
This gives you a cryptographically-secure, randomly-generated 32-character key that will look something like `IYqrSi5QDthvFWe4/WdAxhnra5DZC3RKx3ZSrOJDKsM=`. Set it as an environment variable and
start Metabase as usual:
MB_ENCRYPTION_SECRET_KEY='IYqrSi5QDthvFWe4/WdAxhnra5DZC3RKx3ZSrOJDKsM=' java -jar metabase.jar
Metabase will securely encrypt and store the connection details for any new Databases you add. (Connection details for existing databases will be encrypted as well if you save them in the admin panel).
Existing databases with unencrypted details will continue to work normally.
Take care not to lose this key because you can't decrypt connection details without it. If you lose (or change) it, you'll have to reset all of the connection details that have been encrypted with it in the Admin Panel.
# Customizing the Metabase Jetty webserver
In most cases there will be no reason to modify any of the settings around how Metabase runs its embedded Jetty webserver to host the application, but if you wish to run HTTPS directly with your Metabase server or if you need to run on another port, that's all configurable.
......@@ -289,6 +311,7 @@ To ensure proper reporting it's important that timezones be set consistently in
Common Pitfalls:
1. Your database is using date/time columns without any timezone information. Typically when this happens your database will assume all the data is from whatever timezone the database is configured in or possible just default to UTC (check your database vendor to be sure).
2. Your JVM timezone is not the same as your Metabase `Report Timezone` choice. This is a very common issue and can be corrected by launching java with the `-Duser.timezone=<timezone>` option properly set to match your Metabase report timezone.
......
......@@ -39,6 +39,7 @@ The Table option is good for looking at tabular data (duh), or for lists of thin
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.
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.
* **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.
......
......@@ -23,7 +23,8 @@ export default class App extends Component {
<Navbar location={location} className="flex-no-shrink" />
{ errorPage && errorPage.status === 403 ?
<Unauthorized />
: errorPage && errorPage.status === 404 ?
: errorPage ?
// TODO: different error page for non-404 errors
<NotFound />
:
children
......
......@@ -21,7 +21,7 @@ const SECTIONS = [
type: "string"
},
{
key: "-site-url",
key: "site-url",
display_name: "Site URL",
type: "string"
},
......
import React from 'react';
import renderer from 'react-test-renderer';
import { render } from 'enzyme';
import Button from './Button';
describe('Button', () => {
it('should render correctly', () => {
const tree = renderer.create(
<Button>Clickity click</Button>
).toJSON();
expect(tree).toMatchSnapshot()
})
it('should render correctly with an icon', () => {
const tree = renderer.create(
<Button icon='star'>
Clickity click
</Button>
).toJSON();
expect(tree).toMatchSnapshot()
})
it('should render a primary button given the primary prop', () => {
const button = render(
<Button primary>
Clickity click
</Button>
)
expect(button.find('button.Button--primary').length).toEqual(1)
})
})
......@@ -67,6 +67,7 @@
.ColumnarSelector-row .Icon-check {
visibility: hidden;
padding-right: 0.5rem;
}
.ColumnarSelector-row.ColumnarSelector-row--selected .Icon-check {
......
/* @flow */
import React, { Component, PropTypes } from "react";
import ReactDOM from "react-dom";
import { constrainToScreen } from "metabase/lib/dom";
type Props = {
directions: Array<"top"|"bottom">,
padding: number,
children: React$Element<any>
};
export default class ConstrainToScreen extends Component<*, Props, *> {
static defaultProps = {
directions: ["top", "bottom"],
padding: 10
}
componentDidMount() {
this.componentDidUpdate();
}
componentDidUpdate() {
const { directions, padding } = this.props;
const element = ReactDOM.findDOMNode(this);
for (const direction of directions) {
constrainToScreen(element, direction, padding);
}
}
render() {
return React.Children.only(this.props.children);
}
}
......@@ -65,10 +65,10 @@ ModalHeader.contextTypes = MODAL_CHILD_CONTEXT_TYPES;
export const ModalBody = ({ children }, { fullPageModal, formModal }) =>
<div
className={cx("ModalBody", { "px4": formModal, "flex flex-full": !formModal })}
className={cx("ModalBody", { "px4": formModal })}
>
<div
className="flex-full ml-auto mr-auto flex flex-column"
className="ml-auto mr-auto"
style={{ maxWidth: (formModal && fullPageModal) ? FORM_WIDTH : undefined }}
>
{children}
......
......@@ -5,6 +5,8 @@ import ReactCSSTransitionGroup from "react-addons-css-transition-group";
import OnClickOutsideWrapper from "./OnClickOutsideWrapper";
import Tether from "tether";
import { constrainToScreen } from "metabase/lib/dom";
import cx from "classnames";
export default class Popover extends Component {
......@@ -239,21 +241,15 @@ export default class Popover extends Component {
}
if (this.props.sizeToFit) {
const verticalMargin = 5;
const verticalPadding = 5;
const body = tetherOptions.element.querySelector(".PopoverBody");
if (this._tether.attachment.top === "top") {
let screenBottom = window.innerHeight + window.scrollY;
let overflowY = body.getBoundingClientRect().bottom - screenBottom;
if (overflowY + verticalMargin > 0) {
body.style.maxHeight = (body.getBoundingClientRect().height - overflowY - verticalMargin) + "px";
if (constrainToScreen(body, "bottom", verticalPadding)) {
body.classList.add("scroll-y");
body.classList.add("scroll-show");
}
} else {
let screenTop = window.scrollY;
let overflowY = screenTop - body.getBoundingClientRect().top;
if (overflowY + verticalMargin > 0) {
body.style.maxHeight = (body.getBoundingClientRect().height - overflowY - verticalMargin) + "px";
} else if (this._tether.attachment.top === "bottom") {
if (constrainToScreen(body, "top", verticalPadding)) {
body.classList.add("scroll-y");
body.classList.add("scroll-show");
}
......
exports[`Button should render correctly 1`] = `
<button
className="Button ">
<div
className="flex layout-centered">
<div>
Clickity click
</div>
</div>
</button>
`;
exports[`Button should render correctly with an icon 1`] = `
<button
className="Button ">
<div
className="flex layout-centered">
<svg
className="mr1"
fill="currentcolor"
height={14}
name="star"
size={14}
viewBox="0 0 32 32"
width={14}>
<path
d="M16 0 L21 11 L32 12 L23 19 L26 31 L16 25 L6 31 L9 19 L0 12 L11 11" />
</svg>
<div>
Clickity click
</div>
</div>
</button>
`;
......@@ -7,7 +7,6 @@
/* the login content should always sit on top of the illustration */
.Login-content {
position: relative;
z-index: 1000;
}
.Login-header {
......@@ -15,7 +14,6 @@
}
.brand-scene {
z-index: 4;
overflow: hidden;
height: 180px;
}
......
......@@ -113,7 +113,7 @@ export default class DashCard extends Component {
return (
<div
className={"Card bordered rounded flex flex-column " + cx({
className={"Card bordered rounded flex flex-column hover-parent hover--visibility" + cx({
"Card--recent": dashcard.isAdded,
"Card--unmapped": !isMappedToAllParameters && !isEditing,
"Card--slow": isSlow === "usually-slow"
......
......@@ -4,8 +4,8 @@ import cx from "classnames";
import _ from "underscore";
const SHORTCUTS = [
{ name: "Today", operator: ["=", "<", ">"], values: [["relative_datetime", "current"]]},
{ name: "Yesterday", operator: ["=", "<", ">"], values: [["relative_datetime", -1, "day"]]},
{ name: "Today", operator: ["=", "<", ">"], values: [["relative-datetime", "current"]]},
{ name: "Yesterday", operator: ["=", "<", ">"], values: [["relative-datetime", -1, "day"]]},
{ name: "Past 7 days", operator: "time-interval", values: [-7, "day"]},
{ name: "Past 30 days", operator: "time-interval", values: [-30, "day"]}
];
......@@ -104,11 +104,11 @@ class PredefinedRelativeDatePicker extends Component {
const FILTERS = {
"today": {
name: "Today",
mapping: ["=", null, ["relative_datetime", "current"]]
mapping: ["=", null, ["relative-datetime", "current"]]
},
"yesterday": {
name: "Yesterday",
mapping: ["=", null, ["relative_datetime", -1, "day"]]
mapping: ["=", null, ["relative-datetime", -1, "day"]]
},
"past7days": {
name: "Past 7 Days",
......
......@@ -238,9 +238,6 @@ export const fetchCardDuration = createThunkAction(FETCH_CARD_DURATION, function
};
});
const SET_DASHBOARD_ID = "metabase/dashboard/SET_DASHBOARD_ID";
export const setDashboardId = createAction(SET_DASHBOARD_ID);
export const fetchDashboard = createThunkAction(FETCH_DASHBOARD, function(dashId, queryParams, enableDefaultParameters = true) {
let result;
return async function(dispatch, getState) {
......@@ -259,14 +256,13 @@ export const fetchDashboard = createThunkAction(FETCH_DASHBOARD, function(dashId
result = await DashboardApi.get({ dashId: dashId });
}
dispatch(setDashboardId(dashId));
const parameterValues = {};
if (result.parameters) {
for (const parameter of result.parameters) {
if (queryParams && queryParams[parameter.slug] != null) {
dispatch(setParameterValue(parameter.id, queryParams[parameter.slug]));
parameterValues[parameter.id] = queryParams[parameter.slug];
} else if (enableDefaultParameters && parameter.default != null) {
dispatch(setParameterValue(parameter.id, parameter.default));
parameterValues[parameter.id] = parameter.default;
}
}
}
......@@ -282,7 +278,11 @@ export const fetchDashboard = createThunkAction(FETCH_DASHBOARD, function(dashId
.each((dbId) => dispatch(fetchDatabaseMetadata(dbId)));
}
return normalize(result, dashboard);
return {
...normalize(result, dashboard), // includes `result` and `entities`
dashboardId: dashId,
parameterValues: parameterValues
};
};
});
......@@ -510,7 +510,7 @@ export const deletePublicLink = createAction(DELETE_PUBLIC_LINK, async ({ id })
const dashboardId = handleActions({
[INITIALIZE]: { next: (state) => null },
[SET_DASHBOARD_ID]: { next: (state, { payload }) => payload }
[FETCH_DASHBOARD]: { next: (state, { payload: { dashboardId } }) => dashboardId }
}, null);
const isEditing = handleActions({
......@@ -616,7 +616,8 @@ const cardDurations = handleActions({
const parameterValues = handleActions({
[INITIALIZE]: { next: () => ({}) }, // reset values
[SET_PARAMETER_VALUE]: { next: (state, { payload: { id, value }}) => assoc(state, id, value) },
[REMOVE_PARAMETER]: { next: (state, { payload: { id }}) => dissoc(state, id) }
[REMOVE_PARAMETER]: { next: (state, { payload: { id }}) => dissoc(state, id) },
[FETCH_DASHBOARD]: { next: (state, { payload: { parameterValues }}) => parameterValues },
}, {});
const dashboardListing = handleActions({
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment