diff --git a/app/client/src/ee/components/editorComponents/Debugger/ErrorLogs/getLogIconForEntity.tsx b/app/client/src/ee/components/editorComponents/Debugger/ErrorLogs/getLogIconForEntity.tsx index 0e52fa650793..e6a2b5c033bf 100644 --- a/app/client/src/ee/components/editorComponents/Debugger/ErrorLogs/getLogIconForEntity.tsx +++ b/app/client/src/ee/components/editorComponents/Debugger/ErrorLogs/getLogIconForEntity.tsx @@ -1,24 +1,30 @@ export * from "ce/components/editorComponents/Debugger/ErrorLogs/getLogIconForEntity"; import React from "react"; -import type { LogItemProps } from "components/editorComponents/Debugger/ErrorLogs/ErrorLogItem"; +import type { IconEntityMapper } from "ce/components/editorComponents/Debugger/ErrorLogs/getLogIconForEntity"; import { getIconForEntity as CE_getIconForEntity } from "ce/components/editorComponents/Debugger/ErrorLogs/getLogIconForEntity"; import { importRemixIcon } from "@design-system/widgets-old"; import { ENTITY_TYPE } from "@appsmith/entities/DataTree/types"; import { getModuleIcon } from "pages/Editor/utils"; +import { useSelector } from "react-redux"; +import { getModuleById } from "@appsmith/selectors/modulesSelector"; +import { getModuleInstanceById } from "@appsmith/selectors/moduleInstanceSelectors"; const GuideLineIcon = importRemixIcon( async () => import("remixicon-react/GuideLineIcon"), ); -export const getIconForEntity: Record< - string, - (props: LogItemProps, pluginImages: Record) => any -> = { +export const getIconForEntity: IconEntityMapper = { ...CE_getIconForEntity, [ENTITY_TYPE.MODULE_INPUT]: () => { return ; }, - [ENTITY_TYPE.MODULE_INSTANCE]: (props, pluginImages) => { - return getModuleIcon(undefined, pluginImages); + [ENTITY_TYPE.MODULE_INSTANCE]: (props) => { + const moduleInstance = useSelector((state) => + getModuleInstanceById(state, props.id || ""), + ); + const module = useSelector((state) => + getModuleById(state, moduleInstance?.sourceModuleId || ""), + ); + return getModuleIcon(module, props.pluginImages) || null; }, }; diff --git a/app/client/src/ee/reducers/entityReducers/moduleInstanceEntitiesReducer.ts b/app/client/src/ee/reducers/entityReducers/moduleInstanceEntitiesReducer.ts index 2ddd46a6993f..931ffcc66500 100644 --- a/app/client/src/ee/reducers/entityReducers/moduleInstanceEntitiesReducer.ts +++ b/app/client/src/ee/reducers/entityReducers/moduleInstanceEntitiesReducer.ts @@ -36,6 +36,11 @@ export interface ModuleInstanceEntitiesReducerState { jsCollections: ModuleInstanceJSCollectionData[]; } +export type ModuleInstanceAction = + ModuleInstanceEntitiesReducerState["actions"][number]["config"]; +export type ModuleInstanceJSCollection = + ModuleInstanceEntitiesReducerState["jsCollections"][number]["config"]; + export const initialState: ModuleInstanceEntitiesReducerState = { actions: [], jsCollections: [], diff --git a/app/client/src/ee/sagas/helpers.test.ts b/app/client/src/ee/sagas/helpers.test.ts new file mode 100644 index 000000000000..e0684c607d26 --- /dev/null +++ b/app/client/src/ee/sagas/helpers.test.ts @@ -0,0 +1,263 @@ +import { + ENTITY_TYPE, + PLATFORM_ERROR, +} from "@appsmith/entities/AppsmithConsole/utils"; +import { runSaga } from "redux-saga"; +import { transformAddErrorLogsSaga } from "./helpers"; +import { Severity, type Log, LOG_CATEGORY } from "entities/AppsmithConsole"; +import { klona } from "klona"; +import { set } from "lodash"; +import { MODULE_TYPE } from "@appsmith/constants/ModuleConstants"; + +const queryModuleInstanceErrorLog: Log = { + id: "queryInstanceActionId", + iconId: "644b84d980127e0eff78a732", + logType: 2, + environmentName: "Production", + text: "Execution failed with status SQLSTATE: 42601", + source: { + type: ENTITY_TYPE.ACTION, + name: "QueryModule31", + id: "queryInstanceActionId", + }, + messages: [ + { + message: { + name: "PluginExecutionError", + message: "ERROR: syntax error at end of input\n Position: 35", + }, + type: PLATFORM_ERROR.PLUGIN_EXECUTION, + subType: "INTERNAL_ERROR", + }, + ], + state: { + actionId: "queryInstanceActionId", + requestedAt: 1712125304920, + requestParams: { + Query: { + value: 'SELECT * FROM public."users" LIMIT;', + substitutedParams: {}, + }, + }, + }, + pluginErrorDetails: { + title: "Query execution error", + errorType: "INTERNAL_ERROR", + appsmithErrorCode: "PE-PGS-5000", + appsmithErrorMessage: "Your PostgreSQL query failed to execute.", + downstreamErrorCode: "SQLSTATE: 42601", + downstreamErrorMessage: + "ERROR: syntax error at end of input\n Position: 35", + }, + severity: Severity.ERROR, + timestamp: "1712125307046", + occurrenceCount: 1, + category: LOG_CATEGORY.PLATFORM_GENERATED, + isExpanded: false, +}; + +const jsModuleInstanceErrorLog: Log = { + id: "moduleInstanceJSCollectionId-actionId", + logType: 5, + text: "JS Function execution failed: JSModule11.myFun1", + messages: [ + { + message: { + name: "TypeError", + message: "x.asdkljas is not a function", + }, + type: PLATFORM_ERROR.JS_FUNCTION_EXECUTION, + subType: "PARSE", + }, + ], + source: { + id: "moduleInstanceJSCollectionId", + name: "JSModule11", + type: ENTITY_TYPE.JSACTION, + propertyPath: "myFun1", + }, + severity: Severity.ERROR, + timestamp: "1712132302018", + occurrenceCount: 1, + category: LOG_CATEGORY.PLATFORM_GENERATED, + isExpanded: false, +}; + +const DEFAULT_STATE = { + entities: { + moduleInstances: { + queryModuleInstanceId: { + id: "queryModuleInstanceId", + type: MODULE_TYPE.QUERY, + sourceModuleId: "queryModuleId", + name: "QueryModule31", + contextType: "PAGE", + contextId: "65fc1233b48e3e52a6d91d3b", + applicationId: "65fc1233b48e3e52a6d91d37", + workspaceId: "65fc11fdb48e3e52a6d91d30", + }, + jsModuleInstanceId: { + id: "jsModuleInstanceId", + type: MODULE_TYPE.JS, + sourceModuleId: "jsModuleId", + name: "JSModule11", + contextType: "PAGE", + contextId: "65fc1233b48e3e52a6d91d3b", + applicationId: "65fc1233b48e3e52a6d91d37", + workspaceId: "65fc11fdb48e3e52a6d91d30", + }, + }, + moduleInstanceEntities: { + actions: [ + { + config: { + id: "queryInstanceActionId", + moduleInstanceId: "queryModuleInstanceId", + }, + }, + ], + jsCollections: [ + { + config: { + id: "moduleInstanceJSCollectionId", + moduleInstanceId: "jsModuleInstanceId", + }, + }, + ], + }, + }, +}; + +const widgetErrorLog = { + id: "j1vz86pd0v-defaultText", + iconId: "j1vz86pd0v", + logType: 5, + text: "The value at defaultText is invalid", + messages: [ + { + message: { + name: "SyntaxError", + message: "Unexpected token '('", + }, + type: "PARSE", + }, + { + message: { + name: "TypeError", + message: "This value must be string", + }, + type: "VALIDATION", + }, + ], + source: { + id: "j1vz86pd0v", + name: "Input1", + type: ENTITY_TYPE.WIDGET, + propertyPath: "defaultText", + pluginType: "INPUT_WIDGET_V2", + }, + analytics: { + widgetType: "INPUT_WIDGET_V2", + }, +} as unknown as Log; + +const moduleInputErrorLog = { + id: "6603c72ce98dd96ec6fde480-input1", + iconId: "QUERY_MODULE", + logType: 5, + text: "The value at input1 is invalid", + messages: [ + { + message: { + name: "ReferenceError", + message: "sss is not defined", + }, + type: "PARSE", + }, + ], + source: { + id: "6603c72ce98dd96ec6fde480", + name: "inputs", + type: ENTITY_TYPE.MODULE_INPUT, + propertyPath: "input1", + }, + analytics: {}, +} as unknown as Log; + +describe("transformAddErrorLogsSaga", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("transforms logs with ENTITY_TYPE.ACTION belonging to a module instance", async () => { + const logs: Log[] = [queryModuleInstanceErrorLog]; + const result = await runSaga( + { + getState: () => DEFAULT_STATE, + }, + transformAddErrorLogsSaga, + logs, + ).toPromise(); + + const expectedResult = klona(logs); + expectedResult[0].id = "queryModuleInstanceId"; + if (expectedResult[0].source) { + expectedResult[0].source.name = "QueryModule31"; + expectedResult[0].source.type = ENTITY_TYPE.MODULE_INSTANCE; + expectedResult[0].source.id = "queryModuleInstanceId"; + } + + expect(result).toEqual(expectedResult); + }); + + it("doesn't transform logs with ENTITY_TYPE.ACTION not belonging to a module instance", async () => { + const actionLog = klona(queryModuleInstanceErrorLog); + actionLog.id = "action-id"; + set(actionLog, "source.id", "action-id"); + const logs: Log[] = [actionLog]; + const result = await runSaga( + { + getState: () => DEFAULT_STATE, + }, + transformAddErrorLogsSaga, + logs, + ).toPromise(); + + expect(result).toEqual(logs); + }); + + it("transforms logs with ENTITY_TYPE.JSACTION belonging to a module instance", async () => { + const logs: Log[] = [jsModuleInstanceErrorLog]; + const result = await runSaga( + { + getState: () => DEFAULT_STATE, + }, + transformAddErrorLogsSaga, + logs, + ).toPromise(); + + const expectedResult = klona(logs); + if (expectedResult[0].source) { + expectedResult[0].source.name = "JSModule11"; + expectedResult[0].source.type = ENTITY_TYPE.MODULE_INSTANCE; + expectedResult[0].source.id = "jsModuleInstanceId"; + } + + expect(result).toEqual(expectedResult); + }); + + it("doesn't transform logs with ENTITY_TYPE.JSACTION not belonging to a module instance", async () => { + const logs: Log[] = [widgetErrorLog, moduleInputErrorLog]; + const result = await runSaga( + { + getState: () => DEFAULT_STATE, + }, + transformAddErrorLogsSaga, + logs, + ).toPromise(); + + expect(result).toEqual(logs); + }); + + it("doesn't transform logs for any other entity type than JSACTION and ACTION", async () => {}); +}); diff --git a/app/client/src/ee/sagas/helpers.ts b/app/client/src/ee/sagas/helpers.ts index 6389641de6c4..8d166ca98525 100644 --- a/app/client/src/ee/sagas/helpers.ts +++ b/app/client/src/ee/sagas/helpers.ts @@ -3,6 +3,21 @@ import type { ResolveParentEntityMetadataReturnType } from "ce/sagas/helpers"; import { CreateNewActionKey } from "@appsmith/entities/Engine/actionHelpers"; import { resolveParentEntityMetadata as CE_resolveParentEntityMetadata } from "ce/sagas/helpers"; import type { Action } from "entities/Action"; +import type { Log } from "entities/AppsmithConsole"; +import { ENTITY_TYPE } from "@appsmith/entities/AppsmithConsole/utils"; +import type { + ModuleInstanceAction, + ModuleInstanceJSCollection, +} from "@appsmith/reducers/entityReducers/moduleInstanceEntitiesReducer"; +import { select } from "redux-saga/effects"; +import { + getModuleInstanceActionById, + getModuleInstanceJSCollectionById, +} from "@appsmith/selectors/moduleInstanceSelectors"; +import { getModuleInstanceById } from "@appsmith/selectors/moduleInstanceSelectors"; +import type { ModuleInstance } from "@appsmith/constants/ModuleInstanceConstants"; +import { klona } from "klona"; +import type { DeleteErrorLogPayload } from "actions/debuggerActions"; export const resolveParentEntityMetadata = ( action: Partial, @@ -27,3 +42,77 @@ export const resolveParentEntityMetadata = ( return { parentEntityId: undefined, parentEntityKey: undefined }; }; + +export function* transformAddErrorLogsSaga(logs: Log[]) { + const transformedLogs: Log[] = klona(logs); + for (const log of transformedLogs) { + const { id = "", source } = log; + + if (source?.type === ENTITY_TYPE.ACTION) { + const instanceAction: ModuleInstanceAction | undefined = yield select( + getModuleInstanceActionById, + id, + ); + const { moduleInstanceId = "" } = instanceAction || {}; + const moduleInstance: ModuleInstance | undefined = yield select( + getModuleInstanceById, + moduleInstanceId, + ); + + if (moduleInstance) { + log.id = moduleInstanceId; + if (log.source) { + log.source.id = moduleInstance.id; + log.source.type = ENTITY_TYPE.MODULE_INSTANCE; + log.source.name = moduleInstance.name; + } + } + } + + if (source?.type === ENTITY_TYPE.JSACTION) { + const instanceJSCollection: ModuleInstanceJSCollection | undefined = + yield select(getModuleInstanceJSCollectionById, source?.id || ""); + + const { moduleInstanceId = "" } = instanceJSCollection || {}; + const moduleInstance: ModuleInstance | undefined = yield select( + getModuleInstanceById, + moduleInstanceId, + ); + + if (moduleInstance) { + if (log.source) { + log.source.id = moduleInstance.id; + log.source.type = ENTITY_TYPE.MODULE_INSTANCE; + log.source.name = moduleInstance.name; + } + } + } + } + + return transformedLogs; +} + +export function* transformDeleteErrorLogsSaga(payload: DeleteErrorLogPayload) { + const transformedPayload = klona(payload); + + for (const item of transformedPayload) { + const { id } = item; + + const instanceAction: ModuleInstanceAction | undefined = yield select( + getModuleInstanceActionById, + id, + ); + const instanceJSCollection: ModuleInstanceJSCollection | undefined = + yield select(getModuleInstanceJSCollectionById, id || ""); + + const moduleInstanceId = + instanceAction?.moduleInstanceId || + instanceJSCollection?.moduleInstanceId; + + if (moduleInstanceId) { + item.id = moduleInstanceId; + } + } + + return transformedPayload; +} diff --git a/app/client/src/ee/selectors/entitiesSelector.ts b/app/client/src/ee/selectors/entitiesSelector.ts index 1d0b71151ed5..498728b8f22f 100644 --- a/app/client/src/ee/selectors/entitiesSelector.ts +++ b/app/client/src/ee/selectors/entitiesSelector.ts @@ -345,8 +345,17 @@ export const getAllJSCollections = createSelector( }, ); +/** + * Checking for action.config.datasource as well as there is a case in action execution that + * if an action is successfully executed but in the reducer the action is not found, the result + * is stored in the reducer along with just the id. In that case apart from id every other information + * is missing. This condition makes sure that important attributes are present to qualify for a valid + * private entity action + */ export const getPrivateActions = createSelector(getActions, (actions) => - actions.filter((action) => !action.config.isPublic), + actions.filter( + (action) => !action.config.isPublic && action.config.datasource, + ), ); export const selectFilesForPackageExplorer = createSelector( diff --git a/app/client/src/ee/selectors/moduleInstanceSelectors.ts b/app/client/src/ee/selectors/moduleInstanceSelectors.ts index 502680b7f950..9551a855cad9 100644 --- a/app/client/src/ee/selectors/moduleInstanceSelectors.ts +++ b/app/client/src/ee/selectors/moduleInstanceSelectors.ts @@ -9,7 +9,11 @@ import type { ActionData } from "@appsmith/reducers/entityReducers/actionsReduce import type { QueryModuleInstanceEntity } from "@appsmith/entities/DataTree/types"; import { MODULE_TYPE } from "@appsmith/constants/ModuleConstants"; import type { JSCollection } from "entities/JSCollection"; -import type { ModuleInstanceJSCollectionData } from "@appsmith/reducers/entityReducers/moduleInstanceEntitiesReducer"; +import type { + ModuleInstanceAction, + ModuleInstanceJSCollection, + ModuleInstanceJSCollectionData, +} from "@appsmith/reducers/entityReducers/moduleInstanceEntitiesReducer"; const DEFAULT_SAVING_STATUS = { isSaving: false, @@ -144,3 +148,26 @@ export const getModuleInstanceEvalValues = ( return moduleInstance?.inputs || DEFAULT_INPUT_EVAL_VALUES; }; + +export const getModuleInstanceActionById = ( + state: AppState, + actionId: string, +): ModuleInstanceAction | undefined => { + const actionData = state.entities.moduleInstanceEntities.actions.find( + ({ config }: { config: Action }) => config.id === actionId, + ); + + return actionData?.config; +}; + +export const getModuleInstanceJSCollectionById = ( + state: AppState, + jsCollectionId: string, +): ModuleInstanceJSCollection | undefined => { + const jsCollectionData = + state.entities.moduleInstanceEntities.jsCollections.find( + ({ config }: { config: JSCollection }) => config.id === jsCollectionId, + ); + + return jsCollectionData?.config; +};