PromucFlow_constructor/app/client/src/components/editorComponents/JSResponseView.tsx
Ayush Pahwa 6ebb01727f
feat: remove disable overlay for incorrect js objects (#33990)
## Description
When the js functions fail to parse (eg. incomplete quotes etc), the
jsresponse view is disabled with an overlay. Now since remote js run
component stays as a child of the jsresponse view component and
shouldn't be hidden even if the js is malformed. Refer to the issue
linked for more info.

NOTE: Since this PR is only touching EE functionality, the output for CE
is not changed. The tests present today shall cover this. The tests for
the new experience will be added in the EE PR.

Fixes #33884 

## Automation

/ok-to-test tags="@tag.Sanity, @tag.JS"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/9381657328>
> Commit: 65afce97c51d6fbd5890b7199619024b434f1069
> Cypress dashboard url: <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=9381657328&attempt=1"
target="_blank">Click here!</a>

<!-- end of auto-generated comment: Cypress test results  -->




## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [x] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Bug Fixes**
- Improved error message and log information display based on execution
permissions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-06-05 15:57:58 +05:30

334 lines
11 KiB
TypeScript

import React, { useCallback, useEffect, useMemo, useState } from "react";
import { connect, useDispatch, useSelector } from "react-redux";
import type { RouteComponentProps } from "react-router";
import { withRouter } from "react-router";
import styled from "styled-components";
import { every, includes } from "lodash";
import type { AppState } from "@appsmith/reducers";
import type { JSEditorRouteParams } from "constants/routes";
import {
createMessage,
DEBUGGER_ERRORS,
DEBUGGER_LOGS,
DEBUGGER_RESPONSE,
EXECUTING_FUNCTION,
NO_JS_FUNCTION_RETURN_VALUE,
UPDATING_JS_COLLECTION,
} from "@appsmith/constants/messages";
import type { EditorTheme } from "./CodeEditor/EditorConfig";
import DebuggerLogs from "./Debugger/DebuggerLogs";
import type { JSAction } from "entities/JSCollection";
import ReadOnlyEditor from "components/editorComponents/ReadOnlyEditor";
import { Flex, Text } from "design-system";
import LoadingOverlayScreen from "components/editorComponents/LoadingOverlayScreen";
import type { JSCollectionData } from "@appsmith/reducers/entityReducers/jsActionsReducer";
import type { EvaluationError } from "utils/DynamicBindingUtils";
import { DEBUGGER_TAB_KEYS } from "./Debugger/helpers";
import type { BottomTab } from "./EntityBottomTabs";
import EntityBottomTabs from "./EntityBottomTabs";
import { getIsSavingEntity } from "selectors/editorSelectors";
import { getJSResponseViewState } from "./utils";
import { getFilteredErrors } from "selectors/debuggerSelectors";
import {
NoResponse,
ResponseTabErrorContainer,
ResponseTabErrorContent,
} from "./ApiResponseView";
import LogHelper from "./Debugger/ErrorLogs/components/LogHelper";
import LOG_TYPE from "entities/AppsmithConsole/logtype";
import type { Log, SourceEntity } from "entities/AppsmithConsole";
import { ENTITY_TYPE } from "@appsmith/entities/AppsmithConsole/utils";
import { getJsPaneDebuggerState } from "selectors/jsPaneSelectors";
import { setJsPaneDebuggerState } from "actions/jsPaneActions";
import { getIDEViewMode } from "selectors/ideSelectors";
import { EditorViewMode } from "@appsmith/entities/IDE/constants";
import ErrorLogs from "./Debugger/Errors";
import { isBrowserExecutionAllowed } from "@appsmith/utils/actionExecutionUtils";
import JSRemoteExecutionView from "@appsmith/components/JSRemoteExecutionView";
import { IDEBottomView, ViewHideBehaviour } from "../../IDE";
const ResponseTabWrapper = styled.div`
display: flex;
width: 100%;
height: 100%;
&.disable * {
opacity: 0.8;
pointer-events: none;
}
.response-run {
margin: 0 10px;
}
`;
const NoReturnValueWrapper = styled.div`
padding-left: ${(props) => props.theme.spaces[12]}px;
padding-top: ${(props) => props.theme.spaces[6]}px;
`;
export enum JSResponseState {
IsExecuting = "IsExecuting",
IsDirty = "IsDirty",
IsUpdating = "IsUpdating",
NoResponse = "NoResponse",
ShowResponse = "ShowResponse",
NoReturnValue = "NoReturnValue",
}
interface ReduxStateProps {
errorCount: number;
}
type Props = ReduxStateProps &
RouteComponentProps<JSEditorRouteParams> & {
currentFunction: JSAction | null;
theme?: EditorTheme;
errors: Array<EvaluationError>;
disabled: boolean;
isLoading: boolean;
onButtonClick: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
jsCollectionData: JSCollectionData | undefined;
};
function JSResponseView(props: Props) {
const {
currentFunction,
disabled,
errorCount,
errors,
isLoading,
jsCollectionData,
onButtonClick,
} = props;
const [responseStatus, setResponseStatus] = useState<JSResponseState>(
JSResponseState.NoResponse,
);
const jsObject = jsCollectionData?.config;
const responses = (jsCollectionData && jsCollectionData.data) || {};
const isDirty = (jsCollectionData && jsCollectionData.isDirty) || {};
const isExecuting = (jsCollectionData && jsCollectionData.isExecuting) || {};
const dispatch = useDispatch();
const response =
currentFunction && currentFunction.id && currentFunction.id in responses
? responses[currentFunction.id]
: "";
// parse error found while trying to execute function
const hasExecutionParseErrors = responseStatus === JSResponseState.IsDirty;
// error found while trying to parse JS Object
const hasJSObjectParseError = errors.length > 0;
const isSaving = useSelector(getIsSavingEntity);
useEffect(() => {
setResponseStatus(
getJSResponseViewState(
currentFunction,
isDirty,
isExecuting,
isSaving,
responses,
),
);
}, [responses, isExecuting, currentFunction, isSaving, isDirty]);
const filteredErrors = useSelector(getFilteredErrors);
let errorMessage: string | undefined;
let errorType = "ValidationError";
const localExecutionAllowed = useMemo(() => {
return isBrowserExecutionAllowed(
jsCollectionData?.config,
currentFunction || undefined,
);
}, [jsCollectionData?.config, currentFunction]);
// action source for analytics.
let actionSource: SourceEntity = {
type: ENTITY_TYPE.JSACTION,
name: "",
id: "",
};
try {
let errorObject: Log | undefined;
//get JS execution error from redux store.
if (
jsCollectionData &&
jsCollectionData.config &&
jsCollectionData.activeJSActionId
) {
every(filteredErrors, (error) => {
if (
includes(
error.id,
jsCollectionData?.config.id +
"-" +
jsCollectionData?.activeJSActionId,
)
) {
errorObject = error;
return false;
}
return true;
});
}
// update error message.
if (errorObject) {
if (errorObject.source) {
// update action source.
actionSource = errorObject.source;
}
if (errorObject.messages) {
// update error message.
errorMessage =
errorObject.messages[0].message.name +
": " +
errorObject.messages[0].message.message;
errorType = errorObject.messages[0].message.name;
}
}
} catch (e) {}
const ideViewMode = useSelector(getIDEViewMode);
const tabs: BottomTab[] = [
{
key: "response",
title: createMessage(DEBUGGER_RESPONSE),
panelComponent: (
<>
{localExecutionAllowed &&
(hasExecutionParseErrors ||
(hasJSObjectParseError && errorMessage)) && (
<ResponseTabErrorContainer>
<ResponseTabErrorContent>
<div className="t--js-response-parse-error-call-out">
{errorMessage}
</div>
<LogHelper
logType={LOG_TYPE.EVAL_ERROR}
name={errorType}
source={actionSource}
/>
</ResponseTabErrorContent>
</ResponseTabErrorContainer>
)}
<ResponseTabWrapper
className={errors.length && localExecutionAllowed ? "disable" : ""}
>
<Flex px="spaces-7" width="100%">
<>
{localExecutionAllowed && (
<>
{responseStatus === JSResponseState.NoResponse && (
<NoResponse
isButtonDisabled={disabled}
isQueryRunning={isLoading}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
onRunClick={onButtonClick}
/>
)}
{responseStatus === JSResponseState.IsExecuting && (
<LoadingOverlayScreen theme={props.theme}>
{createMessage(EXECUTING_FUNCTION)}
</LoadingOverlayScreen>
)}
{responseStatus === JSResponseState.NoReturnValue && (
<NoReturnValueWrapper>
<Text kind="body-m">
{createMessage(
NO_JS_FUNCTION_RETURN_VALUE,
currentFunction?.name,
)}
</Text>
</NoReturnValueWrapper>
)}
{responseStatus === JSResponseState.ShowResponse && (
<ReadOnlyEditor
folding
height={"100%"}
input={{
value: response as string,
}}
/>
)}
</>
)}
{!localExecutionAllowed && (
<JSRemoteExecutionView collectionData={jsCollectionData} />
)}
{responseStatus === JSResponseState.IsUpdating && (
<LoadingOverlayScreen theme={props.theme}>
{createMessage(UPDATING_JS_COLLECTION)}
</LoadingOverlayScreen>
)}
</>
</Flex>
</ResponseTabWrapper>
</>
),
},
{
key: DEBUGGER_TAB_KEYS.LOGS_TAB,
title: createMessage(DEBUGGER_LOGS),
panelComponent: <DebuggerLogs searchQuery={jsObject?.name} />,
},
];
if (ideViewMode === EditorViewMode.FullScreen) {
tabs.push({
key: DEBUGGER_TAB_KEYS.ERROR_TAB,
title: createMessage(DEBUGGER_ERRORS),
count: errorCount,
panelComponent: <ErrorLogs />,
});
}
// get the selected tab from the store.
const { open, responseTabHeight, selectedTab } = useSelector(
getJsPaneDebuggerState,
);
// set the selected tab in the store.
const setSelectedResponseTab = useCallback((selectedTab: string) => {
dispatch(setJsPaneDebuggerState({ open: true, selectedTab }));
}, []);
// set the height of the response pane on resize.
const setResponseHeight = useCallback((height: number) => {
dispatch(setJsPaneDebuggerState({ responseTabHeight: height }));
}, []);
// close the debugger
const onToggle = useCallback(
() => dispatch(setJsPaneDebuggerState({ open: !open })),
[open],
);
// Do not render if header tab is selected in the bottom bar.
return (
<IDEBottomView
behaviour={ViewHideBehaviour.COLLAPSE}
className="t--js-editor-bottom-pane-container"
height={responseTabHeight}
hidden={!open}
onHideClick={onToggle}
setHeight={setResponseHeight}
>
<EntityBottomTabs
isCollapsed={!open}
onSelect={setSelectedResponseTab}
selectedTabKey={selectedTab || ""}
tabs={tabs}
/>
</IDEBottomView>
);
}
const mapStateToProps = (state: AppState) => {
const errorCount = state.ui.debugger.context.errorCount;
return {
errorCount,
};
};
export default connect(mapStateToProps)(withRouter(JSResponseView));