From 348281fc848422fb6ecad7e35def79ace421bab5 Mon Sep 17 00:00:00 2001 From: Nick Fitzpatrick <nick@metabase.com> Date: Tue, 5 Apr 2022 17:20:39 -0300 Subject: [PATCH] Document Title and favicon updates for loading questions (#21425) * Document Title and favicon updates for loading questions * Refactoring Dashboard Title and Favicon logic --- frontend/src/metabase/dashboard/actions.js | 71 +++++++++++++------ .../dashboard/containers/DashboardApp.jsx | 39 ++-------- frontend/src/metabase/dashboard/reducers.js | 36 ++++------ .../metabase/dashboard/reducers.unit.spec.js | 3 +- frontend/src/metabase/dashboard/selectors.js | 20 +++--- .../src/metabase/query_builder/actions.js | 62 ++++++++++++++++ .../query_builder/containers/QueryBuilder.jsx | 11 ++- .../src/metabase/query_builder/reducers.js | 27 +++++++ .../src/metabase/query_builder/selectors.js | 21 ++++++ 9 files changed, 198 insertions(+), 92 deletions(-) diff --git a/frontend/src/metabase/dashboard/actions.js b/frontend/src/metabase/dashboard/actions.js index 070820cb721..a508b7d593b 100644 --- a/frontend/src/metabase/dashboard/actions.js +++ b/frontend/src/metabase/dashboard/actions.js @@ -2,6 +2,8 @@ import { assoc, assocIn, dissocIn, getIn } from "icepick"; import _ from "underscore"; +import { t } from "ttag"; + import { createAction, createThunkAction } from "metabase/lib/redux"; import { defer } from "metabase/lib/promise"; import { normalize, schema } from "normalizr"; @@ -57,6 +59,7 @@ import { getDashboardComplete, getParameterValues, getDashboardParameterValuesSearchCache, + getLoadingDashCards, } from "./selectors"; import { getMetadata } from "metabase/selectors/metadata"; import { getCardAfterVisualizationClick } from "metabase/visualizations/lib/utils"; @@ -139,9 +142,13 @@ export const FETCH_DASHBOARD_PARAMETER_FIELD_VALUES = export const SET_SIDEBAR = "metabase/dashboard/SET_SIDEBAR"; export const CLOSE_SIDEBAR = "metabase/dashboard/CLOSE_SIDEBAR"; -export const SET_DASHBOARD_SEEN = "metabase/dashboard/SET_SEEN"; export const SET_SHOW_LOADING_COMPLETE_FAVICON = "metabase/dashboard/SET_SHOW_LOADING_COMPLETE_FAVICON"; +export const SET_DOCUMENT_TITLE = "metabase/dashboard/SET_DOCUMENT_TITLE"; +const setDocumentTitle = createAction(SET_DOCUMENT_TITLE); + +export const SET_LOADING_DASHCARDS_COMPLETE = + "metabase/dashboard/SET_LOADING_DASHCARDS_COMPLETE"; export const initialize = createAction(INITIALIZE); export const reset = createAction(RESET); @@ -150,7 +157,6 @@ export const setEditingDashboard = createAction(SET_EDITING_DASHBOARD); export const setSidebar = createAction(SET_SIDEBAR); export const closeSidebar = createAction(CLOSE_SIDEBAR); -export const setHasSeenLoadedDashboard = createAction(SET_DASHBOARD_SEEN); export const setShowLoadingCompleteFavicon = createAction( SET_SHOW_LOADING_COMPLETE_FAVICON, ); @@ -456,37 +462,50 @@ export const fetchDashboardCardData = createThunkAction( FETCH_DASHBOARD_CARD_DATA, options => (dispatch, getState) => { const dashboard = getDashboardComplete(getState()); + const promises = getAllDashboardCards(dashboard) .map(({ card, dashcard }) => { if (!isVirtualDashCard(dashcard)) { - return dispatch(fetchCardData(card, dashcard, options)); + return dispatch(fetchCardData(card, dashcard, options)).then(() => { + return dispatch(updateLoadingTitle()); + }); } }) .filter(p => !!p); + dispatch(setDocumentTitle(t`0/${promises.length} loaded`)); + Promise.all(promises).then(() => { - dispatch(setShowLoadingCompleteFavicon(true)); - if (!document.hidden) { - dispatch(setHasSeenLoadedDashboard()); - setTimeout(() => { - dispatch(setShowLoadingCompleteFavicon(false)); - }, 3000); - } else { - document.addEventListener( - "visibilitychange", - () => { - dispatch(setHasSeenLoadedDashboard()); - setTimeout(() => { - dispatch(setShowLoadingCompleteFavicon(false)); - }, 3000); - }, - { once: true }, - ); - } + dispatch(loadingComplete()); }); }, ); +const loadingComplete = createThunkAction( + SET_LOADING_DASHCARDS_COMPLETE, + () => dispatch => { + dispatch(setShowLoadingCompleteFavicon(true)); + if (!document.hidden) { + dispatch(setDocumentTitle("")); + setTimeout(() => { + dispatch(setShowLoadingCompleteFavicon(false)); + }, 3000); + } else { + dispatch(setDocumentTitle(t`Your dashboard is ready`)); + document.addEventListener( + "visibilitychange", + () => { + dispatch(setDocumentTitle("")); + setTimeout(() => { + dispatch(setShowLoadingCompleteFavicon(false)); + }, 3000); + }, + { once: true }, + ); + } + }, +); + export const cancelFetchDashboardCardData = createThunkAction( CANCEL_FETCH_DASHBOARD_CARD_DATA, () => (dispatch, getState) => { @@ -673,6 +692,16 @@ export const fetchCardData = createThunkAction(FETCH_CARD_DATA, function( }; }); +const updateLoadingTitle = createThunkAction( + SET_DOCUMENT_TITLE, + () => (dispatch, getState) => { + const loadingDashCards = getLoadingDashCards(getState()); + const totalCards = loadingDashCards.dashcardIds.length; + const loadingComplete = totalCards - loadingDashCards.loadingIds.length; + return `${loadingComplete}/${totalCards} loaded`; + }, +); + export const markCardAsSlow = createAction(MARK_CARD_AS_SLOW, card => ({ id: card.id, result: true, diff --git a/frontend/src/metabase/dashboard/containers/DashboardApp.jsx b/frontend/src/metabase/dashboard/containers/DashboardApp.jsx index 379261c20a3..da582fcb91a 100644 --- a/frontend/src/metabase/dashboard/containers/DashboardApp.jsx +++ b/frontend/src/metabase/dashboard/containers/DashboardApp.jsx @@ -2,7 +2,6 @@ import React, { Component } from "react"; import { connect } from "react-redux"; import { push } from "react-router-redux"; -import { t } from "ttag"; import title from "metabase/hoc/Title"; import favicon from "metabase/hoc/Favicon"; @@ -30,12 +29,8 @@ import { getIsAddParameterPopoverOpen, getSidebar, getShowAddQuestionSidebar, - getIsLoadingDashCards, - getTotalCards, - getCardsLoaded, - getHasSeenLoadedDashboard, - getIsLoadingDashCardsComplete, getFavicon, + getDocumentTitle, } from "../selectors"; import { getDatabases, getMetadata } from "metabase/selectors/metadata"; import { @@ -74,12 +69,8 @@ const mapStateToProps = (state, props) => { isAddParameterPopoverOpen: getIsAddParameterPopoverOpen(state), sidebar: getSidebar(state), showAddQuestionSidebar: getShowAddQuestionSidebar(state), - isLoadingDashCards: getIsLoadingDashCards(state), - isLoadingDashCardsComplete: getIsLoadingDashCardsComplete(state), - totalDashCards: getTotalCards(state), - cardsLoaded: getCardsLoaded(state), - hasSeenLoadedDashboard: getHasSeenLoadedDashboard(state), pageFavicon: getFavicon(state), + documentTitle: getDocumentTitle(state), }; }; @@ -93,28 +84,10 @@ const mapDispatchToProps = { @connect(mapStateToProps, mapDispatchToProps) @favicon(({ pageFavicon }) => pageFavicon) -@title( - ({ - dashboard, - isLoadingDashCards, - totalDashCards, - cardsLoaded, - hasSeenLoadedDashboard, - isLoadingDashCardsComplete, - }) => { - if (isLoadingDashCards) { - return { - title: t`${cardsLoaded}/${totalDashCards} loaded`, - titleIndex: 1, - }; - } - if (isLoadingDashCardsComplete && !hasSeenLoadedDashboard) { - return t`Your dashboard is ready`; - } else { - return dashboard?.name; - } - }, -) +@title(({ dashboard, documentTitle }) => ({ + title: documentTitle || dashboard?.name, + titleIndex: 1, +})) @titleWithLoadingTime("loadingStartTime") // NOTE: should use DashboardControls and DashboardData HoCs here? export default class DashboardApp extends Component { diff --git a/frontend/src/metabase/dashboard/reducers.js b/frontend/src/metabase/dashboard/reducers.js index c4bb2b563a1..cfbad20b771 100644 --- a/frontend/src/metabase/dashboard/reducers.js +++ b/frontend/src/metabase/dashboard/reducers.js @@ -32,7 +32,7 @@ import { CLOSE_SIDEBAR, FETCH_DASHBOARD_PARAMETER_FIELD_VALUES, SAVE_DASHBOARD_AND_CARDS, - SET_DASHBOARD_SEEN, + SET_DOCUMENT_TITLE, SET_SHOW_LOADING_COMPLETE_FAVICON, RESET, } from "./actions"; @@ -61,28 +61,19 @@ const isEditing = handleActions( null, ); -const hasSeenLoadedDashboard = handleActions( +const loadingControls = handleActions( { - [INITIALIZE]: { next: state => false }, - [FETCH_DASHBOARD]: { next: state => false }, - [SET_DASHBOARD_SEEN]: { - next: state => true, - }, - [RESET]: { next: state => false }, - }, - false, -); - -const showLoadingCompleteFavicon = handleActions( - { - [INITIALIZE]: { next: state => false }, - [FETCH_DASHBOARD]: { next: state => false }, - [SET_SHOW_LOADING_COMPLETE_FAVICON]: { - next: (state, { payload }) => payload, - }, - [RESET]: { next: state => false }, + [SET_DOCUMENT_TITLE]: (state, { payload }) => ({ + ...state, + documentTitle: payload, + }), + [SET_SHOW_LOADING_COMPLETE_FAVICON]: (state, { payload }) => ({ + ...state, + showLoadCompleteFavicon: payload, + }), + [RESET]: { next: state => ({}) }, }, - false, + {}, ); function newDashboard(before, after, isDirty) { @@ -402,8 +393,7 @@ const sidebar = handleActions( export default combineReducers({ dashboardId, isEditing, - hasSeenLoadedDashboard, - showLoadingCompleteFavicon, + loadingControls, dashboards, dashcards, dashcardData, diff --git a/frontend/src/metabase/dashboard/reducers.unit.spec.js b/frontend/src/metabase/dashboard/reducers.unit.spec.js index 13ca69ff5e3..110b630a60b 100644 --- a/frontend/src/metabase/dashboard/reducers.unit.spec.js +++ b/frontend/src/metabase/dashboard/reducers.unit.spec.js @@ -32,8 +32,7 @@ describe("dashboard reducers", () => { parameterValuesSearchCache: {}, sidebar: { props: {} }, slowCards: {}, - hasSeenLoadedDashboard: false, - showLoadingCompleteFavicon: false, + loadingControls: {}, }); }); diff --git a/frontend/src/metabase/dashboard/selectors.js b/frontend/src/metabase/dashboard/selectors.js index 6f7ba953860..b8a4e30ed64 100644 --- a/frontend/src/metabase/dashboard/selectors.js +++ b/frontend/src/metabase/dashboard/selectors.js @@ -28,19 +28,10 @@ export const getDashcards = state => state.dashboard.dashcards; export const getCardData = state => state.dashboard.dashcardData; export const getSlowCards = state => state.dashboard.slowCards; export const getParameterValues = state => state.dashboard.parameterValues; -export const getIsLoadingDashCards = state => - state.dashboard.loadingDashCards.loadingIds.length > 0; -export const getCardsLoaded = state => - state.dashboard.loadingDashCards.dashcardIds.length - - state.dashboard.loadingDashCards.loadingIds.length; -export const getTotalCards = state => - state.dashboard.loadingDashCards.dashcardIds.length; -export const getHasSeenLoadedDashboard = state => - state.dashboard.hasSeenLoadedDashboard; -export const getIsLoadingDashCardsComplete = state => - state.dashboard.loadingDashCards.isLoadingComplete; export const getFavicon = state => - state.dashboard.showLoadingCompleteFavicon ? LOAD_COMPLETE_FAVICON : null; + state.dashboard.loadingControls?.showLoadCompleteFavicon + ? LOAD_COMPLETE_FAVICON + : null; export const getLoadingStartTime = state => state.dashboard.loadingDashCards.startTime; export const getIsAddParameterPopoverOpen = state => @@ -62,6 +53,8 @@ export const getDashboard = createSelector( (dashboardId, dashboards) => dashboards[dashboardId], ); +export const getLoadingDashCards = state => state.dashboard.loadingDashCards; + export const getDashboardComplete = createSelector( [getDashboard, getDashcards], (dashboard, dashcards) => @@ -73,6 +66,9 @@ export const getDashboardComplete = createSelector( }, ); +export const getDocumentTitle = state => + state.dashboard.loadingControls.documentTitle; + export const getIsBookmarked = (state, props) => props.bookmarks.some( bookmark => diff --git a/frontend/src/metabase/query_builder/actions.js b/frontend/src/metabase/query_builder/actions.js index 11470876a0f..d31d5708969 100644 --- a/frontend/src/metabase/query_builder/actions.js +++ b/frontend/src/metabase/query_builder/actions.js @@ -65,6 +65,7 @@ import { getTransformedSeries, getZoomedObjectId, isBasedOnExistingQuestion, + getTimeoutId, } from "./selectors"; import { trackNewQuestionSaved } from "./analytics"; @@ -101,6 +102,26 @@ export const setUIControls = createAction(SET_UI_CONTROLS); export const RESET_UI_CONTROLS = "metabase/qb/RESET_UI_CONTROLS"; export const resetUIControls = createAction(RESET_UI_CONTROLS); +export const SET_DOCUMENT_TITLE = "metabase/qb/SET_DOCUMENT_TITLE"; +const setDocumentTitle = createAction(SET_DOCUMENT_TITLE); + +export const SET_SHOW_LOADING_COMPLETE_FAVICON = + "metabase/qb/SET_SHOW_LOADING_COMPLETE_FAVICON"; +const showLoadingCompleteFavicon = createAction( + SET_SHOW_LOADING_COMPLETE_FAVICON, + () => true, +); +const hideLoadingCompleteFavicon = createAction( + SET_SHOW_LOADING_COMPLETE_FAVICON, + () => false, +); + +const LOAD_COMPLETE_UI_CONTROLS = "metabase/qb/LOAD_COMPLETE_UI_CONTROLS"; +const LOAD_START_UI_CONTROLS = "metabase/qb/LOAD_START_UI_CONTROLS"; +export const SET_DOCUMENT_TITLE_TIMEOUT_ID = + "metabase/qb/SET_DOCUMENT_TITLE_TIMEOUT_ID"; +const setDocumentTitleTimeoutId = createAction(SET_DOCUMENT_TITLE_TIMEOUT_ID); + export const setQueryBuilderMode = ( queryBuilderMode, { shouldUpdateUrl = true, datasetEditorTab = "query" } = {}, @@ -1276,6 +1297,7 @@ export const runQuestionQuery = ({ overrideWithCard, } = {}) => { return async (dispatch, getState) => { + dispatch(loadStartUIControls()); const questionFromCard = card => card && new Question(card, getMetadata(getState())); @@ -1329,6 +1351,7 @@ export const runQuestionQuery = ({ duration, ), ); + // clearTimeout(timeoutId); return dispatch(queryCompleted(question, queryResults)); }) .catch(error => dispatch(queryErrored(startTime, error))); @@ -1343,6 +1366,17 @@ export const runQuestionQuery = ({ }; }; +const loadStartUIControls = createThunkAction( + LOAD_START_UI_CONTROLS, + () => (dispatch, getState) => { + dispatch(setDocumentTitle(t`Doing Science...`)); + const timeoutId = setTimeout(() => { + dispatch(setDocumentTitle(t`Still Here...`)); + }, 10000); + dispatch(setDocumentTitleTimeoutId(timeoutId)); + }, +); + export const CLEAR_QUERY_RESULT = "metabase/query_builder/CLEAR_QUERY_RESULT"; export const clearQueryResult = createAction(CLEAR_QUERY_RESULT); @@ -1383,9 +1417,37 @@ export const queryCompleted = (question, queryResults) => { } dispatch.action(QUERY_COMPLETED, { card, queryResults }); + dispatch(loadCompleteUIControls()); }; }; +const loadCompleteUIControls = createThunkAction( + LOAD_COMPLETE_UI_CONTROLS, + () => (dispatch, getState) => { + const timeoutId = getTimeoutId(getState()); + clearTimeout(timeoutId); + dispatch(showLoadingCompleteFavicon()); + if (document.hidden) { + dispatch(setDocumentTitle(t`Your question is ready!`)); + document.addEventListener( + "visibilitychange", + () => { + dispatch(setDocumentTitle("")); + setTimeout(() => { + dispatch(hideLoadingCompleteFavicon()); + }, 3000); + }, + { once: true }, + ); + } else { + dispatch(setDocumentTitle("")); + setTimeout(() => { + dispatch(hideLoadingCompleteFavicon()); + }, 3000); + } + }, +); + /** * Saves to `visualization_settings` property of a question those visualization settings that * 1) don't have a value yet and 2) have `persistDefault` flag enabled. diff --git a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx index cb339d285ae..885634a4d22 100644 --- a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx +++ b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx @@ -24,6 +24,7 @@ import { usePrevious } from "metabase/hooks/use-previous"; import title from "metabase/hoc/Title"; import titleWithLoadingTime from "metabase/hoc/TitleWithLoadingTime"; +import favicon from "metabase/hoc/Favicon"; import View from "../components/view/View"; @@ -74,6 +75,8 @@ import { getFilteredTimelines, getTimeseriesXDomain, getIsAnySidebarOpen, + getDocumentTitle, + getPageFavicon, } from "../selectors"; import * as actions from "../actions"; @@ -171,6 +174,8 @@ const mapStateToProps = (state, props) => { nativeEditorSelectedText: getNativeEditorSelectedText(state), modalSnippet: getModalSnippet(state), snippetCollectionId: getSnippetCollectionId(state), + documentTitle: getDocumentTitle(state), + pageFavicon: getPageFavicon(state), }; }; @@ -369,6 +374,10 @@ export default _.compose( Bookmark.loadList(), Timelines.loadList(timelineProps), connect(mapStateToProps, mapDispatchToProps), - title(({ card }) => card?.name ?? t`Question`), + favicon(({ pageFavicon }) => pageFavicon), + title(({ card, documentTitle }) => ({ + title: documentTitle || card?.name || t`Question`, + titleIndex: 1, + })), titleWithLoadingTime("queryStartTime"), )(QueryBuilder); diff --git a/frontend/src/metabase/query_builder/reducers.js b/frontend/src/metabase/query_builder/reducers.js index 88980980e4c..2df809e6093 100644 --- a/frontend/src/metabase/query_builder/reducers.js +++ b/frontend/src/metabase/query_builder/reducers.js @@ -61,6 +61,9 @@ import { HIDE_TIMELINES, SELECT_TIMELINE_EVENTS, DESELECT_TIMELINE_EVENTS, + SET_DOCUMENT_TITLE, + SET_SHOW_LOADING_COMPLETE_FAVICON, + SET_DOCUMENT_TITLE_TIMEOUT_ID, } from "./actions"; const DEFAULT_UI_CONTROLS = { @@ -84,6 +87,12 @@ const DEFAULT_UI_CONTROLS = { datasetEditorTab: "query", // "query" / "metadata" }; +const DEFAULT_LOADING_CONTROLS = { + showLoadCompleteFavicon: false, + documentTitle: "", + timeoutId: "", +}; + const UI_CONTROLS_SIDEBAR_DEFAULTS = { isShowingSummarySidebar: false, isShowingFilterSidebar: false, @@ -295,6 +304,24 @@ export const uiControls = handleActions( DEFAULT_UI_CONTROLS, ); +export const loadingControls = handleActions( + { + [SET_DOCUMENT_TITLE]: (state, { payload }) => ({ + ...state, + documentTitle: payload, + }), + [SET_SHOW_LOADING_COMPLETE_FAVICON]: (state, { payload }) => ({ + ...state, + showLoadCompleteFavicon: payload, + }), + [SET_DOCUMENT_TITLE_TIMEOUT_ID]: (state, { payload }) => ({ + ...state, + timeoutId: payload, + }), + }, + DEFAULT_LOADING_CONTROLS, +); + export const zoomedRowObjectId = handleActions( { [INITIALIZE_QB]: { diff --git a/frontend/src/metabase/query_builder/selectors.js b/frontend/src/metabase/query_builder/selectors.js index a077453e2d0..e01a79fa6dd 100644 --- a/frontend/src/metabase/query_builder/selectors.js +++ b/frontend/src/metabase/query_builder/selectors.js @@ -34,7 +34,10 @@ import { import Mode from "metabase-lib/lib/Mode"; import ObjectMode from "metabase/modes/components/modes/ObjectMode"; +import { LOAD_COMPLETE_FAVICON } from "metabase/hoc/Favicon"; + export const getUiControls = state => state.qb.uiControls; +const getLoadingControls = state => state.qb.loadingControls; export const getIsShowingTemplateTagsEditor = state => getUiControls(state).isShowingTemplateTagsEditor; @@ -820,3 +823,21 @@ export const isBasedOnExistingQuestion = createSelector( return originalQuestion != null; }, ); + +export const getDocumentTitle = createSelector( + [getLoadingControls], + loadingControls => loadingControls?.documentTitle, +); + +export const getPageFavicon = createSelector( + [getLoadingControls], + loadingControls => + loadingControls?.showLoadCompleteFavicon + ? LOAD_COMPLETE_FAVICON + : undefined, +); + +export const getTimeoutId = createSelector( + [getLoadingControls], + loadingControls => loadingControls.timeoutId, +); -- GitLab