Improve app initialisation for timeouts (#1412)

Fixes: #1510 

Co-authored-by: Arpit Mohan <mohanarpit@users.noreply.github.com>
This commit is contained in:
Hetu Nandu 2020-11-04 17:10:59 +05:30 committed by GitHub
parent 8ae77f7764
commit 20ef86f118
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 327 additions and 153 deletions

View File

@ -1,5 +1,4 @@
import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants";
import { EditorModes } from "../components/editorComponents/CodeEditor/EditorConfig";
import { APP_MODE } from "../reducers/entityReducers/appReducer"; import { APP_MODE } from "../reducers/entityReducers/appReducer";
import { UpdateApplicationPayload } from "api/ApplicationApi"; import { UpdateApplicationPayload } from "api/ApplicationApi";

View File

@ -2,6 +2,7 @@ import {
ReduxActionTypes, ReduxActionTypes,
ReduxAction, ReduxAction,
InitializeEditorPayload, InitializeEditorPayload,
ReduxActionErrorTypes,
} from "constants/ReduxActionConstants"; } from "constants/ReduxActionConstants";
export const initEditor = ( export const initEditor = (
@ -14,3 +15,17 @@ export const initEditor = (
pageId, 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,
},
});

View File

@ -4,13 +4,10 @@ import {
API_REQUEST_HEADERS, API_REQUEST_HEADERS,
} from "constants/ApiConstants"; } from "constants/ApiConstants";
import { ActionApiResponse } from "./ActionAPI"; import { ActionApiResponse } from "./ActionAPI";
import { import { AUTH_LOGIN_URL, PAGE_NOT_FOUND_URL } from "constants/routes";
AUTH_LOGIN_URL,
PAGE_NOT_FOUND_URL,
SERVER_ERROR_URL,
} from "constants/routes";
import history from "utils/history"; import history from "utils/history";
import { convertObjectToQueryParams } from "utils/AppsmithUtils"; import { convertObjectToQueryParams } from "utils/AppsmithUtils";
import { SERVER_API_TIMEOUT_ERROR } from "../constants/messages";
//TODO(abhinav): Refactor this to make more composable. //TODO(abhinav): Refactor this to make more composable.
export const apiRequestConfig = { export const apiRequestConfig = {
@ -22,8 +19,10 @@ export const apiRequestConfig = {
const axiosInstance: AxiosInstance = axios.create(); const axiosInstance: AxiosInstance = axios.create();
export const axiosConnectionAbortedCode = "ECONNABORTED";
const executeActionRegex = /actions\/execute/; const executeActionRegex = /actions\/execute/;
const currentUserRegex = /\/me$/; const timeoutErrorRegex = /timeout of (\d+)ms exceeded/;
axiosInstance.interceptors.request.use((config: any) => { axiosInstance.interceptors.request.use((config: any) => {
return { ...config, timer: performance.now() }; return { ...config, timer: performance.now() };
}); });
@ -50,20 +49,25 @@ axiosInstance.interceptors.response.use(
return response.data; return response.data;
}, },
function(error: any) { function(error: any) {
// Return if the call was cancelled via cancel token
if (axios.isCancel(error)) { if (axios.isCancel(error)) {
return; return;
} }
if (error.code === "ECONNABORTED") { // Return modified response if action execution failed
if (error.config && error.config.url.match(currentUserRegex)) {
history.replace({ pathname: SERVER_ERROR_URL });
}
return Promise.reject({
message: "Please check your internet connection",
});
}
if (error.config && error.config.url.match(executeActionRegex)) { if (error.config && error.config.url.match(executeActionRegex)) {
return makeExecuteActionResponse(error.response); 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) { if (error.response) {
// The request was made and the server responded with a status code // The request was made and the server responded with a status code
// that falls out of the range of 2xx // that falls out of the range of 2xx

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

@ -8,3 +8,20 @@ const APP_STORE_NAMESPACE = "APPSMITH_LOCAL_STORE";
export const getAppStoreName = (appId: string) => export const getAppStoreName = (appId: string) =>
`${APP_STORE_NAMESPACE}-${appId}`; `${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;
};

View File

@ -296,6 +296,7 @@ export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTy
export const ReduxActionErrorTypes: { [key: string]: string } = { export const ReduxActionErrorTypes: { [key: string]: string } = {
INITIALIZE_EDITOR_ERROR: "INITIALIZE_EDITOR_ERROR", INITIALIZE_EDITOR_ERROR: "INITIALIZE_EDITOR_ERROR",
INITIALIZE_PAGE_VIEWER_ERROR: "INITIALIZE_PAGE_VIEWER_ERROR",
API_ERROR: "API_ERROR", API_ERROR: "API_ERROR",
WIDGET_DELETE_ERROR: "WIDGET_DELETE_ERROR", WIDGET_DELETE_ERROR: "WIDGET_DELETE_ERROR",
UPDATE_APPLICATION_ERROR: "UPDATE_APPLICATION_ERROR", UPDATE_APPLICATION_ERROR: "UPDATE_APPLICATION_ERROR",

View File

@ -168,3 +168,6 @@ export const GOOGLE_RECAPTCHA_KEY_ERROR =
"Google Re-Captcha Token Generation failed! Please check the Re-captcha Site Key."; "Google Re-Captcha Token Generation failed! Please check the Re-captcha Site Key.";
export const GOOGLE_RECAPTCHA_DOMAIN_ERROR = export const GOOGLE_RECAPTCHA_DOMAIN_ERROR =
"Google Re-Captcha Token Generation failed! Please check the allowed domains."; "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";

View File

@ -10,7 +10,10 @@ import {
getApplicationViewerPageURL, getApplicationViewerPageURL,
} from "constants/routes"; } from "constants/routes";
import { ReduxActionTypes } from "constants/ReduxActionConstants"; import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { getIsInitialized } from "selectors/appViewSelectors"; import {
getIsInitialized,
getIsInitializeError,
} from "selectors/appViewSelectors";
import { executeAction } from "actions/widgetActions"; import { executeAction } from "actions/widgetActions";
import { ExecuteActionPayload } from "constants/ActionConstants"; import { ExecuteActionPayload } from "constants/ActionConstants";
import { updateWidgetPropertyRequest } from "actions/controlActions"; import { updateWidgetPropertyRequest } from "actions/controlActions";
@ -23,6 +26,8 @@ import {
} from "actions/metaActions"; } from "actions/metaActions";
import { editorInitializer } from "utils/EditorUtils"; import { editorInitializer } from "utils/EditorUtils";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import log from "loglevel";
import ServerTimeout from "../common/ServerTimeout";
const SentryRoute = Sentry.withSentryRouting(Route); const SentryRoute = Sentry.withSentryRouting(Route);
const AppViewerBody = styled.section` const AppViewerBody = styled.section`
@ -36,6 +41,7 @@ const AppViewerBody = styled.section`
export type AppViewerProps = { export type AppViewerProps = {
initializeAppViewer: (applicationId: string, pageId?: string) => void; initializeAppViewer: (applicationId: string, pageId?: string) => void;
isInitialized: boolean; isInitialized: boolean;
isInitializeError: boolean;
executeAction: (actionPayload: ExecuteActionPayload) => void; executeAction: (actionPayload: ExecuteActionPayload) => void;
updateWidgetProperty: ( updateWidgetProperty: (
widgetId: string, widgetId: string,
@ -62,7 +68,7 @@ class AppViewer extends Component<
this.setState({ registered: true }); this.setState({ registered: true });
}); });
const { applicationId, pageId } = this.props.match.params; const { applicationId, pageId } = this.props.match.params;
console.log({ applicationId, pageId }); log.debug({ applicationId, pageId });
if (applicationId) { if (applicationId) {
this.props.initializeAppViewer(applicationId, pageId); this.props.initializeAppViewer(applicationId, pageId);
} }
@ -73,7 +79,10 @@ class AppViewer extends Component<
}; };
public render() { public render() {
const { isInitialized } = this.props; const { isInitialized, isInitializeError } = this.props;
if (isInitializeError) {
return <ServerTimeout />;
}
return ( return (
<EditorContext.Provider <EditorContext.Provider
value={{ value={{
@ -100,6 +109,7 @@ class AppViewer extends Component<
const mapStateToProps = (state: AppState) => ({ const mapStateToProps = (state: AppState) => ({
isInitialized: getIsInitialized(state), isInitialized: getIsInitialized(state),
isInitializeError: getIsInitializeError(state),
}); });
const mapDispatchToProps = (dispatch: any) => ({ const mapDispatchToProps = (dispatch: any) => ({

View File

@ -1,7 +1,7 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { withRouter, RouteComponentProps } from "react-router-dom"; import { RouteComponentProps, withRouter } from "react-router-dom";
import { import {
BuilderRouteParams, BuilderRouteParams,
getApplicationViewerPageURL, getApplicationViewerPageURL,
@ -13,15 +13,16 @@ import TouchBackend from "react-dnd-touch-backend";
import { import {
getCurrentApplicationId, getCurrentApplicationId,
getCurrentPageId, getCurrentPageId,
getPublishingError,
getIsEditorLoading,
getIsEditorInitialized, getIsEditorInitialized,
getIsEditorInitializeError,
getIsEditorLoading,
getIsPublishingApplication, getIsPublishingApplication,
getPublishingError,
} from "selectors/editorSelectors"; } from "selectors/editorSelectors";
import { import {
Dialog,
Classes,
AnchorButton, AnchorButton,
Classes,
Dialog,
Hotkey, Hotkey,
Hotkeys, Hotkeys,
Spinner, Spinner,
@ -40,11 +41,12 @@ import ConfirmRunModal from "pages/Editor/ConfirmRunModal";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import { import {
copyWidget, copyWidget,
pasteWidget,
deleteSelectedWidget,
cutWidget, cutWidget,
deleteSelectedWidget,
pasteWidget,
} from "actions/widgetActions"; } from "actions/widgetActions";
import { isMac } from "utils/helpers"; import { isMac } from "utils/helpers";
import ServerTimeout from "../common/ServerTimeout";
type EditorProps = { type EditorProps = {
currentApplicationId?: string; currentApplicationId?: string;
@ -53,6 +55,7 @@ type EditorProps = {
isPublishing: boolean; isPublishing: boolean;
isEditorLoading: boolean; isEditorLoading: boolean;
isEditorInitialized: boolean; isEditorInitialized: boolean;
isEditorInitializeError: boolean;
errorPublishing: boolean; errorPublishing: boolean;
copySelectedWidget: () => void; copySelectedWidget: () => void;
pasteCopiedWidget: () => void; pasteCopiedWidget: () => void;
@ -189,6 +192,8 @@ class Editor extends Component<Props> {
nextProps.isPublishing !== this.props.isPublishing || nextProps.isPublishing !== this.props.isPublishing ||
nextProps.isEditorLoading !== this.props.isEditorLoading || nextProps.isEditorLoading !== this.props.isEditorLoading ||
nextProps.errorPublishing !== this.props.errorPublishing || nextProps.errorPublishing !== this.props.errorPublishing ||
nextProps.isEditorInitializeError !==
this.props.isEditorInitializeError ||
nextState.isDialogOpen !== this.state.isDialogOpen || nextState.isDialogOpen !== this.state.isDialogOpen ||
nextState.registered !== this.state.registered nextState.registered !== this.state.registered
); );
@ -200,6 +205,9 @@ class Editor extends Component<Props> {
}); });
}; };
public render() { public render() {
if (this.props.isEditorInitializeError) {
return <ServerTimeout />;
}
if (!this.props.isEditorInitialized || !this.state.registered) { if (!this.props.isEditorInitialized || !this.state.registered) {
return ( return (
<CenteredWrapper style={{ height: "calc(100vh - 48px)" }}> <CenteredWrapper style={{ height: "calc(100vh - 48px)" }}>
@ -260,6 +268,7 @@ const mapStateToProps = (state: AppState) => ({
isPublishing: getIsPublishingApplication(state), isPublishing: getIsPublishingApplication(state),
isEditorLoading: getIsEditorLoading(state), isEditorLoading: getIsEditorLoading(state),
isEditorInitialized: getIsEditorInitialized(state), isEditorInitialized: getIsEditorInitialized(state),
isEditorInitializeError: getIsEditorInitializeError(state),
user: getCurrentUser(state), user: getCurrentUser(state),
}); });

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { RouterProps } from "react-router"; import { RouteComponentProps, withRouter } from "react-router";
import styled from "styled-components"; import styled from "styled-components";
import Button from "components/editorComponents/Button"; import Button from "components/editorComponents/Button";
import PageUnavailableImage from "assets/images/404-image.png"; import PageUnavailableImage from "assets/images/404-image.png";
@ -20,7 +20,7 @@ const Wrapper = styled.div`
} }
`; `;
class PageNotFound extends React.PureComponent<RouterProps> { class PageNotFound extends React.PureComponent<RouteComponentProps> {
public render() { public render() {
return ( return (
<> <>
@ -54,4 +54,4 @@ class PageNotFound extends React.PureComponent<RouterProps> {
} }
} }
export default PageNotFound; export default withRouter(PageNotFound);

View File

@ -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 (
<Wrapper>
<img
src={AppTimeoutImage}
alt="Page Unavailable"
className="page-unavailable-img"
/>
<div>
<p className="bold-text">
Appsmith server is taking too long to respond
</p>
<p>Please retry after some time</p>
<RetryButton onClick={() => window.location.reload()}>
{"Retry"}
</RetryButton>
</div>
</Wrapper>
);
};
export default ServerTimeout;

View File

@ -5,12 +5,14 @@ import {
ReduxAction, ReduxAction,
ReduxActionTypes, ReduxActionTypes,
PageListPayload, PageListPayload,
ReduxActionErrorTypes,
} from "constants/ReduxActionConstants"; } from "constants/ReduxActionConstants";
import { FetchPublishedPageSuccessPayload } from "actions/pageActions"; import { FetchPublishedPageSuccessPayload } from "actions/pageActions";
const initialState: AppViewReduxState = { const initialState: AppViewReduxState = {
isFetchingPage: false, isFetchingPage: false,
initialized: false, initialized: false,
initializeError: false,
pages: [], pages: [],
pageWidgetId: "0", pageWidgetId: "0",
}; };
@ -24,6 +26,11 @@ const appViewReducer = createReducer(initialState, {
) => { ) => {
return { ...state, initialized: true }; return { ...state, initialized: true };
}, },
[ReduxActionErrorTypes.INITIALIZE_PAGE_VIEWER_ERROR]: (
state: AppViewReduxState,
) => {
return { ...state, initializeError: true };
},
[ReduxActionTypes.FETCH_PUBLISHED_PAGE_INIT]: (state: AppViewReduxState) => { [ReduxActionTypes.FETCH_PUBLISHED_PAGE_INIT]: (state: AppViewReduxState) => {
return { ...state, dsl: undefined, isFetchingPage: true }; return { ...state, dsl: undefined, isFetchingPage: true };
}, },
@ -45,6 +52,7 @@ const appViewReducer = createReducer(initialState, {
export interface AppViewReduxState { export interface AppViewReduxState {
initialized: boolean; initialized: boolean;
initializeError: boolean;
dsl?: ContainerWidgetProps<WidgetProps>; dsl?: ContainerWidgetProps<WidgetProps>;
isFetchingPage: boolean; isFetchingPage: boolean;
currentLayoutId?: string; currentLayoutId?: string;

View File

@ -5,12 +5,11 @@ import {
ReduxActionTypes, ReduxActionTypes,
ReduxActionErrorTypes, ReduxActionErrorTypes,
} from "constants/ReduxActionConstants"; } from "constants/ReduxActionConstants";
import { WidgetProps } from "widgets/BaseWidget";
import { ContainerWidgetProps } from "widgets/ContainerWidget";
import moment from "moment"; import moment from "moment";
const initialState: EditorReduxState = { const initialState: EditorReduxState = {
initialized: false, initialized: false,
initializationError: false,
loadingStates: { loadingStates: {
publishing: false, publishing: false,
publishingError: false, publishingError: false,
@ -68,11 +67,8 @@ const editorReducer = createReducer(initialState, {
) => { ) => {
return { return {
...state, ...state,
loadingStates: { initializationError: true,
...state.loadingStates, initialized: false,
loading: false,
loadingError: true,
},
}; };
}, },
[ReduxActionTypes.PUBLISH_APPLICATION_INIT]: (state: EditorReduxState) => { [ReduxActionTypes.PUBLISH_APPLICATION_INIT]: (state: EditorReduxState) => {
@ -178,7 +174,7 @@ const editorReducer = createReducer(initialState, {
export interface EditorReduxState { export interface EditorReduxState {
initialized: boolean; initialized: boolean;
dsl?: ContainerWidgetProps<WidgetProps>; initializationError: boolean;
pageWidgetId?: string; pageWidgetId?: string;
currentLayoutId?: string; currentLayoutId?: string;
currentPageName?: string; currentPageName?: string;

View File

@ -11,6 +11,7 @@ import { put, takeLatest, call } from "redux-saga/effects";
import { ERROR_401, ERROR_500, ERROR_0 } from "constants/messages"; import { ERROR_401, ERROR_500, ERROR_0 } from "constants/messages";
import { ToastType } from "react-toastify"; import { ToastType } from "react-toastify";
import log from "loglevel"; import log from "loglevel";
import { axiosConnectionAbortedCode } from "../api/Api";
export function* callAPI(apiCall: any, requestPayload: any) { export function* callAPI(apiCall: any, requestPayload: any) {
try { try {
@ -60,7 +61,7 @@ export function getResponseErrorMessage(response: ApiResponse) {
: undefined; : undefined;
} }
type ErrorPayloadType = { code?: number; message?: string }; type ErrorPayloadType = { code?: number | string; message?: string };
let ActionErrorDisplayMap: { let ActionErrorDisplayMap: {
[key: string]: (error: ErrorPayloadType) => string; [key: string]: (error: ErrorPayloadType) => string;
} = {}; } = {};
@ -92,6 +93,7 @@ export function* errorSaga(
type, type,
payload: { error, show = true }, payload: { error, show = true },
} = errorAction; } = errorAction;
const message = const message =
error && error.message ? error.message : ActionErrorDisplayMap[type](error); error && error.message ? error.message : ActionErrorDisplayMap[type](error);

View File

@ -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 { import {
InitializeEditorPayload, InitializeEditorPayload,
Page,
ReduxAction, ReduxAction,
ReduxActionErrorTypes, ReduxActionErrorTypes,
ReduxActionTypes, ReduxActionTypes,
@ -21,31 +28,17 @@ import { fetchActions, fetchActionsForView } from "actions/actionActions";
import { fetchApplication } from "actions/applicationActions"; import { fetchApplication } from "actions/applicationActions";
import AnalyticsUtil from "utils/AnalyticsUtil"; import AnalyticsUtil from "utils/AnalyticsUtil";
import { getCurrentApplication } from "selectors/applicationSelectors"; 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 { APP_MODE } from "reducers/entityReducers/appReducer";
import { getAppStoreName } from "constants/AppConstants"; import { getAppStore } from "constants/AppConstants";
import { getDefaultPageId } from "./selectors"; import { getDefaultPageId } from "./selectors";
import { populatePageDSLsSaga } from "./PageSagas";
const getAppStore = (appId: string) => { import { initEditorError, initViewerError } from "../actions/initActions";
const appStoreName = getAppStoreName(appId);
const storeString = localStorage.getItem(appStoreName) || "{}";
let store;
try {
store = JSON.parse(storeString);
} catch (e) {
store = {};
}
return store;
};
function* initializeEditorSaga( function* initializeEditorSaga(
initializeEditorAction: ReduxAction<InitializeEditorPayload>, initializeEditorAction: ReduxAction<InitializeEditorPayload>,
) { ) {
const { applicationId, pageId } = initializeEditorAction.payload; const { applicationId, pageId } = initializeEditorAction.payload;
// Step 1: Set App Mode. Start getting all the data needed try {
yield put(setAppMode(APP_MODE.EDIT)); yield put(setAppMode(APP_MODE.EDIT));
yield put({ type: ReduxActionTypes.START_EVALUATION }); yield put({ type: ReduxActionTypes.START_EVALUATION });
yield all([ yield all([
@ -55,24 +48,45 @@ function* initializeEditorSaga(
put(fetchPage(pageId)), put(fetchPage(pageId)),
put(fetchApplication(applicationId, APP_MODE.EDIT)), put(fetchApplication(applicationId, APP_MODE.EDIT)),
]); ]);
// Step 2: Wait for all data to be in the state
yield all([ const resultOfPrimaryCalls = yield race({
success: all([
take(ReduxActionTypes.FETCH_PAGE_LIST_SUCCESS), take(ReduxActionTypes.FETCH_PAGE_LIST_SUCCESS),
take(ReduxActionTypes.FETCH_PAGE_SUCCESS), take(ReduxActionTypes.FETCH_PAGE_SUCCESS),
take(ReduxActionTypes.SWITCH_CURRENT_PAGE_ID), take(ReduxActionTypes.FETCH_APPLICATION_SUCCESS),
take(ReduxActionTypes.FETCH_ACTIONS_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,
]),
});
if (resultOfPrimaryCalls.failure) {
yield put(initEditorError());
return;
}
// Step 3: Call all the APIs which needs Organization Id from PageList API response.
yield all([put(fetchPlugins()), put(fetchDatasources())]); yield all([put(fetchPlugins()), put(fetchDatasources())]);
// Step 4: Wait for all data to be in the state const resultOfSecondaryCalls = yield race({
yield all([ success: all([
take(ReduxActionTypes.FETCH_PLUGINS_SUCCESS), take(ReduxActionTypes.FETCH_PLUGINS_SUCCESS),
take(ReduxActionTypes.FETCH_DATASOURCES_SUCCESS), take(ReduxActionTypes.FETCH_DATASOURCES_SUCCESS),
]); ]),
failure: take([
ReduxActionErrorTypes.FETCH_PLUGINS_ERROR,
ReduxActionErrorTypes.FETCH_DATASOURCES_ERROR,
]),
});
if (resultOfSecondaryCalls.failure) {
yield put(initEditorError());
return;
}
// Step 5: Set app store
yield put(updateAppStore(getAppStore(applicationId))); yield put(updateAppStore(getAppStore(applicationId)));
const currentApplication = yield select(getCurrentApplication); const currentApplication = yield select(getCurrentApplication);
@ -85,64 +99,17 @@ function* initializeEditorSaga(
appName: appName, appName: appName,
}); });
// Step 6: Notify UI that the editor is ready to go
yield put({ yield put({
type: ReduxActionTypes.INITIALIZE_EDITOR_SUCCESS, type: ReduxActionTypes.INITIALIZE_EDITOR_SUCCESS,
}); });
} catch (e) {
yield put(initEditorError());
return;
}
yield call(populatePageDSLsSaga); yield call(populatePageDSLsSaga);
} }
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 function* initializeAppViewerSaga( export function* initializeAppViewerSaga(
action: ReduxAction<{ applicationId: string; pageId: string }>, action: ReduxAction<{ applicationId: string; pageId: string }>,
) { ) {
@ -156,11 +123,23 @@ export function* initializeAppViewerSaga(
put(fetchApplication(applicationId, APP_MODE.PUBLISHED)), put(fetchApplication(applicationId, APP_MODE.PUBLISHED)),
]); ]);
yield all([ const resultOfPrimaryCalls = yield race({
success: all([
take(ReduxActionTypes.FETCH_ACTIONS_VIEW_MODE_SUCCESS), take(ReduxActionTypes.FETCH_ACTIONS_VIEW_MODE_SUCCESS),
take(ReduxActionTypes.FETCH_PAGE_LIST_SUCCESS), take(ReduxActionTypes.FETCH_PAGE_LIST_SUCCESS),
take(ReduxActionTypes.FETCH_APPLICATION_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))); yield put(updateAppStore(getAppStore(applicationId)));
const defaultPageId = yield select(getDefaultPageId); const defaultPageId = yield select(getDefaultPageId);
@ -168,7 +147,15 @@ export function* initializeAppViewerSaga(
if (toLoadPageId) { if (toLoadPageId) {
yield put(fetchPublishedPage(toLoadPageId, true)); 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(setAppMode(APP_MODE.PUBLISHED));
yield put(updateAppStore(getAppStore(applicationId))); yield put(updateAppStore(getAppStore(applicationId)));

View File

@ -1,6 +1,7 @@
import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer"; import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer";
import { AppState } from "reducers"; import { AppState } from "reducers";
import { import {
Page,
PageListPayload, PageListPayload,
ReduxAction, ReduxAction,
ReduxActionErrorTypes, ReduxActionErrorTypes,
@ -43,7 +44,6 @@ import {
select, select,
takeLatest, takeLatest,
takeLeading, takeLeading,
take,
} from "redux-saga/effects"; } from "redux-saga/effects";
import history from "utils/history"; import history from "utils/history";
import { BUILDER_PAGE_URL } from "constants/routes"; import { BUILDER_PAGE_URL } from "constants/routes";
@ -117,6 +117,16 @@ export function* fetchPageListSaga(
PerformanceTracker.stopAsyncTracking( PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.FETCH_PAGE_LIST_API, 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) { } catch (error) {
PerformanceTracker.stopAsyncTracking( PerformanceTracker.stopAsyncTracking(
@ -608,6 +618,57 @@ export function* setDataUrl() {
yield put(setUrlData(urlData)); 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() { export default function* pageSagas() {
yield all([ yield all([
takeLatest(ReduxActionTypes.FETCH_PAGE_INIT, fetchPageSaga), takeLatest(ReduxActionTypes.FETCH_PAGE_INIT, fetchPageSaga),

View File

@ -32,6 +32,11 @@ export const getIsInitialized = createSelector(
(view: AppViewReduxState) => view.initialized, (view: AppViewReduxState) => view.initialized,
); );
export const getIsInitializeError = createSelector(
getAppViewState,
(view: AppViewReduxState) => view.initializeError,
);
export const getCurrentDSLPageId = createSelector( export const getCurrentDSLPageId = createSelector(
getPageListState, getPageListState,
(pageList: PageListReduxState) => pageList.currentPageId, (pageList: PageListReduxState) => pageList.currentPageId,

View File

@ -20,14 +20,12 @@ import { getDataTree } from "selectors/dataTreeSelectors";
import _ from "lodash"; import _ from "lodash";
import { ContainerWidgetProps } from "widgets/ContainerWidget"; import { ContainerWidgetProps } from "widgets/ContainerWidget";
import { DataTreeWidget, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; 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, { import PerformanceTracker, {
PerformanceTransactionName, PerformanceTransactionName,
} from "utils/PerformanceTracker"; } from "utils/PerformanceTracker";
import { getCanvasWidgets } from "./entitiesSelector"; import { getCanvasWidgets } from "./entitiesSelector";
import { MetaState } from "../reducers/entityReducers/metaReducer";
import { WidgetTypes } from "../constants/WidgetConstants"; import { WidgetTypes } from "../constants/WidgetConstants";
const getWidgetConfigs = (state: AppState) => state.entities.widgetConfig; const getWidgetConfigs = (state: AppState) => state.entities.widgetConfig;
@ -45,6 +43,9 @@ const getWidgets = (state: AppState): CanvasWidgetsReduxState =>
export const getIsEditorInitialized = (state: AppState) => export const getIsEditorInitialized = (state: AppState) =>
state.ui.editor.initialized; state.ui.editor.initialized;
export const getIsEditorInitializeError = (state: AppState): boolean =>
state.ui.editor.initializationError;
export const getIsEditorLoading = (state: AppState) => export const getIsEditorLoading = (state: AppState) =>
state.ui.editor.loadingStates.loading; state.ui.editor.loadingStates.loading;