chore: Init Plugin Action Response (#36485)

## Description

Response Pane stuff

- Move Api Response into its own component and sub components
- Move Api Headers response into its own component and sub components
- A lot of these are also used by queries and js so maybe we will create
a common folder for that
- Add a logic to render the bottom tabs in the module. Allows for
extension via hook

Fixes #36155

## Automation

/ok-to-test tags="@tag.Datasource"

### 🔍 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/11026260058>
> Commit: c3b5b4b8f0e0668ff43adae1b22d320a5e6d347d
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=11026260058&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Datasource`
> Spec:
> <hr>Wed, 25 Sep 2024 05:04:24 UTC
<!-- 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

- **New Features**
- Introduced new components for displaying API responses, including
`ApiFormatSegmentedResponse` and `ApiResponseHeaders`.
- Enhanced user interaction with a segmented control for switching
between different API response formats.
  
- **Improvements**
- Added utility functions for improved handling and validation of API
response headers and HTML content.
  
- **Bug Fixes**
- Improved error handling for API response states to ensure accurate
feedback during user interactions.

- **Chores**
- Added tests for new utility functions to validate their functionality
and ensure reliability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Hetu Nandu 2024-09-25 13:14:26 +05:30 committed by GitHub
parent 20fa8de803
commit 5ee7f83cdf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 906 additions and 504 deletions

View File

@ -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

View File

@ -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 (
<CenteredWrapper>
@ -71,9 +74,12 @@ const PluginActionEditor = (props: ChildrenProps) => {
);
}
const actionResponse = actionResponses[action.id];
return (
<PluginActionContextProvider
action={action}
actionResponse={actionResponse}
datasource={datasource}
editorConfig={editorConfig}
plugin={plugin}

View File

@ -1,6 +1,6 @@
import styled from "styled-components";
import FormRow from "../../../../../../components/editorComponents/FormRow";
import FormLabel from "../../../../../../components/editorComponents/FormLabel";
import FormRow from "components/editorComponents/FormRow";
import FormLabel from "components/editorComponents/FormLabel";
import { Button, Icon, Text, Tooltip } from "@appsmith/ads";
import {
API_PANE_AUTO_GENERATED_HEADER,

View File

@ -0,0 +1,67 @@
import React, { useCallback } from "react";
import { IDEBottomView, ViewHideBehaviour } from "IDE";
import { ActionExecutionResizerHeight } from "pages/Editor/APIEditor/constants";
import EntityBottomTabs from "components/editorComponents/EntityBottomTabs";
import { useDispatch, useSelector } from "react-redux";
import { getApiPaneDebuggerState } from "selectors/apiPaneSelectors";
import { setApiPaneDebuggerState } from "actions/apiPaneActions";
import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/helpers";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import { usePluginActionResponseTabs } from "./hooks";
function PluginActionResponse() {
const dispatch = useDispatch();
const tabs = usePluginActionResponseTabs();
// TODO combine API and Query Debugger state
const { open, responseTabHeight, selectedTab } = useSelector(
getApiPaneDebuggerState,
);
const toggleHide = useCallback(
() => 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 (
<IDEBottomView
behaviour={ViewHideBehaviour.COLLAPSE}
className="t--action-bottom-pane-container"
height={responseTabHeight}
hidden={!open}
onHideClick={toggleHide}
setHeight={updateResponsePaneHeight}
>
<EntityBottomTabs
expandedHeight={`${ActionExecutionResizerHeight}px`}
isCollapsed={!open}
onSelect={updateSelectedResponseTab}
selectedTabKey={selectedTab || tabs[0].key}
tabs={tabs}
/>
</IDEBottomView>
);
}
export default PluginActionResponse;

View File

@ -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: (
<ResponseFormatTabs
data={props.actionResponse.body as string | Record<string, unknown>[]}
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 (
<ResponseBodyContainer>
{isString(props.actionResponse?.body) &&
isHtml(props.actionResponse?.body) ? (
<ReadOnlyEditor folding height={"100%"} input={value} />
) : responseTabs && responseTabs.length > 0 && selectedTabIndex !== -1 ? (
<SegmentedControlContainer>
<Flex>
<SegmentedControl
data-testid="t--response-tab-segmented-control"
defaultValue={segmentedControlOptions[0]?.value}
isFullWidth={false}
onChange={onChange}
options={segmentedControlOptions}
value={selectedControl}
/>
</Flex>
<ResponseFormatTabs
data={
props.actionResponse?.body as string | Record<string, unknown>[]
}
responseType={selectedControl || segmentedControlOptions[0]?.value}
tableBodyHeight={props.responseTabHeight}
/>
</SegmentedControlContainer>
) : null}
</ResponseBodyContainer>
);
}
export default ApiFormatSegmentedResponse;

View File

@ -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 (
<Flex h="100%" w="100%">
<NoResponse
isRunDisabled={props.isRunDisabled}
isRunning={props.isRunning}
onRunClick={props.onRunClick}
/>
</Flex>
);
}
const { messages, pluginErrorDetails, request } = props.actionResponse;
const runHasFailed = hasFailed(props.actionResponse);
const requestWithTimestamp = getUpdateTimestamp(request);
return (
<Flex flexDirection="column" h="100%" w="100%">
<ApiResponseMeta
actionName={name}
actionResponse={props.actionResponse}
/>
{Array.isArray(messages) && messages.length > 0 && (
<HelpSection>
{messages.map((message, i) => (
<Callout key={i} kind="warning">
{message}
</Callout>
))}
</HelpSection>
)}
{props.isRunning && (
<ActionExecutionInProgressView actionType="API" theme={props.theme} />
)}
{runHasFailed && !props.isRunning ? (
<ResponseTabErrorContainer>
<ResponseTabErrorContent>
<ResponseTabErrorDefaultMessage>
Your API failed to execute
{pluginErrorDetails && ":"}
</ResponseTabErrorDefaultMessage>
{pluginErrorDetails && (
<>
<div className="t--debugger-log-downstream-message">
{pluginErrorDetails.downstreamErrorMessage}
</div>
{pluginErrorDetails.downstreamErrorCode && (
<LogAdditionalInfo
text={pluginErrorDetails.downstreamErrorCode}
/>
)}
</>
)}
<LogHelper
logType={LOG_TYPE.ACTION_EXECUTION_ERROR}
name="PluginExecutionError"
pluginErrorDetails={pluginErrorDetails}
source={actionSource}
/>
</ResponseTabErrorContent>
{requestWithTimestamp && (
<JsonWrapper className="t--debugger-log-state" onClick={noop}>
<ReactJson src={requestWithTimestamp} {...apiReactJsonProps} />
</JsonWrapper>
)}
</ResponseTabErrorContainer>
) : (
<ResponseDataContainer>
{isEmpty(props.actionResponse.statusCode) ? (
<NoResponse
isRunDisabled={props.isRunDisabled}
isRunning={props.isRunning}
onRunClick={props.onRunClick}
/>
) : (
<ApiFormatSegmentedResponse
actionId={id}
actionResponse={props.actionResponse}
responseTabHeight={props.responseTabHeight}
/>
)}
</ResponseDataContainer>
)}
</Flex>
);
}

View File

@ -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<string, string[]> = {}) => {
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 (
<Flex className="t--headers-tab" h="100%" w="100%">
<NoResponse
isRunDisabled={props.isRunDisabled}
isRunning={props.isRunning}
onRunClick={props.onRunClick}
/>
</Flex>
);
}
const runHasFailed = hasFailed(props.actionResponse);
return (
<Flex className="t--headers-tab" flexDirection="column" h="100%" w="100%">
{runHasFailed && !props.isRunning && (
<Callout kind="error" links={errorCalloutLinks}>
{createMessage(CHECK_REQUEST_BODY)}
</Callout>
)}
<ResponseDataContainer>
{isEmpty(props.actionResponse.statusCode) ? (
<NoResponse
isRunDisabled={props.isRunDisabled}
isRunning={props.isRunning}
onRunClick={props.onRunClick}
/>
) : (
<ReadOnlyEditor folding height={"100%"} input={headersInput} />
)}
</ResponseDataContainer>
</Flex>
);
}

View File

@ -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) => (
<NoResponseContainer>
<img alt="no-response-yet" src={NoResponseSVG} />
<div className="flex gap-2 items-center mt-4">
<StyledText type={TextType.P1}>{EMPTY_RESPONSE_FIRST_HALF()}</StyledText>
<Button
isDisabled={isRunDisabled}
isLoading={isRunning}
onClick={onRunClick}
size="sm"
>
Run
</Button>
<StyledText type={TextType.P1}>{EMPTY_RESPONSE_LAST_HALF()}</StyledText>
</div>
</NoResponseContainer>
);

View File

@ -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<string, unknown>[];
const inputValue = (data: ResponseData) => {
return {
value: isString(data) ? data : JSON.stringify(data, null, 2),
};
};
const tableValue = (data: ResponseData): Record<string, unknown>[] => {
if (isString(data)) {
return [{}];
}
return data;
};
export const ResponseFormatTabs = (props: {
responseType: string;
data: ResponseData;
tableBodyHeight?: number;
}) => {
switch (props.responseType) {
case ResponseDisplayFormats.JSON:
return (
<ReadOnlyEditor
folding
height={"100%"}
input={inputValue(props.data)}
/>
);
case ResponseDisplayFormats.TABLE:
return (
<Table
data={tableValue(props.data)}
tableBodyHeight={props.tableBodyHeight}
/>
);
case ResponseDisplayFormats.RAW:
return (
<ReadOnlyEditor
folding
height={"100%"}
input={inputValue(props.data)}
isRawView
/>
);
default:
return (
<ReadOnlyEditor
folding
height={"100%"}
input={inputValue(props.data)}
isRawView
/>
);
}
};

View File

@ -0,0 +1 @@
export { default as usePluginActionResponseTabs } from "ee/PluginActionEditor/components/PluginActionResponse/hooks/usePluginActionResponseTabs";

View File

@ -0,0 +1 @@
export { default } from "./PluginActionResponse";

View File

@ -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);
});
});

View File

@ -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;

View File

@ -0,0 +1,2 @@
export { default as hasFailed } from "./actionHasFailed";
export { default as isHtml } from "./isHtml";

View File

@ -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 = "<pThis is incomplete";
expect(isHtml(input)).toBe(false);
});
it("returns true for incomplete html", () => {
const input = "<p>This is incomplete";
expect(isHtml(input)).toBe(true);
});
it("returns true for HTML", () => {
const input = "<body><p>This is a html response</p></body>";
expect(isHtml(input)).toBe(true);
});
});

View File

@ -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;

View File

@ -1,7 +0,0 @@
import React from "react";
const PluginActionResponsePane = () => {
return <div />;
};
export default PluginActionResponsePane;

View File

@ -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";

View File

@ -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: <ErrorLogs />,
},
{
key: DEBUGGER_TAB_KEYS.LOGS_TAB,
title: createMessage(DEBUGGER_LOGS),
panelComponent: <DebuggerLogs searchQuery={action.name} />,
},
);
}
if (plugin.type === PluginType.API) {
return tabs.concat([
{
key: DEBUGGER_TAB_KEYS.RESPONSE_TAB,
title: createMessage(DEBUGGER_RESPONSE),
panelComponent: (
<ApiResponse
action={action}
actionResponse={actionResponse}
isRunDisabled={false}
isRunning={false}
onRunClick={noop}
responseTabHeight={responseTabHeight}
theme={EditorTheme.LIGHT}
/>
),
},
{
key: DEBUGGER_TAB_KEYS.HEADER_TAB,
title: createMessage(DEBUGGER_HEADERS),
panelComponent: (
<ApiResponseHeaders
actionResponse={actionResponse}
isRunDisabled={false}
isRunning={false}
onDebugClick={noop}
onRunClick={noop}
/>
),
},
]);
}
return tabs;
}
export default usePluginActionResponseTabs;

View File

@ -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) =>

View File

@ -2,7 +2,7 @@ import React from "react";
import {
PluginActionEditor,
PluginActionForm,
PluginActionResponsePane,
PluginActionResponse,
} from "PluginActionEditor";
import {
ConvertToModuleDisabler,
@ -17,7 +17,7 @@ const AppPluginActionEditor = () => {
<AppPluginActionToolbar />
<ConvertToModuleCallout />
<PluginActionForm />
<PluginActionResponsePane />
<PluginActionResponse />
</ConvertToModuleDisabler>
</PluginActionEditor>
);

View File

@ -63,9 +63,7 @@ const ActionExecutionInProgressView = ({
<Button
className={`t--cancel-action-button`}
kind="secondary"
onClick={() => {
handleCancelActionExecution();
}}
onClick={handleCancelActionExecution}
size="md"
>
{createMessage(ACTION_EXECUTION_CANCEL)}

View File

@ -9,6 +9,8 @@ import { lightTheme } from "selectors/themeSelectors";
import { BrowserRouter as Router } from "react-router-dom";
import { EditorViewMode } from "ee/entities/IDE/constants";
import "@testing-library/jest-dom/extend-expect";
import { APIFactory } from "test/factories/Actions/API";
import { noop } from "lodash";
jest.mock("./EntityBottomTabs", () => ({
__esModule: true,
@ -68,16 +70,20 @@ describe("ApiResponseView", () => {
});
it("the container should have class select-text to enable the selection of text for user", () => {
const Api1 = APIFactory.build({
id: "api_id",
baseId: "api_base_id",
pageId: "pageId",
});
const { container } = render(
<Provider store={store}>
<ThemeProvider theme={lightTheme}>
<Router>
<ApiResponseView
apiName="Api1"
currentActionConfig={Api1}
disabled={false}
isRunning={false}
onRunClick={() => {}}
responseDataTypes={[]}
responseDisplayFormat={{ title: "JSON", value: "JSON" }}
onRunClick={noop}
/>
</Router>
</ThemeProvider>

View File

@ -1,221 +1,50 @@
import React, { useCallback, useState } from "react";
import React, { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import ReactJson from "react-json-view";
import styled from "styled-components";
import type { ActionResponse } from "api/ActionAPI";
import type { SourceEntity } from "entities/AppsmithConsole";
import LOG_TYPE from "entities/AppsmithConsole/logtype";
import { ENTITY_TYPE } from "ee/entities/AppsmithConsole/utils";
import ReadOnlyEditor from "components/editorComponents/ReadOnlyEditor";
import { isArray, isEmpty, isString } from "lodash";
import {
CHECK_REQUEST_BODY,
createMessage,
DEBUGGER_ERRORS,
DEBUGGER_HEADERS,
DEBUGGER_LOGS,
DEBUGGER_RESPONSE,
EMPTY_RESPONSE_FIRST_HALF,
EMPTY_RESPONSE_LAST_HALF,
} from "ee/constants/messages";
import { EditorTheme } from "./CodeEditor/EditorConfig";
import NoResponseSVG from "assets/images/no-response.svg";
import DebuggerLogs from "./Debugger/DebuggerLogs";
import ErrorLogs from "./Debugger/Errors";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import { Classes, Text, TextType } from "@appsmith/ads-old";
import { Button, Callout, Flex, SegmentedControl } from "@appsmith/ads";
import type { BottomTab } from "./EntityBottomTabs";
import EntityBottomTabs from "./EntityBottomTabs";
import { DEBUGGER_TAB_KEYS } from "./Debugger/helpers";
import Table from "pages/Editor/QueryEditor/Table";
import { API_RESPONSE_TYPE_OPTIONS } from "constants/ApiEditorConstants/CommonApiConstants";
import { setActionResponseDisplayFormat } from "actions/pluginActionActions";
import { isHtml } from "./utils";
import { getErrorCount } from "selectors/debuggerSelectors";
import { ActionExecutionResizerHeight } from "pages/Editor/APIEditor/constants";
import LogAdditionalInfo from "./Debugger/ErrorLogs/components/LogAdditionalInfo";
import {
JsonWrapper,
reactJsonProps,
} from "./Debugger/ErrorLogs/components/LogCollapseData";
import LogHelper from "./Debugger/ErrorLogs/components/LogHelper";
import { getUpdateTimestamp } from "./Debugger/ErrorLogs/ErrorLogItem";
import type { Action } from "entities/Action";
import { SegmentedControlContainer } from "../../pages/Editor/QueryEditor/EditorJSONtoForm";
import ActionExecutionInProgressView from "./ActionExecutionInProgressView";
import { EMPTY_RESPONSE } from "./emptyResponse";
import { setApiPaneDebuggerState } from "actions/apiPaneActions";
import { getApiPaneDebuggerState } from "selectors/apiPaneSelectors";
import { getIDEViewMode } from "selectors/ideSelectors";
import { EditorViewMode } from "ee/entities/IDE/constants";
import ApiResponseMeta from "./ApiResponseMeta";
import useDebuggerTriggerClick from "./Debugger/hooks/useDebuggerTriggerClick";
import { IDEBottomView, ViewHideBehaviour } from "IDE";
const ResponseTabWrapper = styled.div`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
&.t--headers-tab {
padding-left: var(--ads-v2-spaces-7);
padding-right: var(--ads-v2-spaces-7);
}
`;
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;
}
`;
const HelpSection = styled.div`
padding-bottom: 5px;
padding-top: 10px;
`;
const ResponseBodyContainer = styled.div`
overflow-y: clip;
height: 100%;
display: grid;
`;
import { ApiResponse } from "PluginActionEditor/components/PluginActionResponse/components/ApiResponse";
import { ApiResponseHeaders } from "PluginActionEditor/components/PluginActionResponse/components/ApiResponseHeaders";
interface Props {
currentActionConfig?: Action;
currentActionConfig: Action;
theme?: EditorTheme;
apiName: string;
disabled?: boolean;
disabled: boolean;
onRunClick: () => void;
responseDataTypes: { key: string; title: string }[];
responseDisplayFormat: { title: string; value: string };
actionResponse?: ActionResponse;
isRunning: boolean;
}
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 const responseTabComponent = (
responseType: string,
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
output: any,
tableBodyHeight?: number,
): JSX.Element => {
return {
[API_RESPONSE_TYPE_OPTIONS.JSON]: (
<ReadOnlyEditor
folding
height={"100%"}
input={{
value: isString(output) ? output : JSON.stringify(output, null, 2),
}}
/>
),
[API_RESPONSE_TYPE_OPTIONS.TABLE]: (
<Table data={output} tableBodyHeight={tableBodyHeight} />
),
[API_RESPONSE_TYPE_OPTIONS.RAW]: (
<ReadOnlyEditor
folding
height={"100%"}
input={{
value: isString(output) ? output : JSON.stringify(output, null, 2),
}}
isRawView
/>
),
}[responseType];
};
const StyledText = styled(Text)`
&&&& {
margin-top: 0;
}
`;
interface NoResponseProps {
isButtonDisabled: boolean | undefined;
isQueryRunning: boolean;
onRunClick: () => void;
}
export const NoResponse = (props: NoResponseProps) => (
<NoResponseContainer>
<img alt="no-response-yet" src={NoResponseSVG} />
<div className="flex gap-2 items-center mt-4">
<StyledText type={TextType.P1}>{EMPTY_RESPONSE_FIRST_HALF()}</StyledText>
<Button
isDisabled={props.isButtonDisabled}
isLoading={props.isQueryRunning}
onClick={props.onRunClick}
size="sm"
>
Run
</Button>
<StyledText type={TextType.P1}>{EMPTY_RESPONSE_LAST_HALF()}</StyledText>
</div>
</NoResponseContainer>
);
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: (
<ResponseTabWrapper>
<ApiResponseMeta
actionName={apiName || currentActionConfig?.name}
actionResponse={actionResponse}
/>
{Array.isArray(messages) && messages.length > 0 && (
<HelpSection>
{messages.map((msg, i) => (
<Callout key={i} kind="warning">
{msg}
</Callout>
))}
</HelpSection>
)}
{isRunning && (
<ActionExecutionInProgressView actionType="API" theme={theme} />
)}
{hasFailed && !isRunning ? (
<ResponseTabErrorContainer>
<ResponseTabErrorContent>
<ResponseTabErrorDefaultMessage>
Your API failed to execute
{actionResponse.pluginErrorDetails && ":"}
</ResponseTabErrorDefaultMessage>
{actionResponse.pluginErrorDetails && (
<>
<div className="t--debugger-log-downstream-message">
{actionResponse.pluginErrorDetails.downstreamErrorMessage}
</div>
{actionResponse.pluginErrorDetails.downstreamErrorCode && (
<LogAdditionalInfo
text={
actionResponse.pluginErrorDetails.downstreamErrorCode
}
/>
)}
</>
)}
<LogHelper
logType={LOG_TYPE.ACTION_EXECUTION_ERROR}
name="PluginExecutionError"
pluginErrorDetails={actionResponse.pluginErrorDetails}
source={actionSource}
/>
</ResponseTabErrorContent>
{actionResponse.request && (
<JsonWrapper
className="t--debugger-log-state"
onClick={(e) => e.stopPropagation()}
>
<ReactJson src={responseState} {...apiReactJsonProps} />
</JsonWrapper>
)}
</ResponseTabErrorContainer>
) : (
<ResponseDataContainer>
{isEmpty(actionResponse.statusCode) ? (
<NoResponse
isButtonDisabled={disabled}
isQueryRunning={isRunning}
onRunClick={onRunClick}
/>
) : (
<ResponseBodyContainer>
{isString(actionResponse?.body) &&
isHtml(actionResponse?.body) ? (
<ReadOnlyEditor
folding
height={"100%"}
input={{
value: actionResponse?.body,
}}
/>
) : responseTabs &&
responseTabs.length > 0 &&
selectedTabIndex !== -1 ? (
<SegmentedControlContainer>
<Flex>
<SegmentedControl
data-testid="t--response-tab-segmented-control"
defaultValue={segmentedControlOptions[0]?.value}
isFullWidth={false}
onChange={(value) => {
setSelectedControl(value);
onResponseTabSelect(value);
}}
options={segmentedControlOptions}
value={selectedControl}
/>
</Flex>
{responseTabComponent(
selectedControl || segmentedControlOptions[0]?.value,
actionResponse?.body,
responseTabHeight,
)}
</SegmentedControlContainer>
) : null}
</ResponseBodyContainer>
)}
</ResponseDataContainer>
)}
</ResponseTabWrapper>
<ApiResponse
action={currentActionConfig}
actionResponse={actionResponse}
isRunDisabled={disabled}
isRunning={isRunning}
onRunClick={onRunClick}
responseTabHeight={responseTabHeight}
theme={theme}
/>
),
},
{
key: "headers",
title: "Headers",
key: DEBUGGER_TAB_KEYS.HEADER_TAB,
title: createMessage(DEBUGGER_HEADERS),
panelComponent: (
<ResponseTabWrapper className="t--headers-tab">
{hasFailed && !isRunning && (
<Callout
kind="error"
links={[
{
children: "Debug",
endIcon: "bug",
onClick: onDebugClick,
to: "",
},
]}
>
{createMessage(CHECK_REQUEST_BODY)}
</Callout>
)}
<ResponseDataContainer>
{isEmpty(actionResponse.statusCode) ? (
<NoResponse
isButtonDisabled={disabled}
isQueryRunning={isRunning}
onRunClick={onRunClick}
/>
) : (
<ReadOnlyEditor
folding
height={"100%"}
input={{
value: !isEmpty(responseHeaders)
? JSON.stringify(responseHeaders, null, 2)
: "",
}}
/>
)}
</ResponseDataContainer>
</ResponseTabWrapper>
<ApiResponseHeaders
actionResponse={actionResponse}
isRunDisabled={disabled}
isRunning={isRunning}
onDebugClick={onDebugClick}
onRunClick={onRunClick}
/>
),
},
];
@ -499,7 +127,7 @@ function ApiResponseView(props: Props) {
{
key: DEBUGGER_TAB_KEYS.LOGS_TAB,
title: createMessage(DEBUGGER_LOGS),
panelComponent: <DebuggerLogs searchQuery={props.apiName} />,
panelComponent: <DebuggerLogs searchQuery={currentActionConfig.name} />,
},
);
}
@ -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 (

View File

@ -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 && (
<NoResponse
isButtonDisabled={disabled}
isQueryRunning={isLoading}
isRunDisabled={disabled}
isRunning={isLoading}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
onRunClick={onButtonClick}

View File

@ -1,5 +1,4 @@
import { JSResponseState } from "./JSResponseView";
import { getJSResponseViewState } from "./utils";
import { getJSResponseViewState, JSResponseState } from "./utils";
const TEST_JS_FUNCTION_ID = "627ccff468e1fa5185b7f901";
const TEST_JS_FUNCTION_BASE_ID = "627ccff468e1fa5185b7f912";

View File

@ -1,15 +1,13 @@
import type { JSAction } from "entities/JSCollection";
import { JSResponseState } from "./JSResponseView";
export const isHtml = (str: string) => {
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

View File

@ -85,29 +85,14 @@ export const HTTP_METHODS_DEFAULT_FORMAT_TYPES: Record<HTTP_METHOD, string> = {
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";

View File

@ -0,0 +1 @@
export { default } from "ce/PluginActionEditor/components/PluginActionResponse/hooks/usePluginActionResponseTabs";

View File

@ -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) {
</TabbedViewContainer>
<ApiResponseView
actionResponse={actionResponse}
apiName={actionName}
currentActionConfig={currentActionConfig}
disabled={!isExecutePermitted}
isRunning={isRunning}
onRunClick={onRunClick}
responseDataTypes={responseDataTypes}
responseDisplayFormat={responseDisplayFormat}
theme={theme}
/>
<RunHistory />

View File

@ -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, unknown>[] | 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: (
<ResponseFormatTabs
data={output}
responseType={dataType.key}
tableBodyHeight={responseTabHeight}
/>
),
};
});
@ -163,9 +167,6 @@ const QueryResponseTab = (props: Props) => {
let error = runErrorMessage;
let hintMessages: Array<string> = [];
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<string, any>[] | 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}
/>
</Flex>
{responseTabComponent(
selectedControl || segmentedControlOptions[0]?.value,
output,
responseTabHeight,
)}
<ResponseFormatTabs
data={output}
responseType={
selectedControl || segmentedControlOptions[0]?.value
}
tableBodyHeight={responseTabHeight}
/>
</ResponseDataContainer>
)}
{!output && !error && (
<NoResponse
isButtonDisabled={!isExecutePermitted}
isQueryRunning={isRunning}
isRunDisabled={!isExecutePermitted}
isRunning={isRunning}
onRunClick={responseTabOnRunClick}
/>
)}