diff --git a/app/client/src/PluginActionEditor/PluginActionContext.tsx b/app/client/src/PluginActionEditor/PluginActionContext.tsx index 670efc348112..42c0d4e67458 100644 --- a/app/client/src/PluginActionEditor/PluginActionContext.tsx +++ b/app/client/src/PluginActionEditor/PluginActionContext.tsx @@ -6,14 +6,16 @@ import React, { } from "react"; import type { Action } from "entities/Action"; import type { Plugin } from "api/PluginApi"; -import type { Datasource } from "entities/Datasource"; +import type { Datasource, EmbeddedRestDatasource } from "entities/Datasource"; +import type { ActionResponse } from "api/ActionAPI"; interface PluginActionContextType { action: Action; + actionResponse?: ActionResponse; editorConfig: unknown[]; settingsConfig: unknown[]; plugin: Plugin; - datasource?: Datasource; + datasource?: EmbeddedRestDatasource | Datasource; } // No need to export this context to use it. Use the hook defined below instead diff --git a/app/client/src/PluginActionEditor/PluginActionEditor.tsx b/app/client/src/PluginActionEditor/PluginActionEditor.tsx index 5ffa8a4d777e..8eaa3bc51bac 100644 --- a/app/client/src/PluginActionEditor/PluginActionEditor.tsx +++ b/app/client/src/PluginActionEditor/PluginActionEditor.tsx @@ -4,6 +4,7 @@ import { identifyEntityFromPath } from "../navigation/FocusEntity"; import { useSelector } from "react-redux"; import { getActionByBaseId, + getActionResponses, getDatasource, getEditorConfig, getPlugin, @@ -39,6 +40,8 @@ const PluginActionEditor = (props: ChildrenProps) => { const editorConfig = useSelector((state) => getEditorConfig(state, pluginId)); + const actionResponses = useSelector(getActionResponses); + if (!isEditorInitialized) { return ( @@ -71,9 +74,12 @@ const PluginActionEditor = (props: ChildrenProps) => { ); } + const actionResponse = actionResponses[action.id]; + return ( dispatch(setApiPaneDebuggerState({ open: !open })), + [dispatch, open], + ); + + const updateSelectedResponseTab = useCallback( + (tabKey: string) => { + if (tabKey === DEBUGGER_TAB_KEYS.ERROR_TAB) { + AnalyticsUtil.logEvent("OPEN_DEBUGGER", { + source: "API_PANE", + }); + } + + dispatch(setApiPaneDebuggerState({ open: true, selectedTab: tabKey })); + }, + [dispatch], + ); + + const updateResponsePaneHeight = useCallback( + (height: number) => { + dispatch(setApiPaneDebuggerState({ responseTabHeight: height })); + }, + [dispatch], + ); + + return ( + + ); +} + +export default PluginActionResponse; diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/components/ApiFormatSegmentedResponse.tsx b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/ApiFormatSegmentedResponse.tsx new file mode 100644 index 000000000000..4fe9f53b9054 --- /dev/null +++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/ApiFormatSegmentedResponse.tsx @@ -0,0 +1,126 @@ +import React, { useCallback, useMemo, useState } from "react"; +import { isArray, isString } from "lodash"; +import { isHtml } from "../utils"; +import ReadOnlyEditor from "components/editorComponents/ReadOnlyEditor"; +import { SegmentedControlContainer } from "pages/Editor/QueryEditor/EditorJSONtoForm"; +import { Flex, SegmentedControl } from "@appsmith/ads"; +import type { ActionResponse } from "api/ActionAPI"; +import { setActionResponseDisplayFormat } from "actions/pluginActionActions"; +import { actionResponseDisplayDataFormats } from "pages/Editor/utils"; +import { ResponseDisplayFormats } from "constants/ApiEditorConstants/CommonApiConstants"; +import { useDispatch } from "react-redux"; +import styled from "styled-components"; +import { ResponseFormatTabs } from "./ResponseFormatTabs"; + +const ResponseBodyContainer = styled.div` + overflow-y: clip; + height: 100%; + display: grid; +`; + +function ApiFormatSegmentedResponse(props: { + actionResponse: ActionResponse; + actionId: string; + responseTabHeight: number; +}) { + const dispatch = useDispatch(); + const onResponseTabSelect = useCallback( + (tab: string) => { + dispatch( + setActionResponseDisplayFormat({ + id: props.actionId, + field: "responseDisplayFormat", + value: tab, + }), + ); + }, + [dispatch, props.actionId], + ); + + const { responseDataTypes, responseDisplayFormat } = + actionResponseDisplayDataFormats(props.actionResponse); + + let filteredResponseDataTypes: { key: string; title: string }[] = [ + ...responseDataTypes, + ]; + + if (!!props.actionResponse.body && !isArray(props.actionResponse.body)) { + filteredResponseDataTypes = responseDataTypes.filter( + (item) => item.key !== ResponseDisplayFormats.TABLE, + ); + + if (responseDisplayFormat.title === ResponseDisplayFormats.TABLE) { + onResponseTabSelect(filteredResponseDataTypes[0]?.title); + } + } + + const responseTabs = filteredResponseDataTypes?.map((dataType, index) => ({ + index: index, + key: dataType.key, + title: dataType.title, + panelComponent: ( + []} + responseType={dataType.key} + tableBodyHeight={props.responseTabHeight} + /> + ), + })); + + const segmentedControlOptions = responseTabs?.map((item) => ({ + value: item.key, + label: item.title, + })); + + const onChange = useCallback( + (value: string) => { + setSelectedControl(value); + onResponseTabSelect(value); + }, + [onResponseTabSelect], + ); + + const [selectedControl, setSelectedControl] = useState( + segmentedControlOptions[0]?.value, + ); + + const selectedTabIndex = filteredResponseDataTypes?.findIndex( + (dataType) => dataType.title === responseDisplayFormat?.title, + ); + + const value = useMemo( + () => ({ value: props.actionResponse.body as string }), + [props.actionResponse.body], + ); + + return ( + + {isString(props.actionResponse?.body) && + isHtml(props.actionResponse?.body) ? ( + + ) : responseTabs && responseTabs.length > 0 && selectedTabIndex !== -1 ? ( + + + + + [] + } + responseType={selectedControl || segmentedControlOptions[0]?.value} + tableBodyHeight={props.responseTabHeight} + /> + + ) : null} + + ); +} + +export default ApiFormatSegmentedResponse; diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/components/ApiResponse.tsx b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/ApiResponse.tsx new file mode 100644 index 000000000000..bb32f43ad198 --- /dev/null +++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/ApiResponse.tsx @@ -0,0 +1,170 @@ +import React, { useMemo } from "react"; +import ReactJson from "react-json-view"; +import { isEmpty, noop } from "lodash"; +import styled from "styled-components"; +import { Callout, Flex } from "@appsmith/ads"; +import { + JsonWrapper, + reactJsonProps, +} from "components/editorComponents/Debugger/ErrorLogs/components/LogCollapseData"; +import type { ActionResponse } from "api/ActionAPI"; +import type { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig"; +import type { SourceEntity } from "entities/AppsmithConsole"; +import ApiResponseMeta from "components/editorComponents/ApiResponseMeta"; +import ActionExecutionInProgressView from "components/editorComponents/ActionExecutionInProgressView"; +import LogAdditionalInfo from "components/editorComponents/Debugger/ErrorLogs/components/LogAdditionalInfo"; +import LogHelper from "components/editorComponents/Debugger/ErrorLogs/components/LogHelper"; +import LOG_TYPE from "entities/AppsmithConsole/logtype"; +import { type Action } from "entities/Action"; +import { hasFailed } from "../utils"; +import { getUpdateTimestamp } from "components/editorComponents/Debugger/ErrorLogs/ErrorLogItem"; +import { ENTITY_TYPE } from "ee/entities/AppsmithConsole/utils"; +import ApiFormatSegmentedResponse from "./ApiFormatSegmentedResponse"; +import { NoResponse } from "./NoResponse"; + +const HelpSection = styled.div` + padding-bottom: 5px; + padding-top: 10px; +`; + +const ResponseDataContainer = styled.div` + flex: 1; + overflow: auto; + display: flex; + flex-direction: column; + + & .CodeEditorTarget { + overflow: hidden; + } +`; + +export const ResponseTabErrorContainer = styled.div` + display: flex; + flex-direction: column; + padding: 8px 16px; + gap: 8px; + height: fit-content; + background: var(--ads-v2-color-bg-error); + border-bottom: 1px solid var(--ads-v2-color-border); +`; + +export const ResponseTabErrorContent = styled.div` + display: flex; + align-items: flex-start; + gap: 4px; + font-size: 12px; + line-height: 16px; +`; + +export const ResponseTabErrorDefaultMessage = styled.div` + flex-shrink: 0; +`; + +export const apiReactJsonProps = { ...reactJsonProps, collapsed: 0 }; + +export function ApiResponse(props: { + action: Action; + actionResponse?: ActionResponse; + isRunning: boolean; + isRunDisabled: boolean; + theme: EditorTheme; + onRunClick: () => void; + responseTabHeight: number; +}) { + const { id, name } = props.action; + const actionSource: SourceEntity = useMemo( + () => ({ + type: ENTITY_TYPE.ACTION, + name, + id, + }), + [name, id], + ); + + if (!props.actionResponse) { + return ( + + + + ); + } + + const { messages, pluginErrorDetails, request } = props.actionResponse; + + const runHasFailed = hasFailed(props.actionResponse); + const requestWithTimestamp = getUpdateTimestamp(request); + + return ( + + + {Array.isArray(messages) && messages.length > 0 && ( + + {messages.map((message, i) => ( + + {message} + + ))} + + )} + {props.isRunning && ( + + )} + {runHasFailed && !props.isRunning ? ( + + + + Your API failed to execute + {pluginErrorDetails && ":"} + + {pluginErrorDetails && ( + <> +
+ {pluginErrorDetails.downstreamErrorMessage} +
+ {pluginErrorDetails.downstreamErrorCode && ( + + )} + + )} + +
+ {requestWithTimestamp && ( + + + + )} +
+ ) : ( + + {isEmpty(props.actionResponse.statusCode) ? ( + + ) : ( + + )} + + )} +
+ ); +} diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/components/ApiResponseHeaders.tsx b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/ApiResponseHeaders.tsx new file mode 100644 index 000000000000..7f055d5bc4f3 --- /dev/null +++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/ApiResponseHeaders.tsx @@ -0,0 +1,111 @@ +import React, { useMemo } from "react"; +import type { ActionResponse } from "api/ActionAPI"; +import { Callout, Flex } from "@appsmith/ads"; +import { CHECK_REQUEST_BODY, createMessage } from "ee/constants/messages"; +import { isArray, isEmpty } from "lodash"; +import ReadOnlyEditor from "components/editorComponents/ReadOnlyEditor"; +import { hasFailed } from "../utils"; +import styled from "styled-components"; +import { NoResponse } from "./NoResponse"; + +const ResponseDataContainer = styled.div` + flex: 1; + overflow: auto; + display: flex; + flex-direction: column; + + & .CodeEditorTarget { + overflow: hidden; + } +`; + +const headersTransformer = (headers: Record = {}) => { + let responseHeaders = {}; + + // if no headers are present in the response, use the default body text. + if (headers) { + Object.entries(headers).forEach(([key, value]) => { + if (isArray(value) && value.length < 2) { + responseHeaders = { + ...responseHeaders, + [key]: value[0], + }; + + return; + } + + responseHeaders = { + ...responseHeaders, + [key]: value, + }; + }); + } + + return responseHeaders; +}; + +export function ApiResponseHeaders(props: { + isRunning: boolean; + onDebugClick: () => void; + actionResponse?: ActionResponse; + isRunDisabled: boolean; + onRunClick: () => void; +}) { + const responseHeaders = useMemo(() => { + return headersTransformer(props.actionResponse?.headers); + }, [props.actionResponse?.headers]); + + const errorCalloutLinks = useMemo(() => { + return [ + { + children: "Debug", + endIcon: "bug", + onClick: props.onDebugClick, + to: "", + }, + ]; + }, [props.onDebugClick]); + + const headersInput = useMemo(() => { + return { + value: !isEmpty(responseHeaders) + ? JSON.stringify(responseHeaders, null, 2) + : "", + }; + }, [responseHeaders]); + + if (!props.actionResponse) { + return ( + + + + ); + } + + const runHasFailed = hasFailed(props.actionResponse); + + return ( + + {runHasFailed && !props.isRunning && ( + + {createMessage(CHECK_REQUEST_BODY)} + + )} + + {isEmpty(props.actionResponse.statusCode) ? ( + + ) : ( + + )} + + + ); +} diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/components/NoResponse.tsx b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/NoResponse.tsx new file mode 100644 index 000000000000..a133ec66556a --- /dev/null +++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/NoResponse.tsx @@ -0,0 +1,65 @@ +import NoResponseSVG from "assets/images/no-response.svg"; +import { Classes, Text, TextType } from "@appsmith/ads-old"; +import { + EMPTY_RESPONSE_FIRST_HALF, + EMPTY_RESPONSE_LAST_HALF, +} from "ee/constants/messages"; +import { Button } from "@appsmith/ads"; +import React from "react"; +import styled from "styled-components"; + +const StyledText = styled(Text)` + &&&& { + margin-top: 0; + } +`; + +const NoResponseContainer = styled.div` + flex: 1; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + + .${Classes.ICON} { + margin-right: 0; + + svg { + width: 150px; + height: 150px; + } + } + + .${Classes.TEXT} { + margin-top: ${(props) => props.theme.spaces[9]}px; + } +`; + +interface NoResponseProps { + isRunDisabled: boolean; + isRunning: boolean; + onRunClick: () => void; +} + +export const NoResponse = ({ + isRunDisabled, + isRunning, + onRunClick, +}: NoResponseProps) => ( + + no-response-yet +
+ {EMPTY_RESPONSE_FIRST_HALF()} + + {EMPTY_RESPONSE_LAST_HALF()} +
+
+); diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/components/ResponseFormatTabs.tsx b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/ResponseFormatTabs.tsx new file mode 100644 index 000000000000..eb8dec8a7bf7 --- /dev/null +++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/ResponseFormatTabs.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import { ResponseDisplayFormats } from "constants/ApiEditorConstants/CommonApiConstants"; +import ReadOnlyEditor from "components/editorComponents/ReadOnlyEditor"; +import { isString } from "lodash"; +import Table from "pages/Editor/QueryEditor/Table"; + +type ResponseData = string | Record[]; + +const inputValue = (data: ResponseData) => { + return { + value: isString(data) ? data : JSON.stringify(data, null, 2), + }; +}; + +const tableValue = (data: ResponseData): Record[] => { + if (isString(data)) { + return [{}]; + } + + return data; +}; + +export const ResponseFormatTabs = (props: { + responseType: string; + data: ResponseData; + tableBodyHeight?: number; +}) => { + switch (props.responseType) { + case ResponseDisplayFormats.JSON: + return ( + + ); + case ResponseDisplayFormats.TABLE: + return ( + + ); + case ResponseDisplayFormats.RAW: + return ( + + ); + default: + return ( + + ); + } +}; diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/hooks/index.ts b/app/client/src/PluginActionEditor/components/PluginActionResponse/hooks/index.ts new file mode 100644 index 000000000000..2ae6129b3bc5 --- /dev/null +++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/hooks/index.ts @@ -0,0 +1 @@ +export { default as usePluginActionResponseTabs } from "ee/PluginActionEditor/components/PluginActionResponse/hooks/usePluginActionResponseTabs"; diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/index.ts b/app/client/src/PluginActionEditor/components/PluginActionResponse/index.ts new file mode 100644 index 000000000000..34d4978ec706 --- /dev/null +++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/index.ts @@ -0,0 +1 @@ +export { default } from "./PluginActionResponse"; diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/utils/actionHasFailed.test.ts b/app/client/src/PluginActionEditor/components/PluginActionResponse/utils/actionHasFailed.test.ts new file mode 100644 index 000000000000..d28645ec5bc5 --- /dev/null +++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/utils/actionHasFailed.test.ts @@ -0,0 +1,30 @@ +import actionHasFailed from "./actionHasFailed"; +import type { ActionResponse } from "api/ActionAPI"; + +describe("actionHasFailed", () => { + it("Should only check the status code", () => { + const input: ActionResponse = { + body: "Success", + dataTypes: [], + duration: "200", + headers: {}, + size: "200", + statusCode: "404", + }; + + expect(actionHasFailed(input)).toBe(true); + }); + + it("Checks the 200 series of status code", () => { + const input: ActionResponse = { + body: "Success", + dataTypes: [], + duration: "200", + headers: {}, + size: "200", + statusCode: "201", + }; + + expect(actionHasFailed(input)).toBe(false); + }); +}); diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/utils/actionHasFailed.ts b/app/client/src/PluginActionEditor/components/PluginActionResponse/utils/actionHasFailed.ts new file mode 100644 index 000000000000..6352a8a09613 --- /dev/null +++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/utils/actionHasFailed.ts @@ -0,0 +1,9 @@ +import type { ActionResponse } from "api/ActionAPI"; + +function hasFailed(actionResponse: ActionResponse) { + return actionResponse.statusCode + ? actionResponse.statusCode[0] !== "2" + : false; +} + +export default hasFailed; diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/utils/index.ts b/app/client/src/PluginActionEditor/components/PluginActionResponse/utils/index.ts new file mode 100644 index 000000000000..62ec8938b3e4 --- /dev/null +++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/utils/index.ts @@ -0,0 +1,2 @@ +export { default as hasFailed } from "./actionHasFailed"; +export { default as isHtml } from "./isHtml"; diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/utils/isHtml.test.ts b/app/client/src/PluginActionEditor/components/PluginActionResponse/utils/isHtml.test.ts new file mode 100644 index 000000000000..6e014afafee7 --- /dev/null +++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/utils/isHtml.test.ts @@ -0,0 +1,39 @@ +import { isHtml } from "./index"; + +describe("isHtml", () => { + it("returns false for empty string", () => { + const input = ""; + + expect(isHtml(input)).toBe(false); + }); + + it("returns false for JSON", () => { + const input = `{"name": "test"}`; + + expect(isHtml(input)).toBe(false); + }); + + it("returns false for string", () => { + const input = "An error string returned"; + + expect(isHtml(input)).toBe(false); + }); + + it("returns false for invalid html", () => { + const input = " { + const input = "

This is incomplete"; + + expect(isHtml(input)).toBe(true); + }); + + it("returns true for HTML", () => { + const input = "

This is a html response

"; + + expect(isHtml(input)).toBe(true); + }); +}); diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/utils/isHtml.ts b/app/client/src/PluginActionEditor/components/PluginActionResponse/utils/isHtml.ts new file mode 100644 index 000000000000..551f1d66bfdf --- /dev/null +++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/utils/isHtml.ts @@ -0,0 +1,25 @@ +import log from "loglevel"; + +const isHtml = (str: string): boolean => { + try { + const doc = new DOMParser().parseFromString(str, "text/html"); + + // Check for parsing errors + const parseError = doc.querySelector("parsererror"); + + if (parseError) { + return false; + } + + // Check for at least one element node in the body + return Array.from(doc.body.childNodes).some( + (node: ChildNode) => node.nodeType === 1, + ); + } catch (error) { + log.error("Error parsing HTML:", error); + + return false; + } +}; + +export default isHtml; diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponsePane.tsx b/app/client/src/PluginActionEditor/components/PluginActionResponsePane.tsx deleted file mode 100644 index 5a0be861970c..000000000000 --- a/app/client/src/PluginActionEditor/components/PluginActionResponsePane.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react"; - -const PluginActionResponsePane = () => { - return
; -}; - -export default PluginActionResponsePane; diff --git a/app/client/src/PluginActionEditor/index.ts b/app/client/src/PluginActionEditor/index.ts index 0a58d00bdaae..20265c8bc5a9 100644 --- a/app/client/src/PluginActionEditor/index.ts +++ b/app/client/src/PluginActionEditor/index.ts @@ -5,4 +5,4 @@ export { } from "./PluginActionContext"; export { default as PluginActionToolbar } from "./components/PluginActionToolbar"; export { default as PluginActionForm } from "./components/PluginActionForm"; -export { default as PluginActionResponsePane } from "./components/PluginActionResponsePane"; +export { default as PluginActionResponse } from "./components/PluginActionResponse"; diff --git a/app/client/src/ce/PluginActionEditor/components/PluginActionResponse/hooks/usePluginActionResponseTabs.tsx b/app/client/src/ce/PluginActionEditor/components/PluginActionResponse/hooks/usePluginActionResponseTabs.tsx new file mode 100644 index 000000000000..8e44395d5a03 --- /dev/null +++ b/app/client/src/ce/PluginActionEditor/components/PluginActionResponse/hooks/usePluginActionResponseTabs.tsx @@ -0,0 +1,87 @@ +import React from "react"; +import { usePluginActionContext } from "PluginActionEditor/PluginActionContext"; +import type { BottomTab } from "components/editorComponents/EntityBottomTabs"; +import { getIDEViewMode } from "selectors/ideSelectors"; +import { useSelector } from "react-redux"; +import { EditorViewMode } from "ee/entities/IDE/constants"; +import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/helpers"; +import { + createMessage, + DEBUGGER_ERRORS, + DEBUGGER_HEADERS, + DEBUGGER_LOGS, + DEBUGGER_RESPONSE, +} from "ee/constants/messages"; +import ErrorLogs from "components/editorComponents/Debugger/Errors"; +import DebuggerLogs from "components/editorComponents/Debugger/DebuggerLogs"; +import { PluginType } from "entities/Action"; +import { ApiResponse } from "PluginActionEditor/components/PluginActionResponse/components/ApiResponse"; +import { ApiResponseHeaders } from "PluginActionEditor/components/PluginActionResponse/components/ApiResponseHeaders"; +import { noop } from "lodash"; +import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig"; +import { getErrorCount } from "selectors/debuggerSelectors"; +import { getApiPaneDebuggerState } from "selectors/apiPaneSelectors"; + +function usePluginActionResponseTabs() { + const { action, actionResponse, plugin } = usePluginActionContext(); + + const IDEViewMode = useSelector(getIDEViewMode); + const errorCount = useSelector(getErrorCount); + + const { responseTabHeight } = useSelector(getApiPaneDebuggerState); + + const tabs: BottomTab[] = []; + + if (IDEViewMode === EditorViewMode.FullScreen) { + tabs.push( + { + key: DEBUGGER_TAB_KEYS.ERROR_TAB, + title: createMessage(DEBUGGER_ERRORS), + count: errorCount, + panelComponent: , + }, + { + key: DEBUGGER_TAB_KEYS.LOGS_TAB, + title: createMessage(DEBUGGER_LOGS), + panelComponent: , + }, + ); + } + + if (plugin.type === PluginType.API) { + return tabs.concat([ + { + key: DEBUGGER_TAB_KEYS.RESPONSE_TAB, + title: createMessage(DEBUGGER_RESPONSE), + panelComponent: ( + + ), + }, + { + key: DEBUGGER_TAB_KEYS.HEADER_TAB, + title: createMessage(DEBUGGER_HEADERS), + panelComponent: ( + + ), + }, + ]); + } + + return tabs; +} + +export default usePluginActionResponseTabs; diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index ce9dd82b126d..b9ca94d7ffda 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -555,7 +555,9 @@ export const NO_LOGS = () => "No logs to show"; export const NO_ERRORS = () => "No signs of trouble here!"; export const DEBUGGER_ERRORS = () => "Errors"; export const DEBUGGER_RESPONSE = () => "Response"; +export const DEBUGGER_HEADERS = () => "Headers"; export const DEBUGGER_LOGS = () => "Logs"; + export const INSPECT_ENTITY = () => "Inspect entity"; export const INSPECT_ENTITY_BLANK_STATE = () => "Select an entity to inspect"; export const VALUE_IS_INVALID = (propertyPath: string) => diff --git a/app/client/src/ce/pages/Editor/AppPluginActionEditor/AppPluginActionEditor.tsx b/app/client/src/ce/pages/Editor/AppPluginActionEditor/AppPluginActionEditor.tsx index e5f9366c05c5..bad7cdba93c1 100644 --- a/app/client/src/ce/pages/Editor/AppPluginActionEditor/AppPluginActionEditor.tsx +++ b/app/client/src/ce/pages/Editor/AppPluginActionEditor/AppPluginActionEditor.tsx @@ -2,7 +2,7 @@ import React from "react"; import { PluginActionEditor, PluginActionForm, - PluginActionResponsePane, + PluginActionResponse, } from "PluginActionEditor"; import { ConvertToModuleDisabler, @@ -17,7 +17,7 @@ const AppPluginActionEditor = () => { - + ); diff --git a/app/client/src/components/editorComponents/ActionExecutionInProgressView.tsx b/app/client/src/components/editorComponents/ActionExecutionInProgressView.tsx index 64af215f18ad..627d1f1de39d 100644 --- a/app/client/src/components/editorComponents/ActionExecutionInProgressView.tsx +++ b/app/client/src/components/editorComponents/ActionExecutionInProgressView.tsx @@ -63,9 +63,7 @@ const ActionExecutionInProgressView = ({
- ), - [API_RESPONSE_TYPE_OPTIONS.RAW]: ( - - ), - }[responseType]; -}; - -const StyledText = styled(Text)` - &&&& { - margin-top: 0; - } -`; - -interface NoResponseProps { - isButtonDisabled: boolean | undefined; - isQueryRunning: boolean; - onRunClick: () => void; -} -export const NoResponse = (props: NoResponseProps) => ( - - no-response-yet -
- {EMPTY_RESPONSE_FIRST_HALF()} - - {EMPTY_RESPONSE_LAST_HALF()} -
-
-); - function ApiResponseView(props: Props) { const { actionResponse = EMPTY_RESPONSE, - apiName, currentActionConfig, disabled, isRunning, - responseDataTypes, - responseDisplayFormat, theme = EditorTheme.LIGHT, } = props; - const hasFailed = actionResponse.statusCode - ? actionResponse.statusCode[0] !== "2" - : false; const dispatch = useDispatch(); const errorCount = useSelector(getErrorCount); @@ -234,256 +63,55 @@ function ApiResponseView(props: Props) { }); }; - const messages = actionResponse?.messages; - let responseHeaders = {}; - - // if no headers are present in the response, use the default body text. - if (actionResponse.headers) { - Object.entries(actionResponse.headers).forEach(([key, value]) => { - if (isArray(value) && value.length < 2) - return (responseHeaders = { - ...responseHeaders, - [key]: value[0], + // update the selected tab in the response pane. + const updateSelectedResponseTab = useCallback( + (tabKey: string) => { + if (tabKey === DEBUGGER_TAB_KEYS.ERROR_TAB) { + AnalyticsUtil.logEvent("OPEN_DEBUGGER", { + source: "API_PANE", }); + } - return (responseHeaders = { - ...responseHeaders, - [key]: value, - }); - }); - } else { - // if the response headers is empty show an empty object. - responseHeaders = {}; - } - - const onResponseTabSelect = (tab: string) => { - dispatch( - setActionResponseDisplayFormat({ - id: currentActionConfig?.id || "", - field: "responseDisplayFormat", - value: tab, - }), - ); - }; - - let filteredResponseDataTypes: { key: string; title: string }[] = [ - ...responseDataTypes, - ]; - - if (!!actionResponse.body && !isArray(actionResponse.body)) { - filteredResponseDataTypes = responseDataTypes.filter( - (item) => item.key !== API_RESPONSE_TYPE_OPTIONS.TABLE, - ); - - if (responseDisplayFormat.title === API_RESPONSE_TYPE_OPTIONS.TABLE) { - onResponseTabSelect(filteredResponseDataTypes[0]?.title); - } - } - - const responseTabs = - filteredResponseDataTypes && - filteredResponseDataTypes.map((dataType, index) => { - return { - index: index, - key: dataType.key, - title: dataType.title, - panelComponent: responseTabComponent( - dataType.key, - actionResponse?.body, - responseTabHeight, - ), - }; - }); - - const segmentedControlOptions = - responseTabs && - responseTabs.map((item) => { - return { value: item.key, label: item.title }; - }); - - const [selectedControl, setSelectedControl] = useState( - segmentedControlOptions[0]?.value, + dispatch(setApiPaneDebuggerState({ open: true, selectedTab: tabKey })); + }, + [dispatch], ); - const selectedTabIndex = - filteredResponseDataTypes && - filteredResponseDataTypes.findIndex( - (dataType) => dataType.title === responseDisplayFormat?.title, - ); - - // update the selected tab in the response pane. - const updateSelectedResponseTab = useCallback((tabKey: string) => { - if (tabKey === DEBUGGER_TAB_KEYS.ERROR_TAB) { - AnalyticsUtil.logEvent("OPEN_DEBUGGER", { - source: "API_PANE", - }); - } - - dispatch(setApiPaneDebuggerState({ open: true, selectedTab: tabKey })); - }, []); - // update the height of the response pane on resize. - const updateResponsePaneHeight = useCallback((height: number) => { - dispatch(setApiPaneDebuggerState({ responseTabHeight: height })); - }, []); + const updateResponsePaneHeight = useCallback( + (height: number) => { + dispatch(setApiPaneDebuggerState({ responseTabHeight: height })); + }, + [dispatch], + ); - // get request timestamp formatted to human readable format. - const responseState = getUpdateTimestamp(actionResponse.request); - // action source for analytics. - const actionSource: SourceEntity = { - type: ENTITY_TYPE.ACTION, - name: currentActionConfig ? currentActionConfig.name : "API", - id: currentActionConfig?.id || "", - }; const tabs: BottomTab[] = [ { - key: "response", + key: DEBUGGER_TAB_KEYS.RESPONSE_TAB, title: createMessage(DEBUGGER_RESPONSE), panelComponent: ( - - - {Array.isArray(messages) && messages.length > 0 && ( - - {messages.map((msg, i) => ( - - {msg} - - ))} - - )} - {isRunning && ( - - )} - {hasFailed && !isRunning ? ( - - - - Your API failed to execute - {actionResponse.pluginErrorDetails && ":"} - - {actionResponse.pluginErrorDetails && ( - <> -
- {actionResponse.pluginErrorDetails.downstreamErrorMessage} -
- {actionResponse.pluginErrorDetails.downstreamErrorCode && ( - - )} - - )} - -
- {actionResponse.request && ( - e.stopPropagation()} - > - - - )} -
- ) : ( - - {isEmpty(actionResponse.statusCode) ? ( - - ) : ( - - {isString(actionResponse?.body) && - isHtml(actionResponse?.body) ? ( - - ) : responseTabs && - responseTabs.length > 0 && - selectedTabIndex !== -1 ? ( - - - { - setSelectedControl(value); - onResponseTabSelect(value); - }} - options={segmentedControlOptions} - value={selectedControl} - /> - - {responseTabComponent( - selectedControl || segmentedControlOptions[0]?.value, - actionResponse?.body, - responseTabHeight, - )} - - ) : null} - - )} - - )} -
+ ), }, { - key: "headers", - title: "Headers", + key: DEBUGGER_TAB_KEYS.HEADER_TAB, + title: createMessage(DEBUGGER_HEADERS), panelComponent: ( - - {hasFailed && !isRunning && ( - - {createMessage(CHECK_REQUEST_BODY)} - - )} - - {isEmpty(actionResponse.statusCode) ? ( - - ) : ( - - )} - - + ), }, ]; @@ -499,7 +127,7 @@ function ApiResponseView(props: Props) { { key: DEBUGGER_TAB_KEYS.LOGS_TAB, title: createMessage(DEBUGGER_LOGS), - panelComponent: , + panelComponent: , }, ); } @@ -508,7 +136,7 @@ function ApiResponseView(props: Props) { //TODO: move this to a common place const toggleHide = useCallback( () => dispatch(setApiPaneDebuggerState({ open: !open })), - [open], + [dispatch, open], ); return ( diff --git a/app/client/src/components/editorComponents/JSResponseView.tsx b/app/client/src/components/editorComponents/JSResponseView.tsx index 42543824be7f..91d8519abe07 100644 --- a/app/client/src/components/editorComponents/JSResponseView.tsx +++ b/app/client/src/components/editorComponents/JSResponseView.tsx @@ -27,13 +27,13 @@ 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 { getJSResponseViewState, JSResponseState } from "./utils"; import { getFilteredErrors } from "selectors/debuggerSelectors"; +import { NoResponse } from "PluginActionEditor/components/PluginActionResponse/components/NoResponse"; import { - NoResponse, ResponseTabErrorContainer, ResponseTabErrorContent, -} from "./ApiResponseView"; +} from "PluginActionEditor/components/PluginActionResponse/components/ApiResponse"; import LogHelper from "./Debugger/ErrorLogs/components/LogHelper"; import LOG_TYPE from "entities/AppsmithConsole/logtype"; import type { Log, SourceEntity } from "entities/AppsmithConsole"; @@ -45,7 +45,7 @@ import { EditorViewMode } from "ee/entities/IDE/constants"; import ErrorLogs from "./Debugger/Errors"; import { isBrowserExecutionAllowed } from "ee/utils/actionExecutionUtils"; import JSRemoteExecutionView from "ee/components/JSRemoteExecutionView"; -import { IDEBottomView, ViewHideBehaviour } from "../../IDE"; +import { IDEBottomView, ViewHideBehaviour } from "IDE"; const ResponseTabWrapper = styled.div` display: flex; @@ -66,15 +66,6 @@ const NoReturnValueWrapper = styled.div` 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; } @@ -229,8 +220,8 @@ function JSResponseView(props: Props) { <> {responseStatus === JSResponseState.NoResponse && ( { - const doc = new DOMParser().parseFromString(str, "text/html"); - - return Array.from(doc.body.childNodes).some( - // TODO: Fix this the next time the file is edited - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (node: any) => node.nodeType === 1, - ); -}; +export enum JSResponseState { + IsExecuting = "IsExecuting", + IsDirty = "IsDirty", + IsUpdating = "IsUpdating", + NoResponse = "NoResponse", + ShowResponse = "ShowResponse", + NoReturnValue = "NoReturnValue", +} /** * Returns state of the JSResponseview editor component diff --git a/app/client/src/constants/ApiEditorConstants/CommonApiConstants.ts b/app/client/src/constants/ApiEditorConstants/CommonApiConstants.ts index 978879ba5e53..eacd44f72e6c 100644 --- a/app/client/src/constants/ApiEditorConstants/CommonApiConstants.ts +++ b/app/client/src/constants/ApiEditorConstants/CommonApiConstants.ts @@ -85,29 +85,14 @@ export const HTTP_METHODS_DEFAULT_FORMAT_TYPES: Record = { PATCH: POST_BODY_FORMAT_OPTIONS.JSON, }; -export const DEFAULT_PROVIDER_OPTION = "Business Software"; export const CONTENT_TYPE_HEADER_KEY = "content-type"; -export enum ApiResponseTypes { +export enum ResponseDisplayFormats { JSON = "JSON", TABLE = "TABLE", RAW = "RAW", } -// export const ApiResponseTypesOptions: -export const API_RESPONSE_TYPE_OPTIONS: { - [key in keyof typeof ApiResponseTypes]: string; -} = { - JSON: "JSON", - TABLE: "TABLE", - RAW: "RAW", -}; -export const POST_BODY_FORMATS = Object.values(POST_BODY_FORMAT_OPTIONS).map( - (option) => { - return option; - }, -); - export const POST_BODY_FORMAT_OPTIONS_ARRAY = Object.values( POST_BODY_FORMAT_OPTIONS, ); @@ -133,6 +118,4 @@ export interface MULTI_PART_DROPDOWN_OPTION { export const MULTI_PART_DROPDOWN_OPTIONS: MULTI_PART_DROPDOWN_OPTION[] = Object.values(MultiPartOptionTypes).map((value) => ({ label: value, value })); -export const DEFAULT_MULTI_PART_DROPDOWN_WIDTH = "77px"; -export const DEFAULT_MULTI_PART_DROPDOWN_HEIGHT = "100%"; export const DEFAULT_MULTI_PART_DROPDOWN_PLACEHOLDER = "Type"; diff --git a/app/client/src/ee/PluginActionEditor/components/PluginActionResponse/hooks/usePluginActionResponseTabs.tsx b/app/client/src/ee/PluginActionEditor/components/PluginActionResponse/hooks/usePluginActionResponseTabs.tsx new file mode 100644 index 000000000000..0e7b0e465461 --- /dev/null +++ b/app/client/src/ee/PluginActionEditor/components/PluginActionResponse/hooks/usePluginActionResponseTabs.tsx @@ -0,0 +1 @@ +export { default } from "ce/PluginActionEditor/components/PluginActionResponse/hooks/usePluginActionResponseTabs"; diff --git a/app/client/src/pages/Editor/APIEditor/CommonEditorForm.tsx b/app/client/src/pages/Editor/APIEditor/CommonEditorForm.tsx index 3c7467e0718d..1696f619724a 100644 --- a/app/client/src/pages/Editor/APIEditor/CommonEditorForm.tsx +++ b/app/client/src/pages/Editor/APIEditor/CommonEditorForm.tsx @@ -212,7 +212,6 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) { const { actionConfigurationHeaders, actionConfigurationParams, - actionName, actionResponse, autoGeneratedActionConfigHeaders, closeEditorLink, @@ -224,8 +223,6 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) { onRunClick, paramsCount, pluginId, - responseDataTypes, - responseDisplayFormat, settingsConfig, } = props; @@ -256,6 +253,8 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) { getPlugin(state, pluginId ?? ""), ); + if (!currentActionConfig) return null; + // this gets the url of the current action's datasource const actionDatasourceUrl = currentActionConfig?.datasource?.datasourceConfiguration?.url || ""; @@ -351,13 +350,10 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) { diff --git a/app/client/src/pages/Editor/QueryEditor/QueryResponseTab.tsx b/app/client/src/pages/Editor/QueryEditor/QueryResponseTab.tsx index 1c366da805e3..ba147dc5bb33 100644 --- a/app/client/src/pages/Editor/QueryEditor/QueryResponseTab.tsx +++ b/app/client/src/pages/Editor/QueryEditor/QueryResponseTab.tsx @@ -3,12 +3,12 @@ import { useDispatch, useSelector } from "react-redux"; import ReactJson from "react-json-view"; import { apiReactJsonProps, - NoResponse, - responseTabComponent, ResponseTabErrorContainer, ResponseTabErrorContent, ResponseTabErrorDefaultMessage, -} from "components/editorComponents/ApiResponseView"; +} from "PluginActionEditor/components/PluginActionResponse/components/ApiResponse"; +import { ResponseFormatTabs } from "PluginActionEditor/components/PluginActionResponse/components/ResponseFormatTabs"; +import { NoResponse } from "PluginActionEditor/components/PluginActionResponse/components/NoResponse"; import LogAdditionalInfo from "components/editorComponents/Debugger/ErrorLogs/components/LogAdditionalInfo"; import LogHelper from "components/editorComponents/Debugger/ErrorLogs/components/LogHelper"; import LOG_TYPE from "entities/AppsmithConsole/logtype"; @@ -102,6 +102,8 @@ const QueryResponseTab = (props: Props) => { const { responseDataTypes, responseDisplayFormat } = actionResponseDisplayDataFormats(actionResponse); + let output: Record[] | string = ""; + const responseBodyTabs = responseDataTypes && responseDataTypes.map((dataType, index) => { @@ -109,10 +111,12 @@ const QueryResponseTab = (props: Props) => { index: index, key: dataType.key, title: dataType.title, - panelComponent: responseTabComponent( - dataType.key, - output, - responseTabHeight, + panelComponent: ( + ), }; }); @@ -163,9 +167,6 @@ const QueryResponseTab = (props: Props) => { let error = runErrorMessage; let hintMessages: Array = []; let showPreparedStatementWarning = false; - // TODO: Fix this the next time the file is edited - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let output: Record[] | null = null; // Query is executed even once during the session, show the response data. if (actionResponse) { @@ -326,17 +327,19 @@ const QueryResponseTab = (props: Props) => { suggestedWidgets={actionResponse?.suggestedWidgets} /> - {responseTabComponent( - selectedControl || segmentedControlOptions[0]?.value, - output, - responseTabHeight, - )} + )} {!output && !error && ( )}