diff --git a/app/client/src/actions/applicationActions.ts b/app/client/src/actions/applicationActions.ts index 9bfdd46847..6a636d4828 100644 --- a/app/client/src/actions/applicationActions.ts +++ b/app/client/src/actions/applicationActions.ts @@ -1,5 +1,4 @@ import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; -import { EditorModes } from "../components/editorComponents/CodeEditor/EditorConfig"; import { APP_MODE } from "../reducers/entityReducers/appReducer"; import { UpdateApplicationPayload } from "api/ApplicationApi"; diff --git a/app/client/src/actions/initActions.ts b/app/client/src/actions/initActions.ts index 9c3854e816..446c85ec8a 100644 --- a/app/client/src/actions/initActions.ts +++ b/app/client/src/actions/initActions.ts @@ -2,6 +2,7 @@ import { ReduxActionTypes, ReduxAction, InitializeEditorPayload, + ReduxActionErrorTypes, } from "constants/ReduxActionConstants"; export const initEditor = ( @@ -14,3 +15,17 @@ export const initEditor = ( pageId, }, }); + +export const initEditorError = (): ReduxAction<{ show: false }> => ({ + type: ReduxActionErrorTypes.INITIALIZE_EDITOR_ERROR, + payload: { + show: false, + }, +}); + +export const initViewerError = (): ReduxAction<{ show: false }> => ({ + type: ReduxActionErrorTypes.INITIALIZE_PAGE_VIEWER_ERROR, + payload: { + show: false, + }, +}); diff --git a/app/client/src/api/Api.tsx b/app/client/src/api/Api.tsx index 894461bdcf..d3503b35dd 100644 --- a/app/client/src/api/Api.tsx +++ b/app/client/src/api/Api.tsx @@ -4,13 +4,10 @@ import { API_REQUEST_HEADERS, } from "constants/ApiConstants"; import { ActionApiResponse } from "./ActionAPI"; -import { - AUTH_LOGIN_URL, - PAGE_NOT_FOUND_URL, - SERVER_ERROR_URL, -} from "constants/routes"; +import { AUTH_LOGIN_URL, PAGE_NOT_FOUND_URL } from "constants/routes"; import history from "utils/history"; import { convertObjectToQueryParams } from "utils/AppsmithUtils"; +import { SERVER_API_TIMEOUT_ERROR } from "../constants/messages"; //TODO(abhinav): Refactor this to make more composable. export const apiRequestConfig = { @@ -22,8 +19,10 @@ export const apiRequestConfig = { const axiosInstance: AxiosInstance = axios.create(); +export const axiosConnectionAbortedCode = "ECONNABORTED"; const executeActionRegex = /actions\/execute/; -const currentUserRegex = /\/me$/; +const timeoutErrorRegex = /timeout of (\d+)ms exceeded/; + axiosInstance.interceptors.request.use((config: any) => { return { ...config, timer: performance.now() }; }); @@ -50,20 +49,25 @@ axiosInstance.interceptors.response.use( return response.data; }, function(error: any) { + // Return if the call was cancelled via cancel token if (axios.isCancel(error)) { return; } - if (error.code === "ECONNABORTED") { - if (error.config && error.config.url.match(currentUserRegex)) { - history.replace({ pathname: SERVER_ERROR_URL }); - } - return Promise.reject({ - message: "Please check your internet connection", - }); - } + // Return modified response if action execution failed if (error.config && error.config.url.match(executeActionRegex)) { return makeExecuteActionResponse(error.response); } + // Return error if any timeout happened in other api calls + if ( + error.code === axiosConnectionAbortedCode && + error.message && + error.message.match(timeoutErrorRegex) + ) { + return Promise.reject({ + ...error, + message: SERVER_API_TIMEOUT_ERROR, + }); + } if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx diff --git a/app/client/src/assets/images/timeout-image.png b/app/client/src/assets/images/timeout-image.png new file mode 100644 index 0000000000..c0eef6cfb5 Binary files /dev/null and b/app/client/src/assets/images/timeout-image.png differ diff --git a/app/client/src/constants/AppConstants.ts b/app/client/src/constants/AppConstants.ts index f20b1da124..be0cf7b4ee 100644 --- a/app/client/src/constants/AppConstants.ts +++ b/app/client/src/constants/AppConstants.ts @@ -8,3 +8,20 @@ const APP_STORE_NAMESPACE = "APPSMITH_LOCAL_STORE"; export const getAppStoreName = (appId: string) => `${APP_STORE_NAMESPACE}-${appId}`; + +export const getAppStore = (appId: string) => { + const appStoreName = getAppStoreName(appId); + let storeString = "{}"; + // Check if localStorage exists + if (localStorage) { + const appStore = localStorage.getItem(appStoreName); + if (appStore) storeString = appStore; + } + let store; + try { + store = JSON.parse(storeString); + } catch (e) { + store = {}; + } + return store; +}; diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index d4d4974207..b6f1c79152 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -296,6 +296,7 @@ export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTy export const ReduxActionErrorTypes: { [key: string]: string } = { INITIALIZE_EDITOR_ERROR: "INITIALIZE_EDITOR_ERROR", + INITIALIZE_PAGE_VIEWER_ERROR: "INITIALIZE_PAGE_VIEWER_ERROR", API_ERROR: "API_ERROR", WIDGET_DELETE_ERROR: "WIDGET_DELETE_ERROR", UPDATE_APPLICATION_ERROR: "UPDATE_APPLICATION_ERROR", diff --git a/app/client/src/constants/messages.ts b/app/client/src/constants/messages.ts index f8e936a8fe..d56633edc7 100644 --- a/app/client/src/constants/messages.ts +++ b/app/client/src/constants/messages.ts @@ -168,3 +168,6 @@ export const GOOGLE_RECAPTCHA_KEY_ERROR = "Google Re-Captcha Token Generation failed! Please check the Re-captcha Site Key."; export const GOOGLE_RECAPTCHA_DOMAIN_ERROR = "Google Re-Captcha Token Generation failed! Please check the allowed domains."; + +export const SERVER_API_TIMEOUT_ERROR = + "Appsmith server is taking too long to respond. Please try again after some time"; diff --git a/app/client/src/pages/AppViewer/index.tsx b/app/client/src/pages/AppViewer/index.tsx index 90806b79ca..0927ccea15 100644 --- a/app/client/src/pages/AppViewer/index.tsx +++ b/app/client/src/pages/AppViewer/index.tsx @@ -10,7 +10,10 @@ import { getApplicationViewerPageURL, } from "constants/routes"; import { ReduxActionTypes } from "constants/ReduxActionConstants"; -import { getIsInitialized } from "selectors/appViewSelectors"; +import { + getIsInitialized, + getIsInitializeError, +} from "selectors/appViewSelectors"; import { executeAction } from "actions/widgetActions"; import { ExecuteActionPayload } from "constants/ActionConstants"; import { updateWidgetPropertyRequest } from "actions/controlActions"; @@ -23,6 +26,8 @@ import { } from "actions/metaActions"; import { editorInitializer } from "utils/EditorUtils"; import * as Sentry from "@sentry/react"; +import log from "loglevel"; +import ServerTimeout from "../common/ServerTimeout"; const SentryRoute = Sentry.withSentryRouting(Route); const AppViewerBody = styled.section` @@ -36,6 +41,7 @@ const AppViewerBody = styled.section` export type AppViewerProps = { initializeAppViewer: (applicationId: string, pageId?: string) => void; isInitialized: boolean; + isInitializeError: boolean; executeAction: (actionPayload: ExecuteActionPayload) => void; updateWidgetProperty: ( widgetId: string, @@ -62,7 +68,7 @@ class AppViewer extends Component< this.setState({ registered: true }); }); const { applicationId, pageId } = this.props.match.params; - console.log({ applicationId, pageId }); + log.debug({ applicationId, pageId }); if (applicationId) { this.props.initializeAppViewer(applicationId, pageId); } @@ -73,7 +79,10 @@ class AppViewer extends Component< }; public render() { - const { isInitialized } = this.props; + const { isInitialized, isInitializeError } = this.props; + if (isInitializeError) { + return ; + } return ( ({ isInitialized: getIsInitialized(state), + isInitializeError: getIsInitializeError(state), }); const mapDispatchToProps = (dispatch: any) => ({ diff --git a/app/client/src/pages/Editor/index.tsx b/app/client/src/pages/Editor/index.tsx index 8dc1b7b639..139cc9ffcc 100644 --- a/app/client/src/pages/Editor/index.tsx +++ b/app/client/src/pages/Editor/index.tsx @@ -1,7 +1,7 @@ import React, { Component } from "react"; import { Helmet } from "react-helmet"; import { connect } from "react-redux"; -import { withRouter, RouteComponentProps } from "react-router-dom"; +import { RouteComponentProps, withRouter } from "react-router-dom"; import { BuilderRouteParams, getApplicationViewerPageURL, @@ -13,15 +13,16 @@ import TouchBackend from "react-dnd-touch-backend"; import { getCurrentApplicationId, getCurrentPageId, - getPublishingError, - getIsEditorLoading, getIsEditorInitialized, + getIsEditorInitializeError, + getIsEditorLoading, getIsPublishingApplication, + getPublishingError, } from "selectors/editorSelectors"; import { - Dialog, - Classes, AnchorButton, + Classes, + Dialog, Hotkey, Hotkeys, Spinner, @@ -40,11 +41,12 @@ import ConfirmRunModal from "pages/Editor/ConfirmRunModal"; import * as Sentry from "@sentry/react"; import { copyWidget, - pasteWidget, - deleteSelectedWidget, cutWidget, + deleteSelectedWidget, + pasteWidget, } from "actions/widgetActions"; import { isMac } from "utils/helpers"; +import ServerTimeout from "../common/ServerTimeout"; type EditorProps = { currentApplicationId?: string; @@ -53,6 +55,7 @@ type EditorProps = { isPublishing: boolean; isEditorLoading: boolean; isEditorInitialized: boolean; + isEditorInitializeError: boolean; errorPublishing: boolean; copySelectedWidget: () => void; pasteCopiedWidget: () => void; @@ -189,6 +192,8 @@ class Editor extends Component { nextProps.isPublishing !== this.props.isPublishing || nextProps.isEditorLoading !== this.props.isEditorLoading || nextProps.errorPublishing !== this.props.errorPublishing || + nextProps.isEditorInitializeError !== + this.props.isEditorInitializeError || nextState.isDialogOpen !== this.state.isDialogOpen || nextState.registered !== this.state.registered ); @@ -200,6 +205,9 @@ class Editor extends Component { }); }; public render() { + if (this.props.isEditorInitializeError) { + return ; + } if (!this.props.isEditorInitialized || !this.state.registered) { return ( @@ -260,6 +268,7 @@ const mapStateToProps = (state: AppState) => ({ isPublishing: getIsPublishingApplication(state), isEditorLoading: getIsEditorLoading(state), isEditorInitialized: getIsEditorInitialized(state), + isEditorInitializeError: getIsEditorInitializeError(state), user: getCurrentUser(state), }); diff --git a/app/client/src/pages/common/PageNotFound.tsx b/app/client/src/pages/common/PageNotFound.tsx index ed0e2d4076..e81f1e8f41 100644 --- a/app/client/src/pages/common/PageNotFound.tsx +++ b/app/client/src/pages/common/PageNotFound.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { RouterProps } from "react-router"; +import { RouteComponentProps, withRouter } from "react-router"; import styled from "styled-components"; import Button from "components/editorComponents/Button"; import PageUnavailableImage from "assets/images/404-image.png"; @@ -20,7 +20,7 @@ const Wrapper = styled.div` } `; -class PageNotFound extends React.PureComponent { +class PageNotFound extends React.PureComponent { public render() { return ( <> @@ -54,4 +54,4 @@ class PageNotFound extends React.PureComponent { } } -export default PageNotFound; +export default withRouter(PageNotFound); diff --git a/app/client/src/pages/common/ServerTimeout.tsx b/app/client/src/pages/common/ServerTimeout.tsx new file mode 100644 index 0000000000..8c2727c741 --- /dev/null +++ b/app/client/src/pages/common/ServerTimeout.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import styled from "styled-components"; +import AppTimeoutImage from "assets/images/timeout-image.png"; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + height: calc(100vh - ${props => props.theme.headerHeight}); + .bold-text { + font-weight: ${props => props.theme.fontWeights[3]}; + font-size: 24px; + } + .page-unavailable-img { + width: 35%; + } + .button-position { + margin: auto; + } +`; + +const RetryButton = styled.button` + background-color: #f3672a; + color: white; + height: 40px; + width: 300px; + border: none; + cursor: pointer; + font-weight: 600; + font-size: 17px; +`; + +const ServerTimeout = () => { + return ( + + Page Unavailable +
+

