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

gsg empty state round 2 (#3499)

* show sections with empty state content

* more tweaks

* more cleanup

* only show sections that have content

* use your words

* they aren't arrays
parent c35a1581
No related branches found
No related tags found
No related merge requests found
......@@ -6,6 +6,7 @@ import Icon from "metabase/components/Icon"
import {
getQuestionUrl,
has,
typeToBgClass,
typeToLinkClass,
} from "../utils";
......@@ -82,11 +83,11 @@ const GuideDetail = ({
</ContextContent>
</div>
{ exploreLinks && exploreLinks.length > 0 && [
{ has(exploreLinks) && [
<div className="mt2">
<ContextHeading key="detailLabel">Explore this metric</ContextHeading>
<div key="detailLinks">
<Link className="text-brand inline-block mr2 link text-bold" to={link}>View this metric</Link>
<h4 className="inline-block mr2 link text-bold">View this metric</h4>
{ exploreLinks.map(link =>
<Link
className="inline-block text-bold text-brand mr2 link"
......
:local(.guideEmpty) {
composes: flex full justify-center from "style";
padding-top: 75px;
}
:local(.guideEmptyWrapper) {
composes: text-centered from "style";
width: 550px;
}
:local(.guideEmptyBody) {
composes: flex justify-center align-center mb4 from "style";
flex-direction: column;
}
:local(.guideEmptyMessage) {
composes: text-dark text-centered mt3 from "style";
max-width: 450px;
font-size: 1.1em;
line-height: 1.457em;
}
:local(.guideEmptyAction) {
composes: pt4 border-top from "style";
}
\ No newline at end of file
import React, { Component, PropTypes } from "react";
import pure from "recompose/pure";
import S from "./GuideEmptyState.css";
const GuideEmptyState = ({
isSuperuser,
startEditing
}) =>
<div className={S.guideEmpty}>
<div className={S.guideEmptyWrapper}>
<div className={S.guideEmptyBody}>
<img className="mb4" src={`/app/img/lightbulb.png`} height="200px" alt="Lightbulb" srcSet={`/app/img/lightbulb@2x.png 2x`} />
<h1 className="text-bold text-dark">Understanding our data</h1>
<span className={S.guideEmptyMessage}>This guide lets you explore all the metrics, segments, and raw data that we currently have in Metabase. Select a section on the left to learn more about our data.</span>
</div>
{ isSuperuser &&
<div className={S.guideEmptyAction}>
<button className="Button Button--large Button--primary" onClick={startEditing}>Create a custom Getting Started guide</button>
</div>
}
</div>
</div>;
GuideEmptyState.propTypes = {
isSuperuser: PropTypes.bool.isRequired,
startEditing: PropTypes.func.isRequired
};
export default pure(GuideEmptyState);
......@@ -5,10 +5,7 @@ import { connect } from 'react-redux';
import { push } from 'react-router-redux';
import { reduxForm } from "redux-form";
import i from "icepick";
import {
getQuestionUrl
} from '../utils';
import cx from "classnames";
import MetabaseAnalytics from "metabase/lib/analytics";
......@@ -17,7 +14,6 @@ import CreateDashboardModal from 'metabase/components/CreateDashboardModal.jsx';
import Modal from 'metabase/components/Modal.jsx';
import EditHeader from "metabase/reference/components/EditHeader.jsx";
import GuideEmptyState from "metabase/reference/components/GuideEmptyState.jsx";
import GuideHeader from "metabase/reference/components/GuideHeader.jsx";
import GuideEditSection from "metabase/reference/components/GuideEditSection.jsx";
import GuideDetail from "metabase/reference/components/GuideDetail.jsx";
......@@ -53,6 +49,8 @@ import {
} from '../selectors';
import {
getQuestionUrl,
has,
isGuideEmpty,
tryUpdateGuide
} from '../utils';
......@@ -218,6 +216,7 @@ export default class ReferenceGettingStartedGuide extends Component {
.map(field => [field.id.value, field.type.value])
.filter(idTypePair => idTypePair[0] !== null);
return (
<form className="full relative py4" style={style} onSubmit={onSubmit}>
{ isDashboardModalOpen &&
......@@ -451,73 +450,90 @@ export default class ReferenceGettingStartedGuide extends Component {
</div>
</GuideEditSection>
</div> :
!guide || isGuideEmpty(guide) ?
<GuideEmptyState
isSuperuser={user && user.is_superuser}
<div>
<GuideHeader
startEditing={startEditing}
/> :
<div>
<GuideHeader
startEditing={startEditing}
isSuperuser={user && user.is_superuser}
/>
<div className="wrapper wrapper--trim">
{ guide.most_important_dashboard !== null && [
<div className="my2">
<SectionHeader key={'dashboardTitle'}>
Our most important dashboard
isSuperuser={user && user.is_superuser}
/>
<div className="wrapper wrapper--trim">
{ (!guide || isGuideEmpty(guide)) && user && user.is_superuser && (
<AdminInstructions>
<h2 className="py2">Help your team get started with your data.</h2>
<GuideText>
Show your team what’s most important by choosing your top dashboard, metrics, and segments.
</GuideText>
<button
className="Button Button--primary"
onClick={startEditing}
>
Get started
</button>
</AdminInstructions>
)}
{ guide.most_important_dashboard !== null && [
<div className="my2">
<SectionHeader key={'dashboardTitle'}>
Our most important dashboard
</SectionHeader>
<GuideDetail
key={'dashboardDetail'}
type="dashboard"
entity={dashboards[guide.most_important_dashboard]}
tables={tables}
/>
</div>
]}
{ Object.keys(metrics) > 0 && (
<div className="my4 pt4">
<SectionHeader trim={guide.important_metrics.length === 0}>
{ guide.important_metrics && guide.important_metrics.length > 0 ? 'Numbers that we pay attention to' : 'Metrics' }
</SectionHeader>
<GuideDetail
key={'dashboardDetail'}
type="dashboard"
entity={dashboards[guide.most_important_dashboard]}
tables={tables}
/>
</div>
]}
<div className="my4">
{ guide.important_metrics && guide.important_metrics.length > 0 && [
<div className="my2">
<SectionHeader key={'metricsTitle'}>
Numbers that we pay attention to
</SectionHeader>
{ guide.important_metrics.map((metricId) =>
<GuideDetail
key={metricId}
type="metric"
entity={metrics[metricId]}
tables={tables}
exploreLinks={guide.metric_important_fields[metricId] &&
guide.metric_important_fields[metricId]
.map(fieldId => metadataFields[fieldId])
.map(field => ({
name: field.display_name || field.name,
url: getQuestionUrl({
dbId: tables[field.table_id] && tables[field.table_id].db_id,
tableId: field.table_id,
fieldId: field.id,
metricId
})
}))
}
/>
)}
<div>
<Link className="Button Button--primary" to={'/reference/metrics'}>
See all metrics
</Link>
{ (guide.important_metrics && guide.important_metrics.length > 0) ? [
<div className="my2">
{ guide.important_metrics.map((metricId) =>
<GuideDetail
key={metricId}
type="metric"
entity={metrics[metricId]}
tables={tables}
exploreLinks={guide.metric_important_fields[metricId] &&
guide.metric_important_fields[metricId]
.map(fieldId => metadataFields[fieldId])
.map(field => ({
name: field.display_name || field.name,
url: getQuestionUrl({
dbId: tables[field.table_id] && tables[field.table_id].db_id,
tableId: field.table_id,
fieldId: field.id,
metricId
})
}))
}
/>
)}
</div>
] :
<GuideText>
Metrics are important numbers your company cares about. They often represent a core indicator of how the business is performing.
</GuideText>
}
<div className="mt4">
<Link className="Button Button--primary" to={'/reference/metrics'}>
See all metrics
</Link>
</div>
]}
</div>
</div>
)
}
<div className="mt4">
{ ((guide.important_segments && guide.important_segments.length > 0) ||
(guide.important_tables && guide.important_tables.length > 0)) && [
<div className="mt4 pt4">
<SectionHeader trim={(!has(guide.important_segments) && !has(guide.important_tables))}>
{ Object.keys(segments) > 0 ? 'Segments and tables' : 'Tables' }
</SectionHeader>
{ has(guide.important_segments) || has(guide.important_tables) ? [
<div className="mt2">
<SectionHeader key={'segmentTitle'}>
Segments and tables
</SectionHeader>
{ guide.important_segments.map((segmentId) =>
<GuideDetail
key={segmentId}
......@@ -534,55 +550,72 @@ export default class ReferenceGettingStartedGuide extends Component {
tables={tables}
/>
)}
</div>,
<div key={'segmentSeeAll'}>
</div>
] : (
<div>
<GuideText>
{ Object.keys(segments) > 0 ? (
<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>
</span>
) : "Tables are the building blocks of your company's data."
}
</GuideText>
<div>
<Link className="Button Button--purple mr2" to={'/reference/segments'}>
See all segments
</Link>
<Link className="text-purple text-bold no-decoration text-underline-hover" to={'/reference/databases'}>
{ has(segments) && (
<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' : has(segments) },
{ 'Button Button--purple' : !has(segments) }
)}
to={'/reference/databases'}
>
See all tables
</Link>
</div>
</div>
]}
</div>
)
}
</div>
<div className="mt4">
{ guide.things_to_know && [
<SectionHeader key={'thingsToKnowTitle'}>
Other things to know about our data
</SectionHeader>,
<p className="text-paragraph text-measure" key={'thingsToKnowDetails'}>
{ guide.things_to_know || `Nothing to know yet`}
</p>,
<Link className="link text-bold" to={'/reference/databases'} key={'thingsToKnowSeeAll'}>
Explore our data
</Link>
]}
</div>
<div className="mt4 pt4">
<SectionHeader trim={!guide.things_to_know}>
{ guide.things_to_know ? 'Other things to know about our data' : 'Find out more' }
</SectionHeader>
<GuideText>
{ guide.things_to_know ? guide.things_to_know : "A good way to get to know your data is by spending a bit of time exploring the different tables and other info avaliable to you. It may take a while, but you'll start to recognize names and meanings over time."
}
</GuideText>
<Link className="link text-bold" to={'/reference/databases'}>
Explore our data
</Link>
</div>
<div className="mt4">
{ guide.contact && (guide.contact.name || guide.contact.email) && [
<SectionHeader key={'contactTitle'}>
Have questions?
</SectionHeader>,
<div className="mb4 pb4" key={'contactDetails'}>
{ guide.contact.name &&
<span className="text-dark mr3">
{`Contact ${guide.contact.name}`}
</span>
}
{ guide.contact.email &&
<a className="text-brand text-bold no-decoration" href={`mailto:${guide.contact.email}`}>
{guide.contact.email}
</a>
}
</div>
]}
</div>
<div className="mt4">
{ guide.contact && (guide.contact.name || guide.contact.email) && [
<SectionHeader key={'contactTitle'}>
Have questions?
</SectionHeader>,
<div className="mb4 pb4" key={'contactDetails'}>
{ guide.contact.name &&
<span className="text-dark mr3">
{`Contact ${guide.contact.name}`}
</span>
}
{ guide.contact.email &&
<a className="text-brand text-bold no-decoration" href={`mailto:${guide.contact.email}`}>
{guide.contact.email}
</a>
}
</div>
]}
</div>
</div>
</div>
}
</LoadingAndErrorWrapper>
</form>
......@@ -590,5 +623,13 @@ export default class ReferenceGettingStartedGuide extends Component {
}
}
const SectionHeader = ({ children }) => // eslint-disable-line react/prop-types
<h2 className="text-dark text-measure mb4">{children}</h2>
const GuideText = ({ children }) => // eslint-disable-line react/prop-types
<p className="text-paragraph text-measure">{children}</p>
const AdminInstructions = ({ children }) => // eslint-disable-line react/prop-types
<div className="bordered border-brand rounded p3 text-brand text-measure text-centered bg-light-blue">
{children}
</div>
const SectionHeader = ({ trim, children }) => // eslint-disable-line react/prop-types
<h2 className={cx('text-dark text-measure', { "mb0" : trim }, { "mb4" : !trim })}>{children}</h2>
......@@ -13,6 +13,8 @@ import {
getQuestionUrl
} from "./utils";
import _ from "underscore";
// there might be a better way to organize sections
// it feels like I'm duplicating a lot of routing logic here
//TODO: refactor to use different container components for each section
......@@ -22,7 +24,7 @@ import {
const referenceSections = {
[`/reference/guide`]: {
id: `/reference/guide`,
name: "Understanding our data",
name: "Start here",
breadcrumb: "Guide",
fetch: {
fetchGuide: [],
......@@ -47,7 +49,10 @@ const referenceSections = {
},
breadcrumb: "Metrics",
// mapping of propname to args of dispatch function
fetch: {fetchMetrics: []},
fetch: {
fetchMetrics: [],
fetchSegments: []
},
get: 'getMetrics',
icon: "ruler"
},
......@@ -63,7 +68,10 @@ const referenceSections = {
adminLink: "http://www.metabase.com/docs/latest/administration-guide/05-segments-and-metrics.html"
},
breadcrumb: "Segments",
fetch: {fetchSegments: []},
fetch: {
fetchMetrics: [],
fetchSegments: []
},
get: 'getSegments',
icon: "segment"
},
......@@ -79,7 +87,11 @@ const referenceSections = {
adminLink: "/admin/databases/create"
},
breadcrumb: "Databases",
fetch: {fetchDatabases: []},
fetch: {
fetchMetrics: [],
fetchSegments: [],
fetchDatabases: []
},
get: 'getDatabases',
icon: "database",
itemIcon: "database"
......@@ -557,11 +569,15 @@ export const getForeignKeys = createSelector(
)
export const getSections = createSelector(
[getSectionId, getMetric, getSegment, getDatabase, getTable, getField, getFieldBySegment, getTableBySegment, getTableByMetric, getUser, getReferenceSections],
(sectionId, metric, segment, database, table, field, fieldBySegment, tableBySegment, tableByMetric, user, referenceSections) => {
[getSectionId, getMetric, getSegment, getDatabase, getTable, getField, getFieldBySegment, getTableBySegment, getTableByMetric, getUser, getReferenceSections, getSegments, getMetrics],
(sectionId, metric, segment, database, table, field, fieldBySegment, tableBySegment, tableByMetric, user, referenceSections, segments, metrics) => {
// can be simplified if we had a single map of all sections
if (referenceSections[sectionId]) {
return referenceSections;
// filter out segments or metrics if we're not on that particular section and there are none
return _.omit(referenceSections,
Object.keys(metrics).length === 0 && sectionId !== "/reference/metrics" && "/reference/metrics",
Object.keys(segments).length === 0 && sectionId !== "/reference/segments" && "/reference/segments",
);
}
const metricSections = getMetricSections(metric, tableByMetric, user);
......
......@@ -465,3 +465,7 @@ export const typeToBgClass = {
segment: 'bg-purple',
table: 'bg-purple'
};
// little utility function to determine if we 'has' things, useful
// for handling entity empty states
export const has = (entity) => entity && entity.length > 0;
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