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

Merge branch 'master' of github.com:metabase/metabase into issue-4720

parents 71681c30 0fecb796
No related branches found
No related tags found
No related merge requests found
Showing
with 368 additions and 197 deletions
...@@ -56,6 +56,13 @@ If you wish to have a parameter locked down to prevent your embedding applicatio ...@@ -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) ![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 ### 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. 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.
......
...@@ -40,7 +40,7 @@ import cx from "classnames"; ...@@ -40,7 +40,7 @@ import cx from "classnames";
return errors; return errors;
} }
}, },
metricFormSelectors) (state, props) => metricFormSelectors(state, props))
export default class MetricForm extends Component { export default class MetricForm extends Component {
updatePreviewSummary(datasetQuery) { updatePreviewSummary(datasetQuery) {
this.props.updatePreviewSummary({ this.props.updatePreviewSummary({
......
...@@ -38,7 +38,7 @@ import cx from "classnames"; ...@@ -38,7 +38,7 @@ import cx from "classnames";
}, },
initialValues: { name: "", description: "", table_id: null, definition: { filter: [] }, revision_message: null } initialValues: { name: "", description: "", table_id: null, definition: { filter: [] }, revision_message: null }
}, },
segmentFormSelectors) (state, props) => segmentFormSelectors(state, props))
export default class SegmentForm extends Component { export default class SegmentForm extends Component {
updatePreviewSummary(datasetQuery) { updatePreviewSummary(datasetQuery) {
this.props.updatePreviewSummary({ this.props.updatePreviewSummary({
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Button should render correctly 1`] = ` exports[`Button should render correctly 1`] = `
<button <button
className="Button "> className="Button "
>
<div <div
className="flex layout-centered"> className="flex layout-centered"
>
<div> <div>
Clickity click Clickity click
</div> </div>
...@@ -12,9 +16,11 @@ exports[`Button should render correctly 1`] = ` ...@@ -12,9 +16,11 @@ exports[`Button should render correctly 1`] = `
exports[`Button should render correctly with an icon 1`] = ` exports[`Button should render correctly with an icon 1`] = `
<button <button
className="Button "> className="Button "
>
<div <div
className="flex layout-centered"> className="flex layout-centered"
>
<svg <svg
className="mr1" className="mr1"
fill="currentcolor" fill="currentcolor"
...@@ -22,9 +28,11 @@ exports[`Button should render correctly with an icon 1`] = ` ...@@ -22,9 +28,11 @@ exports[`Button should render correctly with an icon 1`] = `
name="star" name="star"
size={14} size={14}
viewBox="0 0 32 32" viewBox="0 0 32 32"
width={14}> width={14}
>
<path <path
d="M16 0 L21 11 L32 12 L23 19 L26 31 L16 25 L6 31 L9 19 L0 12 L11 11" /> d="M16 0 L21 11 L32 12 L23 19 L26 31 L16 25 L6 31 L9 19 L0 12 L11 11"
/>
</svg> </svg>
<div> <div>
Clickity click Clickity click
......
...@@ -129,6 +129,10 @@ ...@@ -129,6 +129,10 @@
font-family: Monaco, monospace; font-family: Monaco, monospace;
} }
.text-pre-wrap {
white-space: pre-wrap;
}
.text-measure { .text-measure {
max-width: 620px; max-width: 620px;
} }
...@@ -95,8 +95,10 @@ export default class HomepageApp extends Component { ...@@ -95,8 +95,10 @@ export default class HomepageApp extends Component {
</div> </div>
</div> </div>
<div className="Layout-sidebar flex-no-shrink hide sm-show"> <div className="Layout-sidebar flex-no-shrink hide sm-show">
<NextStep /> <div>
<RecentViews {...this.props} /> <NextStep />
<RecentViews {...this.props} />
</div>
</div> </div>
</div> </div>
</div> </div>
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Button should render "" correctly 1`] = ` exports[`Button should render "" correctly 1`] = `
<button <button
className="Button "> className="Button "
>
<div <div
className="flex layout-centered"> className="flex layout-centered"
>
<div> <div>
Clickity click Clickity click
</div> </div>
...@@ -12,9 +16,11 @@ exports[`Button should render "" correctly 1`] = ` ...@@ -12,9 +16,11 @@ exports[`Button should render "" correctly 1`] = `
exports[`Button should render "primary" correctly 1`] = ` exports[`Button should render "primary" correctly 1`] = `
<button <button
className="Button Button--primary"> className="Button Button--primary"
>
<div <div
className="flex layout-centered"> className="flex layout-centered"
>
<div> <div>
Clickity click Clickity click
</div> </div>
...@@ -24,9 +30,11 @@ exports[`Button should render "primary" correctly 1`] = ` ...@@ -24,9 +30,11 @@ exports[`Button should render "primary" correctly 1`] = `
exports[`Button should render "with an icon" correctly 1`] = ` exports[`Button should render "with an icon" correctly 1`] = `
<button <button
className="Button "> className="Button "
>
<div <div
className="flex layout-centered"> className="flex layout-centered"
>
<svg <svg
className="mr1" className="mr1"
fill="currentcolor" fill="currentcolor"
...@@ -34,9 +42,11 @@ exports[`Button should render "with an icon" correctly 1`] = ` ...@@ -34,9 +42,11 @@ exports[`Button should render "with an icon" correctly 1`] = `
name="star" name="star"
size={14} size={14}
viewBox="0 0 32 32" viewBox="0 0 32 32"
width={14}> width={14}
>
<path <path
d="M16 0 L21 11 L32 12 L23 19 L26 31 L16 25 L6 31 L9 19 L0 12 L11 11" /> d="M16 0 L21 11 L32 12 L23 19 L26 31 L16 25 L6 31 L9 19 L0 12 L11 11"
/>
</svg> </svg>
<div> <div>
Clickity click Clickity click
...@@ -49,7 +59,8 @@ exports[`CheckBox should render "off" correctly 1`] = ` ...@@ -49,7 +59,8 @@ exports[`CheckBox should render "off" correctly 1`] = `
<div <div
className="cursor-pointer" className="cursor-pointer"
onClick={[Function]} onClick={[Function]}
style={undefined}> style={undefined}
>
<div <div
style={ style={
Object { Object {
...@@ -62,7 +73,8 @@ exports[`CheckBox should render "off" correctly 1`] = ` ...@@ -62,7 +73,8 @@ exports[`CheckBox should render "off" correctly 1`] = `
"justifyContent": "center", "justifyContent": "center",
"width": 16, "width": 16,
} }
} /> }
/>
</div> </div>
`; `;
...@@ -74,7 +86,8 @@ exports[`CheckBox should render "on inverted" correctly 1`] = ` ...@@ -74,7 +86,8 @@ exports[`CheckBox should render "on inverted" correctly 1`] = `
Object { Object {
"color": "#509EE3", "color": "#509EE3",
} }
}> }
>
<div <div
style={ style={
Object { Object {
...@@ -87,7 +100,8 @@ exports[`CheckBox should render "on inverted" correctly 1`] = ` ...@@ -87,7 +100,8 @@ exports[`CheckBox should render "on inverted" correctly 1`] = `
"justifyContent": "center", "justifyContent": "center",
"width": 16, "width": 16,
} }
}> }
>
<svg <svg
className="Icon Icon-check" className="Icon Icon-check"
fill="currentcolor" fill="currentcolor"
...@@ -100,9 +114,11 @@ exports[`CheckBox should render "on inverted" correctly 1`] = ` ...@@ -100,9 +114,11 @@ exports[`CheckBox should render "on inverted" correctly 1`] = `
} }
} }
viewBox="0 0 32 32" viewBox="0 0 32 32"
width={12}> width={12}
>
<path <path
d="M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z " /> d="M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z "
/>
</svg> </svg>
</div> </div>
</div> </div>
...@@ -112,7 +128,8 @@ exports[`CheckBox should render "on" correctly 1`] = ` ...@@ -112,7 +128,8 @@ exports[`CheckBox should render "on" correctly 1`] = `
<div <div
className="cursor-pointer" className="cursor-pointer"
onClick={[Function]} onClick={[Function]}
style={undefined}> style={undefined}
>
<div <div
style={ style={
Object { Object {
...@@ -125,7 +142,8 @@ exports[`CheckBox should render "on" correctly 1`] = ` ...@@ -125,7 +142,8 @@ exports[`CheckBox should render "on" correctly 1`] = `
"justifyContent": "center", "justifyContent": "center",
"width": 16, "width": 16,
} }
}> }
>
<svg <svg
className="Icon Icon-check" className="Icon Icon-check"
fill="currentcolor" fill="currentcolor"
...@@ -138,9 +156,11 @@ exports[`CheckBox should render "on" correctly 1`] = ` ...@@ -138,9 +156,11 @@ exports[`CheckBox should render "on" correctly 1`] = `
} }
} }
viewBox="0 0 32 32" viewBox="0 0 32 32"
width={12}> width={12}
>
<path <path
d="M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z " /> d="M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z "
/>
</svg> </svg>
</div> </div>
</div> </div>
...@@ -153,7 +173,8 @@ exports[`StackedCheckBox should render "off" correctly 1`] = ` ...@@ -153,7 +173,8 @@ exports[`StackedCheckBox should render "off" correctly 1`] = `
Object { Object {
"position": "relative", "position": "relative",
} }
}> }
>
<div <div
className="cursor-pointer" className="cursor-pointer"
onClick={[Function]} onClick={[Function]}
...@@ -164,7 +185,8 @@ exports[`StackedCheckBox should render "off" correctly 1`] = ` ...@@ -164,7 +185,8 @@ exports[`StackedCheckBox should render "off" correctly 1`] = `
"top": -3, "top": -3,
"zIndex": -1, "zIndex": -1,
} }
}> }
>
<div <div
style={ style={
Object { Object {
...@@ -177,12 +199,14 @@ exports[`StackedCheckBox should render "off" correctly 1`] = ` ...@@ -177,12 +199,14 @@ exports[`StackedCheckBox should render "off" correctly 1`] = `
"justifyContent": "center", "justifyContent": "center",
"width": 16, "width": 16,
} }
} /> }
/>
</div> </div>
<div <div
className="cursor-pointer" className="cursor-pointer"
onClick={[Function]} onClick={[Function]}
style={Object {}}> style={Object {}}
>
<div <div
style={ style={
Object { Object {
...@@ -195,7 +219,8 @@ exports[`StackedCheckBox should render "off" correctly 1`] = ` ...@@ -195,7 +219,8 @@ exports[`StackedCheckBox should render "off" correctly 1`] = `
"justifyContent": "center", "justifyContent": "center",
"width": 16, "width": 16,
} }
} /> }
/>
</div> </div>
</span> </span>
`; `;
...@@ -208,7 +233,8 @@ exports[`StackedCheckBox should render "on inverted" correctly 1`] = ` ...@@ -208,7 +233,8 @@ exports[`StackedCheckBox should render "on inverted" correctly 1`] = `
"color": "#509EE3", "color": "#509EE3",
"position": "relative", "position": "relative",
} }
}> }
>
<div <div
className="cursor-pointer" className="cursor-pointer"
onClick={[Function]} onClick={[Function]}
...@@ -219,7 +245,8 @@ exports[`StackedCheckBox should render "on inverted" correctly 1`] = ` ...@@ -219,7 +245,8 @@ exports[`StackedCheckBox should render "on inverted" correctly 1`] = `
"top": -3, "top": -3,
"zIndex": -1, "zIndex": -1,
} }
}> }
>
<div <div
style={ style={
Object { Object {
...@@ -232,7 +259,8 @@ exports[`StackedCheckBox should render "on inverted" correctly 1`] = ` ...@@ -232,7 +259,8 @@ exports[`StackedCheckBox should render "on inverted" correctly 1`] = `
"justifyContent": "center", "justifyContent": "center",
"width": 16, "width": 16,
} }
}> }
>
<svg <svg
className="Icon Icon-check" className="Icon Icon-check"
fill="currentcolor" fill="currentcolor"
...@@ -245,16 +273,19 @@ exports[`StackedCheckBox should render "on inverted" correctly 1`] = ` ...@@ -245,16 +273,19 @@ exports[`StackedCheckBox should render "on inverted" correctly 1`] = `
} }
} }
viewBox="0 0 32 32" viewBox="0 0 32 32"
width={12}> width={12}
>
<path <path
d="M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z " /> d="M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z "
/>
</svg> </svg>
</div> </div>
</div> </div>
<div <div
className="cursor-pointer" className="cursor-pointer"
onClick={[Function]} onClick={[Function]}
style={Object {}}> style={Object {}}
>
<div <div
style={ style={
Object { Object {
...@@ -267,7 +298,8 @@ exports[`StackedCheckBox should render "on inverted" correctly 1`] = ` ...@@ -267,7 +298,8 @@ exports[`StackedCheckBox should render "on inverted" correctly 1`] = `
"justifyContent": "center", "justifyContent": "center",
"width": 16, "width": 16,
} }
}> }
>
<svg <svg
className="Icon Icon-check" className="Icon Icon-check"
fill="currentcolor" fill="currentcolor"
...@@ -280,9 +312,11 @@ exports[`StackedCheckBox should render "on inverted" correctly 1`] = ` ...@@ -280,9 +312,11 @@ exports[`StackedCheckBox should render "on inverted" correctly 1`] = `
} }
} }
viewBox="0 0 32 32" viewBox="0 0 32 32"
width={12}> width={12}
>
<path <path
d="M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z " /> d="M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z "
/>
</svg> </svg>
</div> </div>
</div> </div>
...@@ -296,7 +330,8 @@ exports[`StackedCheckBox should render "on" correctly 1`] = ` ...@@ -296,7 +330,8 @@ exports[`StackedCheckBox should render "on" correctly 1`] = `
Object { Object {
"position": "relative", "position": "relative",
} }
}> }
>
<div <div
className="cursor-pointer" className="cursor-pointer"
onClick={[Function]} onClick={[Function]}
...@@ -307,7 +342,8 @@ exports[`StackedCheckBox should render "on" correctly 1`] = ` ...@@ -307,7 +342,8 @@ exports[`StackedCheckBox should render "on" correctly 1`] = `
"top": -3, "top": -3,
"zIndex": -1, "zIndex": -1,
} }
}> }
>
<div <div
style={ style={
Object { Object {
...@@ -320,7 +356,8 @@ exports[`StackedCheckBox should render "on" correctly 1`] = ` ...@@ -320,7 +356,8 @@ exports[`StackedCheckBox should render "on" correctly 1`] = `
"justifyContent": "center", "justifyContent": "center",
"width": 16, "width": 16,
} }
}> }
>
<svg <svg
className="Icon Icon-check" className="Icon Icon-check"
fill="currentcolor" fill="currentcolor"
...@@ -333,16 +370,19 @@ exports[`StackedCheckBox should render "on" correctly 1`] = ` ...@@ -333,16 +370,19 @@ exports[`StackedCheckBox should render "on" correctly 1`] = `
} }
} }
viewBox="0 0 32 32" viewBox="0 0 32 32"
width={12}> width={12}
>
<path <path
d="M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z " /> d="M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z "
/>
</svg> </svg>
</div> </div>
</div> </div>
<div <div
className="cursor-pointer" className="cursor-pointer"
onClick={[Function]} onClick={[Function]}
style={Object {}}> style={Object {}}
>
<div <div
style={ style={
Object { Object {
...@@ -355,7 +395,8 @@ exports[`StackedCheckBox should render "on" correctly 1`] = ` ...@@ -355,7 +395,8 @@ exports[`StackedCheckBox should render "on" correctly 1`] = `
"justifyContent": "center", "justifyContent": "center",
"width": 16, "width": 16,
} }
}> }
>
<svg <svg
className="Icon Icon-check" className="Icon Icon-check"
fill="currentcolor" fill="currentcolor"
...@@ -368,9 +409,11 @@ exports[`StackedCheckBox should render "on" correctly 1`] = ` ...@@ -368,9 +409,11 @@ exports[`StackedCheckBox should render "on" correctly 1`] = `
} }
} }
viewBox="0 0 32 32" viewBox="0 0 32 32"
width={12}> width={12}
>
<path <path
d="M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z " /> d="M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z "
/>
</svg> </svg>
</div> </div>
</div> </div>
...@@ -385,7 +428,8 @@ exports[`Toggle should render "off" correctly 1`] = ` ...@@ -385,7 +428,8 @@ exports[`Toggle should render "off" correctly 1`] = `
Object { Object {
"color": null, "color": null,
} }
} /> }
/>
`; `;
exports[`Toggle should render "on" correctly 1`] = ` exports[`Toggle should render "on" correctly 1`] = `
...@@ -396,5 +440,6 @@ exports[`Toggle should render "on" correctly 1`] = ` ...@@ -396,5 +440,6 @@ exports[`Toggle should render "on" correctly 1`] = `
Object { Object {
"color": null, "color": null,
} }
} /> }
/>
`; `;
// @flow
type ColorName = string;
type Color = string
type ColorFamily = { [name: ColorName]: Color };
export const normal = { export const normal = {
blue: '#509EE3', blue: '#509EE3',
green: '#9CC177', green: '#9CC177',
...@@ -57,3 +63,9 @@ export const harmony = [ ...@@ -57,3 +63,9 @@ export const harmony = [
'#c1a877', '#c1a877',
'#f95c67', '#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)]
}
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)
})
})
...@@ -32,6 +32,28 @@ type Props = { ...@@ -32,6 +32,28 @@ type Props = {
@withRouter @withRouter
export default class EmbedFrame extends Component<*, Props, *> { 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() { _getOptions() {
let options = querystring.parse(window.location.hash.replace(/^#/, "")); let options = querystring.parse(window.location.hash.replace(/^#/, ""));
for (var name in options) { for (var name in options) {
...@@ -44,6 +66,8 @@ export default class EmbedFrame extends Component<*, Props, *> { ...@@ -44,6 +66,8 @@ export default class EmbedFrame extends Component<*, Props, *> {
render() { render() {
const { className, children, actionButtons, location, parameters, parameterValues, setParameterValue } = this.props; const { className, children, actionButtons, location, parameters, parameterValues, setParameterValue } = this.props;
const { innerScroll } = this.state;
const footer = true; const footer = true;
const { bordered, titled, theme } = this._getOptions(); const { bordered, titled, theme } = this._getOptions();
...@@ -52,10 +76,11 @@ export default class EmbedFrame extends Component<*, Props, *> { ...@@ -52,10 +76,11 @@ export default class EmbedFrame extends Component<*, Props, *> {
return ( return (
<div className={cx("EmbedFrame flex flex-column", className, { <div className={cx("EmbedFrame flex flex-column", className, {
"spread": innerScroll,
"bordered rounded shadowed": bordered, "bordered rounded shadowed": bordered,
[`Theme--${theme}`]: !!theme [`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) ? { name || (parameters && parameters.length > 0) ?
<div className="EmbedFrame-header flex align-center p1 sm-p2 lg-p3"> <div className="EmbedFrame-header flex align-center p1 sm-p2 lg-p3">
{ name && ( { name && (
......
...@@ -75,7 +75,6 @@ export default class PublicDashboard extends Component<*, Props, *> { ...@@ -75,7 +75,6 @@ export default class PublicDashboard extends Component<*, Props, *> {
const { dashboard, parameterValues } = this.props; const { dashboard, parameterValues } = this.props;
return ( return (
<EmbedFrame <EmbedFrame
className="spread flex"
name={dashboard && dashboard.name} name={dashboard && dashboard.name}
description={dashboard && dashboard.description} description={dashboard && dashboard.description}
parameters={dashboard && dashboard.parameters} parameters={dashboard && dashboard.parameters}
......
...@@ -141,7 +141,6 @@ export default class PublicQuestion extends Component<*, Props, State> { ...@@ -141,7 +141,6 @@ export default class PublicQuestion extends Component<*, Props, State> {
return ( return (
<EmbedFrame <EmbedFrame
className="relative spread"
name={card && card.name} name={card && card.name}
description={card && card.description} description={card && card.description}
parameters={card && card.parameters} parameters={card && card.parameters}
......
...@@ -28,7 +28,7 @@ const html = ({ iframeUrl }) => ...@@ -28,7 +28,7 @@ const html = ({ iframeUrl }) =>
width="800" width="800"
height="600" height="600"
allowtransparency allowtransparency
/>` ></iframe>`
const jsx = ({ iframeUrl }) => const jsx = ({ iframeUrl }) =>
`<iframe `<iframe
......
...@@ -33,8 +33,10 @@ type Props = { ...@@ -33,8 +33,10 @@ type Props = {
className: string, className: string,
card: CardObject, card: CardObject,
tableMetadata: TableMetadata, tableMetadata: TableMetadata,
setDatasetQuery: (datasetQuery: DatasetQuery) => void, setDatasetQuery: (
runQuery: () => void datasetQuery: DatasetQuery,
options: { run: boolean }
) => void
}; };
type State = { type State = {
...@@ -89,8 +91,7 @@ export default class TimeseriesFilterWidget extends Component<*, Props, State> { ...@@ -89,8 +91,7 @@ export default class TimeseriesFilterWidget extends Component<*, Props, State> {
className, className,
card, card,
tableMetadata, tableMetadata,
setDatasetQuery, setDatasetQuery
runQuery
} = this.props; } = this.props;
const { filter, filterIndex, currentFilter } = this.state; const { filter, filterIndex, currentFilter } = this.state;
let currentDescription; let currentDescription;
...@@ -144,11 +145,11 @@ export default class TimeseriesFilterWidget extends Component<*, Props, State> { ...@@ -144,11 +145,11 @@ export default class TimeseriesFilterWidget extends Component<*, Props, State> {
query = Query.addFilter(query, filter); query = Query.addFilter(query, filter);
} }
// $FlowFixMe // $FlowFixMe
setDatasetQuery({ const datasetQuery: DatasetQuery = {
...card.dataset_query, ...card.dataset_query,
query query
}); };
runQuery(); setDatasetQuery(datasetQuery, { run: true });
} }
if (this._popover) { if (this._popover) {
this._popover.close(); this._popover.close();
......
...@@ -19,15 +19,18 @@ import type { ...@@ -19,15 +19,18 @@ import type {
type Props = { type Props = {
card: CardObject, card: CardObject,
setDatasetQuery: (datasetQuery: DatasetQuery) => void, setDatasetQuery: (
runQuery: () => void datasetQuery: DatasetQuery,
options: { run: boolean }
) => void
}; };
export default class TimeseriesGroupingWidget extends Component<*, Props, *> { export default class TimeseriesGroupingWidget extends Component<*, Props, *> {
_popover: ?any; _popover: ?any;
render() { render() {
const { card, setDatasetQuery, runQuery } = this.props; const { card, setDatasetQuery } = this.props;
if (Card.isStructured(card)) { if (Card.isStructured(card)) {
const query = Card.getQuery(card); const query = Card.getQuery(card);
const breakouts = query && Query.getBreakouts(query); const breakouts = query && Query.getBreakouts(query);
...@@ -58,11 +61,11 @@ export default class TimeseriesGroupingWidget extends Component<*, Props, *> { ...@@ -58,11 +61,11 @@ export default class TimeseriesGroupingWidget extends Component<*, Props, *> {
breakout breakout
); );
// $FlowFixMe // $FlowFixMe
setDatasetQuery({ const datasetQuery: DatasetQuery = {
...card.dataset_query, ...card.dataset_query,
query query
}); };
runQuery(); setDatasetQuery(datasetQuery, { run: true });
if (this._popover) { if (this._popover) {
this._popover.close(); this._popover.close();
} }
......
...@@ -19,72 +19,75 @@ type FieldFilter = (field: Field) => boolean; ...@@ -19,72 +19,75 @@ type FieldFilter = (field: Field) => boolean;
// PivotByAction displays a breakout picker, and optionally filters by the // PivotByAction displays a breakout picker, and optionally filters by the
// clicked dimesion values (and removes corresponding breakouts) // clicked dimesion values (and removes corresponding breakouts)
export default (name: string, icon: string, fieldFilter: FieldFilter) => ( export default (name: string, icon: string, fieldFilter: FieldFilter) =>
{ card, tableMetadata, clicked }: ClickActionProps ({ card, tableMetadata, clicked }: ClickActionProps): ?ClickAction => {
): ?ClickAction => { const query = Card.getQuery(card);
const query = Card.getQuery(card);
// Click target types: metric value // Click target types: metric value
if ( if (
!query || !query ||
!tableMetadata || !tableMetadata ||
(clicked && (clicked &&
(clicked.value === undefined || (clicked.value === undefined ||
clicked.column.source !== "aggregation")) clicked.column.source !== "aggregation"))
) { ) {
return; return;
} }
let dimensions = (clicked && clicked.dimensions) || []; let dimensions = (clicked && clicked.dimensions) || [];
const breakouts = Query.getBreakouts(query); const breakouts = Query.getBreakouts(query);
const usedFields = {}; const usedFields = {};
for (const breakout of breakouts) { for (const breakout of breakouts) {
usedFields[Query.getFieldTargetId(breakout)] = true; usedFields[Query.getFieldTargetId(breakout)] = true;
} }
const fieldOptions = Query.getFieldOptions( const fieldOptions = Query.getFieldOptions(
tableMetadata.fields, tableMetadata.fields,
true, true,
(fields: Field[]): Field[] => { (fields: Field[]): Field[] => {
fields = tableMetadata.breakout_options.validFieldsFilter(fields); fields = tableMetadata.breakout_options.validFieldsFilter(
if (fieldFilter) { fields
fields = fields.filter(fieldFilter); );
} if (fieldFilter) {
return fields; fields = fields.filter(fieldFilter);
}, }
usedFields return fields;
); },
usedFields
);
const customFieldOptions = Query.getExpressions(query); const customFieldOptions = Query.getExpressions(query);
if (fieldOptions.count === 0) { if (fieldOptions.count === 0) {
return null; return null;
} }
return { return {
title: ( title: (
<span> <span>
Pivot by Pivot by
{" "} {" "}
<span className="text-dark">{name.toLowerCase()}</span> <span className="text-dark">{name.toLowerCase()}</span>
</span> </span>
), ),
icon: icon, icon: icon,
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
popover: ({ onChangeCardAndRun, onClose }: ClickActionPopoverProps) => ( popover: (
<BreakoutPopover { onChangeCardAndRun, onClose }: ClickActionPopoverProps
tableMetadata={tableMetadata} ) => (
fieldOptions={fieldOptions} <BreakoutPopover
customFieldOptions={customFieldOptions} tableMetadata={tableMetadata}
onCommitBreakout={breakout => { fieldOptions={fieldOptions}
onChangeCardAndRun( customFieldOptions={customFieldOptions}
pivot(card, breakout, tableMetadata, dimensions) onCommitBreakout={breakout => {
); onChangeCardAndRun(
}} pivot(card, breakout, tableMetadata, dimensions)
onClose={onClose} );
/> }}
) onClose={onClose}
/>
)
};
}; };
};
...@@ -8,9 +8,9 @@ import Modal from "metabase/components/Modal"; ...@@ -8,9 +8,9 @@ import Modal from "metabase/components/Modal";
import { reduxForm } from "redux-form"; import { reduxForm } from "redux-form";
import { normal } from "metabase/lib/colors"; import { normal, getRandomColor } from "metabase/lib/colors";
@reduxForm({ const formConfig = {
form: 'collection', form: 'collection',
fields: ['id', 'name', 'description', 'color'], fields: ['id', 'name', 'description', 'color'],
validate: (values) => { validate: (values) => {
...@@ -29,25 +29,43 @@ import { normal } from "metabase/lib/colors"; ...@@ -29,25 +29,43 @@ import { normal } from "metabase/lib/colors";
name: "", name: "",
description: "", description: "",
// pick a random color to start so everything isn't blue all the time // 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() { render() {
const { fields, handleSubmit, invalid, onClose } = this.props; const { fields, onClose } = this.props;
return ( return (
<Modal <Modal
inline inline
form form
title={fields.id.value != null ? fields.name.value : "New collection"} title={getFormTitle(fields)}
footer={[ footer={<CollectionEditorFormActions {...this.props} />}
<Button className="mr1" onClick={onClose}>
Cancel
</Button>,
<Button primary disabled={invalid} onClick={handleSubmit}>
{ fields.id.value != null ? "Update" : "Create" }
</Button>
]}
onClose={onClose} onClose={onClose}
> >
<div className="NewForm ml-auto mr-auto mt4 pt2" style={{ width: 540 }}> <div className="NewForm ml-auto mr-auto mt4 pt2" style={{ width: 540 }}>
...@@ -83,3 +101,5 @@ export default class CollectionEditorForm extends Component { ...@@ -83,3 +101,5 @@ export default class CollectionEditorForm extends Component {
) )
} }
} }
export default reduxForm(formConfig)(CollectionEditorForm)
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')
)
})
})
...@@ -133,10 +133,10 @@ const ItemTitle = ({ title, link, linkColorClass, linkHoverClass }) => ...@@ -133,10 +133,10 @@ const ItemTitle = ({ title, link, linkColorClass, linkHoverClass }) =>
</h2> </h2>
const ContextHeading = ({ children }) => const ContextHeading = ({ children }) =>
<h3 className="mb1 text-grey-4">{ children }</h3> <h3 className="my2 text-grey-4">{ children }</h3>
const ContextContent = ({ empty, children }) => const ContextContent = ({ empty, children }) =>
<p className={cx('m0 text-paragraph text-measure', { 'text-grey-3': empty })}> <p className={cx('m0 text-paragraph text-measure text-pre-wrap', { 'text-grey-3': empty })}>
{ children } { children }
</p> </p>
......
...@@ -488,7 +488,7 @@ export default class ReferenceGettingStartedGuide extends Component { ...@@ -488,7 +488,7 @@ export default class ReferenceGettingStartedGuide extends Component {
/> />
</div> </div>
]} ]}
{ Object.keys(metrics) > 0 && ( { Object.keys(metrics).length > 0 && (
<div className="my4 pt4"> <div className="my4 pt4">
<SectionHeader trim={guide.important_metrics.length === 0}> <SectionHeader trim={guide.important_metrics.length === 0}>
{ guide.important_metrics && guide.important_metrics.length > 0 ? 'Numbers that we pay attention to' : 'Metrics' } { guide.important_metrics && guide.important_metrics.length > 0 ? 'Numbers that we pay attention to' : 'Metrics' }
...@@ -522,7 +522,7 @@ export default class ReferenceGettingStartedGuide extends Component { ...@@ -522,7 +522,7 @@ export default class ReferenceGettingStartedGuide extends Component {
Metrics are important numbers your company cares about. They often represent a core indicator of how the business is performing. Metrics are important numbers your company cares about. They often represent a core indicator of how the business is performing.
</GuideText> </GuideText>
} }
<div className="mt4"> <div>
<Link className="Button Button--primary" to={'/reference/metrics'}> <Link className="Button Button--primary" to={'/reference/metrics'}>
See all metrics See all metrics
</Link> </Link>
...@@ -533,56 +533,53 @@ export default class ReferenceGettingStartedGuide extends Component { ...@@ -533,56 +533,53 @@ export default class ReferenceGettingStartedGuide extends Component {
<div className="mt4 pt4"> <div className="mt4 pt4">
<SectionHeader trim={(!has(guide.important_segments) && !has(guide.important_tables))}> <SectionHeader trim={(!has(guide.important_segments) && !has(guide.important_tables))}>
{ Object.keys(segments) > 0 ? 'Segments and tables' : 'Tables' } { Object.keys(segments).length > 0 ? 'Segments and tables' : 'Tables' }
</SectionHeader> </SectionHeader>
{ has(guide.important_segments) || has(guide.important_tables) ? [ { has(guide.important_segments) || has(guide.important_tables) ?
<div className="mt2"> <div className="my2">
{ guide.important_segments.map((segmentId) => { guide.important_segments.map((segmentId) =>
<GuideDetail <GuideDetail
key={segmentId} key={segmentId}
type="segment" type="segment"
entity={segments[segmentId]} entity={segments[segmentId]}
tables={tables} tables={tables}
/> />
)} )}
{ guide.important_tables.map((tableId) => { guide.important_tables.map((tableId) =>
<GuideDetail <GuideDetail
key={tableId} key={tableId}
type="table" type="table"
entity={tables[tableId]} entity={tables[tableId]}
tables={tables} tables={tables}
/> />
)} )}
</div> </div>
] : ( :
<div> <GuideText>
<GuideText> { Object.keys(segments).length > 0 ? (
{ Object.keys(segments) > 0 ? ( <span>
<span> Segments and tables are the building blocks of your company's data. Tables are collections of the raw information while segments are specific slices with specific meanings, like <b>"Recent orders."</b>
Segments and tables are the building blocks of your company's data. Tables are collections of the raw information while segments are specific slices with specific meanings, like <b>"Recent orders."</b> </span>
</span> ) : "Tables are the building blocks of your company's data."
) : "Tables are the building blocks of your company's data." }
} </GuideText>
</GuideText>
<div>
{ Object.keys(segments) > 0 && (
<Link className="Button Button--purple mr2" to={'/reference/segments'}>
See all segments
</Link>
)}
<Link
className={cx(
{ 'text-purple text-bold no-decoration text-underline-hover' : Object.keys(segments) > 0 },
{ 'Button Button--purple' : Object.keys(segments) === 0 }
)}
to={'/reference/databases'}
>
See all tables
</Link>
</div>
</div>
)
} }
<div>
{ Object.keys(segments).length > 0 && (
<Link className="Button Button--purple mr2" to={'/reference/segments'}>
See all segments
</Link>
)}
<Link
className={cx(
{ 'text-purple text-bold no-decoration text-underline-hover' : Object.keys(segments).length > 0 },
{ 'Button Button--purple' : Object.keys(segments).length === 0 }
)}
to={'/reference/databases'}
>
See all tables
</Link>
</div>
</div> </div>
<div className="mt4 pt4"> <div className="mt4 pt4">
......
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