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

Consolidate empty states (#8187)

* refactor EmptyState and apply to search / collection landing

* consolidate 404 and unauthorized pages

* use EmptyState for CollectionEmptyState

* clean up component

* port Archived state

* intermediate legacy handling [ci skip]

* add errors to internal app for checks

* port generic error

* center buttons, fix 404 illustration
parent 7fbe5271
Branches
Tags
No related merge requests found
Showing
with 222 additions and 245 deletions
......@@ -7,10 +7,12 @@ import Navbar from "metabase/nav/containers/Navbar.jsx";
import UndoListing from "metabase/containers/UndoListing";
import NotFound from "metabase/components/NotFound.jsx";
import Unauthorized from "metabase/components/Unauthorized.jsx";
import Archived from "metabase/components/Archived.jsx";
import GenericError from "metabase/components/GenericError.jsx";
import {
Archived,
NotFound,
GenericError,
Unauthorized,
} from "metabase/containers/ErrorPages";
const mapStateToProps = (state, props) => ({
errorPage: state.app.errorPage,
......
import React from "react";
import EmptyState from "metabase/components/EmptyState";
import Link from "metabase/components/Link";
import { t } from "c-3po";
import fitViewport from "metabase/hoc/FitViewPort";
// TODO: port to ErrorMessage for more consistent style
const Archived = ({ entityName, linkTo }) => (
<div className="full-height flex justify-center align-center">
<EmptyState
message={
<div>
<div>{t`This ${entityName} has been archived`}</div>
<Link
to={linkTo}
className="my2 link"
style={{ fontSize: "14px" }}
>{t`View the archive`}</Link>
</div>
}
icon="viewArchive"
/>
</div>
);
export default fitViewport(Archived);
import React from "react";
import { Box } from "grid-styled";
import { t } from "c-3po";
import RetinaImage from "react-retina-image";
import { withRouter } from "react-router";
import Subhead from "metabase/components/Subhead";
import Link from "metabase/components/Link";
import * as Urls from "metabase/lib/urls";
import { normal } from "metabase/lib/colors";
import EmptyState from "metabase/components/EmptyState";
const CollectionEmptyState = ({ params }) => {
return (
<Box py={2}>
<Box mb={3}>
<EmptyState
title={t`This collection is empty, like a blank canvas`}
message={t`You can use collections to organize and group dashboards, questions and pulses for your team or yourself`}
illustrationElement={
<RetinaImage
src="app/img/collection-empty-state.png"
className="block ml-auto mr-auto"
/>
</Box>
<Box className="text-centered">
<Subhead color={normal.grey2}>
{t`This collection is empty, like a blank canvas`}
</Subhead>
<p className="text-paragraph text-medium">
{t`You can use collections to organize and group dashboards, questions and pulses for your team or yourself`}
</p>
}
link={
<Link
className="link text-bold"
mt={2}
to={Urls.newCollection(params.collectionId)}
>{t`Create another collection`}</Link>
</Box>
</Box>
}
/>
);
};
......
......@@ -42,50 +42,43 @@ import PinPositionDropTarget from "metabase/containers/dnd/PinPositionDropTarget
import PinDropTarget from "metabase/containers/dnd/PinDropTarget";
import ItemsDragLayer from "metabase/containers/dnd/ItemsDragLayer";
import EmptyState from "metabase/components/EmptyState";
const ROW_HEIGHT = 72;
const PAGE_PADDING = [2, 3, 4];
const ANALYTICS_CONTEXT = "Collection Landing";
const EmptyStateWrapper = ({ children }) => (
<Flex
align="center"
justify="center"
p={5}
flexDirection="column"
w={1}
h={"200px"}
className="text-medium"
>
<Box p={5} w={1} h={"200px"}>
{children}
</Flex>
</Box>
);
const DashboardEmptyState = () => (
<EmptyStateWrapper>
<Box>
<Icon name="dashboard" size={32} />
</Box>
<h3>{t`Dashboards let you collect and share data in one place.`}</h3>
<EmptyState
message={t`Dashboards let you collect and share data in one place.`}
illustrationElement={<Icon name="dashboard" size={32} />}
/>
</EmptyStateWrapper>
);
const PulseEmptyState = () => (
<EmptyStateWrapper>
<Box>
<Icon name="pulse" size={32} />
</Box>
<h3
>{t`Pulses let you send out the latest data to your team on a schedule via email or slack.`}</h3>
<EmptyState
message={t`Pulses let you send out the latest data to your team on a schedule via email or slack.`}
illustrationElement={<Icon name="pulse" size={32} />}
/>
</EmptyStateWrapper>
);
const QuestionEmptyState = () => (
<EmptyStateWrapper>
<Box>
<Icon name="beaker" size={32} />
</Box>
<h3>{t`Questions are a saved look at your data.`}</h3>
<EmptyState
message={t`Questions are a saved look at your data.`}
illustrationElement={<Icon name="beaker" size={32} />}
/>
</EmptyStateWrapper>
);
......
/* @flow */
import React from "react";
import { Link } from "react-router";
import cx from "classnames";
/*
* EmptyState is a component that can
* 1) introduce a new section of Metabase to a user and encourage the user to take an action
* 2) indicate an empty result after a user-triggered search/query
*/
import { Box, Flex } from "grid-styled";
import Icon from "metabase/components/Icon.jsx";
import Button from "metabase/components/Button";
import Icon from "metabase/components/Icon";
import Link from "metabase/components/Link";
import Text from "metabase/components/Text";
type EmptyStateProps = {
message: string | React$Element<any>,
message: React$Element<any>,
title?: string,
icon?: string,
image?: string,
imageHeight?: string, // for reducing ui flickering when the image is loading
imageClassName?: string,
action?: string,
link?: string,
illustrationElement: React$Element<any>,
onActionClick?: () => void,
smallDescription?: boolean,
};
// Don't break existing empty states
// TODO - remove these and update empty states with proper usage of illustrationElement
const LegacyIcon = props =>
props.icon ? <Icon name={props.icon} size={40} /> : null;
const LegacyImage = props =>
props.image ? (
<img
src={`${props.image}.png`}
width="300px"
height={props.imageHeight}
alt={props.message}
srcSet={`${props.image}@2x.png 2x`}
className={props.imageClassName}
/>
) : null;
const EmptyState = ({
title,
message,
icon,
image,
imageHeight,
imageClassName,
action,
link,
illustrationElement,
onActionClick,
smallDescription = false,
...rest
}: EmptyStateProps) => (
<div
className="text-centered text-brand-light my2"
style={smallDescription ? {} : { width: "350px" }}
>
{title && <h2 className="text-brand mb4">{title}</h2>}
{icon && <Icon name={icon} size={40} />}
{image && (
<img
src={`${image}.png`}
width="300px"
height={imageHeight}
alt={message}
srcSet={`${image}@2x.png 2x`}
className={imageClassName}
/>
)}
<div className="flex justify-center">
<h2
className={cx("text-light text-normal mt2 mb4", {
"text-paragraph": smallDescription,
})}
style={{ lineHeight: "1.5em" }}
>
{message}
</h2>
</div>
{action &&
link && (
<Link
to={link}
className="Button Button--primary mt4"
target={link.startsWith("http") ? "_blank" : ""}
>
{action}
</Link>
)}
{action &&
onActionClick && (
<a onClick={onActionClick} className="Button Button--primary mt4">
{action}
</a>
)}
</div>
<Box>
<Flex justify="center" flexDirection="column" align="center">
{illustrationElement && <Box mb={[2, 3]}>{illustrationElement}</Box>}
<Box>
<LegacyIcon {...rest} />
<LegacyImage {...rest} />
</Box>
{title && <h2>{title}</h2>}
{message && <Text color="medium">{message}</Text>}
</Flex>
{/* TODO - we should make this children or some other more flexible way to
add actions
*/}
<Flex mt={2}>
<Flex align="center" ml="auto" mr="auto">
{action &&
link && (
<Link to={link} target={link.startsWith("http") ? "_blank" : ""}>
<Button primary>{action}</Button>
</Link>
)}
{action &&
onActionClick && (
<Button onClick={onActionClick} primary>
{action}
</Button>
)}
</Flex>
</Flex>
</Box>
);
export default EmptyState;
import React from "react";
import { t } from "c-3po";
import { Flex } from "grid-styled";
import fitViewport from "metabase/hoc/FitViewPort";
import ErrorMessage from "metabase/components/ErrorMessage";
import ErrorDetails from "metabase/components/ErrorDetails";
const GenericError = ({
title = t`Something's gone wrong`,
message = t`We've run into an error. You can try refreshing the page, or just go back.`,
details = null,
}) => (
<Flex align="center" justify="center" className="full-height">
<ErrorMessage type="serverError" title={title} message={message} />
<ErrorDetails className="pt2" details={details} centered />
</Flex>
);
export default fitViewport(GenericError);
import React, { Component } from "react";
import { Link } from "react-router";
import { t } from "c-3po";
import * as Urls from "metabase/lib/urls";
// TODO: port to ErrorMessage for more consistent style
export default class NotFound extends Component {
render() {
return (
<div className="layout-centered flex full">
<div className="p4 text-bold">
<h1 className="text-brand text-light mb3">{t`We're a little lost...`}</h1>
<p className="h4 mb1">
{t`The page you asked for couldn't be found`}.
</p>
<p className="h4">{t`You might've been tricked by a ninja, but in all likelihood, you were just given a bad link.`}</p>
<p className="h4 my4">{t`You can always:`}</p>
<div className="flex align-center">
<Link to={Urls.question()} className="Button Button--primary">
<div className="p1">{t`Ask a new question.`}</div>
</Link>
<span className="mx2">{t`or`}</span>
<a
className="Button Button--withIcon"
target="_blank"
href="https://giphy.com/tv/search/kitten"
>
<div className="p1 flex align-center relative">
<span className="h2">😸</span>
<span className="ml1">{t`Take a kitten break.`}</span>
</div>
</a>
</div>
</div>
</div>
);
}
}
import styled from "styled-components";
import { space } from "system-components";
import colors from "metabase/lib/colors";
const Text = styled.p`
${space};
color: ${props => colors[`text-${props.color}`]};
`;
Text.defaultProps = {
className: "text-paragraph",
color: "dark",
};
export default Text;
import React, { Component } from "react";
import { t } from "c-3po";
import { Flex } from "grid-styled";
import fitViewPort from "metabase/hoc/FitViewPort";
import Icon from "metabase/components/Icon";
// TODO: port to ErrorMessage for more consistent style
@fitViewPort
export default class Unauthorized extends Component {
render() {
return (
<Flex
className={this.props.fitClassNames}
flexDirection="column"
align="center"
justifyContent="center"
color=""
>
<Icon name="key" size={100} />
<h1 className="mt4">{t`Sorry, you don’t have permission to see that.`}</h1>
</Flex>
);
}
}
import React from "react";
import { Flex } from "grid-styled";
import { t } from "c-3po";
import * as Urls from "metabase/lib/urls";
import fitViewport from "metabase/hoc/FitViewPort";
import Icon from "metabase/components/Icon";
import EmptyState from "metabase/components/EmptyState";
import ErrorDetails from "metabase/components/ErrorDetails";
const ErrorPageWrapper = fitViewport(({ fitClassNames, children }) => (
<Flex
align="center"
flexDirection="column"
justify="center"
className={fitClassNames}
>
{children}
</Flex>
));
export const GenericError = ({
title = t`Something's gone wrong`,
message = t`We've run into an error. You can try refreshing the page, or just go back.`,
details = null,
}) => (
<ErrorPageWrapper>
<EmptyState
title={title}
message={message}
illustrationElement={
<div className="QueryError-image QueryError-image--serverError" />
}
/>
<ErrorDetails className="pt2" details={details} centered />
</ErrorPageWrapper>
);
export const NotFound = () => (
<ErrorPageWrapper>
<EmptyState
illustrationElement={<img src="../app/assets/img/no_results.svg" />}
title={t`We're a little lost...`}
message={t`The page you asked for couldn't be found.`}
link={Urls.question()}
/>
</ErrorPageWrapper>
);
export const Unauthorized = () => (
<ErrorPageWrapper>
<EmptyState
title={t`Sorry, you don’t have permission to see that.`}
illustrationElement={<Icon name="key" size={100} />}
/>
</ErrorPageWrapper>
);
export const Archived = ({ entityName, linkTo }) => (
<ErrorPageWrapper>
<EmptyState
title={t`This ${entityName} has been archived`}
illustrationElement={<Icon name="viewArchive" size={100} />}
link={linkTo}
/>
</ErrorPageWrapper>
);
......@@ -9,6 +9,7 @@ import { Box, Flex } from "grid-styled";
import EntityListLoader from "metabase/entities/containers/EntityListLoader";
import Card from "metabase/components/Card";
import EmptyState from "metabase/components/EmptyState";
import EntityItem from "metabase/components/EntityItem";
import Subhead from "metabase/components/Subhead";
import ItemTypeFilterBar, {
......@@ -44,15 +45,15 @@ export default class SearchApp extends React.Component {
{({ list }) => {
if (list.length === 0) {
return (
<Flex align="center" justify="center" my={4} py={4}>
<Box>
<img src="../app/assets/img/no_results.svg" />
</Box>
<Box mt={4}>
<Subhead>{t`It's quiet around here...`}</Subhead>
<p>{t`Metabase couldn't find any results for this.`}</p>
</Box>
</Flex>
<Card>
<EmptyState
title={t`No results`}
message={t`Metabase couldn't find any results for your search.`}
illustrationElement={
<img src="../app/assets/img/no_results.svg" />
}
/>
</Card>
);
}
......
......@@ -3,6 +3,15 @@
import React from "react";
import { Link, Route, IndexRoute } from "react-router";
import {
Archived,
GenericError,
NotFound,
Unauthorized,
} from "metabase/containers/ErrorPages";
const ErrorWithDetails = () => <GenericError details="Example error message" />;
// $FlowFixMe: doesn't know about require.context
const req = require.context(
"metabase/internal/components",
......@@ -64,5 +73,12 @@ export default (
<Route path={name.toLowerCase()} component={Component} />
)),
)}
<Route path="errors">
<Route path="404" component={NotFound} />
<Route path="archived" component={Archived} />
<Route path="unauthorized" component={Unauthorized} />
<Route path="generic" component={GenericError} />
<Route path="details" component={ErrorWithDetails} />
</Route>
</Route>
);
......@@ -9,6 +9,9 @@ import { t } from "c-3po";
import type { Metric } from "metabase/meta/types/Metric";
import type Metadata from "metabase-lib/lib/metadata/Metadata";
import EmptyState from "metabase/components/EmptyState";
import { Flex } from "grid-styled";
import fitViewPort from "metabase/hoc/FitViewPort";
import type { StructuredQuery } from "metabase/meta/types/Query";
import { getCurrentQuery } from "metabase/new_query/selectors";
......@@ -87,25 +90,30 @@ export default class MetricSearch extends Component {
/>
);
} else {
return (
<div className="mt2 flex-full flex align-center justify-center">
<EmptyState
message={
<span>
{t`Defining common metrics for your team makes it even easier to ask questions`}
</span>
}
image="app/img/metrics_illustration"
action={t`How to create metrics`}
link="http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html"
className="mt2"
imageClassName="mln2"
/>
</div>
);
return <MetricEmptyState />;
}
}}
</LoadingAndErrorWrapper>
);
}
}
const MetricEmptyState = fitViewPort(({ fitClassNames }) => (
<Flex
mt={2}
align="center"
flexDirection="column"
justify="center"
className={fitClassNames}
>
<EmptyState
message={t`Defining common metrics for your team makes it even easier to ask questions`}
title={t`No metrics`}
image="app/img/metrics_illustration"
action={t`How to create metrics`}
link="http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html"
className="mt2"
imageClassName="mln2"
/>
</Flex>
));
......@@ -55,8 +55,8 @@ import {
} from "metabase/new_query/router_wrappers";
import CreateDashboardModal from "metabase/components/CreateDashboardModal";
import NotFound from "metabase/components/NotFound.jsx";
import Unauthorized from "metabase/components/Unauthorized.jsx";
import { NotFound, Unauthorized } from "metabase/containers/ErrorPages";
// Reference Guide
import GettingStartedGuideContainer from "metabase/reference/guide/GettingStartedGuideContainer.jsx";
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment