Skip to content

Commit

Permalink
Merge branch 'master' of github.com:metabase/metabase into non-root-p…
Browse files Browse the repository at this point in the history
…ath-v2
  • Loading branch information
tlrobinson committed Apr 15, 2017
2 parents e2c31c3 + 66a042c commit b899771
Show file tree
Hide file tree
Showing 24 changed files with 291 additions and 66 deletions.
7 changes: 7 additions & 0 deletions docs/administration-guide/13-embedding.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ If you wish to have a parameter locked down to prevent your embedding applicatio

![Locked parameters](images/embedding/06-locked.png)

### Resizing Dashboards to fit their content
Dashboards are a fixed aspect ratio, so if you'd like to ensure they're automatically sized vertically to fit their contents you can use the [iFrame Resizer](https://github.com/davidjbradshaw/iframe-resizer) script. Metabase serves a copy for convenience:
```
<script src="http://metabase.example.com/app/iframeResizer.js"></script>
<iframe src="http://metabase.example.com/embed/dashboard/TOKEN" onload="iFrameResize({}, this)"></iframe>
```

### Reference applications
To see concrete examples of how to embed Metabase in applications under a number of common frameworks, check out our [reference implementations](https://github.com/metabase/embedding-reference-apps) on Github.

Expand Down
13 changes: 12 additions & 1 deletion docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,25 @@ $ yarn run build-watch

Run unit tests with

yarn run jest # Jest
yarn run test # Karma
yarn run test-e2e # Selenium Webdriver

Run the linters and type checker with

yarn run lint
yarn run flow

#### End-to-end tests

End-to-end tests are written with [webschauffeur](https://github.com/metabase/webchauffeur) which is a wrapper around [`selenium-webdriver`](https://www.npmjs.com/package/selenium-webdriver).

Run E2E tests once with

yarn run test-e2e

or use a persistent browser session with

yarn run test-e2e-dev

## Backend development
Leiningen and your REPL are the main development tools for the backend. There are some directions below on how to setup your REPL for easier development.
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/metabase/home/containers/HomepageApp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,10 @@ export default class HomepageApp extends Component {
</div>
</div>
<div className="Layout-sidebar flex-no-shrink hide sm-show">
<NextStep />
<RecentViews {...this.props} />
<div>
<NextStep />
<RecentViews {...this.props} />
</div>
</div>
</div>
</div>
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/metabase/lib/colors.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
// @flow

type ColorName = string;
type Color = string
type ColorFamily = { [name: ColorName]: Color };

export const normal = {
blue: '#509EE3',
green: '#9CC177',
Expand Down Expand Up @@ -57,3 +63,9 @@ export const harmony = [
'#c1a877',
'#f95c67',
]

export const getRandomColor = (family: ColorFamily): Color => {
// $FlowFixMe: Object.values doesn't preserve the type :-/
const colors: Color[] = Object.values(family)
return colors[Math.floor(Math.random() * colors.length)]
}
8 changes: 8 additions & 0 deletions frontend/src/metabase/lib/colors.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { getRandomColor, normal } from 'metabase/lib/colors'

describe('getRandomColor', () => {
it('should return a color string from the proper family', () => {
const color = getRandomColor(normal)
expect(Object.values(normal)).toContain(color)
})
})
3 changes: 1 addition & 2 deletions frontend/src/metabase/nav/containers/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,7 @@ export default class Navbar extends Component {
</li>
<li className="pl3 hide sm-show">
<Link to={Urls.question()} data-metabase-event={"Navbar;New Question"} style={this.styles.newQuestion} className="NavNewQuestion rounded inline-block bg-white text-brand text-bold cursor-pointer px2 no-decoration transition-all">
New
<span>Question</span>
New <span>Question</span>
</Link>
</li>
<li className="flex-align-right transition-background">
Expand Down
27 changes: 26 additions & 1 deletion frontend/src/metabase/public/components/EmbedFrame.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,28 @@ type Props = {

@withRouter
export default class EmbedFrame extends Component<*, Props, *> {
state = {
innerScroll: true
}

componentWillMount() {
if (window.iFrameResizer) {
console.error("iFrameResizer resizer already defined.")
} else {
window.iFrameResizer = {
autoResize: true,
heightCalculationMethod: "bodyScroll",
readyCallback: () => {
this.setState({ innerScroll: false })
}
}
// $FlowFixMe: flow doesn't know about require.ensure
require.ensure([], () => {
require("iframe-resizer/js/iframeResizer.contentWindow.js")
});
}
}

_getOptions() {
let options = querystring.parse(window.location.hash.replace(/^#/, ""));
for (var name in options) {
Expand All @@ -44,6 +66,8 @@ export default class EmbedFrame extends Component<*, Props, *> {

render() {
const { className, children, actionButtons, location, parameters, parameterValues, setParameterValue } = this.props;
const { innerScroll } = this.state;

const footer = true;

const { bordered, titled, theme } = this._getOptions();
Expand All @@ -52,10 +76,11 @@ export default class EmbedFrame extends Component<*, Props, *> {

return (
<div className={cx("EmbedFrame flex flex-column", className, {
"spread": innerScroll,
"bordered rounded shadowed": bordered,
[`Theme--${theme}`]: !!theme
})}>
<div className="flex flex-column flex-full scroll-y relative">
<div className={cx("flex flex-column flex-full relative", { "scroll-y": innerScroll })}>
{ name || (parameters && parameters.length > 0) ?
<div className="EmbedFrame-header flex align-center p1 sm-p2 lg-p3">
{ name && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ export default class PublicDashboard extends Component<*, Props, *> {
const { dashboard, parameterValues } = this.props;
return (
<EmbedFrame
className="spread flex"
name={dashboard && dashboard.name}
description={dashboard && dashboard.description}
parameters={dashboard && dashboard.parameters}
Expand Down
1 change: 0 additions & 1 deletion frontend/src/metabase/public/containers/PublicQuestion.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ export default class PublicQuestion extends Component<*, Props, State> {

return (
<EmbedFrame
className="relative spread"
name={card && card.name}
description={card && card.description}
parameters={card && card.parameters}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/metabase/public/lib/code.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const html = ({ iframeUrl }) =>
width="800"
height="600"
allowtransparency
/>`
></iframe>`

const jsx = ({ iframeUrl }) =>
`<iframe
Expand Down
15 changes: 8 additions & 7 deletions frontend/src/metabase/qb/components/TimeseriesFilterWidget.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ type Props = {
className: string,
card: CardObject,
tableMetadata: TableMetadata,
setDatasetQuery: (datasetQuery: DatasetQuery) => void,
runQuery: () => void
setDatasetQuery: (
datasetQuery: DatasetQuery,
options: { run: boolean }
) => void
};

type State = {
Expand Down Expand Up @@ -89,8 +91,7 @@ export default class TimeseriesFilterWidget extends Component<*, Props, State> {
className,
card,
tableMetadata,
setDatasetQuery,
runQuery
setDatasetQuery
} = this.props;
const { filter, filterIndex, currentFilter } = this.state;
let currentDescription;
Expand Down Expand Up @@ -144,11 +145,11 @@ export default class TimeseriesFilterWidget extends Component<*, Props, State> {
query = Query.addFilter(query, filter);
}
// $FlowFixMe
setDatasetQuery({
const datasetQuery: DatasetQuery = {
...card.dataset_query,
query
});
runQuery();
};
setDatasetQuery(datasetQuery, { run: true });
}
if (this._popover) {
this._popover.close();
Expand Down
15 changes: 9 additions & 6 deletions frontend/src/metabase/qb/components/TimeseriesGroupingWidget.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,18 @@ import type {

type Props = {
card: CardObject,
setDatasetQuery: (datasetQuery: DatasetQuery) => void,
runQuery: () => void
setDatasetQuery: (
datasetQuery: DatasetQuery,
options: { run: boolean }
) => void
};

export default class TimeseriesGroupingWidget extends Component<*, Props, *> {
_popover: ?any;

render() {
const { card, setDatasetQuery, runQuery } = this.props;
const { card, setDatasetQuery } = this.props;

if (Card.isStructured(card)) {
const query = Card.getQuery(card);
const breakouts = query && Query.getBreakouts(query);
Expand Down Expand Up @@ -58,11 +61,11 @@ export default class TimeseriesGroupingWidget extends Component<*, Props, *> {
breakout
);
// $FlowFixMe
setDatasetQuery({
const datasetQuery: DatasetQuery = {
...card.dataset_query,
query
});
runQuery();
};
setDatasetQuery(datasetQuery, { run: true });
if (this._popover) {
this._popover.close();
}
Expand Down
50 changes: 35 additions & 15 deletions frontend/src/metabase/questions/containers/CollectionEditorForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import Modal from "metabase/components/Modal";

import { reduxForm } from "redux-form";

import { normal } from "metabase/lib/colors";
import { normal, getRandomColor } from "metabase/lib/colors";

@reduxForm({
const formConfig = {
form: 'collection',
fields: ['id', 'name', 'description', 'color'],
validate: (values) => {
Expand All @@ -29,25 +29,43 @@ import { normal } from "metabase/lib/colors";
name: "",
description: "",
// pick a random color to start so everything isn't blue all the time
color: normal[Math.floor(Math.random() * normal.length)]
color: getRandomColor(normal)
}
})
export default class CollectionEditorForm extends Component {
}

export const getFormTitle = ({ id, name }) =>
id.value ? name.value : "New collection"

export const getActionText = ({ id }) =>
id.value ? "Update": "Create"


export const CollectionEditorFormActions = ({ handleSubmit, invalid, onClose, fields}) =>
<div>
<Button className="mr1" onClick={onClose}>
Cancel
</Button>
<Button primary disabled={invalid} onClick={handleSubmit}>
{ getActionText(fields) }
</Button>
</div>

export class CollectionEditorForm extends Component {
props: {
fields: Object,
onClose: Function,
invalid: Boolean,
handleSubmit: Function,
}

render() {
const { fields, handleSubmit, invalid, onClose } = this.props;
const { fields, onClose } = this.props;
return (
<Modal
inline
form
title={fields.id.value != null ? fields.name.value : "New collection"}
footer={[
<Button className="mr1" onClick={onClose}>
Cancel
</Button>,
<Button primary disabled={invalid} onClick={handleSubmit}>
{ fields.id.value != null ? "Update" : "Create" }
</Button>
]}
title={getFormTitle(fields)}
footer={<CollectionEditorFormActions {...this.props} />}
onClose={onClose}
>
<div className="NewForm ml-auto mr-auto mt4 pt2" style={{ width: 540 }}>
Expand Down Expand Up @@ -83,3 +101,5 @@ export default class CollectionEditorForm extends Component {
)
}
}

export default reduxForm(formConfig)(CollectionEditorForm)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
getFormTitle,
getActionText
} from './CollectionEditorForm'

const FORM_FIELDS = {
id: { value: 4 },
name: { value: 'Test collection' },
color: { value: '#409ee3' },
initialValues: {
color: '#409ee3'
}
}
const NEW_COLLECTION_FIELDS = { ...FORM_FIELDS, id: '', color: '' }

describe('CollectionEditorForm', () => {

describe('Title', () => {
it('should have a default title if no collection exists', () =>
expect(getFormTitle(NEW_COLLECTION_FIELDS)).toEqual('New collection')
)

it('should have the title of the colleciton if one exists', () =>
expect(getFormTitle(FORM_FIELDS)).toEqual(FORM_FIELDS.name.value)
)
})

describe('Form actions', () => {
it('should have a "create" primary action if no collection exists', () =>
expect(getActionText(NEW_COLLECTION_FIELDS)).toEqual('Create')
)

it('should have an "update" primary action if no collection exists', () =>
expect(getActionText(FORM_FIELDS)).toEqual('Update')
)
})

})
6 changes: 4 additions & 2 deletions frontend/src/metabase/questions/containers/EntityList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,10 @@ export default class EntityList extends Component {

const section = this.getSection();

const hasEntitiesInPlainState = entityIds.length > 0 || section.section !== "all";

const showActionHeader = (editable && selectedCount > 0);
const showSearchHeader = (entityIds.length > 0 && showSearchWidget);
const showSearchHeader = (hasEntitiesInPlainState && showSearchWidget);
const showEntityFilterWidget = onChangeSection;
return (
<div style={style}>
Expand All @@ -201,7 +203,7 @@ export default class EntityList extends Component {
:
null
}
{ showEntityFilterWidget && entityIds.length > 0 &&
{ showEntityFilterWidget && hasEntitiesInPlainState &&
<EntityFilterWidget
section={section}
onChange={onChangeSection}
Expand Down
Empty file.
Loading

0 comments on commit b899771

Please sign in to comment.