Skip to content
Snippets Groups Projects
Unverified Commit ceb2f674 authored by Tom Robinson's avatar Tom Robinson Committed by GitHub
Browse files

Remove broken Getting Started Guide (#11843)

* Remove broken GSG

* remove gsg test

* remove MetabaseApi.e2e.spec.js test
parent 7bc827d4
No related branches found
No related tags found
No related merge requests found
Showing
with 0 additions and 5529 deletions
import React from "react";
import PropTypes from "prop-types";
import { Link } from "react-router";
import pure from "recompose/pure";
import cx from "classnames";
import { t } from "ttag";
import Icon from "metabase/components/Icon";
import * as Urls from "metabase/lib/urls";
import { getQuestionUrl, has, typeToBgClass, typeToLinkClass } from "../utils";
const GuideDetail = ({
entity = {},
tables,
type,
exploreLinks,
detailLabelClasses,
}) => {
const title = entity.display_name || entity.name;
const { caveats, points_of_interest } = entity;
const typeToLink = {
dashboard: Urls.dashboard(entity.id),
metric: getQuestionUrl({
dbId: tables[entity.table_id] && tables[entity.table_id].db_id,
tableId: entity.table_id,
metricId: entity.id,
}),
segment: getQuestionUrl({
dbId: tables[entity.table_id] && tables[entity.table_id].db_id,
tableId: entity.table_id,
segmentId: entity.id,
}),
table: getQuestionUrl({
dbId: entity.db_id,
tableId: entity.id,
}),
};
const link = typeToLink[type];
const typeToLearnMoreLink = {
metric: `/reference/metrics/${entity.id}`,
segment: `/reference/segments/${entity.id}`,
table: `/reference/databases/${entity.db_id}/tables/${entity.id}`,
};
const learnMoreLink = typeToLearnMoreLink[type];
const linkClass = typeToLinkClass[type];
const linkHoverClass = `${typeToLinkClass[type]}-hover`;
const bgClass = typeToBgClass[type];
const hasLearnMore =
type === "metric" || type === "segment" || type === "table";
return (
<div className="relative mt2 pb3">
<div className="flex align-center">
<div
style={{
width: 40,
height: 40,
left: -60,
}}
className={cx(
"absolute text-white flex align-center justify-center",
bgClass,
)}
>
<Icon name={type === "metric" ? "ruler" : type} />
</div>
{title && (
<ItemTitle
link={link}
title={title}
linkColorClass={linkClass}
linkHoverClass={linkHoverClass}
/>
)}
</div>
<div className="mt2">
<ContextHeading>
{type === "dashboard"
? t`Why this ${type} is important`
: t`Why this ${type} is interesting`}
</ContextHeading>
<ContextContent empty={!points_of_interest}>
{points_of_interest ||
(type === "dashboard"
? t`Nothing important yet`
: t`Nothing interesting yet`)}
</ContextContent>
<div className="mt2">
<ContextHeading>
{t`Things to be aware of about this ${type}`}
</ContextHeading>
<ContextContent empty={!caveats}>
{caveats || t`Nothing to be aware of yet`}
</ContextContent>
</div>
{has(exploreLinks) && [
<div className="mt2">
<ContextHeading key="detailLabel">{t`Explore this metric`}</ContextHeading>
<div key="detailLinks">
<h4 className="inline-block mr2 link text-bold">{t`View this metric`}</h4>
{exploreLinks.map(link => (
<Link
className="inline-block text-bold text-brand mr2 link"
key={link.url}
to={link.url}
>
{t`By ${link.name}`}
</Link>
))}
</div>
</div>,
]}
{hasLearnMore && (
<Link
className={cx(
"block mt3 no-decoration text-underline-hover text-bold",
linkClass,
)}
to={learnMoreLink}
>
{t`Learn more`}
</Link>
)}
</div>
</div>
);
};
GuideDetail.propTypes = {
entity: PropTypes.object,
type: PropTypes.string,
exploreLinks: PropTypes.array,
};
const ItemTitle = ({ title, link, linkColorClass, linkHoverClass }) => (
<h2>
<Link
className={cx(linkColorClass, linkHoverClass)}
style={{ textDecoration: "none" }}
to={link}
>
{title}
</Link>
</h2>
);
const ContextHeading = ({ children }) => (
<h3 className="my2 text-medium">{children}</h3>
);
const ContextContent = ({ empty, children }) => (
<p
className={cx("m0 text-paragraph text-measure text-pre-wrap", {
"text-medium": empty,
})}
>
{children}
</p>
);
export default pure(GuideDetail);
:local(.guideDetailEditor):last-child {
margin-bottom: 0;
}
:local(.guideDetailEditorTextarea) {
composes: text-dark input p2 mb4 from "style";
resize: none;
font-size: 16px;
width: 100%;
min-height: 100px;
background-color: unset;
}
:local(.guideDetailEditorTextarea):last-child {
margin-bottom: 0;
}
import React from "react";
import PropTypes from "prop-types";
// FIXME: using pure seems to mess with redux form updates
// import pure from "recompose/pure";
import cx from "classnames";
import { t } from "ttag";
import S from "./GuideDetailEditor.css";
import Select from "metabase/components/Select";
import Icon from "metabase/components/Icon";
import Tooltip from "metabase/components/Tooltip";
import { typeToBgClass } from "../utils.js";
import { SchemaTableAndSegmentDataSelector } from "metabase/query_builder/components/DataSelector";
const GuideDetailEditor = ({
className,
type,
entities,
metadata = {},
selectedIds = [],
selectedIdTypePairs = [],
formField,
removeField,
editLabelClasses,
}) => {
const {
databases,
tables,
segments,
metrics,
fields,
metricImportantFields,
} = metadata;
const bgClass = typeToBgClass[type];
const entityId = formField.id.value;
const disabled =
formField.id.value === null || formField.id.value === undefined;
const tableId = metrics && metrics[entityId] && metrics[entityId].table_id;
const tableFields =
(tables && tables[tableId] && tables[tableId].fields) || [];
const fieldsByMetric =
type === "metric" ? tableFields.map(fieldId => fields[fieldId]) : [];
const selectClasses = "input h3 px2 py1";
return (
<div className={cx("mb2 border-bottom pb4 text-measure", className)}>
<div className="relative mt2 flex align-center">
<div
style={{
width: 40,
height: 40,
left: -60,
}}
className={cx(
"absolute text-white flex align-center justify-center",
bgClass,
)}
>
<Icon name={type === "metric" ? "ruler" : type} />
</div>
<div className="py2">
{entities ? (
<Select
value={entities[formField.id.value]}
options={Object.values(entities)}
disabledOptionIds={selectedIds}
optionNameFn={option => option.display_name || option.name}
onChange={entity => {
//TODO: refactor into function
formField.id.onChange(entity.id);
formField.points_of_interest.onChange(
entity.points_of_interest || "",
);
formField.caveats.onChange(entity.caveats || "");
if (type === "metric") {
formField.important_fields.onChange(
metricImportantFields[entity.id] &&
metricImportantFields[entity.id].map(
fieldId => fields[fieldId],
),
);
}
}}
placeholder={t`Select...`}
/>
) : (
<SchemaTableAndSegmentDataSelector
className={cx(
selectClasses,
"inline-block",
"rounded",
"text-bold",
)}
triggerIconSize={12}
selectedTableId={
formField.type.value === "table" &&
Number.parseInt(formField.id.value)
}
selectedDatabaseId={
formField.type.value === "table" &&
tables[formField.id.value] &&
tables[formField.id.value].db_id
}
selectedSegmentId={
formField.type.value === "segment" &&
Number.parseInt(formField.id.value)
}
databases={Object.values(databases).map(database => ({
...database,
tables: database.tables.map(tableId => tables[tableId]),
}))}
setDatabaseFn={() => null}
tables={Object.values(tables)}
disabledTableIds={selectedIdTypePairs
.filter(idTypePair => idTypePair[1] === "table")
.map(idTypePair => idTypePair[0])}
setSourceTableFn={tableId => {
const table = tables[tableId];
formField.id.onChange(table.id);
formField.type.onChange("table");
formField.points_of_interest.onChange(
table.points_of_interest || null,
);
formField.caveats.onChange(table.caveats || null);
}}
segments={Object.values(segments)}
disabledSegmentIds={selectedIdTypePairs
.filter(idTypePair => idTypePair[1] === "segment")
.map(idTypePair => idTypePair[0])}
setSourceSegmentFn={segmentId => {
const segment = segments[segmentId];
formField.id.onChange(segment.id);
formField.type.onChange("segment");
formField.points_of_interest.onChange(
segment.points_of_interest || "",
);
formField.caveats.onChange(segment.caveats || "");
}}
/>
)}
</div>
<div className="ml-auto cursor-pointer text-light">
<Tooltip tooltip={t`Remove item`}>
<Icon name="close" width={16} height={16} onClick={removeField} />
</Tooltip>
</div>
</div>
<div className="mt2 text-measure">
<div className={cx("mb2", { disabled: disabled })}>
<EditLabel>
{type === "dashboard"
? t`Why is this dashboard the most important?`
: t`What is useful or interesting about this ${type}?`}
</EditLabel>
<textarea
className={S.guideDetailEditorTextarea}
placeholder={t`Write something helpful here`}
{...formField.points_of_interest}
disabled={disabled}
/>
</div>
<div className={cx("mb2", { disabled: disabled })}>
<EditLabel>
{type === "dashboard"
? t`Is there anything users of this dashboard should be aware of?`
: t`Anything users should be aware of about this ${type}?`}
</EditLabel>
<textarea
className={S.guideDetailEditorTextarea}
placeholder={t`Write something helpful here`}
{...formField.caveats}
disabled={disabled}
/>
</div>
{type === "metric" && (
<div className={cx("mb2", { disabled: disabled })}>
<EditLabel key="metricFieldsLabel">
{t`Which 2-3 fields do you usually group this metric by?`}
</EditLabel>
<Select
options={fieldsByMetric}
optionNameFn={option => option.display_name || option.name}
placeholder={t`Select...`}
values={formField.important_fields.value || []}
disabledOptionIds={
formField.important_fields.value &&
formField.important_fields.value.length === 3
? fieldsByMetric
.filter(
field =>
!formField.important_fields.value.includes(field),
)
.map(field => field.id)
: []
}
onChange={field => {
const importantFields = formField.important_fields.value || [];
return importantFields.includes(field)
? formField.important_fields.onChange(
importantFields.filter(
importantField => importantField !== field,
),
)
: importantFields.length < 3 &&
formField.important_fields.onChange(
importantFields.concat(field),
);
}}
disabled={
formField.id.value === null || formField.id.value === undefined
}
/>
</div>
)}
</div>
</div>
);
};
const EditLabel = ({ children }) => <h3 className="mb1">{children}</h3>;
GuideDetailEditor.propTypes = {
className: PropTypes.string,
type: PropTypes.string.isRequired,
entities: PropTypes.object,
metadata: PropTypes.object,
selectedIds: PropTypes.array,
selectedIdTypePairs: PropTypes.array,
formField: PropTypes.object.isRequired,
removeField: PropTypes.func.isRequired,
};
export default GuideDetailEditor;
:local(.guideEditSectionCollapsed) {
composes: flex flex-full align-center mt4 p3 input text-brand text-bold from "style";
font-size: 16px;
}
:local(.guideEditSectionDisabled) {
composes: text-medium from "style";
}
:local(.guideEditSectionCollapsedIcon) {
composes: mr3 from "style";
}
:local(.guideEditSectionCollapsedTitle) {
composes: flex-full mr3 from "style";
}
:local(.guideEditSectionCollapsedLink) {
composes: text-brand no-decoration from "style";
font-size: 14px;
}
import React from "react";
import PropTypes from "prop-types";
import { Link } from "react-router";
import pure from "recompose/pure";
import cx from "classnames";
import S from "./GuideEditSection.css";
import Icon from "metabase/components/Icon";
const GuideEditSection = ({
children,
isCollapsed,
isDisabled,
showLink,
collapsedIcon,
collapsedTitle,
linkMessage,
link,
action,
expand,
}) =>
isCollapsed ? (
<div
className={cx("text-measure", S.guideEditSectionCollapsed, {
"cursor-pointer border-brand-hover": !isDisabled,
[S.guideEditSectionDisabled]: isDisabled,
})}
onClick={!isDisabled && expand}
>
<Icon
className={S.guideEditSectionCollapsedIcon}
name={collapsedIcon}
size={24}
/>
<span className={S.guideEditSectionCollapsedTitle}>{collapsedTitle}</span>
{(showLink || isDisabled) &&
(link ? (
link.startsWith("http") ? (
<a
className={S.guideEditSectionCollapsedLink}
href={link}
target="_blank"
>
{linkMessage}
</a>
) : (
<Link className={S.guideEditSectionCollapsedLink} to={link}>
{linkMessage}
</Link>
)
) : (
action && (
<a className={S.guideEditSectionCollapsedLink} onClick={action}>
{linkMessage}
</a>
)
))}
</div>
) : (
<div className={cx("my4", S.guideEditSection)}>{children}</div>
);
GuideEditSection.propTypes = {
isCollapsed: PropTypes.bool.isRequired,
};
export default pure(GuideEditSection);
import React from "react";
import PropTypes from "prop-types";
import pure from "recompose/pure";
import { t } from "ttag";
import Button from "metabase/components/Button";
const GuideHeader = ({ startEditing, isSuperuser }) => (
<div>
<div className="wrapper wrapper--trim sm-py4 sm-my3">
<div className="flex align-center">
<h1 className="text-dark" style={{ fontWeight: 700 }}>
{t`Start here`}.
</h1>
{isSuperuser && (
<span className="ml-auto">
<Button primary icon="pencil" onClick={startEditing}>
{t`Edit`}
</Button>
</span>
)}
</div>
<p
className="text-paragraph"
style={{ maxWidth: 620 }}
>{t`This is the perfect place to start if you’re new to your company’s data, or if you just want to check in on what’s going on.`}</p>
</div>
</div>
);
GuideHeader.propTypes = {
startEditing: PropTypes.func.isRequired,
isSuperuser: PropTypes.bool,
};
export default pure(GuideHeader);
/* eslint "react/prop-types": "warn" */
import React, { Component } from "react";
import PropTypes from "prop-types";
import { Link } from "react-router";
import { connect } from "react-redux";
import { t, jt } from "ttag";
import cx from "classnames";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
import GuideHeader from "metabase/reference/components/GuideHeader";
import GuideDetail from "metabase/reference/components/GuideDetail";
import * as metadataActions from "metabase/redux/metadata";
import * as actions from "metabase/reference/reference";
import { setRequestUnloaded } from "metabase/redux/requests";
import Dashboards from "metabase/entities/dashboards";
import { updateSetting } from "metabase/admin/settings/settings";
import {
getGuide,
getUser,
getDashboards,
getLoading,
getError,
getIsEditing,
getTables,
getFields,
getMetrics,
getSegments,
} from "../selectors";
import { getQuestionUrl, has } from "../utils";
const isGuideEmpty = ({
things_to_know,
contact,
most_important_dashboard,
important_metrics,
important_segments,
important_tables,
} = {}) =>
things_to_know
? false
: contact && contact.name
? false
: contact && contact.email
? false
: most_important_dashboard
? false
: important_metrics && important_metrics.length !== 0
? false
: important_segments && important_segments.length !== 0
? false
: important_tables && important_tables.length !== 0
? false
: true;
// This function generates a link for each important field of a Metric.
// The link goes to a question comprised of this Metric broken out by
// That important field.
const exploreLinksForMetric = (metricId, guide, metadataFields, tables) => {
if (guide.metric_important_fields[metricId]) {
return 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,
}),
}));
}
};
const mapStateToProps = (state, props) => ({
guide: getGuide(state, props),
user: getUser(state, props),
dashboards: getDashboards(state, props),
metrics: getMetrics(state, props),
segments: getSegments(state, props),
tables: getTables(state, props),
// FIXME: avoids naming conflict, tried using the propNamespace option
// version but couldn't quite get it to work together with passing in
// dynamic initialValues
metadataFields: getFields(state, props),
loading: getLoading(state, props),
// naming this 'error' will conflict with redux form
loadingError: getError(state, props),
isEditing: getIsEditing(state, props),
});
const mapDispatchToProps = {
updateDashboard: Dashboards.actions.update,
createDashboard: Dashboards.actions.create,
updateSetting,
setRequestUnloaded,
...metadataActions,
...actions,
};
@connect(
mapStateToProps,
mapDispatchToProps,
)
export default class GettingStartedGuide extends Component {
static propTypes = {
fields: PropTypes.object,
style: PropTypes.object,
guide: PropTypes.object,
user: PropTypes.object,
dashboards: PropTypes.object,
metrics: PropTypes.object,
segments: PropTypes.object,
tables: PropTypes.object,
metadataFields: PropTypes.object,
loadingError: PropTypes.any,
loading: PropTypes.bool,
startEditing: PropTypes.func,
};
render() {
const {
style,
guide,
user,
dashboards,
metrics,
segments,
tables,
metadataFields,
loadingError,
loading,
startEditing,
} = this.props;
return (
<div className="full relative p3" style={style}>
<LoadingAndErrorWrapper
className="full"
style={style}
loading={!loadingError && loading}
error={loadingError}
>
{() => (
<div>
<GuideHeader
startEditing={startEditing}
isSuperuser={user && user.is_superuser}
/>
<div className="wrapper wrapper--trim">
{(!guide || isGuideEmpty(guide)) && user && user.is_superuser && (
<AdminInstructions>
<h2 className="py2">{t`Help your team get started with your data.`}</h2>
<GuideText>
{t`Show your team what’s most important by choosing your top dashboard, metrics, and segments.`}
</GuideText>
<button
className="Button Button--primary"
onClick={startEditing}
>
{t`Get started`}
</button>
</AdminInstructions>
)}
{guide.most_important_dashboard !== null && [
<div className="my2">
<SectionHeader key={"dashboardTitle"}>
{t`Our most important dashboard`}
</SectionHeader>
<GuideDetail
key={"dashboardDetail"}
type="dashboard"
entity={dashboards[guide.most_important_dashboard]}
tables={tables}
/>
</div>,
]}
{Object.keys(metrics).length > 0 && (
<div className="my4 pt4">
<SectionHeader trim={guide.important_metrics.length === 0}>
{guide.important_metrics &&
guide.important_metrics.length > 0
? t`Numbers that we pay attention to`
: t`Metrics`}
</SectionHeader>
{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={exploreLinksForMetric(
metricId,
guide,
metadataFields,
tables,
)}
/>
))}
</div>,
]
) : (
<GuideText>
{t`Metrics are important numbers your company cares about. They often represent a core indicator of how the business is performing.`}
</GuideText>
)}
<div>
<Link
className="Button Button--primary"
to={"/reference/metrics"}
>
{t`See all metrics`}
</Link>
</div>
</div>
)}
<div className="mt4 pt4">
<SectionHeader
trim={
!has(guide.important_segments) &&
!has(guide.important_tables)
}
>
{Object.keys(segments).length > 0
? t`Segments and tables`
: t`Tables`}
</SectionHeader>
{has(guide.important_segments) ||
has(guide.important_tables) ? (
<div className="my2">
{guide.important_segments.map(segmentId => (
<GuideDetail
key={segmentId}
type="segment"
entity={segments[segmentId]}
tables={tables}
/>
))}
{guide.important_tables.map(tableId => (
<GuideDetail
key={tableId}
type="table"
entity={tables[tableId]}
tables={tables}
/>
))}
</div>
) : (
<GuideText>
{Object.keys(segments).length > 0 ? (
<span>
{jt`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>
) : (
t`Tables are the building blocks of your company's data.`
)}
</GuideText>
)}
<div>
{Object.keys(segments).length > 0 && (
<Link
className="Button Button--purple mr2"
to={"/reference/segments"}
>
{t`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"}
>
{t`See all tables`}
</Link>
</div>
</div>
<div className="mt4 pt4">
<SectionHeader trim={!guide.things_to_know}>
{guide.things_to_know
? t`Other things to know about our data`
: t`Find out more`}
</SectionHeader>
<GuideText>
{guide.things_to_know
? guide.things_to_know
: t`A good way to get to know your data is by spending a bit of time exploring the different tables and other info available to you. It may take a while, but you'll start to recognize names and meanings over time.`}
</GuideText>
<Link
className="Button link text-bold"
to={"/reference/databases"}
>
{t`Explore our data`}
</Link>
</div>
<div className="mt4">
{guide.contact &&
(guide.contact.name || guide.contact.email) && [
<SectionHeader key={"contactTitle"}>
{t`Have questions?`}
</SectionHeader>,
<div className="mb4 pb4" key={"contactDetails"}>
{guide.contact.name && (
<span className="text-dark mr3">
{t`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>
</div>
);
}
}
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>
);
/* eslint "react/prop-types": "warn" */
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import GettingStartedGuide from "metabase/reference/guide/GettingStartedGuide";
import GettingStartedGuideEditForm from "metabase/reference/guide/GettingStartedGuideEditForm";
import * as metadataActions from "metabase/redux/metadata";
import * as actions from "metabase/reference/reference";
import { getDatabaseId, getIsEditing } from "../selectors";
import Dashboards from "metabase/entities/dashboards";
const mapStateToProps = (state, props) => ({
databaseId: getDatabaseId(state, props),
isEditing: getIsEditing(state, props),
});
const mapDispatchToProps = {
fetchDashboards: Dashboards.actions.fetchList,
...metadataActions,
...actions,
};
@connect(
mapStateToProps,
mapDispatchToProps,
)
export default class GettingStartedGuideContainer extends Component {
static propTypes = {
params: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
databaseId: PropTypes.number.isRequired,
isEditing: PropTypes.bool,
};
async fetchContainerData() {
await actions.wrappedFetchGuide(this.props);
}
componentWillMount() {
this.fetchContainerData();
}
componentWillReceiveProps(newProps) {
if (this.props.location.pathname === newProps.location.pathname) {
return;
}
actions.clearState(newProps);
}
render() {
return (
<div>
{this.props.isEditing ? (
<GettingStartedGuideEditForm {...this.props} />
) : (
<GettingStartedGuide {...this.props} />
)}
</div>
);
}
}
/* eslint "react/prop-types": "warn" */
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { reduxForm } from "redux-form";
import { t } from "ttag";
import cx from "classnames";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
import CreateDashboardModal from "metabase/components/CreateDashboardModal";
import Modal from "metabase/components/Modal";
import EditHeader from "metabase/reference/components/EditHeader";
import GuideEditSection from "metabase/reference/components/GuideEditSection";
import GuideDetailEditor from "metabase/reference/components/GuideDetailEditor";
import * as metadataActions from "metabase/redux/metadata";
import * as actions from "metabase/reference/reference";
import { setRequestUnloaded } from "metabase/redux/requests";
import MetabaseSettings from "metabase/lib/settings";
import Dashboards from "metabase/entities/dashboards";
import { updateSetting } from "metabase/admin/settings/settings";
import S from "../components/GuideDetailEditor.css";
import {
getGuide,
getDashboards,
getLoading,
getError,
getIsEditing,
getIsDashboardModalOpen,
getDatabases,
getTables,
getFields,
getMetrics,
getSegments,
} from "../selectors";
const mapStateToProps = (state, props) => {
const guide = getGuide(state, props);
const dashboards = getDashboards(state, props);
const metrics = getMetrics(state, props);
const segments = getSegments(state, props);
const tables = getTables(state, props);
const fields = getFields(state, props);
const databases = getDatabases(state, props);
// redux-form populates fields with stale values after update
// if we dont specify nulls here
// could use a lot of refactoring
const initialValues = guide && {
things_to_know: guide.things_to_know || null,
contact: guide.contact || { name: null, email: null },
most_important_dashboard:
dashboards !== null && guide.most_important_dashboard !== null
? dashboards[guide.most_important_dashboard]
: {},
important_metrics:
guide.important_metrics && guide.important_metrics.length > 0
? guide.important_metrics.map(
metricId =>
metrics[metricId] && {
...metrics[metricId],
important_fields:
guide.metric_important_fields[metricId] &&
guide.metric_important_fields[metricId].map(
fieldId => fields[fieldId],
),
},
)
: [],
important_segments_and_tables:
(guide.important_segments && guide.important_segments.length > 0) ||
(guide.important_tables && guide.important_tables.length > 0)
? guide.important_segments
.map(
segmentId =>
segments[segmentId] && {
...segments[segmentId],
type: "segment",
},
)
.concat(
guide.important_tables.map(
tableId =>
tables[tableId] && { ...tables[tableId], type: "table" },
),
)
: [],
};
return {
guide,
dashboards,
metrics,
segments,
tables,
databases,
// FIXME: avoids naming conflict, tried using the propNamespace option
// version but couldn't quite get it to work together with passing in
// dynamic initialValues
metadataFields: fields,
loading: getLoading(state, props),
// naming this 'error' will conflict with redux form
loadingError: getError(state, props),
isEditing: getIsEditing(state, props),
isDashboardModalOpen: getIsDashboardModalOpen(state, props),
// redux form doesn't pass this through to component
// need to use to reset form field arrays
initialValues: initialValues,
initialFormValues: initialValues,
};
};
const mapDispatchToProps = {
updateDashboard: Dashboards.actions.update,
createDashboard: Dashboards.actions.create,
updateSetting,
setRequestUnloaded,
...metadataActions,
...actions,
};
@connect(
mapStateToProps,
mapDispatchToProps,
)
@reduxForm({
form: "guide",
fields: [
"things_to_know",
"contact.name",
"contact.email",
"most_important_dashboard.id",
"most_important_dashboard.caveats",
"most_important_dashboard.points_of_interest",
"important_metrics[].id",
"important_metrics[].caveats",
"important_metrics[].points_of_interest",
"important_metrics[].important_fields",
"important_segments_and_tables[].id",
"important_segments_and_tables[].type",
"important_segments_and_tables[].caveats",
"important_segments_and_tables[].points_of_interest",
],
})
export default class GettingStartedGuideEditForm extends Component {
static propTypes = {
fields: PropTypes.object,
style: PropTypes.object,
guide: PropTypes.object,
dashboards: PropTypes.object,
metrics: PropTypes.object,
segments: PropTypes.object,
tables: PropTypes.object,
databases: PropTypes.object,
metadataFields: PropTypes.object,
loadingError: PropTypes.any,
loading: PropTypes.bool,
isEditing: PropTypes.bool,
endEditing: PropTypes.func,
handleSubmit: PropTypes.func,
submitting: PropTypes.bool,
initialFormValues: PropTypes.object,
initializeForm: PropTypes.func,
createDashboard: PropTypes.func,
isDashboardModalOpen: PropTypes.bool,
showDashboardModal: PropTypes.func,
hideDashboardModal: PropTypes.func,
};
render() {
const {
fields: {
things_to_know,
contact,
most_important_dashboard,
important_metrics,
important_segments_and_tables,
},
style,
guide,
dashboards,
metrics,
segments,
tables,
databases,
metadataFields,
loadingError,
loading,
isEditing,
endEditing,
handleSubmit,
submitting,
initialFormValues,
initializeForm,
createDashboard,
isDashboardModalOpen,
showDashboardModal,
hideDashboardModal,
} = this.props;
const onSubmit = handleSubmit(
async fields => await actions.tryUpdateGuide(fields, this.props),
);
const getSelectedIds = fields =>
fields.map(field => field.id.value).filter(id => id !== null);
const getSelectedIdTypePairs = fields =>
fields
.map(field => [field.id.value, field.type.value])
.filter(idTypePair => idTypePair[0] !== null);
return (
<form className="full relative py4" style={style} onSubmit={onSubmit}>
{isDashboardModalOpen && (
<Modal>
<CreateDashboardModal
createDashboard={async newDashboard => {
try {
await createDashboard(newDashboard, { redirect: true });
} catch (error) {
console.error(error);
}
}}
onClose={hideDashboardModal}
/>
</Modal>
)}
{isEditing && (
<EditHeader
endEditing={endEditing}
// resetForm doesn't reset field arrays
reinitializeForm={() => initializeForm(initialFormValues)}
submitting={submitting}
/>
)}
<LoadingAndErrorWrapper
className="full"
style={style}
loading={!loadingError && loading}
error={loadingError}
>
{() => (
<div className="wrapper wrapper--trim">
<div className="mt4 py2">
<h1 className="my3 text-dark">
{t`Help new Metabase users find their way around.`}
</h1>
<p className="text-paragraph text-measure">
{t`The Getting Started guide highlights the dashboard, metrics, segments, and tables that matter most, and informs your users of important things they should know before digging into the data.`}
</p>
</div>
<GuideEditSection
isCollapsed={most_important_dashboard.id.value === undefined}
isDisabled={!dashboards || Object.keys(dashboards).length === 0}
collapsedTitle={t`Is there an important dashboard for your team?`}
collapsedIcon="dashboard"
linkMessage={t`Create a dashboard now`}
action={showDashboardModal}
expand={() => most_important_dashboard.id.onChange(null)}
>
<div>
<SectionHeader>
{t`What is your most important dashboard?`}
</SectionHeader>
<GuideDetailEditor
type="dashboard"
entities={dashboards}
selectedIds={[most_important_dashboard.id.value]}
formField={most_important_dashboard}
removeField={() => {
most_important_dashboard.id.onChange(null);
most_important_dashboard.points_of_interest.onChange("");
most_important_dashboard.caveats.onChange("");
}}
/>
</div>
</GuideEditSection>
<GuideEditSection
isCollapsed={important_metrics.length === 0}
isDisabled={!metrics || Object.keys(metrics).length === 0}
collapsedTitle={t`Do you have any commonly referenced metrics?`}
collapsedIcon="ruler"
linkMessage={t`Learn how to define a metric`}
link={MetabaseSettings.docsUrl(
"administration-guide/07-segments-and-metrics",
"creating-a-metric",
)}
expand={() =>
important_metrics.addField({
id: null,
caveats: null,
points_of_interest: null,
important_fields: null,
})
}
>
<div className="my2">
<SectionHeader>
{t`What are your 3-5 most commonly referenced metrics?`}
</SectionHeader>
<div>
{important_metrics.map(
(metricField, index, metricFields) => (
<GuideDetailEditor
key={index}
type="metric"
metadata={{
tables,
metrics,
fields: metadataFields,
metricImportantFields:
guide.metric_important_fields,
}}
entities={metrics}
formField={metricField}
selectedIds={getSelectedIds(metricFields)}
removeField={() => {
if (metricFields.length > 1) {
return metricFields.removeField(index);
}
metricField.id.onChange(null);
metricField.points_of_interest.onChange("");
metricField.caveats.onChange("");
metricField.important_fields.onChange(null);
}}
/>
),
)}
</div>
{important_metrics.length < 5 &&
important_metrics.length < Object.keys(metrics).length && (
<button
className="Button Button--primary Button--large"
type="button"
onClick={() =>
important_metrics.addField({
id: null,
caveats: null,
points_of_interest: null,
})
}
>
{t`Add another metric`}
</button>
)}
</div>
</GuideEditSection>
<GuideEditSection
isCollapsed={important_segments_and_tables.length === 0}
isDisabled={
(!segments || Object.keys(segments).length === 0) &&
(!tables || Object.keys(tables).length === 0)
}
showLink={!segments || Object.keys(segments).length === 0}
collapsedTitle={t`Do you have any commonly referenced segments or tables?`}
collapsedIcon="table2"
linkMessage={t`Learn how to create a segment`}
link={MetabaseSettings.docsUrl(
"administration-guide/07-segments-and-metrics",
"creating-a-segment",
)}
expand={() =>
important_segments_and_tables.addField({
id: null,
type: null,
caveats: null,
points_of_interest: null,
})
}
>
<div>
<h2 className="text-measure text-dark">
{t`What are 3-5 commonly referenced segments or tables that would be useful for this audience?`}
</h2>
<div className="mb2">
{important_segments_and_tables.map(
(segmentOrTableField, index, segmentOrTableFields) => (
<GuideDetailEditor
key={index}
type="segment"
metadata={{
databases,
tables,
segments,
}}
formField={segmentOrTableField}
selectedIdTypePairs={getSelectedIdTypePairs(
segmentOrTableFields,
)}
removeField={() => {
if (segmentOrTableFields.length > 1) {
return segmentOrTableFields.removeField(index);
}
segmentOrTableField.id.onChange(null);
segmentOrTableField.type.onChange(null);
segmentOrTableField.points_of_interest.onChange("");
segmentOrTableField.caveats.onChange("");
}}
/>
),
)}
</div>
{important_segments_and_tables.length < 5 &&
important_segments_and_tables.length <
Object.keys(tables).concat(Object.keys.segments)
.length && (
<button
className="Button Button--primary Button--large"
type="button"
onClick={() =>
important_segments_and_tables.addField({
id: null,
type: null,
caveats: null,
points_of_interest: null,
})
}
>
{t`Add another segment or table`}
</button>
)}
</div>
</GuideEditSection>
<GuideEditSection
isCollapsed={things_to_know.value === null}
isDisabled={false}
collapsedTitle={t`Is there anything your users should understand or know before they start accessing the data?`}
collapsedIcon="reference"
expand={() => things_to_know.onChange("")}
>
<div className="text-measure">
<SectionHeader>
{t`What should a user of this data know before they start accessing it?`}
</SectionHeader>
<textarea
className={S.guideDetailEditorTextarea}
placeholder={t`E.g., expectations around data privacy and use, common pitfalls or misunderstandings, information about data warehouse performance, legal notices, etc.`}
{...things_to_know}
/>
</div>
</GuideEditSection>
<GuideEditSection
isCollapsed={
contact.name.value === null && contact.email.value === null
}
isDisabled={false}
collapsedTitle={t`Is there someone your users could contact for help if they're confused about this guide?`}
collapsedIcon="mail"
expand={() => {
contact.name.onChange("");
contact.email.onChange("");
}}
>
<div>
<SectionHeader>
{t`Who should users contact for help if they're confused about this data?`}
</SectionHeader>
<div className="flex">
<div className="flex-full">
<h3 className="mb1">{t`Name`}</h3>
<input
className="input text-paragraph"
placeholder="Julie McHelpfulson"
type="text"
{...contact.name}
/>
</div>
<div className="flex-full">
<h3 className="mb1">{t`Email address`}</h3>
<input
className="input text-paragraph"
placeholder="julie.mchelpfulson@acme.com"
type="text"
{...contact.email}
/>
</div>
</div>
</div>
</GuideEditSection>
</div>
)}
</LoadingAndErrorWrapper>
</form>
);
}
}
const SectionHeader = (
{ trim, children }, // eslint-disable-line react/prop-types
) => (
<h2 className={cx("text-dark text-measure", { mb0: trim }, { mb4: !trim })}>
{children}
</h2>
);
......@@ -54,8 +54,6 @@ import CreateDashboardModal from "metabase/components/CreateDashboardModal";
import { NotFound, Unauthorized } from "metabase/containers/ErrorPages";
// Reference Guide
import GettingStartedGuideContainer from "metabase/reference/guide/GettingStartedGuideContainer";
// Reference Metrics
import MetricListContainer from "metabase/reference/metrics/MetricListContainer";
import MetricDetailContainer from "metabase/reference/metrics/MetricDetailContainer";
......@@ -253,11 +251,6 @@ export const getRoutes = store => (
{/* REFERENCE */}
<Route path="/reference" title={`Data Reference`}>
<IndexRedirect to="/reference/databases" />
<Route
path="guide"
title={`Getting Started`}
component={GettingStartedGuideContainer}
/>
<Route path="metrics" component={MetricListContainer} />
<Route path="metrics/:metricId" component={MetricDetailContainer} />
<Route
......
import { useSharedAdminLogin, createTestStore } from "__support__/e2e";
import React from "react";
import { mount } from "enzyme";
import { SegmentApi, MetricApi } from "metabase/services";
import {
FETCH_DATABASE_METADATA,
FETCH_METRICS,
FETCH_SEGMENTS,
} from "metabase/redux/metadata";
import GettingStartedGuideContainer from "metabase/reference/guide/GettingStartedGuideContainer";
describe("The Reference Section", () => {
// Test data
const segmentDef = {
name: "A Segment",
description: "I did it!",
table_id: 1,
show_in_getting_started: true,
definition: {
"source-table": 1,
filter: ["time-interval", ["field-id", 1], -30, "day"],
},
};
const anotherSegmentDef = {
name: "Another Segment",
description: "I did it again!",
table_id: 1,
show_in_getting_started: true,
definition: {
"source-table": 1,
filter: ["time-interval", ["field-id", 1], -30, "day"],
},
};
const metricDef = {
name: "A Metric",
description: "I did it!",
table_id: 1,
show_in_getting_started: true,
definition: { database: 1, query: { aggregation: [["count"]] } },
};
const anotherMetricDef = {
name: "Another Metric",
description: "I did it again!",
table_id: 1,
show_in_getting_started: true,
definition: { database: 1, query: { aggregation: [["count"]] } },
};
// Scaffolding
beforeAll(async () => {
useSharedAdminLogin();
});
describe("The Getting Started Guide", async () => {
it("Should show an empty guide for non-admin users", async () => {
const store = await createTestStore();
store.pushPath("/reference/");
mount(store.connectContainer(<GettingStartedGuideContainer />));
await store.waitForActions([
FETCH_DATABASE_METADATA,
FETCH_SEGMENTS,
FETCH_METRICS,
]);
});
xit("Should show an empty guide with a creation CTA for admin users", async () => {});
xit("A non-admin attempting to edit the guide should get an error", async () => {});
it("Adding metrics should to the guide should make them appear", async () => {
expect(0).toBe(0);
const metric = await MetricApi.create(metricDef);
expect(1).toBe(1);
const metric2 = await MetricApi.create(anotherMetricDef);
expect(2).toBe(2);
await MetricApi.delete({
metricId: metric.id,
revision_message: "Please",
});
expect(1).toBe(1);
await MetricApi.delete({
metricId: metric2.id,
revision_message: "Please",
});
expect(0).toBe(0);
});
it("Adding segments should to the guide should make them appear", async () => {
expect(0).toBe(0);
const segment = await SegmentApi.create(segmentDef);
expect(1).toBe(1);
const anotherSegment = await SegmentApi.create(anotherSegmentDef);
expect(2).toBe(2);
await SegmentApi.delete({
segmentId: segment.id,
revision_message: "Please",
});
expect(1).toBe(1);
await SegmentApi.delete({
segmentId: anotherSegment.id,
revision_message: "Please",
});
expect(0).toBe(0);
});
});
});
import { useSharedAdminLogin } from "__support__/e2e";
import { MetabaseApi } from "metabase/services";
describe("MetabaseApi", () => {
beforeAll(() => useSharedAdminLogin());
describe("table_query_metadata", () => {
// these table IDs correspond to the sample dataset in the fixture db
[1, 2, 3, 4].map(tableId =>
it(`should have the correct metadata for table ${tableId}`, async () => {
expect(
stripKeys(await MetabaseApi.table_query_metadata({ tableId })),
).toMatchSnapshot();
}),
);
});
});
function stripKeys(object) {
// handles both arrays and objects
if (object && typeof object === "object") {
for (const key in object) {
if (
/^((updated|created)_at|last_analyzed|timezone|is_on_demand|fields_hash)$/.test(
key,
)
) {
delete object[key];
} else {
stripKeys(object[key]);
}
}
}
return object;
}
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