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:
parent
20fa8de803
commit
5ee7f83cdf
|
|
@ -6,14 +6,16 @@ import React, {
|
||||||
} from "react";
|
} from "react";
|
||||||
import type { Action } from "entities/Action";
|
import type { Action } from "entities/Action";
|
||||||
import type { Plugin } from "api/PluginApi";
|
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 {
|
interface PluginActionContextType {
|
||||||
action: Action;
|
action: Action;
|
||||||
|
actionResponse?: ActionResponse;
|
||||||
editorConfig: unknown[];
|
editorConfig: unknown[];
|
||||||
settingsConfig: unknown[];
|
settingsConfig: unknown[];
|
||||||
plugin: Plugin;
|
plugin: Plugin;
|
||||||
datasource?: Datasource;
|
datasource?: EmbeddedRestDatasource | Datasource;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No need to export this context to use it. Use the hook defined below instead
|
// No need to export this context to use it. Use the hook defined below instead
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { identifyEntityFromPath } from "../navigation/FocusEntity";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import {
|
import {
|
||||||
getActionByBaseId,
|
getActionByBaseId,
|
||||||
|
getActionResponses,
|
||||||
getDatasource,
|
getDatasource,
|
||||||
getEditorConfig,
|
getEditorConfig,
|
||||||
getPlugin,
|
getPlugin,
|
||||||
|
|
@ -39,6 +40,8 @@ const PluginActionEditor = (props: ChildrenProps) => {
|
||||||
|
|
||||||
const editorConfig = useSelector((state) => getEditorConfig(state, pluginId));
|
const editorConfig = useSelector((state) => getEditorConfig(state, pluginId));
|
||||||
|
|
||||||
|
const actionResponses = useSelector(getActionResponses);
|
||||||
|
|
||||||
if (!isEditorInitialized) {
|
if (!isEditorInitialized) {
|
||||||
return (
|
return (
|
||||||
<CenteredWrapper>
|
<CenteredWrapper>
|
||||||
|
|
@ -71,9 +74,12 @@ const PluginActionEditor = (props: ChildrenProps) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const actionResponse = actionResponses[action.id];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PluginActionContextProvider
|
<PluginActionContextProvider
|
||||||
action={action}
|
action={action}
|
||||||
|
actionResponse={actionResponse}
|
||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
editorConfig={editorConfig}
|
editorConfig={editorConfig}
|
||||||
plugin={plugin}
|
plugin={plugin}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import FormRow from "../../../../../../components/editorComponents/FormRow";
|
import FormRow from "components/editorComponents/FormRow";
|
||||||
import FormLabel from "../../../../../../components/editorComponents/FormLabel";
|
import FormLabel from "components/editorComponents/FormLabel";
|
||||||
import { Button, Icon, Text, Tooltip } from "@appsmith/ads";
|
import { Button, Icon, Text, Tooltip } from "@appsmith/ads";
|
||||||
import {
|
import {
|
||||||
API_PANE_AUTO_GENERATED_HEADER,
|
API_PANE_AUTO_GENERATED_HEADER,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
|
@ -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
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as usePluginActionResponseTabs } from "ee/PluginActionEditor/components/PluginActionResponse/hooks/usePluginActionResponseTabs";
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from "./PluginActionResponse";
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as hasFailed } from "./actionHasFailed";
|
||||||
|
export { default as isHtml } from "./isHtml";
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const PluginActionResponsePane = () => {
|
|
||||||
return <div />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PluginActionResponsePane;
|
|
||||||
|
|
@ -5,4 +5,4 @@ export {
|
||||||
} from "./PluginActionContext";
|
} from "./PluginActionContext";
|
||||||
export { default as PluginActionToolbar } from "./components/PluginActionToolbar";
|
export { default as PluginActionToolbar } from "./components/PluginActionToolbar";
|
||||||
export { default as PluginActionForm } from "./components/PluginActionForm";
|
export { default as PluginActionForm } from "./components/PluginActionForm";
|
||||||
export { default as PluginActionResponsePane } from "./components/PluginActionResponsePane";
|
export { default as PluginActionResponse } from "./components/PluginActionResponse";
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -555,7 +555,9 @@ export const NO_LOGS = () => "No logs to show";
|
||||||
export const NO_ERRORS = () => "No signs of trouble here!";
|
export const NO_ERRORS = () => "No signs of trouble here!";
|
||||||
export const DEBUGGER_ERRORS = () => "Errors";
|
export const DEBUGGER_ERRORS = () => "Errors";
|
||||||
export const DEBUGGER_RESPONSE = () => "Response";
|
export const DEBUGGER_RESPONSE = () => "Response";
|
||||||
|
export const DEBUGGER_HEADERS = () => "Headers";
|
||||||
export const DEBUGGER_LOGS = () => "Logs";
|
export const DEBUGGER_LOGS = () => "Logs";
|
||||||
|
|
||||||
export const INSPECT_ENTITY = () => "Inspect entity";
|
export const INSPECT_ENTITY = () => "Inspect entity";
|
||||||
export const INSPECT_ENTITY_BLANK_STATE = () => "Select an entity to inspect";
|
export const INSPECT_ENTITY_BLANK_STATE = () => "Select an entity to inspect";
|
||||||
export const VALUE_IS_INVALID = (propertyPath: string) =>
|
export const VALUE_IS_INVALID = (propertyPath: string) =>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import React from "react";
|
||||||
import {
|
import {
|
||||||
PluginActionEditor,
|
PluginActionEditor,
|
||||||
PluginActionForm,
|
PluginActionForm,
|
||||||
PluginActionResponsePane,
|
PluginActionResponse,
|
||||||
} from "PluginActionEditor";
|
} from "PluginActionEditor";
|
||||||
import {
|
import {
|
||||||
ConvertToModuleDisabler,
|
ConvertToModuleDisabler,
|
||||||
|
|
@ -17,7 +17,7 @@ const AppPluginActionEditor = () => {
|
||||||
<AppPluginActionToolbar />
|
<AppPluginActionToolbar />
|
||||||
<ConvertToModuleCallout />
|
<ConvertToModuleCallout />
|
||||||
<PluginActionForm />
|
<PluginActionForm />
|
||||||
<PluginActionResponsePane />
|
<PluginActionResponse />
|
||||||
</ConvertToModuleDisabler>
|
</ConvertToModuleDisabler>
|
||||||
</PluginActionEditor>
|
</PluginActionEditor>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -63,9 +63,7 @@ const ActionExecutionInProgressView = ({
|
||||||
<Button
|
<Button
|
||||||
className={`t--cancel-action-button`}
|
className={`t--cancel-action-button`}
|
||||||
kind="secondary"
|
kind="secondary"
|
||||||
onClick={() => {
|
onClick={handleCancelActionExecution}
|
||||||
handleCancelActionExecution();
|
|
||||||
}}
|
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
{createMessage(ACTION_EXECUTION_CANCEL)}
|
{createMessage(ACTION_EXECUTION_CANCEL)}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import { lightTheme } from "selectors/themeSelectors";
|
||||||
import { BrowserRouter as Router } from "react-router-dom";
|
import { BrowserRouter as Router } from "react-router-dom";
|
||||||
import { EditorViewMode } from "ee/entities/IDE/constants";
|
import { EditorViewMode } from "ee/entities/IDE/constants";
|
||||||
import "@testing-library/jest-dom/extend-expect";
|
import "@testing-library/jest-dom/extend-expect";
|
||||||
|
import { APIFactory } from "test/factories/Actions/API";
|
||||||
|
import { noop } from "lodash";
|
||||||
|
|
||||||
jest.mock("./EntityBottomTabs", () => ({
|
jest.mock("./EntityBottomTabs", () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
|
|
@ -68,16 +70,20 @@ describe("ApiResponseView", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("the container should have class select-text to enable the selection of text for user", () => {
|
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(
|
const { container } = render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ThemeProvider theme={lightTheme}>
|
<ThemeProvider theme={lightTheme}>
|
||||||
<Router>
|
<Router>
|
||||||
<ApiResponseView
|
<ApiResponseView
|
||||||
apiName="Api1"
|
currentActionConfig={Api1}
|
||||||
|
disabled={false}
|
||||||
isRunning={false}
|
isRunning={false}
|
||||||
onRunClick={() => {}}
|
onRunClick={noop}
|
||||||
responseDataTypes={[]}
|
|
||||||
responseDisplayFormat={{ title: "JSON", value: "JSON" }}
|
|
||||||
/>
|
/>
|
||||||
</Router>
|
</Router>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|
|
||||||
|
|
@ -1,221 +1,50 @@
|
||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
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 { 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 {
|
import {
|
||||||
CHECK_REQUEST_BODY,
|
|
||||||
createMessage,
|
createMessage,
|
||||||
DEBUGGER_ERRORS,
|
DEBUGGER_ERRORS,
|
||||||
|
DEBUGGER_HEADERS,
|
||||||
DEBUGGER_LOGS,
|
DEBUGGER_LOGS,
|
||||||
DEBUGGER_RESPONSE,
|
DEBUGGER_RESPONSE,
|
||||||
EMPTY_RESPONSE_FIRST_HALF,
|
|
||||||
EMPTY_RESPONSE_LAST_HALF,
|
|
||||||
} from "ee/constants/messages";
|
} from "ee/constants/messages";
|
||||||
import { EditorTheme } from "./CodeEditor/EditorConfig";
|
import { EditorTheme } from "./CodeEditor/EditorConfig";
|
||||||
import NoResponseSVG from "assets/images/no-response.svg";
|
|
||||||
import DebuggerLogs from "./Debugger/DebuggerLogs";
|
import DebuggerLogs from "./Debugger/DebuggerLogs";
|
||||||
import ErrorLogs from "./Debugger/Errors";
|
import ErrorLogs from "./Debugger/Errors";
|
||||||
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
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 type { BottomTab } from "./EntityBottomTabs";
|
||||||
import EntityBottomTabs from "./EntityBottomTabs";
|
import EntityBottomTabs from "./EntityBottomTabs";
|
||||||
import { DEBUGGER_TAB_KEYS } from "./Debugger/helpers";
|
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 { getErrorCount } from "selectors/debuggerSelectors";
|
||||||
import { ActionExecutionResizerHeight } from "pages/Editor/APIEditor/constants";
|
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 type { Action } from "entities/Action";
|
||||||
import { SegmentedControlContainer } from "../../pages/Editor/QueryEditor/EditorJSONtoForm";
|
|
||||||
import ActionExecutionInProgressView from "./ActionExecutionInProgressView";
|
|
||||||
import { EMPTY_RESPONSE } from "./emptyResponse";
|
import { EMPTY_RESPONSE } from "./emptyResponse";
|
||||||
import { setApiPaneDebuggerState } from "actions/apiPaneActions";
|
import { setApiPaneDebuggerState } from "actions/apiPaneActions";
|
||||||
import { getApiPaneDebuggerState } from "selectors/apiPaneSelectors";
|
import { getApiPaneDebuggerState } from "selectors/apiPaneSelectors";
|
||||||
import { getIDEViewMode } from "selectors/ideSelectors";
|
import { getIDEViewMode } from "selectors/ideSelectors";
|
||||||
import { EditorViewMode } from "ee/entities/IDE/constants";
|
import { EditorViewMode } from "ee/entities/IDE/constants";
|
||||||
import ApiResponseMeta from "./ApiResponseMeta";
|
|
||||||
import useDebuggerTriggerClick from "./Debugger/hooks/useDebuggerTriggerClick";
|
import useDebuggerTriggerClick from "./Debugger/hooks/useDebuggerTriggerClick";
|
||||||
import { IDEBottomView, ViewHideBehaviour } from "IDE";
|
import { IDEBottomView, ViewHideBehaviour } from "IDE";
|
||||||
|
import { ApiResponse } from "PluginActionEditor/components/PluginActionResponse/components/ApiResponse";
|
||||||
const ResponseTabWrapper = styled.div`
|
import { ApiResponseHeaders } from "PluginActionEditor/components/PluginActionResponse/components/ApiResponseHeaders";
|
||||||
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;
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
currentActionConfig?: Action;
|
currentActionConfig: Action;
|
||||||
theme?: EditorTheme;
|
theme?: EditorTheme;
|
||||||
apiName: string;
|
disabled: boolean;
|
||||||
disabled?: boolean;
|
|
||||||
onRunClick: () => void;
|
onRunClick: () => void;
|
||||||
responseDataTypes: { key: string; title: string }[];
|
|
||||||
responseDisplayFormat: { title: string; value: string };
|
|
||||||
actionResponse?: ActionResponse;
|
actionResponse?: ActionResponse;
|
||||||
isRunning: boolean;
|
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) {
|
function ApiResponseView(props: Props) {
|
||||||
const {
|
const {
|
||||||
actionResponse = EMPTY_RESPONSE,
|
actionResponse = EMPTY_RESPONSE,
|
||||||
apiName,
|
|
||||||
currentActionConfig,
|
currentActionConfig,
|
||||||
disabled,
|
disabled,
|
||||||
isRunning,
|
isRunning,
|
||||||
responseDataTypes,
|
|
||||||
responseDisplayFormat,
|
|
||||||
theme = EditorTheme.LIGHT,
|
theme = EditorTheme.LIGHT,
|
||||||
} = props;
|
} = props;
|
||||||
const hasFailed = actionResponse.statusCode
|
|
||||||
? actionResponse.statusCode[0] !== "2"
|
|
||||||
: false;
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const errorCount = useSelector(getErrorCount);
|
const errorCount = useSelector(getErrorCount);
|
||||||
|
|
@ -234,256 +63,55 @@ function ApiResponseView(props: Props) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const messages = actionResponse?.messages;
|
// update the selected tab in the response pane.
|
||||||
let responseHeaders = {};
|
const updateSelectedResponseTab = useCallback(
|
||||||
|
(tabKey: string) => {
|
||||||
// if no headers are present in the response, use the default body text.
|
if (tabKey === DEBUGGER_TAB_KEYS.ERROR_TAB) {
|
||||||
if (actionResponse.headers) {
|
AnalyticsUtil.logEvent("OPEN_DEBUGGER", {
|
||||||
Object.entries(actionResponse.headers).forEach(([key, value]) => {
|
source: "API_PANE",
|
||||||
if (isArray(value) && value.length < 2)
|
|
||||||
return (responseHeaders = {
|
|
||||||
...responseHeaders,
|
|
||||||
[key]: value[0],
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (responseHeaders = {
|
dispatch(setApiPaneDebuggerState({ open: true, selectedTab: tabKey }));
|
||||||
...responseHeaders,
|
},
|
||||||
[key]: value,
|
[dispatch],
|
||||||
});
|
|
||||||
});
|
|
||||||
} 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,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
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.
|
// update the height of the response pane on resize.
|
||||||
const updateResponsePaneHeight = useCallback((height: number) => {
|
const updateResponsePaneHeight = useCallback(
|
||||||
dispatch(setApiPaneDebuggerState({ responseTabHeight: height }));
|
(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[] = [
|
const tabs: BottomTab[] = [
|
||||||
{
|
{
|
||||||
key: "response",
|
key: DEBUGGER_TAB_KEYS.RESPONSE_TAB,
|
||||||
title: createMessage(DEBUGGER_RESPONSE),
|
title: createMessage(DEBUGGER_RESPONSE),
|
||||||
panelComponent: (
|
panelComponent: (
|
||||||
<ResponseTabWrapper>
|
<ApiResponse
|
||||||
<ApiResponseMeta
|
action={currentActionConfig}
|
||||||
actionName={apiName || currentActionConfig?.name}
|
actionResponse={actionResponse}
|
||||||
actionResponse={actionResponse}
|
isRunDisabled={disabled}
|
||||||
/>
|
isRunning={isRunning}
|
||||||
{Array.isArray(messages) && messages.length > 0 && (
|
onRunClick={onRunClick}
|
||||||
<HelpSection>
|
responseTabHeight={responseTabHeight}
|
||||||
{messages.map((msg, i) => (
|
theme={theme}
|
||||||
<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>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "headers",
|
key: DEBUGGER_TAB_KEYS.HEADER_TAB,
|
||||||
title: "Headers",
|
title: createMessage(DEBUGGER_HEADERS),
|
||||||
panelComponent: (
|
panelComponent: (
|
||||||
<ResponseTabWrapper className="t--headers-tab">
|
<ApiResponseHeaders
|
||||||
{hasFailed && !isRunning && (
|
actionResponse={actionResponse}
|
||||||
<Callout
|
isRunDisabled={disabled}
|
||||||
kind="error"
|
isRunning={isRunning}
|
||||||
links={[
|
onDebugClick={onDebugClick}
|
||||||
{
|
onRunClick={onRunClick}
|
||||||
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>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -499,7 +127,7 @@ function ApiResponseView(props: Props) {
|
||||||
{
|
{
|
||||||
key: DEBUGGER_TAB_KEYS.LOGS_TAB,
|
key: DEBUGGER_TAB_KEYS.LOGS_TAB,
|
||||||
title: createMessage(DEBUGGER_LOGS),
|
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
|
//TODO: move this to a common place
|
||||||
const toggleHide = useCallback(
|
const toggleHide = useCallback(
|
||||||
() => dispatch(setApiPaneDebuggerState({ open: !open })),
|
() => dispatch(setApiPaneDebuggerState({ open: !open })),
|
||||||
[open],
|
[dispatch, open],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,13 @@ import { DEBUGGER_TAB_KEYS } from "./Debugger/helpers";
|
||||||
import type { BottomTab } from "./EntityBottomTabs";
|
import type { BottomTab } from "./EntityBottomTabs";
|
||||||
import EntityBottomTabs from "./EntityBottomTabs";
|
import EntityBottomTabs from "./EntityBottomTabs";
|
||||||
import { getIsSavingEntity } from "selectors/editorSelectors";
|
import { getIsSavingEntity } from "selectors/editorSelectors";
|
||||||
import { getJSResponseViewState } from "./utils";
|
import { getJSResponseViewState, JSResponseState } from "./utils";
|
||||||
import { getFilteredErrors } from "selectors/debuggerSelectors";
|
import { getFilteredErrors } from "selectors/debuggerSelectors";
|
||||||
|
import { NoResponse } from "PluginActionEditor/components/PluginActionResponse/components/NoResponse";
|
||||||
import {
|
import {
|
||||||
NoResponse,
|
|
||||||
ResponseTabErrorContainer,
|
ResponseTabErrorContainer,
|
||||||
ResponseTabErrorContent,
|
ResponseTabErrorContent,
|
||||||
} from "./ApiResponseView";
|
} from "PluginActionEditor/components/PluginActionResponse/components/ApiResponse";
|
||||||
import LogHelper from "./Debugger/ErrorLogs/components/LogHelper";
|
import LogHelper from "./Debugger/ErrorLogs/components/LogHelper";
|
||||||
import LOG_TYPE from "entities/AppsmithConsole/logtype";
|
import LOG_TYPE from "entities/AppsmithConsole/logtype";
|
||||||
import type { Log, SourceEntity } from "entities/AppsmithConsole";
|
import type { Log, SourceEntity } from "entities/AppsmithConsole";
|
||||||
|
|
@ -45,7 +45,7 @@ import { EditorViewMode } from "ee/entities/IDE/constants";
|
||||||
import ErrorLogs from "./Debugger/Errors";
|
import ErrorLogs from "./Debugger/Errors";
|
||||||
import { isBrowserExecutionAllowed } from "ee/utils/actionExecutionUtils";
|
import { isBrowserExecutionAllowed } from "ee/utils/actionExecutionUtils";
|
||||||
import JSRemoteExecutionView from "ee/components/JSRemoteExecutionView";
|
import JSRemoteExecutionView from "ee/components/JSRemoteExecutionView";
|
||||||
import { IDEBottomView, ViewHideBehaviour } from "../../IDE";
|
import { IDEBottomView, ViewHideBehaviour } from "IDE";
|
||||||
|
|
||||||
const ResponseTabWrapper = styled.div`
|
const ResponseTabWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -66,15 +66,6 @@ const NoReturnValueWrapper = styled.div`
|
||||||
padding-top: ${(props) => props.theme.spaces[6]}px;
|
padding-top: ${(props) => props.theme.spaces[6]}px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export enum JSResponseState {
|
|
||||||
IsExecuting = "IsExecuting",
|
|
||||||
IsDirty = "IsDirty",
|
|
||||||
IsUpdating = "IsUpdating",
|
|
||||||
NoResponse = "NoResponse",
|
|
||||||
ShowResponse = "ShowResponse",
|
|
||||||
NoReturnValue = "NoReturnValue",
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ReduxStateProps {
|
interface ReduxStateProps {
|
||||||
errorCount: number;
|
errorCount: number;
|
||||||
}
|
}
|
||||||
|
|
@ -229,8 +220,8 @@ function JSResponseView(props: Props) {
|
||||||
<>
|
<>
|
||||||
{responseStatus === JSResponseState.NoResponse && (
|
{responseStatus === JSResponseState.NoResponse && (
|
||||||
<NoResponse
|
<NoResponse
|
||||||
isButtonDisabled={disabled}
|
isRunDisabled={disabled}
|
||||||
isQueryRunning={isLoading}
|
isRunning={isLoading}
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
onRunClick={onButtonClick}
|
onRunClick={onButtonClick}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { JSResponseState } from "./JSResponseView";
|
import { getJSResponseViewState, JSResponseState } from "./utils";
|
||||||
import { getJSResponseViewState } from "./utils";
|
|
||||||
|
|
||||||
const TEST_JS_FUNCTION_ID = "627ccff468e1fa5185b7f901";
|
const TEST_JS_FUNCTION_ID = "627ccff468e1fa5185b7f901";
|
||||||
const TEST_JS_FUNCTION_BASE_ID = "627ccff468e1fa5185b7f912";
|
const TEST_JS_FUNCTION_BASE_ID = "627ccff468e1fa5185b7f912";
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
import type { JSAction } from "entities/JSCollection";
|
import type { JSAction } from "entities/JSCollection";
|
||||||
import { JSResponseState } from "./JSResponseView";
|
|
||||||
|
|
||||||
export const isHtml = (str: string) => {
|
export enum JSResponseState {
|
||||||
const doc = new DOMParser().parseFromString(str, "text/html");
|
IsExecuting = "IsExecuting",
|
||||||
|
IsDirty = "IsDirty",
|
||||||
return Array.from(doc.body.childNodes).some(
|
IsUpdating = "IsUpdating",
|
||||||
// TODO: Fix this the next time the file is edited
|
NoResponse = "NoResponse",
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
ShowResponse = "ShowResponse",
|
||||||
(node: any) => node.nodeType === 1,
|
NoReturnValue = "NoReturnValue",
|
||||||
);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns state of the JSResponseview editor component
|
* Returns state of the JSResponseview editor component
|
||||||
|
|
|
||||||
|
|
@ -85,29 +85,14 @@ export const HTTP_METHODS_DEFAULT_FORMAT_TYPES: Record<HTTP_METHOD, string> = {
|
||||||
PATCH: POST_BODY_FORMAT_OPTIONS.JSON,
|
PATCH: POST_BODY_FORMAT_OPTIONS.JSON,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_PROVIDER_OPTION = "Business Software";
|
|
||||||
export const CONTENT_TYPE_HEADER_KEY = "content-type";
|
export const CONTENT_TYPE_HEADER_KEY = "content-type";
|
||||||
|
|
||||||
export enum ApiResponseTypes {
|
export enum ResponseDisplayFormats {
|
||||||
JSON = "JSON",
|
JSON = "JSON",
|
||||||
TABLE = "TABLE",
|
TABLE = "TABLE",
|
||||||
RAW = "RAW",
|
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(
|
export const POST_BODY_FORMAT_OPTIONS_ARRAY = Object.values(
|
||||||
POST_BODY_FORMAT_OPTIONS,
|
POST_BODY_FORMAT_OPTIONS,
|
||||||
);
|
);
|
||||||
|
|
@ -133,6 +118,4 @@ export interface MULTI_PART_DROPDOWN_OPTION {
|
||||||
export const MULTI_PART_DROPDOWN_OPTIONS: MULTI_PART_DROPDOWN_OPTION[] =
|
export const MULTI_PART_DROPDOWN_OPTIONS: MULTI_PART_DROPDOWN_OPTION[] =
|
||||||
Object.values(MultiPartOptionTypes).map((value) => ({ label: value, value }));
|
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";
|
export const DEFAULT_MULTI_PART_DROPDOWN_PLACEHOLDER = "Type";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from "ce/PluginActionEditor/components/PluginActionResponse/hooks/usePluginActionResponseTabs";
|
||||||
|
|
@ -212,7 +212,6 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
|
||||||
const {
|
const {
|
||||||
actionConfigurationHeaders,
|
actionConfigurationHeaders,
|
||||||
actionConfigurationParams,
|
actionConfigurationParams,
|
||||||
actionName,
|
|
||||||
actionResponse,
|
actionResponse,
|
||||||
autoGeneratedActionConfigHeaders,
|
autoGeneratedActionConfigHeaders,
|
||||||
closeEditorLink,
|
closeEditorLink,
|
||||||
|
|
@ -224,8 +223,6 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
|
||||||
onRunClick,
|
onRunClick,
|
||||||
paramsCount,
|
paramsCount,
|
||||||
pluginId,
|
pluginId,
|
||||||
responseDataTypes,
|
|
||||||
responseDisplayFormat,
|
|
||||||
settingsConfig,
|
settingsConfig,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|
@ -256,6 +253,8 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
|
||||||
getPlugin(state, pluginId ?? ""),
|
getPlugin(state, pluginId ?? ""),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!currentActionConfig) return null;
|
||||||
|
|
||||||
// this gets the url of the current action's datasource
|
// this gets the url of the current action's datasource
|
||||||
const actionDatasourceUrl =
|
const actionDatasourceUrl =
|
||||||
currentActionConfig?.datasource?.datasourceConfiguration?.url || "";
|
currentActionConfig?.datasource?.datasourceConfiguration?.url || "";
|
||||||
|
|
@ -351,13 +350,10 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
|
||||||
</TabbedViewContainer>
|
</TabbedViewContainer>
|
||||||
<ApiResponseView
|
<ApiResponseView
|
||||||
actionResponse={actionResponse}
|
actionResponse={actionResponse}
|
||||||
apiName={actionName}
|
|
||||||
currentActionConfig={currentActionConfig}
|
currentActionConfig={currentActionConfig}
|
||||||
disabled={!isExecutePermitted}
|
disabled={!isExecutePermitted}
|
||||||
isRunning={isRunning}
|
isRunning={isRunning}
|
||||||
onRunClick={onRunClick}
|
onRunClick={onRunClick}
|
||||||
responseDataTypes={responseDataTypes}
|
|
||||||
responseDisplayFormat={responseDisplayFormat}
|
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
<RunHistory />
|
<RunHistory />
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@ import { useDispatch, useSelector } from "react-redux";
|
||||||
import ReactJson from "react-json-view";
|
import ReactJson from "react-json-view";
|
||||||
import {
|
import {
|
||||||
apiReactJsonProps,
|
apiReactJsonProps,
|
||||||
NoResponse,
|
|
||||||
responseTabComponent,
|
|
||||||
ResponseTabErrorContainer,
|
ResponseTabErrorContainer,
|
||||||
ResponseTabErrorContent,
|
ResponseTabErrorContent,
|
||||||
ResponseTabErrorDefaultMessage,
|
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 LogAdditionalInfo from "components/editorComponents/Debugger/ErrorLogs/components/LogAdditionalInfo";
|
||||||
import LogHelper from "components/editorComponents/Debugger/ErrorLogs/components/LogHelper";
|
import LogHelper from "components/editorComponents/Debugger/ErrorLogs/components/LogHelper";
|
||||||
import LOG_TYPE from "entities/AppsmithConsole/logtype";
|
import LOG_TYPE from "entities/AppsmithConsole/logtype";
|
||||||
|
|
@ -102,6 +102,8 @@ const QueryResponseTab = (props: Props) => {
|
||||||
const { responseDataTypes, responseDisplayFormat } =
|
const { responseDataTypes, responseDisplayFormat } =
|
||||||
actionResponseDisplayDataFormats(actionResponse);
|
actionResponseDisplayDataFormats(actionResponse);
|
||||||
|
|
||||||
|
let output: Record<string, unknown>[] | string = "";
|
||||||
|
|
||||||
const responseBodyTabs =
|
const responseBodyTabs =
|
||||||
responseDataTypes &&
|
responseDataTypes &&
|
||||||
responseDataTypes.map((dataType, index) => {
|
responseDataTypes.map((dataType, index) => {
|
||||||
|
|
@ -109,10 +111,12 @@ const QueryResponseTab = (props: Props) => {
|
||||||
index: index,
|
index: index,
|
||||||
key: dataType.key,
|
key: dataType.key,
|
||||||
title: dataType.title,
|
title: dataType.title,
|
||||||
panelComponent: responseTabComponent(
|
panelComponent: (
|
||||||
dataType.key,
|
<ResponseFormatTabs
|
||||||
output,
|
data={output}
|
||||||
responseTabHeight,
|
responseType={dataType.key}
|
||||||
|
tableBodyHeight={responseTabHeight}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -163,9 +167,6 @@ const QueryResponseTab = (props: Props) => {
|
||||||
let error = runErrorMessage;
|
let error = runErrorMessage;
|
||||||
let hintMessages: Array<string> = [];
|
let hintMessages: Array<string> = [];
|
||||||
let showPreparedStatementWarning = false;
|
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.
|
// Query is executed even once during the session, show the response data.
|
||||||
if (actionResponse) {
|
if (actionResponse) {
|
||||||
|
|
@ -326,17 +327,19 @@ const QueryResponseTab = (props: Props) => {
|
||||||
suggestedWidgets={actionResponse?.suggestedWidgets}
|
suggestedWidgets={actionResponse?.suggestedWidgets}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
{responseTabComponent(
|
<ResponseFormatTabs
|
||||||
selectedControl || segmentedControlOptions[0]?.value,
|
data={output}
|
||||||
output,
|
responseType={
|
||||||
responseTabHeight,
|
selectedControl || segmentedControlOptions[0]?.value
|
||||||
)}
|
}
|
||||||
|
tableBodyHeight={responseTabHeight}
|
||||||
|
/>
|
||||||
</ResponseDataContainer>
|
</ResponseDataContainer>
|
||||||
)}
|
)}
|
||||||
{!output && !error && (
|
{!output && !error && (
|
||||||
<NoResponse
|
<NoResponse
|
||||||
isButtonDisabled={!isExecutePermitted}
|
isRunDisabled={!isExecutePermitted}
|
||||||
isQueryRunning={isRunning}
|
isRunning={isRunning}
|
||||||
onRunClick={responseTabOnRunClick}
|
onRunClick={responseTabOnRunClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user