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";
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
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";
|
||||
|
|
|
|||
|
|
@ -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 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) =>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -63,9 +63,7 @@ const ActionExecutionInProgressView = ({
|
|||
<Button
|
||||
className={`t--cancel-action-button`}
|
||||
kind="secondary"
|
||||
onClick={() => {
|
||||
handleCancelActionExecution();
|
||||
}}
|
||||
onClick={handleCancelActionExecution}
|
||||
size="md"
|
||||
>
|
||||
{createMessage(ACTION_EXECUTION_CANCEL)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "ce/PluginActionEditor/components/PluginActionResponse/hooks/usePluginActionResponseTabs";
|
||||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user