PromucFlow_constructor/app/client/src/sagas/ActionExecutionSagas.ts
2020-10-07 06:46:23 +05:30

699 lines
19 KiB
TypeScript

import {
Page,
ReduxAction,
ReduxActionErrorTypes,
ReduxActionTypes,
} from "constants/ReduxActionConstants";
import {
EventType,
ExecuteActionPayload,
ExecuteActionPayloadEvent,
PageAction,
} from "constants/ActionConstants";
import * as log from "loglevel";
import {
all,
call,
put,
race,
select,
take,
takeEvery,
takeLatest,
} from "redux-saga/effects";
import {
evaluateDataTreeWithFunctions,
evaluateDataTreeWithoutFunctions,
} from "selectors/dataTreeSelectors";
import {
getDynamicBindings,
getDynamicValue,
isDynamicValue,
} from "utils/DynamicBindingUtils";
import {
ActionDescription,
RunActionPayload,
} from "entities/DataTree/dataTreeFactory";
import { AppToaster } from "components/editorComponents/ToastComponent";
import { executeAction, executeActionError } from "actions/widgetActions";
import {
getCurrentApplicationId,
getCurrentPageId,
getPageList,
} from "selectors/editorSelectors";
import _ from "lodash";
import AnalyticsUtil from "utils/AnalyticsUtil";
import history from "utils/history";
import {
BUILDER_PAGE_URL,
getApplicationViewerPageURL,
} from "constants/routes";
import {
executeApiActionRequest,
executeApiActionSuccess,
updateAction,
showRunActionConfirmModal,
} from "actions/actionActions";
import { Action, RestAction } from "entities/Action";
import ActionAPI, {
ActionApiResponse,
ActionResponse,
ExecuteActionRequest,
PaginationField,
Property,
} from "api/ActionAPI";
import {
getAction,
getCurrentPageNameByActionId,
isActionDirty,
isActionSaving,
} from "selectors/entitiesSelector";
import { AppState } from "reducers";
import { mapToPropList } from "utils/AppsmithUtils";
import { validateResponse } from "sagas/ErrorSagas";
import { ToastType } from "react-toastify";
import { PLUGIN_TYPE_API } from "constants/ApiEditorConstants";
import { DEFAULT_EXECUTE_ACTION_TIMEOUT_MS } from "constants/ApiConstants";
import { updateAppStore } from "actions/pageActions";
import { getAppStoreName } from "constants/AppConstants";
import downloadjs from "downloadjs";
import { getType, Types } from "utils/TypeHelpers";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
function* navigateActionSaga(
action: { pageNameOrUrl: string; params: Record<string, string> },
event: ExecuteActionPayloadEvent,
) {
const pageList = yield select(getPageList);
const applicationId = yield select(getCurrentApplicationId);
const page = _.find(
pageList,
(page: Page) => page.pageName === action.pageNameOrUrl,
);
if (page) {
AnalyticsUtil.logEvent("NAVIGATE", {
pageName: action.pageNameOrUrl,
pageParams: action.params,
});
// TODO need to make this check via RENDER_MODE;
const path =
history.location.pathname.indexOf("/edit") !== -1
? BUILDER_PAGE_URL(applicationId, page.pageId, action.params)
: getApplicationViewerPageURL(
applicationId,
page.pageId,
action.params,
);
history.push(path);
if (event.callback) event.callback({ success: true });
} else {
AnalyticsUtil.logEvent("NAVIGATE", {
navUrl: action.pageNameOrUrl,
});
// Add a default protocol if it doesn't exist.
let url = action.pageNameOrUrl;
if (url.indexOf("://") === -1) {
url = "https://" + url;
}
window.location.assign(url);
}
}
function* storeValueLocally(
action: { key: string; value: string },
event: ExecuteActionPayloadEvent,
) {
try {
const appId = yield select(getCurrentApplicationId);
const appStoreName = getAppStoreName(appId);
const existingStore = yield localStorage.getItem(appStoreName) || "{}";
const storeObj = JSON.parse(existingStore);
storeObj[action.key] = action.value;
const storeString = JSON.stringify(storeObj);
yield localStorage.setItem(appStoreName, storeString);
yield put(updateAppStore(storeObj));
if (event.callback) event.callback({ success: true });
} catch (err) {
if (event.callback) event.callback({ success: false });
}
}
function* downloadSaga(
action: { data: any; name: string; type: string },
event: ExecuteActionPayloadEvent,
) {
try {
const { data, name, type } = action;
if (!name) {
AppToaster.show({
message: "Download failed. File name was not provided",
type: "error",
});
if (event.callback) event.callback({ success: false });
return;
}
const dataType = getType(data);
if (dataType === Types.ARRAY || dataType === Types.OBJECT) {
const jsonString = JSON.stringify(data, null, 2);
downloadjs(jsonString, name, type);
} else {
downloadjs(data, name, type);
}
if (event.callback) event.callback({ success: true });
} catch (err) {
AppToaster.show({
message: `Download failed. ${err}`,
type: "error",
});
if (event.callback) event.callback({ success: false });
}
}
export const getActionTimeout = (
state: AppState,
actionId: string,
): number | undefined => {
const action = _.find(state.entities.actions, a => a.config.id === actionId);
if (action) {
const timeout = _.get(
action,
"config.actionConfiguration.timeoutInMillisecond",
DEFAULT_EXECUTE_ACTION_TIMEOUT_MS,
);
if (timeout) {
// Extra timeout padding to account for network calls
return timeout + 5000;
}
return undefined;
}
return undefined;
};
const createActionExecutionResponse = (
response: ActionApiResponse,
): ActionResponse => ({
...response.data,
...response.clientMeta,
});
const isErrorResponse = (response: ActionApiResponse) => {
return !response.data.isExecutionSuccess;
};
export function* evaluateDynamicBoundValueSaga(path: string): any {
log.debug("Evaluating data tree to get action binding value");
const tree = yield select(evaluateDataTreeWithoutFunctions);
const dynamicResult = getDynamicValue(`{{${path}}}`, tree);
return dynamicResult.result;
}
const EXECUTION_PARAM_PATH = "this.params";
const getExecutionParamPath = (key: string) => `${EXECUTION_PARAM_PATH}.${key}`;
export function* getActionParams(
bindings: string[] | undefined,
executionParams?: Record<string, any>,
) {
if (_.isNil(bindings)) return [];
let dataTreeBindings = bindings;
if (executionParams && Object.keys(executionParams).length) {
// List of params in the path format
const executionParamsPathList = Object.keys(executionParams).map(
getExecutionParamPath,
);
const paramSearchRegex = new RegExp(executionParamsPathList.join("|"), "g");
// Bindings with references to execution params
const executionBindings = bindings.filter(binding =>
paramSearchRegex.test(binding),
);
// Replace references with values
const replacedBindings = executionBindings.map(binding => {
let replaced = binding;
const matches = binding.match(paramSearchRegex);
if (matches && matches.length) {
matches.forEach(match => {
// we add one for substring index to account for '.'
const paramKey = match.substring(EXECUTION_PARAM_PATH.length + 1);
let paramValue = executionParams[paramKey];
if (paramValue) {
if (typeof paramValue === "object") {
paramValue = JSON.stringify(paramValue);
}
replaced = replaced.replace(match, paramValue);
}
});
}
return replaced;
});
// Replace binding with replaced bindings for evaluation
dataTreeBindings = dataTreeBindings.map(key => {
if (executionBindings.includes(key)) {
return replacedBindings[executionBindings.indexOf(key)];
}
return key;
});
}
// Evaluate all values
const values: any = yield all(
dataTreeBindings.map((binding: string) => {
return call(evaluateDynamicBoundValueSaga, binding);
}),
);
// convert to object and transform non string values
const actionParams: Record<string, string> = {};
bindings.forEach((key, i) => {
let value = values[i];
if (typeof value === "object") value = JSON.stringify(value);
actionParams[key] = value;
});
return mapToPropList(actionParams);
}
export function extractBindingsFromAction(action: Action) {
const bindings: string[] = [];
action.dynamicBindingPathList.forEach(a => {
const value = _.get(action, a.key);
if (isDynamicValue(value)) {
const { jsSnippets } = getDynamicBindings(value);
bindings.push(...jsSnippets.filter(jsSnippet => !!jsSnippet));
}
});
return bindings;
}
export function* executeActionSaga(
apiAction: RunActionPayload,
event: ExecuteActionPayloadEvent,
) {
const { actionId, onSuccess, onError, params } = apiAction;
PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.EXECUTE_ACTION,
{
actionId: actionId,
},
actionId,
);
try {
const api: RestAction = yield select(getAction, actionId);
AnalyticsUtil.logEvent("EXECUTE_ACTION", {
type: api.pluginType,
name: api.name,
pageId: api.pageId,
});
if (api.confirmBeforeExecute) {
const confirmed = yield call(confirmRunActionSaga);
if (!confirmed) {
if (event.callback) {
event.callback({ success: false });
}
return;
}
}
yield put(executeApiActionRequest({ id: apiAction.actionId }));
const actionParams: Property[] = yield call(
getActionParams,
api.jsonPathKeys,
params,
);
const pagination =
event.type === EventType.ON_NEXT_PAGE
? "NEXT"
: event.type === EventType.ON_PREV_PAGE
? "PREV"
: undefined;
const executeActionRequest: ExecuteActionRequest = {
action: { id: actionId },
params: actionParams,
paginationField: pagination,
};
const timeout = yield select(getActionTimeout, actionId);
const response: ActionApiResponse = yield ActionAPI.executeAction(
executeActionRequest,
timeout,
);
const payload = createActionExecutionResponse(response);
yield put(
executeApiActionSuccess({
id: actionId,
response: payload,
}),
);
if (isErrorResponse(response)) {
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.EXECUTE_ACTION,
{ failed: true },
actionId,
);
if (onError) {
yield put(
executeAction({
dynamicString: onError,
event: {
...event,
type: EventType.ON_ERROR,
},
responseData: payload,
}),
);
} else {
if (event.callback) {
event.callback({ success: false });
}
}
AppToaster.show({
message:
api.name + " failed to execute. Please check it's configuration",
type: "error",
});
} else {
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.EXECUTE_ACTION,
undefined,
actionId,
);
if (onSuccess) {
yield put(
executeAction({
dynamicString: onSuccess,
event: {
...event,
type: EventType.ON_SUCCESS,
},
responseData: payload,
}),
);
} else {
if (event.callback) {
event.callback({ success: true });
}
}
}
return response;
} catch (error) {
yield put(
executeActionError({
actionId: actionId,
error,
}),
);
AppToaster.show({
message: "Action execution failed",
type: "error",
});
if (onError) {
yield put(
executeAction({
dynamicString: `{{${onError}}}`,
event: {
...event,
type: EventType.ON_ERROR,
},
responseData: {},
}),
);
} else {
if (event.callback) {
event.callback({ success: false });
}
}
}
}
function* executeActionTriggers(
trigger: ActionDescription<any>,
event: ExecuteActionPayloadEvent,
) {
try {
switch (trigger.type) {
case "RUN_ACTION":
yield call(executeActionSaga, trigger.payload, event);
break;
case "NAVIGATE_TO":
yield call(navigateActionSaga, trigger.payload, event);
break;
case "SHOW_ALERT":
AppToaster.show({
message: trigger.payload.message,
type: trigger.payload.style,
});
if (event.callback) event.callback({ success: true });
break;
case "SHOW_MODAL_BY_NAME":
yield put(trigger);
if (event.callback) event.callback({ success: true });
break;
case "CLOSE_MODAL":
yield put(trigger);
if (event.callback) event.callback({ success: true });
break;
case "STORE_VALUE":
yield call(storeValueLocally, trigger.payload, event);
break;
case "DOWNLOAD":
yield call(downloadSaga, trigger.payload, event);
break;
default:
yield put(
executeActionError({
error: "Trigger type unknown",
actionId: "",
}),
);
}
} catch (e) {
yield put(
executeActionError({
error: "Failed to execute action",
actionId: "",
}),
);
if (event.callback) event.callback({ success: false });
}
}
function* executeAppAction(action: ReduxAction<ExecuteActionPayload>) {
const { dynamicString, event, responseData } = action.payload;
log.debug("Evaluating data tree to get action trigger");
log.debug({ dynamicString });
const tree = yield select(evaluateDataTreeWithFunctions);
log.debug({ tree });
const { triggers } = getDynamicValue(dynamicString, tree, responseData, true);
log.debug({ triggers });
if (triggers && triggers.length) {
yield all(
triggers.map(trigger => call(executeActionTriggers, trigger, event)),
);
} else {
if (event.callback) event.callback({ success: true });
}
}
function* runActionInitSaga(
reduxAction: ReduxAction<{
id: string;
paginationField: PaginationField;
}>,
) {
const action = yield select(getAction, reduxAction.payload.id);
if (action.confirmBeforeExecute) {
const confirmed = yield call(confirmRunActionSaga);
if (!confirmed) return;
}
yield put({
type: ReduxActionTypes.RUN_ACTION_REQUEST,
payload: reduxAction.payload,
});
}
function* runActionSaga(
reduxAction: ReduxAction<{
id: string;
paginationField: PaginationField;
}>,
) {
try {
const actionId = reduxAction.payload.id;
const isSaving = yield select(isActionSaving(actionId));
const isDirty = yield select(isActionDirty(actionId));
if (isSaving || isDirty) {
if (isDirty && !isSaving) {
yield put(updateAction({ id: actionId }));
}
yield take(ReduxActionTypes.UPDATE_ACTION_SUCCESS);
}
const actionObject = yield select(getAction, actionId);
const action: ExecuteActionRequest["action"] = { id: actionId };
const jsonPathKeys = actionObject.jsonPathKeys;
const { paginationField } = reduxAction.payload;
const params = yield call(getActionParams, jsonPathKeys);
const timeout = yield select(getActionTimeout, actionId);
const response: ActionApiResponse = yield ActionAPI.executeAction(
{
action,
params,
paginationField,
},
timeout,
);
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
const payload = createActionExecutionResponse(response);
const pageName = yield select(getCurrentPageNameByActionId, actionId);
const eventName =
actionObject.pluginType === PLUGIN_TYPE_API ? "RUN_API" : "RUN_QUERY";
AnalyticsUtil.logEvent(eventName, {
actionId,
actionName: actionObject.name,
pageName: pageName,
responseTime: response.clientMeta.duration,
apiType: "INTERNAL",
});
yield put({
type: ReduxActionTypes.RUN_ACTION_SUCCESS,
payload: { [actionId]: payload },
});
if (payload.isExecutionSuccess) {
AppToaster.show({
message: "Action ran successfully",
type: ToastType.SUCCESS,
});
} else {
AppToaster.show({
message: "Action returned an error response",
type: ToastType.WARNING,
});
}
} else {
let error = "An unexpected error occurred";
if (response.data.body) {
error = response.data.body.toString();
}
yield put({
type: ReduxActionErrorTypes.RUN_ACTION_ERROR,
payload: { error, id: reduxAction.payload.id },
});
}
} catch (error) {
console.error(error);
yield put({
type: ReduxActionErrorTypes.RUN_ACTION_ERROR,
payload: { error, id: reduxAction.payload.id },
});
}
}
function* confirmRunActionSaga() {
yield put(showRunActionConfirmModal(true));
const { accept } = yield race({
cancel: take(ReduxActionTypes.CANCEL_RUN_ACTION_CONFIRM_MODAL),
accept: take(ReduxActionTypes.ACCEPT_RUN_ACTION_CONFIRM_MODAL),
});
return !!accept;
}
function* executePageLoadAction(pageAction: PageAction) {
PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.EXECUTE_ACTION,
{
actionId: pageAction.id,
},
pageAction.id,
PerformanceTransactionName.EXECUTE_PAGE_LOAD_ACTIONS,
);
const pageId = yield select(getCurrentPageId);
yield put(executeApiActionRequest({ id: pageAction.id }));
const params: Property[] = yield call(
getActionParams,
pageAction.jsonPathKeys,
);
const executeActionRequest: ExecuteActionRequest = {
action: { id: pageAction.id },
params,
};
AnalyticsUtil.logEvent("EXECUTE_ACTION", {
type: pageAction.pluginType,
name: pageAction.name,
pageId: pageId,
onPageLoad: true,
});
const response: ActionApiResponse = yield ActionAPI.executeAction(
executeActionRequest,
pageAction.timeoutInMillisecond,
);
if (isErrorResponse(response)) {
yield put(
executeActionError({
actionId: pageAction.id,
error: response.responseMeta.error,
isPageLoad: true,
}),
);
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.EXECUTE_ACTION,
{
failed: true,
},
pageAction.id,
);
} else {
const payload = createActionExecutionResponse(response);
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.EXECUTE_ACTION,
undefined,
pageAction.id,
);
yield put(
executeApiActionSuccess({
id: pageAction.id,
response: payload,
isPageLoad: true,
}),
);
}
}
function* executePageLoadActionsSaga(action: ReduxAction<PageAction[][]>) {
const pageActions = action.payload;
const actionCount = _.flatten(pageActions).length;
PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.EXECUTE_PAGE_LOAD_ACTIONS,
{ numActions: actionCount },
);
for (const actionSet of pageActions) {
// Load all sets in parallel
yield* yield all(
actionSet.map(apiAction => call(executePageLoadAction, apiAction)),
);
}
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.EXECUTE_PAGE_LOAD_ACTIONS,
);
}
export function* watchActionExecutionSagas() {
yield all([
takeEvery(ReduxActionTypes.EXECUTE_ACTION, executeAppAction),
takeLatest(ReduxActionTypes.RUN_ACTION_REQUEST, runActionSaga),
takeLatest(ReduxActionTypes.RUN_ACTION_INIT, runActionInitSaga),
takeLatest(
ReduxActionTypes.EXECUTE_PAGE_LOAD_ACTIONS,
executePageLoadActionsSaga,
),
]);
}