Improve JS error reporting in the debugger (#4854)

This commit is contained in:
Apeksha Bhosale 2021-06-04 12:39:36 +05:30 committed by GitHub
parent a7a7390d08
commit 179d5ae8a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 302 additions and 213 deletions

View File

@ -50,12 +50,19 @@ import "codemirror/addon/fold/foldgutter";
import "codemirror/addon/fold/foldgutter.css"; import "codemirror/addon/fold/foldgutter.css";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import { removeNewLineChars, getInputValue } from "./codeEditorUtils"; import { removeNewLineChars, getInputValue } from "./codeEditorUtils";
import { getEntityNameAndPropertyPath } from "workers/evaluationUtils";
const LightningMenu = lazy(() => const LightningMenu = lazy(() =>
retryPromise(() => import("components/editorComponents/LightningMenu")), retryPromise(() => import("components/editorComponents/LightningMenu")),
); );
const AUTOCOMPLETE_CLOSE_KEY_CODES = ["Enter", "Tab", "Escape", "Comma"]; const AUTOCOMPLETE_CLOSE_KEY_CODES = [
"Enter",
"Tab",
"Escape",
"Comma",
"Backspace",
];
interface ReduxStateProps { interface ReduxStateProps {
dynamicData: DataTree; dynamicData: DataTree;
@ -341,16 +348,26 @@ class CodeEditor extends Component<Props, State> {
if (!dataTreePath) { if (!dataTreePath) {
return { isValid: true, validationMessage: "", jsErrorMessage: "" }; return { isValid: true, validationMessage: "", jsErrorMessage: "" };
} }
const isValidPath = dataTreePath.replace("evaluatedValues", "invalidProps"); const { entityName, propertyPath } = getEntityNameAndPropertyPath(
const validationMessagePath = dataTreePath.replace( dataTreePath,
);
let isValidPath, validationMessagePath, jsErrorMessagePath;
if (dataTreePath && dataTreePath.match(/evaluatedValues/g)) {
isValidPath = dataTreePath.replace("evaluatedValues", "invalidProps");
validationMessagePath = dataTreePath.replace(
"evaluatedValues", "evaluatedValues",
"validationMessages", "validationMessages",
); );
const jsErrorMessagePath = dataTreePath.replace( jsErrorMessagePath = dataTreePath.replace(
"evaluatedValues", "evaluatedValues",
"jsErrorMessages", "jsErrorMessages",
); );
} else {
isValidPath = entityName + "invalidProps" + propertyPath;
validationMessagePath =
entityName + ".validationMessages." + propertyPath;
jsErrorMessagePath = entityName + ".jsErrorMessages." + propertyPath;
}
const isValid = !_.get(dataTree, isValidPath, false); const isValid = !_.get(dataTree, isValidPath, false);
const validationMessage = _.get( const validationMessage = _.get(
dataTree, dataTree,

View File

@ -20,7 +20,7 @@ export function InputText(props: {
const dataTreePath = actionPathFromName(actionName, name); const dataTreePath = actionPathFromName(actionName, name);
return ( return (
<div style={{ width: "50vh", height: "55px" }}> <div style={{ width: "50vh", minHeight: "55px" }}>
<FormLabel> <FormLabel>
{label} {isRequired && "*"} {label} {isRequired && "*"}
</FormLabel> </FormLabel>

View File

@ -5,6 +5,7 @@ enum LOG_TYPE {
ACTION_EXECUTION_SUCCESS, ACTION_EXECUTION_SUCCESS,
ENTITY_DELETED, ENTITY_DELETED,
EVAL_ERROR, EVAL_ERROR,
ACTION_UPDATE,
} }
export default LOG_TYPE; export default LOG_TYPE;

View File

@ -45,5 +45,6 @@ export const generateDataTreeAction = (
isLoading: action.isLoading, isLoading: action.isLoading,
bindingPaths: getBindingPathsOfAction(action.config, editorConfig), bindingPaths: getBindingPathsOfAction(action.config, editorConfig),
dependencyMap, dependencyMap,
logBlackList: {},
}; };
}; };

View File

@ -56,6 +56,8 @@ export interface DataTreeAction
bindingPaths: Record<string, EvaluationSubstitutionType>; bindingPaths: Record<string, EvaluationSubstitutionType>;
ENTITY_TYPE: ENTITY_TYPE.ACTION; ENTITY_TYPE: ENTITY_TYPE.ACTION;
dependencyMap: DependencyMap; dependencyMap: DependencyMap;
jsErrorMessages?: Record<string, string>;
logBlackList: Record<string, true>;
} }
export interface DataTreeWidget extends WidgetProps { export interface DataTreeWidget extends WidgetProps {
@ -63,6 +65,7 @@ export interface DataTreeWidget extends WidgetProps {
triggerPaths: Record<string, boolean>; triggerPaths: Record<string, boolean>;
validationPaths: Record<string, VALIDATION_TYPES>; validationPaths: Record<string, VALIDATION_TYPES>;
ENTITY_TYPE: ENTITY_TYPE.WIDGET; ENTITY_TYPE: ENTITY_TYPE.WIDGET;
logBlackList: Record<string, true>;
} }
export interface DataTreeAppsmith extends Omit<AppDataState, "store"> { export interface DataTreeAppsmith extends Omit<AppDataState, "store"> {

View File

@ -221,6 +221,10 @@ describe("generateDataTreeWidget", () => {
key: "value", key: "value",
}, },
], ],
logBlackList: {
isValid: true,
value: true,
},
value: "{{Input1.text}}", value: "{{Input1.text}}",
isDirty: true, isDirty: true,
isFocused: false, isFocused: false,

View File

@ -45,6 +45,10 @@ export const generateDataTreeWidget = (
unInitializedDefaultProps[propertyName] = undefined; unInitializedDefaultProps[propertyName] = undefined;
} }
}); });
const blockedDerivedProps: Record<string, true> = {};
Object.keys(derivedProps).forEach((propertyName) => {
blockedDerivedProps[propertyName] = true;
});
const { const {
bindingPaths, bindingPaths,
triggerPaths, triggerPaths,
@ -62,6 +66,10 @@ export const generateDataTreeWidget = (
...derivedProps, ...derivedProps,
...unInitializedDefaultProps, ...unInitializedDefaultProps,
dynamicBindingPathList, dynamicBindingPathList,
logBlackList: {
...widget.logBlackList,
...blockedDerivedProps,
},
bindingPaths, bindingPaths,
triggerPaths, triggerPaths,
validationPaths, validationPaths,

View File

@ -886,6 +886,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
widgets: { [widgetId: string]: FlattenedWidgetProps }, widgets: { [widgetId: string]: FlattenedWidgetProps },
) => { ) => {
let template = {}; let template = {};
const logBlackListMap: any = {};
const container = get( const container = get(
widgets, widgets,
`${get(widget, "children.0.children.0")}`, `${get(widget, "children.0.children.0")}`,
@ -901,6 +902,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
canvas.children && canvas.children &&
get(canvas, "children", []).forEach((child: string) => { get(canvas, "children", []).forEach((child: string) => {
const childWidget = cloneDeep(get(widgets, `${child}`)); const childWidget = cloneDeep(get(widgets, `${child}`));
const logBlackList: { [key: string]: boolean } = {};
const keys = Object.keys(childWidget); const keys = Object.keys(childWidget);
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
@ -927,6 +929,12 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
} }
} }
Object.keys(childWidget).map((key) => {
logBlackList[key] = true;
});
logBlackListMap[childWidget.widgetId] = logBlackList;
template = { template = {
...template, ...template,
[childWidget.widgetName]: childWidget, [childWidget.widgetName]: childWidget,
@ -945,6 +953,18 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
propertyValue: template, propertyValue: template,
}, },
]; ];
// add logBlackList to updateProperyMap for all children
updatePropertyMap = updatePropertyMap.concat(
Object.keys(logBlackListMap).map((logBlackListMapKey) => {
return {
widgetId: logBlackListMapKey,
propertyName: "logBlackList",
propertyValue: logBlackListMap[logBlackListMapKey],
};
}),
);
return updatePropertyMap; return updatePropertyMap;
}, },
}, },
@ -961,6 +981,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
if (!parentId) return { widgets }; if (!parentId) return { widgets };
const widget = { ...widgets[widgetId] }; const widget = { ...widgets[widgetId] };
const parent = { ...widgets[parentId] }; const parent = { ...widgets[parentId] };
const logBlackList: { [key: string]: boolean } = {};
const disallowedWidgets = [WidgetTypes.FILE_PICKER_WIDGET]; const disallowedWidgets = [WidgetTypes.FILE_PICKER_WIDGET];
@ -998,7 +1019,15 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
parent.template = template; parent.template = template;
// add logBlackList for the children being added
Object.keys(widget).map((key) => {
logBlackList[key] = true;
});
widget.logBlackList = logBlackList;
widgets[parentId] = parent; widgets[parentId] = parent;
widgets[widgetId] = widget;
return { widgets }; return { widgets };
}, },

