diff --git a/frontend/src/metabase/reference/components/Detail.jsx b/frontend/src/metabase/reference/components/Detail.jsx index 210d4f11167d328deb124402eb486cc10a46dff5..b59fc544554bd23cbde2f37e5894f566c848cf25 100644 --- a/frontend/src/metabase/reference/components/Detail.jsx +++ b/frontend/src/metabase/reference/components/Detail.jsx @@ -23,7 +23,7 @@ const Detail = ({ name, description, placeholder, subtitleClass, url, icon, isEd placeholder={placeholder} {...field} //FIXME: use initialValues from redux forms instead of default value - // to allow for reinitializing on cancel (see ReferenceGettingStartedGuide.jsx) + // to allow for reinitializing on cancel (see GettingStartedGuide.jsx) defaultValue={description} /> : <span className={subtitleClass}>{description || placeholder || 'No description yet'}</span> diff --git a/frontend/src/metabase/reference/guide/GettingStartedGuide.jsx b/frontend/src/metabase/reference/guide/GettingStartedGuide.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7d0cb015c5d9c5deb8e8d339eeed1315c0ffcba7 --- /dev/null +++ b/frontend/src/metabase/reference/guide/GettingStartedGuide.jsx @@ -0,0 +1,296 @@ +/* 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 cx from "classnames"; + +import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx"; + +import GuideHeader from "metabase/reference/components/GuideHeader.jsx"; +import GuideDetail from "metabase/reference/components/GuideDetail.jsx"; + +import * as metadataActions from 'metabase/redux/metadata'; +import * as actions from 'metabase/reference/reference'; +import { clearRequestState } from "metabase/redux/requests"; +import { createDashboard, updateDashboard } from 'metabase/dashboards/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; + +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, + createDashboard, + updateSetting, + clearRequestState, + ...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 py4" 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">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).length > 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> + { (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> + <Link className="Button Button--primary" to={'/reference/metrics'}> + 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 ? 'Segments and tables' : '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> + 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> + { Object.keys(segments).length > 0 && ( + <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' : Object.keys(segments).length > 0 }, + { 'Button Button--purple' : Object.keys(segments).length === 0 } + )} + to={'/reference/databases'} + > + See all tables + </Link> + </div> + </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 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'}> + 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> + </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> diff --git a/frontend/src/metabase/reference/guide/GettingStartedGuideContainer.jsx b/frontend/src/metabase/reference/guide/GettingStartedGuideContainer.jsx index 54f40a982b0296c7426ad83f4cd3c0ce5cd31ca1..74d2525f6d0b5657661996a59f922aff4ac634cd 100644 --- a/frontend/src/metabase/reference/guide/GettingStartedGuideContainer.jsx +++ b/frontend/src/metabase/reference/guide/GettingStartedGuideContainer.jsx @@ -3,7 +3,8 @@ import React, { Component } from 'react'; import PropTypes from "prop-types"; import { connect } from 'react-redux'; -import ReferenceGettingStartedGuide from "metabase/reference/guide/ReferenceGettingStartedGuide.jsx" +import GettingStartedGuide from "metabase/reference/guide/GettingStartedGuide.jsx" +import GettingStartedGuideEditForm from "metabase/reference/guide/GettingStartedGuideEditForm.jsx" import * as metadataActions from 'metabase/redux/metadata'; import * as actions from 'metabase/reference/reference'; @@ -55,9 +56,14 @@ export default class GettingStartedGuideContainer extends Component { } render() { - return ( - <ReferenceGettingStartedGuide {...this.props} /> + <div> + + { this.props.isEditing ? + <GettingStartedGuideEditForm {...this.props} /> : + <GettingStartedGuide {...this.props} /> + } + </div> ); } } diff --git a/frontend/src/metabase/reference/guide/ReferenceGettingStartedGuide.jsx b/frontend/src/metabase/reference/guide/GettingStartedGuideEditForm.jsx similarity index 63% rename from frontend/src/metabase/reference/guide/ReferenceGettingStartedGuide.jsx rename to frontend/src/metabase/reference/guide/GettingStartedGuideEditForm.jsx index e572ebb59b3935ee9ebec44a617e0c316a9b5c89..07a6570c0cb86738d07f889adccffa91c03d9347 100644 --- a/frontend/src/metabase/reference/guide/ReferenceGettingStartedGuide.jsx +++ b/frontend/src/metabase/reference/guide/GettingStartedGuideEditForm.jsx @@ -1,7 +1,6 @@ /* 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 { reduxForm } from "redux-form"; @@ -12,9 +11,7 @@ import CreateDashboardModal from 'metabase/components/CreateDashboardModal.jsx'; import Modal from 'metabase/components/Modal.jsx'; import EditHeader from "metabase/reference/components/EditHeader.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"; import GuideDetailEditor from "metabase/reference/components/GuideDetailEditor.jsx"; import * as metadataActions from 'metabase/redux/metadata'; @@ -30,7 +27,6 @@ import S from "../components/GuideDetailEditor.css"; import { getGuide, - getUser, getDashboards, getLoading, getError, @@ -43,26 +39,6 @@ import { 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; const mapStateToProps = (state, props) => { const guide = getGuide(state, props); @@ -102,7 +78,6 @@ const mapStateToProps = (state, props) => { return { guide, - user: getUser(state, props), dashboards, metrics, segments, @@ -153,12 +128,11 @@ const mapDispatchToProps = { 'important_segments_and_tables[].points_of_interest' ] }) -export default class ReferenceGettingStartedGuide extends Component { +export default class GettingStartedGuideEditForm 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, @@ -168,7 +142,6 @@ export default class ReferenceGettingStartedGuide extends Component { loadingError: PropTypes.any, loading: PropTypes.bool, isEditing: PropTypes.bool, - startEditing: PropTypes.func, endEditing: PropTypes.func, handleSubmit: PropTypes.func, submitting: PropTypes.bool, @@ -191,7 +164,6 @@ export default class ReferenceGettingStartedGuide extends Component { }, style, guide, - user, dashboards, metrics, segments, @@ -201,7 +173,6 @@ export default class ReferenceGettingStartedGuide extends Component { loadingError, loading, isEditing, - startEditing, endEditing, handleSubmit, submitting, @@ -252,7 +223,7 @@ export default class ReferenceGettingStartedGuide extends Component { /> } <LoadingAndErrorWrapper className="full" style={style} loading={!loadingError && loading} error={loadingError}> - { () => isEditing ? + { () => <div className="wrapper wrapper--trim"> <div className="mt4 py2"> <h1 className="my3 text-dark"> @@ -455,170 +426,7 @@ export default class ReferenceGettingStartedGuide extends Component { </div> </div> </GuideEditSection> - </div> : - <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">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).length > 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> - { (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> - <Link className="Button Button--primary" to={'/reference/metrics'}> - 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 ? 'Segments and tables' : '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> - 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> - { Object.keys(segments).length > 0 && ( - <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' : Object.keys(segments).length > 0 }, - { 'Button Button--purple' : Object.keys(segments).length === 0 } - )} - to={'/reference/databases'} - > - See all tables - </Link> - </div> - </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 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'}> - 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> - </div> + </div> } </LoadingAndErrorWrapper> </form> @@ -626,13 +434,5 @@ export default class ReferenceGettingStartedGuide extends Component { } } -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>