diff --git a/frontend/src/metabase/dashboard/containers/DashboardApp.jsx b/frontend/src/metabase/dashboard/containers/DashboardApp.jsx index 7a5b93546b93f12160735a00016af891aa2d3c64..188e6cd26572674b8ed0e5f8117b495fb1471b48 100644 --- a/frontend/src/metabase/dashboard/containers/DashboardApp.jsx +++ b/frontend/src/metabase/dashboard/containers/DashboardApp.jsx @@ -40,6 +40,7 @@ import { getFavicon, getDocumentTitle, getIsRunning, + getIsLoadingComplete, } from "../selectors"; import { getDatabases, getMetadata } from "metabase/selectors/metadata"; import { @@ -81,6 +82,7 @@ const mapStateToProps = (state, props) => { pageFavicon: getFavicon(state), documentTitle: getDocumentTitle(state), isRunning: getIsRunning(state), + isLoadingComplete: getIsLoadingComplete(state), }; }; @@ -96,16 +98,17 @@ const mapDispatchToProps = { const DashboardApp = props => { const options = parseHashOptions(window.location.hash); - const { isRunning, dashboard } = props; + const { isRunning, isLoadingComplete, dashboard } = props; const [editingOnLoad] = useState(options.edit); const [addCardOnLoad] = useState(options.add && parseInt(options.add)); - const [shouldSendNotification, setShouldSendNotification] = useState(false); const [isShowingToaster, setIsShowingToaster] = useState(false); const onTimeout = useCallback(() => { - setIsShowingToaster(true); + if (Notification.permission === "default") { + setIsShowingToaster(true); + } }, []); useLoadingTimer(isRunning, { @@ -118,26 +121,20 @@ const DashboardApp = props => { useOnUnmount(props.reset); useEffect(() => { - if (!isRunning) { + if (isLoadingComplete) { setIsShowingToaster(false); - } - if (!isRunning && shouldSendNotification) { - if (document.hidden) { + if (Notification.permission === "granted" && document.hidden) { showNotification( - t`All Set! ${dashboard.name} is ready.`, + t`All Set! ${dashboard?.name} is ready.`, t`All questions loaded`, ); } - setShouldSendNotification(false); } - }, [isRunning, shouldSendNotification, showNotification, dashboard?.name]); + }, [isLoadingComplete, showNotification, dashboard?.name]); const onConfirmToast = useCallback(async () => { - const result = await requestPermission(); - if (result === "granted") { - setIsShowingToaster(false); - setShouldSendNotification(true); - } + await requestPermission(); + setIsShowingToaster(false); }, [requestPermission]); const onDismissToast = useCallback(() => { diff --git a/frontend/src/metabase/dashboard/reducers.js b/frontend/src/metabase/dashboard/reducers.js index cfbad20b7719738aed669dee3d5256e1f712a601..9996bc8f8e4569c166993a4ced4ca3f8bfdeeae1 100644 --- a/frontend/src/metabase/dashboard/reducers.js +++ b/frontend/src/metabase/dashboard/reducers.js @@ -298,7 +298,7 @@ const loadingDashCards = handleActions( [INITIALIZE]: { next: state => ({ ...state, - isLoadingComplete: false, + loadingStatus: "idle", }), }, [FETCH_DASHBOARD]: { @@ -307,13 +307,14 @@ const loadingDashCards = handleActions( dashcardIds: Object.values(payload.entities.dashcard || {}) .filter(dc => !isVirtualDashCard(dc)) .map(dc => dc.id), - isLoadingComplete: false, + loadingStatus: "idle", }), }, [FETCH_DASHBOARD_CARD_DATA]: { next: state => ({ ...state, loadingIds: state.dashcardIds, + loadingStatus: "running", startTime: state.dashcardIds.length > 0 && // check that performance is defined just in case @@ -329,7 +330,7 @@ const loadingDashCards = handleActions( ...state, loadingIds, ...(loadingIds.length === 0 - ? { startTime: null, isLoadingComplete: true } + ? { startTime: null, loadingStatus: "complete" } : {}), }; }, @@ -340,16 +341,14 @@ const loadingDashCards = handleActions( return { ...state, loadingIds, - ...(loadingIds.length === 0 - ? { startTime: null, isLoadingComplete: true } - : {}), + ...(loadingIds.length === 0 ? { startTime: null } : {}), }; }, }, [RESET]: { next: state => ({ ...state, - isLoadingComplete: false, + loadingStatus: "idle", }), }, }, @@ -357,7 +356,7 @@ const loadingDashCards = handleActions( dashcardIds: [], loadingIds: [], startTime: null, - isLoadingComplete: false, + loadingStatus: "idle", }, ); diff --git a/frontend/src/metabase/dashboard/reducers.unit.spec.js b/frontend/src/metabase/dashboard/reducers.unit.spec.js index 110b630a60b074df4f40d9e3840acce4b12ff116..d749012b82e671bdc4a77656e9b4c0ece32d0543 100644 --- a/frontend/src/metabase/dashboard/reducers.unit.spec.js +++ b/frontend/src/metabase/dashboard/reducers.unit.spec.js @@ -26,7 +26,7 @@ describe("dashboard reducers", () => { dashcardIds: [], loadingIds: [], startTime: null, - isLoadingComplete: false, + loadingStatus: "idle", }, parameterValues: {}, parameterValuesSearchCache: {}, diff --git a/frontend/src/metabase/dashboard/selectors.js b/frontend/src/metabase/dashboard/selectors.js index 158ea6a0af58aff3473bfc349334ba905f6c3ac5..dd9c3cbe4ed6880a5edf4ca3e3c28415ce2c32ad 100644 --- a/frontend/src/metabase/dashboard/selectors.js +++ b/frontend/src/metabase/dashboard/selectors.js @@ -34,7 +34,9 @@ export const getFavicon = state => : null; export const getIsRunning = state => - state.dashboard.loadingDashCards.loadingIds > 0; + state.dashboard.loadingDashCards.loadingStatus === "running"; +export const getIsLoadingComplete = state => + state.dashboard.loadingDashCards.loadingStatus === "complete"; export const getLoadingStartTime = state => state.dashboard.loadingDashCards.startTime; diff --git a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx index 3f82b6f2bba3fc63e21b3b3dcc0ec6d3d57c3164..2488c3eeff6d677d27b870e7f39393096e02d6fa 100644 --- a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx +++ b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx @@ -86,6 +86,7 @@ import { getDocumentTitle, getPageFavicon, getIsTimeseries, + getIsLoadingComplete, } from "../selectors"; import * as actions from "../actions"; @@ -186,6 +187,7 @@ const mapStateToProps = (state, props) => { snippetCollectionId: getSnippetCollectionId(state), documentTitle: getDocumentTitle(state), pageFavicon: getPageFavicon(state), + isLoadingComplete: getIsLoadingComplete(state), }; }; @@ -222,6 +224,7 @@ function QueryBuilder(props) { allLoaded, showTimelinesForCollection, card, + isLoadingComplete, } = props; const forceUpdate = useForceUpdate(); @@ -365,13 +368,14 @@ function QueryBuilder(props) { } }); - const { isRunning } = uiControls; - - const [shouldSendNotification, setShouldSendNotification] = useState(false); const [isShowingToaster, setIsShowingToaster] = useState(false); + const { isRunning } = uiControls; + const onTimeout = useCallback(() => { - setIsShowingToaster(true); + if (Notification.permission === "default") { + setIsShowingToaster(true); + } }, []); useLoadingTimer(isRunning, { @@ -382,26 +386,21 @@ function QueryBuilder(props) { const [requestPermission, showNotification] = useWebNotification(); useEffect(() => { - if (!isRunning) { + if (isLoadingComplete) { setIsShowingToaster(false); - } - if (!isRunning && shouldSendNotification) { - if (document.hidden) { + + if (Notification.permission === "granted" && document.hidden) { showNotification( t`All Set! Your question is ready.`, t`${card.name} is loaded.`, ); } - setShouldSendNotification(false); } - }, [isRunning, shouldSendNotification, showNotification, card?.name]); + }, [isLoadingComplete, showNotification, card?.name]); const onConfirmToast = useCallback(async () => { - const result = await requestPermission(); - if (result === "granted") { - setIsShowingToaster(false); - setShouldSendNotification(true); - } + await requestPermission(); + setIsShowingToaster(false); }, [requestPermission]); const onDismissToast = useCallback(() => { diff --git a/frontend/src/metabase/query_builder/reducers.js b/frontend/src/metabase/query_builder/reducers.js index 77f9deef0dc128c4e8725d657f10ad7b745d7fab..c11e3b8a878f1347e4fb695e5af6425745fb549c 100644 --- a/frontend/src/metabase/query_builder/reducers.js +++ b/frontend/src/metabase/query_builder/reducers.js @@ -72,6 +72,7 @@ const DEFAULT_UI_CONTROLS = { isShowingNewbModal: false, isEditing: false, isRunning: false, + isQueryComplete: false, isShowingSummarySidebar: false, isShowingFilterSidebar: false, isShowingChartTypeSidebar: false, @@ -93,6 +94,8 @@ const DEFAULT_LOADING_CONTROLS = { timeoutId: "", }; +const DEFAULT_QUERY_STATUS = "idle"; + const UI_CONTROLS_SIDEBAR_DEFAULTS = { isShowingSummarySidebar: false, isShowingFilterSidebar: false, @@ -196,12 +199,18 @@ export const uiControls = handleActions( next: (state, { payload }) => ({ ...state, isEditing: false }), }, - [RUN_QUERY]: state => ({ ...state, isRunning: true }), + [RUN_QUERY]: state => ({ + ...state, + isRunning: true, + }), [CANCEL_QUERY]: { next: (state, { payload }) => ({ ...state, isRunning: false }), }, [QUERY_COMPLETED]: { - next: (state, { payload }) => ({ ...state, isRunning: false }), + next: (state, { payload }) => ({ + ...state, + isRunning: false, + }), }, [QUERY_ERRORED]: { next: (state, { payload }) => ({ ...state, isRunning: false }), @@ -324,6 +333,15 @@ export const loadingControls = handleActions( DEFAULT_LOADING_CONTROLS, ); +export const queryStatus = handleActions( + { + [RUN_QUERY]: state => "running", + [QUERY_COMPLETED]: state => "complete", + [CANCEL_QUERY]: state => "idle", + }, + DEFAULT_QUERY_STATUS, +); + 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 56ff851d5db176e39a977279f5332583a4fed73b..6c79ae5dc92873b7a1b71263037dc1d53575b61d 100644 --- a/frontend/src/metabase/query_builder/selectors.js +++ b/frontend/src/metabase/query_builder/selectors.js @@ -38,6 +38,7 @@ import ObjectMode from "metabase/modes/components/modes/ObjectMode"; import { LOAD_COMPLETE_FAVICON } from "metabase/hoc/Favicon"; export const getUiControls = state => state.qb.uiControls; +const getQueryStatus = state => state.qb.queryStatus; const getLoadingControls = state => state.qb.loadingControls; export const getIsShowingTemplateTagsEditor = state => @@ -69,6 +70,8 @@ export const getIsAnySidebarOpen = createSelector([getUiControls], uiControls => export const getIsEditing = state => getUiControls(state).isEditing; export const getIsRunning = state => getUiControls(state).isRunning; +export const getIsLoadingComplete = state => + getQueryStatus(state) === "complete"; export const getCard = state => state.qb.card; export const getOriginalCard = state => state.qb.originalCard; diff --git a/resources/frontend_client/app/assets/img/blue_check.png b/resources/frontend_client/app/assets/img/blue_check.png index 8939ee67880fd4bb569a89cabd013e585d89ea79..f690a5d1716490a31abbde02f23c1ec1863d3dd0 100644 Binary files a/resources/frontend_client/app/assets/img/blue_check.png and b/resources/frontend_client/app/assets/img/blue_check.png differ