View File

@ -624,15 +624,21 @@ function* setActionPropertySaga(action: ReduxAction<SetActionPropertyPayload>) {
if (propertyName === "name") return; if (propertyName === "name") return;
const actionObj = yield select(getAction, actionId); const actionObj = yield select(getAction, actionId);
const fieldToBeUpdated = propertyName.replace(
"actionConfiguration",
"config",
);
AppsmithConsole.info({ AppsmithConsole.info({
logType: LOG_TYPE.ACTION_UPDATE,
text: "Configuration updated", text: "Configuration updated",
source: { source: {
type: ENTITY_TYPE.ACTION, type: ENTITY_TYPE.ACTION,
name: actionObj.name, name: actionObj.name,
id: actionId, id: actionId,
propertyPath: fieldToBeUpdated,
}, },
state: { state: {
[propertyName]: value, [fieldToBeUpdated]: value,
}, },
}); });

View File

@ -1,73 +1,12 @@
import { debuggerLog, errorLog, updateErrorLog } from "actions/debuggerActions"; import { debuggerLog, errorLog, updateErrorLog } from "actions/debuggerActions";
import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants";
import { WidgetTypes } from "constants/WidgetConstants";
import { LogActionPayload, Message } from "entities/AppsmithConsole"; import { LogActionPayload, Message } from "entities/AppsmithConsole";
import { import { all, call, fork, put, select, takeEvery } from "redux-saga/effects";
all, import { set } from "lodash";
put,
takeEvery,
select,
take,
fork,
call,
} from "redux-saga/effects";
import { getDataTree } from "selectors/dataTreeSelectors";
import { isEmpty, set, get } from "lodash";
import { getDebuggerErrors } from "selectors/debuggerSelectors"; import { getDebuggerErrors } from "selectors/debuggerSelectors";
import { getAction } from "selectors/entitiesSelector"; import { getAction } from "selectors/entitiesSelector";
import { Action, PluginType } from "entities/Action"; import { Action, PluginType } from "entities/Action";
import LOG_TYPE from "entities/AppsmithConsole/logtype"; import LOG_TYPE from "entities/AppsmithConsole/logtype";
import { DataTree, DataTreeWidget } from "entities/DataTree/dataTreeFactory";
import { isWidget } from "workers/evaluationUtils";
import { getWidget } from "./selectors";
import { WidgetProps } from "widgets/BaseWidget";
function* onWidgetUpdateSaga(payload: LogActionPayload) {
if (!payload.source) return;
// Wait for data tree update
yield take(ReduxActionTypes.SET_EVALUATED_TREE);
const dataTree: DataTree = yield select(getDataTree);
const widget = dataTree[payload.source.name];
if (
!isWidget(widget) ||
!widget.validationMessages ||
!widget.jsErrorMessages
)
return;
// Ignore canvas widget updates
if (widget.type === WidgetTypes.CANVAS_WIDGET) {
return;
}
const source = payload.source;
// If widget properties no longer have validation errors update the same
if (payload.state) {
const propertyPath = Object.keys(payload.state)[0];
const validationMessages = widget.validationMessages;
const validationMessage = validationMessages[propertyPath];
const jsErrorMessages = widget.jsErrorMessages;
const jsErrorMessage = jsErrorMessages[propertyPath];
const errors = yield select(getDebuggerErrors);
const errorId = `${source.id}-${propertyPath}`;
const widgetErrorLog = errors[errorId];
if (!widgetErrorLog) return;
const noError = isEmpty(validationMessage);
const noJsError = isEmpty(jsErrorMessage);
if (noError && noJsError) {
delete errors[errorId];
yield put({
type: ReduxActionTypes.DEBUGGER_UPDATE_ERROR_LOGS,
payload: errors,
});
}
}
}
function* formatActionRequestSaga(payload: LogActionPayload, request?: any) { function* formatActionRequestSaga(payload: LogActionPayload, request?: any) {
if (!payload.source || !payload.state || !request || !request.headers) { if (!payload.source || !payload.state || !request || !request.headers) {
@ -128,7 +67,9 @@ function* debuggerLogSaga(action: ReduxAction<Message>) {
switch (payload.logType) { switch (payload.logType) {
case LOG_TYPE.WIDGET_UPDATE: case LOG_TYPE.WIDGET_UPDATE:
yield call(onWidgetUpdateSaga, payload); yield put(debuggerLog(payload));
return;
case LOG_TYPE.ACTION_UPDATE:
yield put(debuggerLog(payload)); yield put(debuggerLog(payload));
return; return;
case LOG_TYPE.EVAL_ERROR: case LOG_TYPE.EVAL_ERROR:
@ -178,42 +119,6 @@ function* debuggerLogSaga(action: ReduxAction<Message>) {
} }
} }
// Pass through error list once after on page load actions executions are complete
function* onExecutePageActionsCompleteSaga() {
yield take(ReduxActionTypes.SET_EVALUATED_TREE);
const dataTree: DataTree = yield select(getDataTree);
const errors = yield select(getDebuggerErrors);
const updatedErrors = { ...errors };
const errorIds = Object.keys(errors);
for (const id of errorIds) {
const splits = id.split("-");
const entityId = splits[0];
const propertyName = splits[1];
const widget: WidgetProps | null = yield select(getWidget, entityId);
if (widget) {
const dataTreeWidget = dataTree[widget.widgetName] as DataTreeWidget;
if (!get(dataTreeWidget.invalidProps, propertyName, null)) {
delete updatedErrors[id];
}
}
}
yield put({
type: ReduxActionTypes.DEBUGGER_UPDATE_ERROR_LOGS,
payload: updatedErrors,
});
}
export default function* debuggerSagasListeners() { export default function* debuggerSagasListeners() {
yield all([ yield all([takeEvery(ReduxActionTypes.DEBUGGER_LOG_INIT, debuggerLogSaga)]);
takeEvery(ReduxActionTypes.DEBUGGER_LOG_INIT, debuggerLogSaga),
takeEvery(
ReduxActionTypes.EXECUTE_PAGE_LOAD_ACTIONS_COMPLETE,
onExecutePageActionsCompleteSaga,
),
]);
} }

View File

@ -28,29 +28,127 @@ import { WidgetProps } from "widgets/BaseWidget";
import PerformanceTracker, { import PerformanceTracker, {
PerformanceTransactionName, PerformanceTransactionName,
} from "../utils/PerformanceTracker"; } from "../utils/PerformanceTracker";
import { Variant } from "components/ads/common";
import { Toaster } from "components/ads/Toast";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import { Action } from "redux"; import { Action } from "redux";
import _ from "lodash"; import _ from "lodash";
import { ENTITY_TYPE, Message, Severity } from "entities/AppsmithConsole";
import LOG_TYPE from "entities/AppsmithConsole/logtype";
import { DataTree } from "entities/DataTree/dataTreeFactory";
import { AppState } from "reducers";
import {
getEntityNameAndPropertyPath,
isAction,
isWidget,
} from "workers/evaluationUtils";
import moment from "moment/moment";
import { Toaster } from "components/ads/Toast";
import { Variant } from "components/ads/common";
import AppsmithConsole from "utils/AppsmithConsole";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { import {
createMessage, createMessage,
ERROR_EVAL_ERROR_GENERIC, ERROR_EVAL_ERROR_GENERIC,
ERROR_EVAL_TRIGGER, ERROR_EVAL_TRIGGER,
} from "constants/messages"; } from "constants/messages";
import AppsmithConsole from "utils/AppsmithConsole";
import LOG_TYPE from "entities/AppsmithConsole/logtype";
import AnalyticsUtil from "utils/AnalyticsUtil";
let widgetTypeConfigMap: WidgetTypeConfigMap; let widgetTypeConfigMap: WidgetTypeConfigMap;
const worker = new GracefulWorkerService(Worker); const worker = new GracefulWorkerService(Worker);
const evalErrorHandler = (errors: EvalError[]) => { const getDebuggerErrors = (state: AppState) => state.ui.debugger.errors;
if (!errors) return;
function getLatestEvalPropertyErrors(
currentDebuggerErrors: Record<string, Message>,
dataTree: DataTree,
evaluationOrder: Array<string>,
) {
const updatedDebuggerErrors: Record<string, Message> = {
...currentDebuggerErrors,
};
for (const evaluatedPath of evaluationOrder) {
const { entityName, propertyPath } = getEntityNameAndPropertyPath(
evaluatedPath,
);
const entity = dataTree[entityName];
if (isWidget(entity) || isAction(entity)) {
if (propertyPath in entity.logBlackList) {
continue;
}
const jsError = _.get(entity, `jsErrorMessages.${propertyPath}`, "");
const validationError = _.get(
entity,
`validationMessages.${propertyPath}`,
"",
);
const evaluatedValue = _.get(
entity,
`evaluatedValues.${propertyPath}`,
"",
);
const error = jsError || validationError;
const idField = isWidget(entity) ? entity.widgetId : entity.actionId;
const nameField = isWidget(entity) ? entity.widgetName : entity.name;
const entityType = isWidget(entity)
? ENTITY_TYPE.WIDGET
: ENTITY_TYPE.ACTION;
const debuggerKey = idField + "-" + propertyPath;
// if dataTree has error but debugger does not -> add
// if debugger has error and data tree has error -> update error
// if debugger has error but data tree does not -> remove
// if debugger or data tree does not have an error -> no change
if (_.isString(error) && error !== "") {
// Add or update
updatedDebuggerErrors[debuggerKey] = {
logType: LOG_TYPE.EVAL_ERROR,
text: `The value at ${propertyPath} is invalid`,
message: error,
severity: Severity.ERROR,
timestamp: moment().format("hh:mm:ss"),
source: {
id: idField,
name: nameField,
type: entityType,
propertyPath: propertyPath,
},
state: {
value: evaluatedValue,
},
};
} else if (debuggerKey in updatedDebuggerErrors) {
// Remove
delete updatedDebuggerErrors[debuggerKey];
}
}
}
return updatedDebuggerErrors;
}
function* evalErrorHandler(
errors: EvalError[],
dataTree?: DataTree,
evaluationOrder?: Array<string>,
): any {
if (dataTree && evaluationOrder) {
const currentDebuggerErrors: Record<string, Message> = yield select(
getDebuggerErrors,
);
const evalPropertyErrors = getLatestEvalPropertyErrors(
currentDebuggerErrors,
dataTree,
evaluationOrder,
);
yield put({
type: ReduxActionTypes.DEBUGGER_UPDATE_ERROR_LOGS,
payload: evalPropertyErrors,
});
}
errors.forEach((error) => { errors.forEach((error) => {
switch (error.type) { switch (error.type) {
case EvalErrorTypes.DEPENDENCY_ERROR: { case EvalErrorTypes.CYCLICAL_DEPENDENCY_ERROR: {
if (error.context) { if (error.context) {
// Add more info about node for the toast // Add more info about node for the toast
const { entityType, node } = error.context; const { entityType, node } = error.context;
@ -104,33 +202,13 @@ const evalErrorHandler = (errors: EvalError[]) => {
}); });
break; break;
} }
case EvalErrorTypes.EVAL_ERROR: {
log.debug(error);
AppsmithConsole.error({
logType: LOG_TYPE.EVAL_ERROR,
text: `The value at ${error.context?.source.propertyPath} is invalid`,
message: error.message,
source: error.context?.source,
});
break;
}
case EvalErrorTypes.WIDGET_PROPERTY_VALIDATION_ERROR: {
AppsmithConsole.error({
logType: LOG_TYPE.WIDGET_PROPERTY_VALIDATION_ERROR,
text: `The value at ${error.context?.source.propertyPath} is invalid`,
message: error.message,
source: error.context?.source,
state: error.context?.state,
});
break;
}
default: { default: {
Sentry.captureException(error); Sentry.captureException(error);
log.debug(error); log.debug(error);
} }
} }
}); });
}; }
function* postEvalActionDispatcher( function* postEvalActionDispatcher(
actions: Array<ReduxAction<unknown> | ReduxActionWithoutPayload>, actions: Array<ReduxAction<unknown> | ReduxActionWithoutPayload>,
@ -156,10 +234,16 @@ function* evaluateTreeSaga(
widgetTypeConfigMap, widgetTypeConfigMap,
}, },
); );
const { dataTree, dependencies, errors, logs } = workerResponse; const {
dataTree,
dependencies,
errors,
evaluationOrder,
logs,
} = workerResponse;
log.debug({ dataTree: dataTree }); log.debug({ dataTree: dataTree });
logs.forEach((evalLog: any) => log.debug(evalLog)); logs.forEach((evalLog: any) => log.debug(evalLog));
evalErrorHandler(errors); yield call(evalErrorHandler, errors, dataTree, evaluationOrder);
PerformanceTracker.stopAsyncTracking( PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.DATA_TREE_EVALUATION, PerformanceTransactionName.DATA_TREE_EVALUATION,
); );
@ -197,7 +281,7 @@ export function* evaluateActionBindings(
const { errors, values } = workerResponse; const { errors, values } = workerResponse;
evalErrorHandler(errors); yield call(evalErrorHandler, errors);
return values; return values;
} }
@ -214,7 +298,7 @@ export function* evaluateDynamicTrigger(
); );
const { errors, triggers } = workerResponse; const { errors, triggers } = workerResponse;
evalErrorHandler(errors); yield call(evalErrorHandler, errors);
return triggers; return triggers;
} }
@ -302,7 +386,6 @@ const EVALUATE_REDUX_ACTIONS = [
]; ];
const shouldProcessAction = (action: ReduxAction<unknown>) => { const shouldProcessAction = (action: ReduxAction<unknown>) => {
// debugger;
if ( if (
action.type === ReduxActionTypes.BATCH_UPDATES_SUCCESS && action.type === ReduxActionTypes.BATCH_UPDATES_SUCCESS &&
Array.isArray(action.payload) Array.isArray(action.payload)

View File

@ -264,6 +264,7 @@ const createLoadingWidget = (
bindingPaths: {}, bindingPaths: {},
triggerPaths: {}, triggerPaths: {},
validationPaths: {}, validationPaths: {},
logBlackList: {},
isLoading: true, isLoading: true,
}; };
}; };

View File

@ -84,12 +84,10 @@ export const getDynamicBindings = (
}; };
export enum EvalErrorTypes { export enum EvalErrorTypes {
DEPENDENCY_ERROR = "DEPENDENCY_ERROR", CYCLICAL_DEPENDENCY_ERROR = "CYCLICAL_DEPENDENCY_ERROR",
EVAL_PROPERTY_ERROR = "EVAL_PROPERTY_ERROR", EVAL_PROPERTY_ERROR = "EVAL_PROPERTY_ERROR",
WIDGET_PROPERTY_VALIDATION_ERROR = "WIDGET_PROPERTY_VALIDATION_ERROR", WIDGET_PROPERTY_VALIDATION_ERROR = "WIDGET_PROPERTY_VALIDATION_ERROR",
EVAL_TREE_ERROR = "EVAL_TREE_ERROR", EVAL_TREE_ERROR = "EVAL_TREE_ERROR",
UNESCAPE_STRING_ERROR = "UNESCAPE_STRING_ERROR",
EVAL_ERROR = "EVAL_ERROR",
UNKNOWN_ERROR = "UNKNOWN_ERROR", UNKNOWN_ERROR = "UNKNOWN_ERROR",
BAD_UNEVAL_TREE_ERROR = "BAD_UNEVAL_TREE_ERROR", BAD_UNEVAL_TREE_ERROR = "BAD_UNEVAL_TREE_ERROR",
EVAL_TRIGGER_ERROR = "EVAL_TRIGGER_ERROR", EVAL_TRIGGER_ERROR = "EVAL_TRIGGER_ERROR",

View File

@ -22,7 +22,7 @@ import defaultTemplate from "templates/default";
import { generateReactKey } from "./generators"; import { generateReactKey } from "./generators";
import { ChartDataPoint } from "widgets/ChartWidget"; import { ChartDataPoint } from "widgets/ChartWidget";
import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer"; import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
import { has, isString, omit, set } from "lodash"; import { get, has, isString, omit, set } from "lodash";
import log from "loglevel"; import log from "loglevel";
import { import {
migrateTablePrimaryColumnsBindings, migrateTablePrimaryColumnsBindings,
@ -764,6 +764,11 @@ const transformDSL = (currentDSL: ContainerWidgetProps<WidgetProps>) => {
currentDSL.version = LATEST_PAGE_VERSION; currentDSL.version = LATEST_PAGE_VERSION;
} }
if (currentDSL.version === 22) {
currentDSL = addLogBlackListToAllListWidgetChildren(currentDSL);
currentDSL.version = 23;
}
return currentDSL; return currentDSL;
}; };
@ -1164,3 +1169,41 @@ export const generateWidgetProps = (
} else throw Error("Failed to create widget: Parent was not provided "); } else throw Error("Failed to create widget: Parent was not provided ");
} }
}; };
/**
* adds logBlackList key for all list widget children
*
* @param currentDSL
* @returns
*/
const addLogBlackListToAllListWidgetChildren = (
currentDSL: ContainerWidgetProps<WidgetProps>,
) => {
currentDSL.children = currentDSL.children?.map((children: WidgetProps) => {
if (children.type === WidgetTypes.LIST_WIDGET) {
const widgets = get(
children,
"children.0.children.0.children.0.children",
);
widgets.map((widget: any, index: number) => {
const logBlackList: { [key: string]: boolean } = {};
Object.keys(widget).map((key) => {
logBlackList[key] = true;
});
if (!widget.logBlackList) {
set(
children,
`children.0.children.0.children.0.children.${index}.logBlackList`,
logBlackList,
);
}
});
}
return children;
});
return currentDSL;
};

