import React, { useEffect, useRef, RefObject, useCallback, useState, } from "react"; import { connect, useDispatch } from "react-redux"; import { withRouter, RouteComponentProps } from "react-router"; import styled from "styled-components"; import { AppState } from "reducers"; import { JSEditorRouteParams } from "constants/routes"; import { createMessage, DEBUGGER_ERRORS, DEBUGGER_LOGS, EXECUTING_FUNCTION, PARSING_ERROR, EMPTY_RESPONSE_FIRST_HALF, EMPTY_JS_RESPONSE_LAST_HALF, NO_JS_FUNCTION_RETURN_VALUE, JS_ACTION_EXECUTION_ERROR, } from "@appsmith/constants/messages"; import { EditorTheme } from "./CodeEditor/EditorConfig"; import DebuggerLogs from "./Debugger/DebuggerLogs"; import ErrorLogs from "./Debugger/Errors"; import Resizer, { ResizerCSS } from "./Debugger/Resizer"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { JSCollection, JSAction } from "entities/JSCollection"; import ReadOnlyEditor from "components/editorComponents/ReadOnlyEditor"; import Text, { TextType } from "components/ads/Text"; import { Classes } from "components/ads/common"; import LoadingOverlayScreen from "components/editorComponents/LoadingOverlayScreen"; import { JSCollectionData } from "reducers/entityReducers/jsActionsReducer"; import Callout from "components/ads/Callout"; import { Variant } from "components/ads/common"; import { EvaluationError } from "utils/DynamicBindingUtils"; import { DebugButton } from "./Debugger/DebugCTA"; import { setCurrentTab } from "actions/debuggerActions"; import { DEBUGGER_TAB_KEYS } from "./Debugger/helpers"; import EntityBottomTabs from "./EntityBottomTabs"; import Icon from "components/ads/Icon"; import { TAB_MIN_HEIGHT } from "components/ads/Tabs"; import { theme } from "constants/DefaultTheme"; import { Button, Size } from "components/ads"; import { CodeEditorWithGutterStyles } from "pages/Editor/JSEditor/constants"; const ResponseContainer = styled.div` ${ResizerCSS} width: 100%; // Minimum height of bottom tabs as it can be resized min-height: ${TAB_MIN_HEIGHT}; background-color: ${(props) => props.theme.colors.apiPane.responseBody.bg}; height: ${({ theme }) => theme.actionsBottomTabInitialHeight}; .react-tabs__tab-panel { ${CodeEditorWithGutterStyles} overflow-y: auto; height: calc(100% - ${TAB_MIN_HEIGHT}); } `; const ResponseTabWrapper = styled.div` display: flex; height: 100%; width: 100%; &.disable * { opacity: 0.8; pointer-events: none; } .response-run { margin: 0 10px; } `; const TabbedViewWrapper = styled.div` height: 100%; &&& { ul.react-tabs__tab-list { padding: 0px ${(props) => props.theme.spaces[12]}px; height: ${TAB_MIN_HEIGHT}; } } `; const ResponseViewer = styled.div` width: 100%; `; const NoResponseContainer = styled.div` height: 100%; width: max-content; display: flex; align-items: center; justify-content: center; flex-direction: column; margin: 0 auto; &.empty { background-color: #fafafa; } .${Classes.ICON} { margin-right: 0px; svg { width: auto; height: 150px; } } .${Classes.TEXT} { margin-top: ${(props) => props.theme.spaces[9]}px; color: #090707; } `; const HelpSection = styled.div` padding-bottom: 5px; padding-top: 10px; `; const FailedMessage = styled.div` display: flex; align-items: center; margin-left: 5px; `; const StyledCallout = styled(Callout)` .${Classes.TEXT} { line-height: normal; } `; const NoReturnValueWrapper = styled.div` padding-left: ${(props) => props.theme.spaces[12]}px; padding-top: ${(props) => props.theme.spaces[6]}px; `; const InlineButton = styled(Button)` display: inline-flex; margin: 0 4px; `; enum JSResponseState { IsExecuting = "IsExecuting", IsDirty = "IsDirty", NoResponse = "NoResponse", ShowResponse = "ShowResponse", NoReturnValue = "NoReturnValue", } interface ReduxStateProps { responses: Record; isExecuting: Record; isDirtyList: Record; } type Props = ReduxStateProps & RouteComponentProps & { currentFunction: JSAction | null; theme?: EditorTheme; jsObject: JSCollection; errors: Array; disabled: boolean; isLoading: boolean; onButtonClick: (e: React.MouseEvent) => void; }; function JSResponseView(props: Props) { const { currentFunction, disabled, errors, isDirtyList, isExecuting, isLoading, jsObject, onButtonClick, responses, } = props; const [responseStatus, setResponseStatus] = useState( JSResponseState.NoResponse, ); const panelRef: RefObject = useRef(null); const dispatch = useDispatch(); const response = currentFunction && currentFunction.id && currentFunction.id in responses ? responses[currentFunction.id] : ""; const onDebugClick = useCallback(() => { AnalyticsUtil.logEvent("OPEN_DEBUGGER", { source: "JS_OBJECT", }); dispatch(setCurrentTab(DEBUGGER_TAB_KEYS.ERROR_TAB)); }, []); useEffect(() => { if (!currentFunction) { setResponseStatus(JSResponseState.NoResponse); } else if (isExecuting[currentFunction.id]) { setResponseStatus(JSResponseState.IsExecuting); } else if ( !responses.hasOwnProperty(currentFunction.id) && !isExecuting.hasOwnProperty(currentFunction.id) ) { setResponseStatus(JSResponseState.NoResponse); } else if ( responses.hasOwnProperty(currentFunction.id) && isDirtyList[currentFunction.id] ) { setResponseStatus(JSResponseState.IsDirty); } else if ( responses.hasOwnProperty(currentFunction.id) && responses[currentFunction.id] === undefined ) { setResponseStatus(JSResponseState.NoReturnValue); } else if (responses.hasOwnProperty(currentFunction.id)) { setResponseStatus(JSResponseState.ShowResponse); } }, [responses, isExecuting, currentFunction]); const tabs = [ { key: "body", title: "Response", panelComponent: ( <> {(errors.length > 0 || responseStatus === JSResponseState.IsDirty) && ( } text={ responseStatus === JSResponseState.IsDirty ? createMessage( JS_ACTION_EXECUTION_ERROR, `${jsObject.name}.${currentFunction?.name}`, ) : createMessage(PARSING_ERROR) } variant={Variant.danger} /> )} <> {responseStatus === JSResponseState.NoResponse && ( {createMessage(EMPTY_RESPONSE_FIRST_HALF)} {createMessage(EMPTY_JS_RESPONSE_LAST_HALF)} )} {responseStatus === JSResponseState.IsExecuting && ( {createMessage(EXECUTING_FUNCTION)} )} {responseStatus === JSResponseState.NoReturnValue && ( {createMessage( NO_JS_FUNCTION_RETURN_VALUE, currentFunction?.name, )} )} {responseStatus === JSResponseState.ShowResponse && ( )} ), }, { key: DEBUGGER_TAB_KEYS.ERROR_TAB, title: createMessage(DEBUGGER_ERRORS), panelComponent: , }, { key: DEBUGGER_TAB_KEYS.LOGS_TAB, title: createMessage(DEBUGGER_LOGS), panelComponent: , }, ]; return ( ); } const mapStateToProps = ( state: AppState, props: { jsObject: JSCollection }, ) => { const jsActions = state.entities.jsActions; const { jsObject } = props; const seletedJsObject = jsObject && jsActions.find( (action: JSCollectionData) => action.config.id === jsObject.id, ); const responses = (seletedJsObject && seletedJsObject.data) || {}; const isDirtyList = (seletedJsObject && seletedJsObject.isDirty) || {}; const isExecuting = (seletedJsObject && seletedJsObject.isExecuting) || {}; return { responses, isExecuting, isDirtyList, }; }; export default connect(mapStateToProps)(withRouter(JSResponseView));