Skip to content

Commit

Permalink
Merge pull request #7048 from metabase/automagic-dashboards-stage1
Browse files Browse the repository at this point in the history
Automagic dashboards
  • Loading branch information
salsakran committed Mar 15, 2018
2 parents ba2cabd + 58b391b commit 2a185a0
Show file tree
Hide file tree
Showing 75 changed files with 4,513 additions and 516 deletions.
5 changes: 5 additions & 0 deletions frontend/interfaces/underscore.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ declare module "underscore" {

declare function debounce<T: any => any>(func: T): T;

declare function partition<T>(
array: T[],
pred: (val: T) => boolean,
): [T[], T[]];

// TODO: improve this
declare function chain<S>(obj: S): any;

Expand Down
68 changes: 68 additions & 0 deletions frontend/src/metabase/components/ExplorePane.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* @flow */

import React from "react";
import { Link } from "react-router";

import Icon from "metabase/components/Icon";
import MetabotLogo from "metabase/components/MetabotLogo";

import { t } from "c-3po";

import type { Candidate } from "metabase/meta/types/Auto";

const DEFAULT_TITLE = t`Hi, Metabot here.`;
const DEFAULT_DESCRIPTION = "";

export const ExplorePane = ({
options,
// $FlowFixMe
title = DEFAULT_TITLE,
// $FlowFixMe
description = DEFAULT_DESCRIPTION,
}: {
options?: ?(Candidate[]),
title?: ?string,
description?: ?string,
}) => (
<div>
{title && (
<div className="flex align-center mb2">
<MetabotLogo className="mr2" />
<h3>
<span>{title}</span>
</h3>
</div>
)}
{description && (
<div className="mb4">
<span>{description}</span>
</div>
)}
{options && <ExploreList options={options} />}
</div>
);

export const ExploreList = ({ options }: { options: Candidate[] }) => (
<ol className="Grid Grid--1of2 Grid--gutters">
{options &&
options.map((option, index) => (
<li className="Grid-cell" key={index}>
<ExploreOption option={option} />
</li>
))}
</ol>
);

export const ExploreOption = ({ option }: { option: Candidate }) => (
<Link to={option.url} className="link flex align-center text-bold">
<div
className="bg-slate-almost-extra-light p2 flex align-center rounded mr1 justify-center text-gold"
style={{ width: 48, height: 48 }}
>
<Icon name="bolt" size={32} />
</div>
<span>{option.title}</span>
</Link>
);

export default ExplorePane;
11 changes: 11 additions & 0 deletions frontend/src/metabase/components/MetabotLogo.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from "react";
import cx from "classnames";

const MetabotLogo = ({ className }) => (
<div
style={{ width: 58, height: 40 }}
className={cx("bg-brand rounded", className)}
/>
);

export default MetabotLogo;
42 changes: 42 additions & 0 deletions frontend/src/metabase/components/Quotes.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* @flow */

import React, { Component } from "react";

type Props = {
period: number,
quotes: string[],
};
type State = {
count: number,
};

export default class Quotes extends Component {
props: Props;
state: State = {
count: 0,
};

_timer: ?number = null;

static defaultProps = {
quotes: [],
period: 1000,
};

componentWillMount() {
this._timer = setInterval(
() => this.setState({ count: this.state.count + 1 }),
this.props.period,
);
}
componentWillUnmount() {
if (this._timer != null) {
clearInterval(this._timer);
}
}
render() {
const { quotes } = this.props;
const { count } = this.state;
return <span>{quotes[count % quotes.length]}</span>;
}
}
135 changes: 135 additions & 0 deletions frontend/src/metabase/dashboard/containers/AutomaticDashboardApp.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React from "react";

import { connect } from "react-redux";
import { push } from "react-router-redux";
import { Link } from "react-router";
import { withBackground } from "metabase/hoc/Background";
import ActionButton from "metabase/components/ActionButton";
import Icon from "metabase/components/Icon";
import cxs from "cxs";

import { Dashboard } from "./Dashboard";
import DashboardData from "metabase/dashboard/hoc/DashboardData";
import Parameters from "metabase/parameters/components/Parameters";

import { DashboardApi } from "metabase/services";
import * as Urls from "metabase/lib/urls";

import { dissoc } from "icepick";

const suggestionClasses = cxs({
":hover h3": {
color: "#509ee3",
},
":hover .Icon": {
color: "#F9D45C",
},
});

const SuggestionsList = ({ suggestions }) => (
<ol className="px2">
{suggestions.map((s, i) => (
<li key={i} className={suggestionClasses}>
<Link
to={s.url}
className="bordered rounded bg-white shadowed mb2 p2 flex no-decoration"
>
<div
className="bg-slate-extra-light rounded flex align-center justify-center text-slate mr1 flex-no-shrink"
style={{ width: 48, height: 48 }}
>
<Icon name="bolt" className="Icon text-grey-1" size={22} />
</div>
<div>
<h3 className="m0 mb1 ml1">{s.title}</h3>
<p className="text-grey-4 ml1 mt0 mb0">{s.description}</p>
</div>
</Link>
</li>
))}
</ol>
);

const SuggestionsSidebar = ({ related }) => (
<div className="flex flex-column">
<div className="py2 text-centered my3">
<h3>More explorations</h3>
</div>
{Object.values(related).map(suggestions => (
<SuggestionsList suggestions={suggestions} />
))}
</div>
);

const getDashboardId = (state, { params: { splat } }) =>
`/auto/dashboard/${splat}`;

const mapStateToProps = (state, props) => ({
dashboardId: getDashboardId(state, props),
});

@connect(mapStateToProps, { push })
@DashboardData
class AutomaticDashboardApp extends React.Component {
save = async () => {
const { dashboard, push } = this.props;
// remove the transient id before trying to save
const newDashboard = await DashboardApi.save(dissoc(dashboard, "id"));
push(Urls.dashboard(newDashboard.id));
};

render() {
const {
dashboard,
parameters,
parameterValues,
setParameterValue,
location,
} = this.props;
return (
<div className="flex">
<div className="flex-full">
<div className="bg-white border-bottom py2">
<div className="wrapper flex align-center">
<Icon name="bolt" className="text-gold mr1" size={24} />
<h2>{dashboard && dashboard.name}</h2>
<ActionButton
className="ml-auto bg-green text-white"
borderless
actionFn={this.save}
>
Save this
</ActionButton>
</div>
</div>
<div className="px3 pb4">
{parameters &&
parameters.length > 0 && (
<div className="px1 pt1">
<Parameters
parameters={parameters.map(p => ({
...p,
value: parameterValues && parameterValues[p.id],
}))}
query={location.query}
setParameterValue={setParameterValue}
syncQueryString
isQB
/>
</div>
)}
<Dashboard {...this.props} />
</div>
</div>
{dashboard &&
dashboard.related && (
<div className="Layout-sidebar flex-no-shrink">
<SuggestionsSidebar related={dashboard.related} />
</div>
)}
</div>
);
}
}

export default withBackground("bg-slate-extra-light")(AutomaticDashboardApp);
60 changes: 60 additions & 0 deletions frontend/src/metabase/dashboard/containers/Dashboard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/* @flow */

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

import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
import DashboardGrid from "metabase/dashboard/components/DashboardGrid";
import DashboardData from "metabase/dashboard/hoc/DashboardData";

import type { Dashboard as _Dashboard } from "metabase/meta/types/Dashboard";
import type { Parameter } from "metabase/meta/types/Parameter";

type Props = {
location?: { query: { [key: string]: string } },
dashboardId: string,

dashboard?: _Dashboard,
parameters: Parameter[],
parameterValues: { [key: string]: string },

initialize: () => void,
isFullscreen: boolean,
isNightMode: boolean,
fetchDashboard: (
dashId: string,
query?: { [key: string]: string },
) => Promise<void>,
fetchDashboardCardData: (options: {
reload: boolean,
clear: boolean,
}) => Promise<void>,
setParameterValue: (id: string, value: string) => void,
setErrorPage: (error: { status: number }) => void,
};

export class Dashboard extends Component {
props: Props;

render() {
const { dashboard } = this.props;

return (
<LoadingAndErrorWrapper
className={cx("Dashboard p1 flex-full")}
loading={!dashboard}
>
{() => (
<DashboardGrid
{...this.props}
className={"spread"}
// Don't allow clicking titles on public dashboards
navigateToNewCardFromDashboard={null}
/>
)}
</LoadingAndErrorWrapper>
);
}
}

export default DashboardData(Dashboard);
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { parseHashOptions } from "metabase/lib/browser";

const mapStateToProps = (state, props) => {
return {
dashboardId: props.params.dashboardId,
dashboardId: props.dashboardId || props.params.dashboardId,

isAdmin: getUserIsAdmin(state, props),
isEditing: getIsEditing(state, props),
Expand Down
Loading

0 comments on commit 2a185a0

Please sign in to comment.