View File

@ -112,6 +112,10 @@ export default class DataTreeEvaluator {
}, },
}; };
this.logs.push({ timeTakenForFirstTree }); this.logs.push({ timeTakenForFirstTree });
return {
dataTree: this.evalTree,
evaluationOrder: this.sortedDependencies,
};
} }
isDynamicLeaf(unEvalTree: DataTree, propertyPath: string) { isDynamicLeaf(unEvalTree: DataTree, propertyPath: string) {
@ -206,7 +210,10 @@ export default class DataTreeEvaluator {
evaluate: (evalStop - evalStart).toFixed(2), evaluate: (evalStop - evalStart).toFixed(2),
}; };
this.logs.push({ timeTakenForSubTreeEval }); this.logs.push({ timeTakenForSubTreeEval });
return this.evalTree; return {
dataTree: this.evalTree,
evaluationOrder: evaluationOrder,
};
} }
getCompleteSortOrder( getCompleteSortOrder(
@ -502,7 +509,7 @@ export default class DataTreeEvaluator {
entityType = entity.pluginType; entityType = entity.pluginType;
} }
this.errors.push({ this.errors.push({
type: EvalErrorTypes.DEPENDENCY_ERROR, type: EvalErrorTypes.CYCLICAL_DEPENDENCY_ERROR,
message: "Cyclic dependency found while evaluating.", message: "Cyclic dependency found while evaluating.",
context: { context: {
node, node,
@ -612,22 +619,14 @@ export default class DataTreeEvaluator {
fullPropertyPath, fullPropertyPath,
); );
_.set(data, `${entityName}.jsErrorMessages.${propertyPath}`, e.message); _.set(data, `${entityName}.jsErrorMessages.${propertyPath}`, e.message);
const entity = data[entityName]; } else {
if (isWidget(entity)) { // TODO clean up
// This is to handle situations with evaluation of triggers for execution
this.errors.push({ this.errors.push({
type: EvalErrorTypes.EVAL_ERROR, type: EvalErrorTypes.EVAL_PROPERTY_ERROR,
message: e.message, message: e.message,
context: {
source: {
id: entity.widgetId,
name: entity.widgetName,
type: ENTITY_TYPE.WIDGET,
propertyPath: propertyPath,
},
},
}); });
} }
}
return { result: undefined, triggers: [] }; return { result: undefined, triggers: [] };
} }
} }
@ -668,23 +667,7 @@ export default class DataTreeEvaluator {
: transformed; : transformed;
const safeEvaluatedValue = removeFunctions(evaluatedValue); const safeEvaluatedValue = removeFunctions(evaluatedValue);
_.set(widget, `evaluatedValues.${propertyPath}`, safeEvaluatedValue); _.set(widget, `evaluatedValues.${propertyPath}`, safeEvaluatedValue);
const jsError = _.get(widget, `jsErrorMessages.${propertyPath}`); if (!isValid) {
if (!isValid && !jsError) {
this.errors.push({
type: EvalErrorTypes.WIDGET_PROPERTY_VALIDATION_ERROR,
message: message || "",
context: {
source: {
id: widget.widgetId,
name: widget.widgetName,
type: ENTITY_TYPE.WIDGET,
propertyPath: propertyPath,
},
state: {
value: safeEvaluatedValue,
},
},
});
_.set(widget, `invalidProps.${propertyPath}`, true); _.set(widget, `invalidProps.${propertyPath}`, true);
_.set(widget, `validationMessages.${propertyPath}`, message); _.set(widget, `validationMessages.${propertyPath}`, message);
} else { } else {

View File

@ -384,9 +384,9 @@ describe("DataTreeEvaluator", () => {
text: "Hey there", text: "Hey there",
}, },
}; };
const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); const { dataTree } = evaluator.updateDataTree(updatedUnEvalTree);
expect(updatedEvalTree).toHaveProperty("Text2.text", "Hey there"); expect(dataTree).toHaveProperty("Text2.text", "Hey there");
expect(updatedEvalTree).toHaveProperty("Text3.text", "Hey there"); expect(dataTree).toHaveProperty("Text3.text", "Hey there");
}); });
it("Evaluates a dependency change in update run", () => { it("Evaluates a dependency change in update run", () => {
@ -397,10 +397,10 @@ describe("DataTreeEvaluator", () => {
text: "Label 3", text: "Label 3",
}, },
}; };
const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); const { dataTree } = evaluator.updateDataTree(updatedUnEvalTree);
const updatedDependencyMap = evaluator.dependencyMap; const updatedDependencyMap = evaluator.dependencyMap;
expect(updatedEvalTree).toHaveProperty("Text2.text", "Label"); expect(dataTree).toHaveProperty("Text2.text", "Label");
expect(updatedEvalTree).toHaveProperty("Text3.text", "Label 3"); expect(dataTree).toHaveProperty("Text3.text", "Label 3");
expect(updatedDependencyMap).toStrictEqual({ expect(updatedDependencyMap).toStrictEqual({
Text1: ["Text1.text"], Text1: ["Text1.text"],
Text2: ["Text2.text"], Text2: ["Text2.text"],
@ -445,8 +445,8 @@ describe("DataTreeEvaluator", () => {
}, },
}; };
const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); const { dataTree } = evaluator.updateDataTree(updatedUnEvalTree);
expect(updatedEvalTree).toHaveProperty("Input1.text", "Default value"); expect(dataTree).toHaveProperty("Input1.text", "Default value");
}); });
it("Evaluates for value changes in nested diff paths", () => { it("Evaluates for value changes in nested diff paths", () => {
@ -481,11 +481,8 @@ describe("DataTreeEvaluator", () => {
}, },
}, },
}; };
const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); const { dataTree } = evaluator.updateDataTree(updatedUnEvalTree);
expect(updatedEvalTree).toHaveProperty( expect(dataTree).toHaveProperty("Dropdown2.options.0.label", "newValue");
"Dropdown2.options.0.label",
"newValue",
);
}); });
it("Adds an entity with a complicated binding", () => { it("Adds an entity with a complicated binding", () => {
@ -504,9 +501,9 @@ describe("DataTreeEvaluator", () => {
], ],
}, },
}; };
const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); const { dataTree } = evaluator.updateDataTree(updatedUnEvalTree);
const updatedDependencyMap = evaluator.dependencyMap; const updatedDependencyMap = evaluator.dependencyMap;
expect(updatedEvalTree).toHaveProperty("Table1.tableData", [ expect(dataTree).toHaveProperty("Table1.tableData", [
{ {
test: "Hey", test: "Hey",
raw: "Label", raw: "Label",
@ -568,9 +565,9 @@ describe("DataTreeEvaluator", () => {
], ],
}, },
}; };
const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); const { dataTree } = evaluator.updateDataTree(updatedUnEvalTree);
const updatedDependencyMap = evaluator.dependencyMap; const updatedDependencyMap = evaluator.dependencyMap;
expect(updatedEvalTree).toHaveProperty("Table1.tableData", [ expect(dataTree).toHaveProperty("Table1.tableData", [
{ {
test: "Hey", test: "Hey",
raw: "Label", raw: "Label",
@ -580,7 +577,7 @@ describe("DataTreeEvaluator", () => {
raw: "Label", raw: "Label",
}, },
]); ]);
expect(updatedEvalTree).toHaveProperty("Text4.text", "Hey"); expect(dataTree).toHaveProperty("Text4.text", "Hey");
expect(updatedDependencyMap).toStrictEqual({ expect(updatedDependencyMap).toStrictEqual({
Api1: ["Api1.data"], Api1: ["Api1.data"],
Text1: ["Text1.text"], Text1: ["Text1.text"],
@ -657,14 +654,14 @@ describe("DataTreeEvaluator", () => {
}, },
}, },
}; };
const evaluatedDataTree2 = evaluator.updateDataTree(updatedTree2); const { dataTree } = evaluator.updateDataTree(updatedTree2);
expect(evaluator.dependencyMap["Api2.config.body"]).toStrictEqual([ expect(evaluator.dependencyMap["Api2.config.body"]).toStrictEqual([
"Text1.text", "Text1.text",
"Api2.config.pluginSpecifiedTemplates[0].value", "Api2.config.pluginSpecifiedTemplates[0].value",
]); ]);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
expect(evaluatedDataTree2.Api2.config.body).toBe("{ 'name': Test }"); expect(dataTree.Api2.config.body).toBe("{ 'name': Test }");
const updatedTree3 = { const updatedTree3 = {
...updatedTree2, ...updatedTree2,
Api2: { Api2: {
@ -683,13 +680,14 @@ describe("DataTreeEvaluator", () => {
}, },
}, },
}; };
const evaluatedDataTree3 = evaluator.updateDataTree(updatedTree3); const evaluatedDataTreeObject = evaluator.updateDataTree(updatedTree3);
const dataTree3 = evaluatedDataTreeObject.dataTree;
expect(evaluator.dependencyMap["Api2.config.body"]).toStrictEqual([ expect(evaluator.dependencyMap["Api2.config.body"]).toStrictEqual([
"Text1.text", "Text1.text",
"Api2.config.pluginSpecifiedTemplates[0].value", "Api2.config.pluginSpecifiedTemplates[0].value",
]); ]);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
expect(evaluatedDataTree3.Api2.config.body).toBe("{ 'name': \"Test\" }"); expect(dataTree3.Api2.config.body).toBe("{ 'name': \"Test\" }");
}); });
}); });

