Skip to content
Snippets Groups Projects
Commit 92058200 authored by Kyle Doherty's avatar Kyle Doherty
Browse files

Merge pull request #2638 from metabase/admin-checklist

admin checklist
parents f1af87c6 22c7ddfa
No related merge requests found
......@@ -7,6 +7,7 @@ import SettingsHeader from "./SettingsHeader.jsx";
import SettingsSetting from "./SettingsSetting.jsx";
import SettingsEmailForm from "./SettingsEmailForm.jsx";
import SettingsSlackForm from "./SettingsSlackForm.jsx";
import SettingsSetupList from "./SettingsSetupList.jsx";
import SettingsUpdatesForm from "./SettingsUpdatesForm.jsx";
import _ from "underscore";
......@@ -76,6 +77,13 @@ export default class SettingsEditor extends Component {
/>
</div>
);
} else if (section.name === "Setup") {
return (
<div className="px2">
<SettingsSetupList
ref="settingsForm" />
</div>
);
} else if (section.name === "Slack") {
return (
<div className="px2">
......
import React, { Component, PropTypes } from "react";
import Icon from "metabase/components/Icon.jsx";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
const TaskList = ({tasks}) =>
<ol>
{ tasks.map((task, index) => <li className="mb2" key={index}><Task {...task} /></li>)}
</ol>
const TaskSectionHeader = ({name}) =>
<h4 className="text-grey-4 text-bold text-uppercase pb2">{name}</h4>
const TaskSection = ({name, tasks}) =>
<div className="mb4">
<TaskSectionHeader name={name} />
<TaskList tasks={tasks} />
</div>
const TaskTitle = ({title, titleClassName}) =>
<h3 className={titleClassName}>{title}</h3>
const TaskDescription = ({description}) => <p className="m0 mt1">{description}</p>
const CompletionBadge = ({completed}) =>
<div className="mr2 flex align-center justify-center flex-no-shrink" style={{
borderWidth: 1,
borderStyle: 'solid',
borderColor: completed ? '#9CC177' : '#DCE9EA',
backgroundColor: completed ? '#9CC177' : '#fff',
width: 32,
height: 32,
borderRadius: 99
}}>
{ completed && <Icon name="check" color={'#fff'} />}
</div>
export const Task = ({title, description, completed, link}) =>
<a className="bordered border-brand-hover rounded transition-border flex align-center p2 no-decoration" href={link}>
<CompletionBadge completed={completed} />
<div>
<TaskTitle title={title} titleClassName={
completed ? 'text-success': 'text-brand'
} />
{ !completed ? <TaskDescription description={description} /> : null }
</div>
</a>
export default class SettingsSetupList extends Component {
constructor(props, context) {
super(props, context);
this.state = {
tasks: null,
error: null
};
}
async componentWillMount() {
let response = await fetch("/api/setup/admin_checklist", { credentials: 'same-origin' });
if (response.status !== 200) {
this.setState({ error: await response.json() })
} else {
this.setState({ tasks: await response.json() });
}
}
render() {
let tasks, nextTask;
if (this.state.tasks) {
tasks = this.state.tasks.map(section => ({
...section,
tasks: section.tasks.filter(task => {
if (task.is_next_step) {
nextTask = task;
}
return !task.is_next_step;
})
}))
}
return (
<div className="px2">
<h2>Getting set up</h2>
<p className="mt1">A few things you can do to get the most out of Metabase.</p>
<LoadingAndErrorWrapper loading={!this.state.tasks} error={this.state.error} >
{ () =>
<div style={{maxWidth: 468}}>
<TaskSection name="Recommended next step" tasks={[nextTask]} />
{
tasks.map((section, index) =>
<TaskSection {...section} key={index} />
)
}
</div>
}
</LoadingAndErrorWrapper>
</div>
);
}
}
......@@ -9,6 +9,10 @@ import MetabaseSettings from 'metabase/lib/settings';
var SettingsAdminControllers = angular.module('metabase.admin.settings.controllers', ['metabase.services']);
const SECTIONS = [
{
name: "Setup",
settings: []
},
{
name: "General",
settings: [
......
......@@ -10,3 +10,7 @@
.transition-all {
transition: all .2s linear;
}
.transition-border {
transition: border .3s linear;
}
......@@ -7,6 +7,7 @@ import Activity from "./Activity.jsx";
import RecentViews from "./RecentViews.jsx";
import Smile from './Smile.jsx';
import NewUserOnboardingModal from './NewUserOnboardingModal.jsx';
import NextStep from "./NextStep.jsx";
export default class Homepage extends Component {
......@@ -70,6 +71,7 @@ export default class Homepage extends Component {
</div>
</div>
<div className="Layout-sidebar flex-no-shrink">
<NextStep />
<RecentViews {...this.props} />
</div>
</div>
......
import React, { Component, PropTypes } from "react";
import SidebarSection from "./SidebarSection.jsx";
export default class NextStep extends Component {
constructor(props, context) {
super(props, context);
this.state = {
next: null
};
}
async componentWillMount() {
let response = await fetch("/api/setup/admin_checklist", { credentials: 'same-origin' });
if (response.status === 200) {
let sections = await response.json();
for (let section of sections) {
for (let task of section.tasks) {
if (task.is_next_step) {
this.setState({ next: task });
break;
}
}
}
}
}
render() {
const { next } = this.state;
if (next) {
return (
<SidebarSection title="Setup Tip" icon="clock" extra={<a className="text-brand no-decoration" href="/admin/settings">View all</a>}>
<a className="block p3 no-decoration" href={next.link}>
<h4 className="text-brand text-bold">{next.title}</h4>
<p className="m0 mt1">{next.description}</p>
</a>
</SidebarSection>
)
} else {
return <span className="hide" />
}
}
}
import React, { Component, PropTypes } from "react";
import Icon from "metabase/components/Icon.jsx";
import SidebarSection from "./SidebarSection.jsx";
import Urls from "metabase/lib/urls";
export default class RecentViews extends Component {
......@@ -43,29 +44,23 @@ export default class RecentViews extends Component {
let { recentViews } = this.props;
return (
<div className="p2">
<div className="text-dark-grey clearfix pt2 pb2">
<Icon className="float-left" name={'clock'} width={18} height={18}></Icon>
<span className="pl1 Sidebar-header">Recently Viewed</span>
</div>
<div className="rounded bg-white" style={{border: '1px solid #E5E5E5'}}>
{recentViews.length > 0 ?
<ul className="p2">
{recentViews.map((item, index) =>
<li key={index} className="py1 ml1 flex align-center clearfix">
{this.renderIllustration(item)}
<a data-metabase-event={"Recent Views;"+item.model+";"+item.cnt} className="ml1 flex-full link" href={Urls.modelToUrl(item.model, item.model_id)}>{item.model_object.name}</a>
</li>
)}
</ul>
:
<div className="flex flex-column layout-centered text-normal text-grey-2">
<span className="QuestionCircle mt4">!</span>
<p className="p3 text-centered text-grey-4" style={{ "maxWidth": "100%" }}>You haven't looked at any Dashboards or Questions recently</p>
</div>
}
</div>
</div>
<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)}
<a data-metabase-event={"Recent Views;"+item.model+";"+item.cnt} className="ml1 flex-full link" href={Urls.modelToUrl(item.model, item.model_id)}>{item.model_object.name}</a>
</li>
)}
</ul>
:
<div className="flex flex-column layout-centered text-normal text-grey-2">
<span className="QuestionCircle mt4">!</span>
<p className="p3 text-centered text-grey-4" style={{ "maxWidth": "100%" }}>You haven't looked at any Dashboards or Questions recently</p>
</div>
}
</SidebarSection>
);
}
}
import React, { Component, PropTypes } from "react";
import Icon from "metabase/components/Icon.jsx";
const SidebarSection = ({ title, icon, extra, children }) =>
<div className="px2 pt1">
<div className="text-dark-grey clearfix pt2 pb2">
<Icon className="float-left" name={icon} width={18} height={18}></Icon>
<span className="pl1 Sidebar-header">{title}</span>
{ extra && <span className="float-right">{extra}</span>}
</div>
<div className="rounded bg-white" style={{border: '1px solid #E5E5E5'}}>
{ children }
</div>
</div>
export default SidebarSection;
......@@ -20,5 +20,6 @@ log4j.appender.metabase=metabase.logger.Appender
log4j.logger.metabase=INFO
log4j.logger.metabase.db=DEBUG
log4j.logger.metabase.driver=DEBUG
log4j.logger.metabase.middleware=DEBUG
log4j.logger.metabase.query-processor=DEBUG
log4j.logger.metabase.sync-database=DEBUG
(ns metabase.api.setup
(:require [compojure.core :refer [defroutes POST]]
(:require [compojure.core :refer [defroutes GET POST]]
(metabase.api [common :refer :all]
[database :refer [annotation:DBEngine]])
(metabase [db :as db]
[driver :as driver]
[email :as email]
[events :as events])
[metabase.integrations.slack :as slack]
(metabase.models [database :refer [Database]]
[session :refer [Session]]
[setting :as setting]
......@@ -86,4 +88,108 @@
(catch Throwable e
(response-invalid :general (.getMessage e))))))
;;; Admin Checklist
(defn- admin-checklist-values []
(let [has-dbs? (db/exists? Database)
has-dashboards? (db/exists? 'Dashboard)
has-pulses? (db/exists? 'Pulse)
has-labels? (db/exists? 'Label)
has-hidden-tables? (db/exists? 'Table, :visibility_type [:not= nil])
has-metrics? (db/exists? 'Metric)
has-segments? (db/exists? 'Segment)
num-tables (db/select-one-count 'Table)
num-cards (db/select-one-count 'Card)
num-users (db/select-one-count 'User)]
[{:title "Add a database"
:group "Get connected"
:description "TODO - Write something good here"
:link "/admin/databases/create"
:completed has-dbs?
:triggered :always}
{:title "Set up email"
:group "Get connected"
:description "Add email credentials so you can more easily invite team members and get updates via Pulses."
:link "/admin/settings/?section=Email"
:completed (email/email-configured?)
:triggered :always}
{:title "Set Slack credentials"
:group "Get connected"
:description "Does your team use Slack? If so, you can send automated updates via pulses and ask questions with Metabot."
:link "/admin/settings/?section=Slack"
:completed (slack/slack-configured?)
:triggered :always}
{:title "Invite team members"
:group "Get connected"
:description "Share answers and data with the rest of your team."
:link "/admin/people/"
:completed (>= num-users 1)
:triggered (or has-dashboards?
has-pulses?
(>= num-cards 5))}
{:title "Hide irrelevant tables"
:group "Curate your data"
:description "If your data contains technical or irrelevant info you can hide it."
:link "/admin/datamodel/database"
:completed has-hidden-tables?
:triggered (>= num-tables 20)}
{:title "Organize questions"
:group "Curate your data"
:description "Have a lot of saved questions in Metabase? Create labels to help manage them and add context."
:link "/questions/all"
:completed (not has-labels?)
:triggered (>= num-cards 30)}
{:title "Create metrics"
:group "Curate your data"
:description "Define canonical metrics to make it easier for the rest of your team to get the right answers."
:link "/admin/datamodel/database"
:completed has-metrics?
:triggered (>= num-cards 30)}
{:title "Create segments"
:group "Curate your data"
:description "Keep everyone on the same page by creating canonnical sets of filters anyone can use while asking questions."
:link "/admin/datamodel/database"
:completed has-segments?
:triggered (>= num-cards 30)}
{:title "Create a getting started guide"
:group "Curate your data"
:description "Have a lot of data in Metabase? A getting started guide can help your team find their way around."
:completed false ; TODO - how do we determine this?
:triggered (and (>= num-cards 10)
(>= num-users 5))}]))
(defn- add-next-step-info
"Add `is_next_step` key to all the STEPS from `admin-checklist`.
The next step is the *first* step where `:triggered` is `true` and `:completed` is `false`."
[steps]
(loop [acc [], found-next-step? false, [step & more] steps]
(if-not step
acc
(let [is-next-step? (boolean (and (not found-next-step?)
(:triggered step)
(not (:completed step))))
step (-> (assoc step :is_next_step is-next-step?)
(update :triggered boolean))]
(recur (conj acc step)
(or found-next-step? is-next-step?)
more)))))
(defn- partition-steps-into-groups
"Partition the admin checklist steps into a sequence of groups."
[steps]
(for [[{group-name :group}, :as tasks] (partition-by :group steps)]
{:name group-name
:tasks tasks}))
(defn- admin-checklist []
(partition-steps-into-groups (add-next-step-info (admin-checklist-values))))
(defendpoint GET "/admin_checklist"
"Return various \"admin checklist\" steps and whether they've been completed. You must be a superuser to see this!"
[]
(check-superuser)
(admin-checklist))
(define-routes)
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