Skip to content
Snippets Groups Projects
Unverified Commit 348281fc authored by Nick Fitzpatrick's avatar Nick Fitzpatrick Committed by GitHub
Browse files

Document Title and favicon updates for loading questions (#21425)

* Document Title and favicon updates for loading questions
* Refactoring Dashboard Title and Favicon logic
parent fbf0f0d7
No related branches found
No related tags found
No related merge requests found
......@@ -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,
......
......@@ -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 {
......
......@@ -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,
......
......@@ -32,8 +32,7 @@ describe("dashboard reducers", () => {
parameterValuesSearchCache: {},
sidebar: { props: {} },
slowCards: {},
hasSeenLoadedDashboard: false,
showLoadingCompleteFavicon: false,
loadingControls: {},
});
});
......
......@@ -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 =>
......
......@@ -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.
......
......@@ -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);
......@@ -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]: {
......
......@@ -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,
);
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