View File

@ -47,18 +47,24 @@ ctx.addEventListener(
let errors: EvalError[] = []; let errors: EvalError[] = [];
let logs: any[] = []; let logs: any[] = [];
let dependencies: DependencyMap = {}; let dependencies: DependencyMap = {};
let dataTreeObject: any = {};
let evaluationOrder: Array<string> = [];
try { try {
if (!dataTreeEvaluator) { if (!dataTreeEvaluator) {
dataTreeEvaluator = new DataTreeEvaluator(widgetTypeConfigMap); dataTreeEvaluator = new DataTreeEvaluator(widgetTypeConfigMap);
dataTreeEvaluator.createFirstTree(unevalTree); dataTreeObject = dataTreeEvaluator.createFirstTree(unevalTree);
dataTree = dataTreeEvaluator.evalTree; dataTree = dataTreeObject.dataTree;
evaluationOrder = dataTreeObject.evaluationOrder;
// dataTreeEvaluator.sortedDepedencies
} else { } else {
dataTree = dataTreeEvaluator.updateDataTree(unevalTree); dataTreeObject = dataTreeEvaluator.updateDataTree(unevalTree);
dataTree = dataTreeObject.dataTree;
evaluationOrder = dataTreeObject.evaluationOrder;
} }
// We need to clean it to remove any possible functions inside the tree. // We need to clean it to remove any possible functions inside the tree.
// If functions exist, it will crash the web worker // If functions exist, it will crash the web worker
dataTree = JSON.parse(JSON.stringify(dataTree)); dataTree = dataTree && JSON.parse(JSON.stringify(dataTree));
dependencies = dataTreeEvaluator.inverseDependencyMap; dependencies = dataTreeEvaluator.inverseDependencyMap;
errors = dataTreeEvaluator.errors; errors = dataTreeEvaluator.errors;
dataTreeEvaluator.clearErrors(); dataTreeEvaluator.clearErrors();
@ -79,10 +85,12 @@ ctx.addEventListener(
dataTree = getSafeToRenderDataTree(unevalTree, widgetTypeConfigMap); dataTree = getSafeToRenderDataTree(unevalTree, widgetTypeConfigMap);
dataTreeEvaluator = undefined; dataTreeEvaluator = undefined;
} }
// step 6: eval order
return { return {
dataTree, dataTree,
dependencies, dependencies,
errors, errors,
evaluationOrder,
logs, logs,
}; };
} }
@ -108,7 +116,8 @@ ctx.addEventListener(
if (!dataTreeEvaluator) { if (!dataTreeEvaluator) {
return { triggers: [], errors: [] }; return { triggers: [], errors: [] };
} }
const evalTree = dataTreeEvaluator.updateDataTree(dataTree); dataTreeEvaluator.updateDataTree(dataTree);
const evalTree = dataTreeEvaluator.evalTree;
const triggers = dataTreeEvaluator.getDynamicValue( const triggers = dataTreeEvaluator.getDynamicValue(
dynamicTrigger, dynamicTrigger,
evalTree, evalTree,
@ -120,7 +129,7 @@ ctx.addEventListener(
// Transforming eval errors into eval trigger errors. Since trigger // Transforming eval errors into eval trigger errors. Since trigger
// errors occur less, we want to treat it separately // errors occur less, we want to treat it separately
const errors = dataTreeEvaluator.errors.map((error) => { const errors = dataTreeEvaluator.errors.map((error) => {
if (error.type === EvalErrorTypes.EVAL_ERROR) { if (error.type === EvalErrorTypes.EVAL_PROPERTY_ERROR) {
return { return {
...error, ...error,
type: EvalErrorTypes.EVAL_TRIGGER_ERROR, type: EvalErrorTypes.EVAL_TRIGGER_ERROR,