+ Appsmith server is taking too long to respond +

+

Please retry after some time

+ window.location.reload()}> + {"Retry"} + +
+
+ ); +}; + +export default ServerTimeout; diff --git a/app/client/src/reducers/uiReducers/appViewReducer.tsx b/app/client/src/reducers/uiReducers/appViewReducer.tsx index 5fc46771c2..31c7d24476 100644 --- a/app/client/src/reducers/uiReducers/appViewReducer.tsx +++ b/app/client/src/reducers/uiReducers/appViewReducer.tsx @@ -5,12 +5,14 @@ import { ReduxAction, ReduxActionTypes, PageListPayload, + ReduxActionErrorTypes, } from "constants/ReduxActionConstants"; import { FetchPublishedPageSuccessPayload } from "actions/pageActions"; const initialState: AppViewReduxState = { isFetchingPage: false, initialized: false, + initializeError: false, pages: [], pageWidgetId: "0", }; @@ -24,6 +26,11 @@ const appViewReducer = createReducer(initialState, { ) => { return { ...state, initialized: true }; }, + [ReduxActionErrorTypes.INITIALIZE_PAGE_VIEWER_ERROR]: ( + state: AppViewReduxState, + ) => { + return { ...state, initializeError: true }; + }, [ReduxActionTypes.FETCH_PUBLISHED_PAGE_INIT]: (state: AppViewReduxState) => { return { ...state, dsl: undefined, isFetchingPage: true }; }, @@ -45,6 +52,7 @@ const appViewReducer = createReducer(initialState, { export interface AppViewReduxState { initialized: boolean; + initializeError: boolean; dsl?: ContainerWidgetProps; isFetchingPage: boolean; currentLayoutId?: string; diff --git a/app/client/src/reducers/uiReducers/editorReducer.tsx b/app/client/src/reducers/uiReducers/editorReducer.tsx index 3e9d9d73ad..f46aa352f4 100644 --- a/app/client/src/reducers/uiReducers/editorReducer.tsx +++ b/app/client/src/reducers/uiReducers/editorReducer.tsx @@ -5,12 +5,11 @@ import { ReduxActionTypes, ReduxActionErrorTypes, } from "constants/ReduxActionConstants"; -import { WidgetProps } from "widgets/BaseWidget"; -import { ContainerWidgetProps } from "widgets/ContainerWidget"; import moment from "moment"; const initialState: EditorReduxState = { initialized: false, + initializationError: false, loadingStates: { publishing: false, publishingError: false, @@ -68,11 +67,8 @@ const editorReducer = createReducer(initialState, { ) => { return { ...state, - loadingStates: { - ...state.loadingStates, - loading: false, - loadingError: true, - }, + initializationError: true, + initialized: false, }; }, [ReduxActionTypes.PUBLISH_APPLICATION_INIT]: (state: EditorReduxState) => { @@ -178,7 +174,7 @@ const editorReducer = createReducer(initialState, { export interface EditorReduxState { initialized: boolean; - dsl?: ContainerWidgetProps; + initializationError: boolean; pageWidgetId?: string; currentLayoutId?: string; currentPageName?: string; diff --git a/app/client/src/sagas/ErrorSagas.tsx b/app/client/src/sagas/ErrorSagas.tsx index 9d245866eb..7dfcb2ba6a 100644 --- a/app/client/src/sagas/ErrorSagas.tsx +++ b/app/client/src/sagas/ErrorSagas.tsx @@ -11,6 +11,7 @@ import { put, takeLatest, call } from "redux-saga/effects"; import { ERROR_401, ERROR_500, ERROR_0 } from "constants/messages"; import { ToastType } from "react-toastify"; import log from "loglevel"; +import { axiosConnectionAbortedCode } from "../api/Api"; export function* callAPI(apiCall: any, requestPayload: any) { try { @@ -60,7 +61,7 @@ export function getResponseErrorMessage(response: ApiResponse) { : undefined; } -type ErrorPayloadType = { code?: number; message?: string }; +type ErrorPayloadType = { code?: number | string; message?: string }; let ActionErrorDisplayMap: { [key: string]: (error: ErrorPayloadType) => string; } = {}; @@ -92,6 +93,7 @@ export function* errorSaga( type, payload: { error, show = true }, } = errorAction; + const message = error && error.message ? error.message : ActionErrorDisplayMap[type](error); diff --git a/app/client/src/sagas/InitSagas.ts b/app/client/src/sagas/InitSagas.ts index e0d822ca1a..70332af810 100644 --- a/app/client/src/sagas/InitSagas.ts +++ b/app/client/src/sagas/InitSagas.ts @@ -1,7 +1,14 @@ -import { all, call, put, select, take, takeLatest } from "redux-saga/effects"; +import { + all, + call, + put, + select, + take, + takeLatest, + race, +} from "redux-saga/effects"; import { InitializeEditorPayload, - Page, ReduxAction, ReduxActionErrorTypes, ReduxActionTypes, @@ -21,126 +28,86 @@ import { fetchActions, fetchActionsForView } from "actions/actionActions"; import { fetchApplication } from "actions/applicationActions"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { getCurrentApplication } from "selectors/applicationSelectors"; -import { AppState } from "reducers"; -import PageApi, { FetchPageResponse } from "api/PageApi"; -import { validateResponse } from "./ErrorSagas"; -import { extractCurrentDSL } from "utils/WidgetPropsUtils"; import { APP_MODE } from "reducers/entityReducers/appReducer"; -import { getAppStoreName } from "constants/AppConstants"; +import { getAppStore } from "constants/AppConstants"; import { getDefaultPageId } from "./selectors"; - -const getAppStore = (appId: string) => { - const appStoreName = getAppStoreName(appId); - const storeString = localStorage.getItem(appStoreName) || "{}"; - let store; - try { - store = JSON.parse(storeString); - } catch (e) { - store = {}; - } - return store; -}; +import { populatePageDSLsSaga } from "./PageSagas"; +import { initEditorError, initViewerError } from "../actions/initActions"; function* initializeEditorSaga( initializeEditorAction: ReduxAction, ) { const { applicationId, pageId } = initializeEditorAction.payload; - // Step 1: Set App Mode. Start getting all the data needed - yield put(setAppMode(APP_MODE.EDIT)); - yield put({ type: ReduxActionTypes.START_EVALUATION }); - yield all([ - put(fetchPageList(applicationId, APP_MODE.EDIT)), - put(fetchEditorConfigs()), - put(fetchActions(applicationId)), - put(fetchPage(pageId)), - put(fetchApplication(applicationId, APP_MODE.EDIT)), - ]); - // Step 2: Wait for all data to be in the state - yield all([ - take(ReduxActionTypes.FETCH_PAGE_LIST_SUCCESS), - take(ReduxActionTypes.FETCH_PAGE_SUCCESS), - take(ReduxActionTypes.SWITCH_CURRENT_PAGE_ID), - take(ReduxActionTypes.FETCH_ACTIONS_SUCCESS), - ]); - - // Step 3: Call all the APIs which needs Organization Id from PageList API response. - yield all([put(fetchPlugins()), put(fetchDatasources())]); - - // Step 4: Wait for all data to be in the state - yield all([ - take(ReduxActionTypes.FETCH_PLUGINS_SUCCESS), - take(ReduxActionTypes.FETCH_DATASOURCES_SUCCESS), - ]); - - // Step 5: Set app store - yield put(updateAppStore(getAppStore(applicationId))); - - const currentApplication = yield select(getCurrentApplication); - - const appName = currentApplication ? currentApplication.name : ""; - const appId = currentApplication ? currentApplication.id : ""; - - AnalyticsUtil.logEvent("EDITOR_OPEN", { - appId: appId, - appName: appName, - }); - - // Step 6: Notify UI that the editor is ready to go - yield put({ - type: ReduxActionTypes.INITIALIZE_EDITOR_SUCCESS, - }); - yield call(populatePageDSLsSaga); -} - -function* fetchPageDSLSaga(pageId: string) { try { - const fetchPageResponse: FetchPageResponse = yield call(PageApi.fetchPage, { - id: pageId, + yield put(setAppMode(APP_MODE.EDIT)); + yield put({ type: ReduxActionTypes.START_EVALUATION }); + yield all([ + put(fetchPageList(applicationId, APP_MODE.EDIT)), + put(fetchEditorConfigs()), + put(fetchActions(applicationId)), + put(fetchPage(pageId)), + put(fetchApplication(applicationId, APP_MODE.EDIT)), + ]); + + const resultOfPrimaryCalls = yield race({ + success: all([ + take(ReduxActionTypes.FETCH_PAGE_LIST_SUCCESS), + take(ReduxActionTypes.FETCH_PAGE_SUCCESS), + take(ReduxActionTypes.FETCH_APPLICATION_SUCCESS), + take(ReduxActionTypes.FETCH_ACTIONS_SUCCESS), + ]), + failure: take([ + ReduxActionErrorTypes.FETCH_PAGE_LIST_ERROR, + ReduxActionErrorTypes.FETCH_PAGE_ERROR, + ReduxActionErrorTypes.FETCH_APPLICATION_ERROR, + ReduxActionErrorTypes.FETCH_ACTIONS_ERROR, + ]), }); - const isValidResponse = yield validateResponse(fetchPageResponse); - if (isValidResponse) { - return { - pageId: pageId, - dsl: extractCurrentDSL(fetchPageResponse), - }; + + if (resultOfPrimaryCalls.failure) { + yield put(initEditorError()); + return; } - } catch (error) { - yield put({ - type: ReduxActionTypes.FETCH_PAGE_DSL_ERROR, - payload: { - pageId: pageId, - error, - show: false, - }, - }); - } -} -export function* populatePageDSLsSaga() { - try { - yield put({ - type: ReduxActionTypes.POPULATE_PAGEDSLS_INIT, + yield all([put(fetchPlugins()), put(fetchDatasources())]); + + const resultOfSecondaryCalls = yield race({ + success: all([ + take(ReduxActionTypes.FETCH_PLUGINS_SUCCESS), + take(ReduxActionTypes.FETCH_DATASOURCES_SUCCESS), + ]), + failure: take([ + ReduxActionErrorTypes.FETCH_PLUGINS_ERROR, + ReduxActionErrorTypes.FETCH_DATASOURCES_ERROR, + ]), }); - const pageIds: string[] = yield select((state: AppState) => - state.entities.pageList.pages.map((page: Page) => page.pageId), - ); - const pageDSLs = yield all( - pageIds.map((pageId: string) => { - return call(fetchPageDSLSaga, pageId); - }), - ); - yield put({ - type: ReduxActionTypes.FETCH_PAGE_DSLS_SUCCESS, - payload: pageDSLs, + + if (resultOfSecondaryCalls.failure) { + yield put(initEditorError()); + return; + } + + yield put(updateAppStore(getAppStore(applicationId))); + + const currentApplication = yield select(getCurrentApplication); + + const appName = currentApplication ? currentApplication.name : ""; + const appId = currentApplication ? currentApplication.id : ""; + + AnalyticsUtil.logEvent("EDITOR_OPEN", { + appId: appId, + appName: appName, }); - } catch (error) { + yield put({ - type: ReduxActionErrorTypes.POPULATE_PAGEDSLS_ERROR, - payload: { - error, - }, + type: ReduxActionTypes.INITIALIZE_EDITOR_SUCCESS, }); + } catch (e) { + yield put(initEditorError()); + return; } + + yield call(populatePageDSLsSaga); } export function* initializeAppViewerSaga( @@ -156,11 +123,23 @@ export function* initializeAppViewerSaga( put(fetchApplication(applicationId, APP_MODE.PUBLISHED)), ]); - yield all([ - take(ReduxActionTypes.FETCH_ACTIONS_VIEW_MODE_SUCCESS), - take(ReduxActionTypes.FETCH_PAGE_LIST_SUCCESS), - take(ReduxActionTypes.FETCH_APPLICATION_SUCCESS), - ]); + const resultOfPrimaryCalls = yield race({ + success: all([ + take(ReduxActionTypes.FETCH_ACTIONS_VIEW_MODE_SUCCESS), + take(ReduxActionTypes.FETCH_PAGE_LIST_SUCCESS), + take(ReduxActionTypes.FETCH_APPLICATION_SUCCESS), + ]), + failure: take([ + ReduxActionErrorTypes.FETCH_ACTIONS_VIEW_MODE_ERROR, + ReduxActionErrorTypes.FETCH_PAGE_LIST_ERROR, + ReduxActionErrorTypes.FETCH_APPLICATION_ERROR, + ]), + }); + + if (resultOfPrimaryCalls.failure) { + yield put(initViewerError()); + return; + } yield put(updateAppStore(getAppStore(applicationId))); const defaultPageId = yield select(getDefaultPageId); @@ -168,7 +147,15 @@ export function* initializeAppViewerSaga( if (toLoadPageId) { yield put(fetchPublishedPage(toLoadPageId, true)); - yield take(ReduxActionTypes.FETCH_PUBLISHED_PAGE_SUCCESS); + + const resultOfFetchPage = yield race({ + success: take(ReduxActionTypes.FETCH_PUBLISHED_PAGE_SUCCESS), + failure: take(ReduxActionErrorTypes.FETCH_PUBLISHED_PAGE_ERROR), + }); + if (resultOfFetchPage.failure) { + yield put(initViewerError()); + return; + } yield put(setAppMode(APP_MODE.PUBLISHED)); yield put(updateAppStore(getAppStore(applicationId))); diff --git a/app/client/src/sagas/PageSagas.tsx b/app/client/src/sagas/PageSagas.tsx index fd1ea55832..129297a4dc 100644 --- a/app/client/src/sagas/PageSagas.tsx +++ b/app/client/src/sagas/PageSagas.tsx @@ -1,6 +1,7 @@ import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer"; import { AppState } from "reducers"; import { + Page, PageListPayload, ReduxAction, ReduxActionErrorTypes, @@ -43,7 +44,6 @@ import { select, takeLatest, takeLeading, - take, } from "redux-saga/effects"; import history from "utils/history"; import { BUILDER_PAGE_URL } from "constants/routes"; @@ -117,6 +117,16 @@ export function* fetchPageListSaga( PerformanceTracker.stopAsyncTracking( PerformanceTransactionName.FETCH_PAGE_LIST_API, ); + } else { + PerformanceTracker.stopAsyncTracking( + PerformanceTransactionName.FETCH_PAGE_LIST_API, + ); + yield put({ + type: ReduxActionErrorTypes.FETCH_PAGE_LIST_ERROR, + payload: { + error: response.responseMeta.error, + }, + }); } } catch (error) { PerformanceTracker.stopAsyncTracking( @@ -608,6 +618,57 @@ export function* setDataUrl() { yield put(setUrlData(urlData)); } +function* fetchPageDSLSaga(pageId: string) { + try { + const fetchPageResponse: FetchPageResponse = yield call(PageApi.fetchPage, { + id: pageId, + }); + const isValidResponse = yield validateResponse(fetchPageResponse); + if (isValidResponse) { + return { + pageId: pageId, + dsl: extractCurrentDSL(fetchPageResponse), + }; + } + } catch (error) { + yield put({ + type: ReduxActionTypes.FETCH_PAGE_DSL_ERROR, + payload: { + pageId: pageId, + error, + show: false, + }, + }); + } +} + +export function* populatePageDSLsSaga() { + try { + yield put({ + type: ReduxActionTypes.POPULATE_PAGEDSLS_INIT, + }); + const pageIds: string[] = yield select((state: AppState) => + state.entities.pageList.pages.map((page: Page) => page.pageId), + ); + const pageDSLs = yield all( + pageIds.map((pageId: string) => { + return call(fetchPageDSLSaga, pageId); + }), + ); + yield put({ + type: ReduxActionTypes.FETCH_PAGE_DSLS_SUCCESS, + payload: pageDSLs, + }); + } catch (error) { + yield put({ + type: ReduxActionErrorTypes.POPULATE_PAGEDSLS_ERROR, + payload: { + error, + }, + }); + } +} + export default function* pageSagas() { yield all([ takeLatest(ReduxActionTypes.FETCH_PAGE_INIT, fetchPageSaga), diff --git a/app/client/src/selectors/appViewSelectors.tsx b/app/client/src/selectors/appViewSelectors.tsx index 477ded716b..f289a3160a 100644 --- a/app/client/src/selectors/appViewSelectors.tsx +++ b/app/client/src/selectors/appViewSelectors.tsx @@ -32,6 +32,11 @@ export const getIsInitialized = createSelector( (view: AppViewReduxState) => view.initialized, ); +export const getIsInitializeError = createSelector( + getAppViewState, + (view: AppViewReduxState) => view.initializeError, +); + export const getCurrentDSLPageId = createSelector( getPageListState, (pageList: PageListReduxState) => pageList.currentPageId, diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index 1f1c4da845..2a787d1fd5 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -20,14 +20,12 @@ import { getDataTree } from "selectors/dataTreeSelectors"; import _ from "lodash"; import { ContainerWidgetProps } from "widgets/ContainerWidget"; import { DataTreeWidget, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; -import { getActions, getWidgetsMeta } from "sagas/selectors"; +import { getActions } from "sagas/selectors"; -import * as log from "loglevel"; import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; import { getCanvasWidgets } from "./entitiesSelector"; -import { MetaState } from "../reducers/entityReducers/metaReducer"; import { WidgetTypes } from "../constants/WidgetConstants"; const getWidgetConfigs = (state: AppState) => state.entities.widgetConfig; @@ -45,6 +43,9 @@ const getWidgets = (state: AppState): CanvasWidgetsReduxState => export const getIsEditorInitialized = (state: AppState) => state.ui.editor.initialized; +export const getIsEditorInitializeError = (state: AppState): boolean => + state.ui.editor.initializationError; + export const getIsEditorLoading = (state: AppState) => state.ui.editor.loadingStates.loading;