import React, { 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 { ActionResponse } from "api/ActionAPI"; import { formatBytes } from "utils/helpers"; import { APIEditorRouteParams } from "constants/routes"; import LoadingOverlayScreen from "components/editorComponents/LoadingOverlayScreen"; import ReadOnlyEditor from "components/editorComponents/ReadOnlyEditor"; import { getActionResponses } from "selectors/entitiesSelector"; import { Colors } from "constants/Colors"; import { isArray, isEmpty, isString } from "lodash"; import { CHECK_REQUEST_BODY, createMessage, DEBUGGER_ERRORS, DEBUGGER_LOGS, EMPTY_RESPONSE_FIRST_HALF, EMPTY_RESPONSE_LAST_HALF, INSPECT_ENTITY, } from "@appsmith/constants/messages"; import Text, { TextType } from "components/ads/Text"; import { Text as BlueprintText } from "@blueprintjs/core"; import Icon from "components/ads/Icon"; import { Classes, Variant } from "components/ads/common"; import { EditorTheme } from "./CodeEditor/EditorConfig"; import Callout from "components/ads/Callout"; import DebuggerLogs from "./Debugger/DebuggerLogs"; import ErrorLogs from "./Debugger/Errors"; import Resizer, { ResizerCSS } from "./Debugger/Resizer"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { DebugButton } from "./Debugger/DebugCTA"; import EntityDeps from "./Debugger/EntityDependecies"; import Button, { Size } from "components/ads/Button"; import EntityBottomTabs from "./EntityBottomTabs"; import { DEBUGGER_TAB_KEYS } from "./Debugger/helpers"; import { setCurrentTab } from "actions/debuggerActions"; import Table from "pages/Editor/QueryEditor/Table"; import { API_RESPONSE_TYPE_OPTIONS } from "constants/ApiEditorConstants"; import { setActionResponseDisplayFormat, UpdateActionPropertyActionPayload, } from "actions/pluginActionActions"; import { isHtml } from "./utils"; type TextStyleProps = { accent: "primary" | "secondary" | "error"; }; export const BaseText = styled(BlueprintText)``; const ResponseContainer = styled.div` ${ResizerCSS} // Initial height of bottom tabs height: ${(props) => props.theme.actionsBottomTabInitialHeight}; width: 100%; // Minimum height of bottom tabs as it can be resized min-height: 36px; background-color: ${(props) => props.theme.colors.apiPane.responseBody.bg}; .react-tabs__tab-panel { overflow: hidden; } .react-tabs__tab-panel > * { padding-bottom: 10px; } `; const ResponseMetaInfo = styled.div` display: flex; ${BaseText} { color: #768896; margin-left: ${(props) => props.theme.spaces[9]}px; } `; const ResponseMetaWrapper = styled.div` align-items: center; display: flex; position: absolute; right: ${(props) => props.theme.spaces[12]}px; top: ${(props) => props.theme.spaces[4]}px; `; const ResponseTabWrapper = styled.div` display: flex; flex-direction: column; height: 100%; width: 100%; `; const TabbedViewWrapper = styled.div` height: 100%; &&& { ul.react-tabs__tab-list { margin: 0px ${(props) => props.theme.spaces[11]}px; } } & { .react-tabs__tab-panel { height: calc(100% - 32px); } } `; const SectionDivider = styled.div` height: 1px; width: 100%; background: ${(props) => props.theme.colors.apiPane.dividerBg}; `; const Flex = styled.div` display: flex; align-items: center; margin-left: 20px; span:first-child { margin-right: ${(props) => props.theme.spaces[1] + 1}px; } `; const NoResponseContainer = styled.div` flex: 1; width: 100%; display: flex; align-items: center; justify-content: center; flex-direction: column; .${Classes.ICON} { margin-right: 0px; svg { width: 150px; height: 150px; } } .${Classes.TEXT} { margin-top: ${(props) => props.theme.spaces[9]}px; } `; const FailedMessage = styled.div` display: flex; align-items: center; margin-left: 5px; .api-debugcta { margin-top: 0px; } `; const StyledCallout = styled(Callout)` .${Classes.TEXT} { line-height: normal; } `; export const InlineButton = styled(Button)` display: inline-flex; margin: 0 8px; `; const HelpSection = styled.div` padding-bottom: 5px; padding-top: 10px; `; const ResponseBodyContainer = styled.div` padding-top: 10px; overflow-y: auto; height: 100%; display: grid; `; interface ReduxStateProps { responses: Record; isRunning: Record; } interface ReduxDispatchProps { updateActionResponseDisplayFormat: ({ field, id, value, }: UpdateActionPropertyActionPayload) => void; } type Props = ReduxStateProps & ReduxDispatchProps & RouteComponentProps & { theme?: EditorTheme; apiName: string; onRunClick: () => void; responseDataTypes: { key: string; title: string }[]; responseDisplayFormat: { title: string; value: string }; }; export const EMPTY_RESPONSE: ActionResponse = { statusCode: "", duration: "", body: {}, headers: {}, request: { headers: {}, body: {}, httpMethod: "", url: "", }, size: "", responseDisplayFormat: "", dataTypes: [], }; const StatusCodeText = styled(BaseText)<{ code: string }>` color: ${(props) => props.code.startsWith("2") ? props.theme.colors.primaryOld : Colors.RED}; cursor: pointer; width: 38px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; &:hover { width: 100%; } `; const ResponseDataContainer = styled.div` flex: 1; overflow: auto; display: flex; padding-bottom: 10px; flex-direction: column; & .CodeEditorTarget { overflow: hidden; } `; export const TableCellHeight = 39; export const responseTabComponent = ( responseType: string, output: any, tableBodyHeight?: number, ): JSX.Element => { return { [API_RESPONSE_TYPE_OPTIONS.JSON]: ( ), [API_RESPONSE_TYPE_OPTIONS.TABLE]: ( ), [API_RESPONSE_TYPE_OPTIONS.RAW]: ( ), }[responseType]; }; function ApiResponseView(props: Props) { const { match: { params: { apiId }, }, responseDataTypes, responseDisplayFormat, responses, updateActionResponseDisplayFormat, } = props; let response: ActionResponse = EMPTY_RESPONSE; let isRunning = false; let hasFailed = false; if (apiId && apiId in responses) { response = responses[apiId] || EMPTY_RESPONSE; isRunning = props.isRunning[apiId]; hasFailed = response.statusCode ? response.statusCode[0] !== "2" : false; } const panelRef: RefObject = useRef(null); const dispatch = useDispatch(); const onDebugClick = useCallback(() => { AnalyticsUtil.logEvent("OPEN_DEBUGGER", { source: "API", }); dispatch(setCurrentTab(DEBUGGER_TAB_KEYS.ERROR_TAB)); }, []); const onRunClick = () => { props.onRunClick(); AnalyticsUtil.logEvent("RESPONSE_TAB_RUN_ACTION_CLICK", { source: "API_PANE", }); }; const [tableBodyHeight, setTableBodyHeightHeight] = useState( window.innerHeight, ); const messages = response?.messages; let responseHeaders = {}; // if no headers are present in the response, use the default body text. if (response.headers) { Object.entries(response.headers).forEach(([key, value]) => { if (isArray(value) && value.length < 2) return (responseHeaders = { ...responseHeaders, [key]: value[0], }); return (responseHeaders = { ...responseHeaders, [key]: value, }); }); } else { // if the response headers is empty show an empty object. responseHeaders = {}; } const responseTabs = responseDataTypes && responseDataTypes.map((dataType, index) => { return { index: index, key: dataType.key, title: dataType.title, panelComponent: responseTabComponent( dataType.key, response.body, tableBodyHeight, ), }; }); const onResponseTabSelect = (tab: any) => { updateActionResponseDisplayFormat({ id: apiId ? apiId : "", field: "responseDisplayFormat", value: tab.title, }); }; const selectedTabIndex = responseDataTypes && responseDataTypes.findIndex( (dataType) => dataType.title === responseDisplayFormat?.title, ); const tabs = [ { key: "response", title: "Response", panelComponent: ( {Array.isArray(messages) && messages.length > 0 && ( {messages.map((msg, i) => ( ))} )} {hasFailed && !isRunning && ( } text={createMessage(CHECK_REQUEST_BODY)} variant={Variant.danger} /> )} {isEmpty(response.statusCode) ? ( {EMPTY_RESPONSE_FIRST_HALF()} {EMPTY_RESPONSE_LAST_HALF()} ) : ( {isString(response.body) && isHtml(response.body) ? ( ) : responseTabs && responseTabs.length > 0 && selectedTabIndex !== -1 ? ( ) : null} )} ), }, { key: "headers", title: "Headers", panelComponent: ( {hasFailed && !isRunning && ( } text={createMessage(CHECK_REQUEST_BODY)} variant={Variant.danger} /> )} {isEmpty(response.statusCode) ? ( {EMPTY_RESPONSE_FIRST_HALF()} {EMPTY_RESPONSE_LAST_HALF()} ) : ( )} ), }, { key: DEBUGGER_TAB_KEYS.ERROR_TAB, title: createMessage(DEBUGGER_ERRORS), panelComponent: , }, { key: DEBUGGER_TAB_KEYS.LOGS_TAB, title: createMessage(DEBUGGER_LOGS), panelComponent: , }, { key: DEBUGGER_TAB_KEYS.INSPECT_TAB, title: createMessage(INSPECT_ENTITY), panelComponent: , }, ]; return ( // TableCellHeight in this case is the height of one table cell in pixels. setTableBodyHeightHeight(height - TableCellHeight) } /> {isRunning && ( Sending Request )} {response.statusCode && ( {response.statusCode && ( Status: {response.statusCode} )} {response.duration && ( Time: {response.duration} ms )} {response.size && ( Size: {formatBytes(parseInt(response.size))} )} {!isEmpty(response.body) && Array.isArray(response.body) && ( Result: {`${response.body.length} Record${ response.body.length > 1 ? "s" : "" }`} )} )} ); } const mapStateToProps = (state: AppState): ReduxStateProps => { return { responses: getActionResponses(state), isRunning: state.ui.apiPane.isRunning, }; }; const mapDispatchToProps = (dispatch: any): ReduxDispatchProps => ({ updateActionResponseDisplayFormat: ({ field, id, value, }: UpdateActionPropertyActionPayload) => { dispatch(setActionResponseDisplayFormat({ id, field, value })); }, }); export default connect( mapStateToProps, mapDispatchToProps, )(withRouter(ApiResponseView));