chore: action response view refactor (#29031)

## Description
Refactor PR for action response view and action execution saga

#### PR fixes following issue(s)
Refactor PR for https://github.com/appsmithorg/appsmith-ee/pull/2936

#### Media
> A video or a GIF is preferred. when using Loom, don’t embed because it
looks like it’s a GIF. instead, just link to the video
>
>
#### Type of change
- Chore (housekeeping or task changes that don't impact user perception)

## Testing
>
#### How Has This Been Tested?
> Please describe the tests that you ran to verify your changes. Also
list any relevant details for your test configuration.
> Delete anything that is not relevant
- [ ] Manual
- [ ] JUnit
- [ ] Jest
- [ ] Cypress
>
>
#### Test Plan
> Add Testsmith test cases links that relate to this PR
>
>
#### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking
(copy link from comments dropped on this PR)
>
>
>
## Checklist:
#### Dev activity
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed

---------

Co-authored-by: Rishabh-Rathod <rishabh.rathod@appsmith.com>
This commit is contained in:
ashit-rath 2023-11-24 11:34:06 +05:30 committed by GitHub
parent ffcf12d8e5
commit dea2fd736c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 640 additions and 615 deletions

View File

@ -228,11 +228,16 @@ export const executePluginActionRequest = (payload: { id: string }) => ({
payload: payload, payload: payload,
}); });
export const executePluginActionSuccess = (payload: { export interface ExecutePluginActionSuccessPayload {
id: string; id: string;
response: ActionResponse; response: ActionResponse;
isPageLoad?: boolean; isPageLoad?: boolean;
}) => ({ isActionCreatedInApp: boolean;
}
export const executePluginActionSuccess = (
payload: ExecutePluginActionSuccessPayload,
) => ({
type: ReduxActionTypes.EXECUTE_PLUGIN_ACTION_SUCCESS, type: ReduxActionTypes.EXECUTE_PLUGIN_ACTION_SUCCESS,
payload: payload, payload: payload,
}); });

View File

@ -8,7 +8,10 @@ import type { ActionResponse } from "api/ActionAPI";
import type { ExecuteErrorPayload } from "constants/AppsmithActionConstants/ActionConstants"; import type { ExecuteErrorPayload } from "constants/AppsmithActionConstants/ActionConstants";
import _ from "lodash"; import _ from "lodash";
import type { Action } from "entities/Action"; import type { Action } from "entities/Action";
import type { UpdateActionPropertyActionPayload } from "actions/pluginActionActions"; import type {
ExecutePluginActionSuccessPayload,
UpdateActionPropertyActionPayload,
} from "actions/pluginActionActions";
export interface ActionData { export interface ActionData {
isLoading: boolean; isLoading: boolean;
@ -173,8 +176,10 @@ export const handlers = {
}, },
[ReduxActionTypes.EXECUTE_PLUGIN_ACTION_SUCCESS]: ( [ReduxActionTypes.EXECUTE_PLUGIN_ACTION_SUCCESS]: (
draftMetaState: Array<ActionData | PartialActionData>, draftMetaState: Array<ActionData | PartialActionData>,
action: ReduxAction<{ id: string; response: ActionResponse }>, action: ReduxAction<ExecutePluginActionSuccessPayload>,
) => { ) => {
if (!action.payload.isActionCreatedInApp) return;
const foundAction = draftMetaState.find((stateAction) => { const foundAction = draftMetaState.find((stateAction) => {
return stateAction.config.id === action.payload.id; return stateAction.config.id === action.payload.id;
}); });

View File

@ -0,0 +1,5 @@
import type { Action } from "entities/Action";
export function getIsActionCreatedInApp(action: Action) {
return !!action;
}

View File

@ -1,19 +1,14 @@
import type { PropsWithChildren, RefObject } from "react"; import type { PropsWithChildren, RefObject } from "react";
import React, { useCallback, useRef, useState } from "react"; import React, { useCallback, useRef, useState } from "react";
import { connect, useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import type { RouteComponentProps } from "react-router";
import { withRouter } from "react-router";
import ReactJson from "react-json-view"; import ReactJson from "react-json-view";
import styled from "styled-components"; import styled from "styled-components";
import type { AppState } from "@appsmith/reducers";
import type { ActionResponse } from "api/ActionAPI"; import type { ActionResponse } from "api/ActionAPI";
import { formatBytes } from "utils/helpers"; import { formatBytes } from "utils/helpers";
import type { APIEditorRouteParams } from "constants/routes";
import type { SourceEntity } from "entities/AppsmithConsole"; import type { SourceEntity } from "entities/AppsmithConsole";
import LOG_TYPE from "entities/AppsmithConsole/logtype"; import LOG_TYPE from "entities/AppsmithConsole/logtype";
import { ENTITY_TYPE } from "entities/AppsmithConsole"; import { ENTITY_TYPE } from "entities/AppsmithConsole";
import ReadOnlyEditor from "components/editorComponents/ReadOnlyEditor"; import ReadOnlyEditor from "components/editorComponents/ReadOnlyEditor";
import { getActionResponses } from "@appsmith/selectors/entitiesSelector";
import { isArray, isEmpty, isString } from "lodash"; import { isArray, isEmpty, isString } from "lodash";
import { import {
CHECK_REQUEST_BODY, CHECK_REQUEST_BODY,
@ -25,7 +20,7 @@ import {
DEBUGGER_ERRORS, DEBUGGER_ERRORS,
} from "@appsmith/constants/messages"; } from "@appsmith/constants/messages";
import { Text as BlueprintText } from "@blueprintjs/core"; import { Text as BlueprintText } from "@blueprintjs/core";
import type { EditorTheme } from "./CodeEditor/EditorConfig"; import { EditorTheme } from "./CodeEditor/EditorConfig";
import NoResponseSVG from "assets/images/no-response.svg"; 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";
@ -38,11 +33,11 @@ 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 Table from "pages/Editor/QueryEditor/Table";
import { API_RESPONSE_TYPE_OPTIONS } from "constants/ApiEditorConstants/CommonApiConstants"; import { API_RESPONSE_TYPE_OPTIONS } from "constants/ApiEditorConstants/CommonApiConstants";
import type { UpdateActionPropertyActionPayload } from "actions/pluginActionActions";
import { setActionResponseDisplayFormat } from "actions/pluginActionActions"; import { setActionResponseDisplayFormat } from "actions/pluginActionActions";
import { isHtml } from "./utils"; import { isHtml } from "./utils";
import { import {
getDebuggerSelectedTab, getDebuggerSelectedTab,
getErrorCount,
getResponsePaneHeight, getResponsePaneHeight,
} from "selectors/debuggerSelectors"; } from "selectors/debuggerSelectors";
import { ActionExecutionResizerHeight } from "pages/Editor/APIEditor/constants"; import { ActionExecutionResizerHeight } from "pages/Editor/APIEditor/constants";
@ -179,29 +174,17 @@ const ResponseBodyContainer = styled.div`
display: grid; display: grid;
`; `;
interface ReduxStateProps { interface Props {
responses: Record<string, ActionResponse | undefined>; currentActionConfig?: Action;
isRunning: Record<string, boolean>; theme?: EditorTheme;
errorCount: number; apiName: string;
disabled?: boolean;
onRunClick: () => void;
responseDataTypes: { key: string; title: string }[];
responseDisplayFormat: { title: string; value: string };
actionResponse?: ActionResponse;
isRunning: boolean;
} }
interface ReduxDispatchProps {
updateActionResponseDisplayFormat: ({
field,
id,
value,
}: UpdateActionPropertyActionPayload) => void;
}
type Props = ReduxStateProps &
ReduxDispatchProps &
RouteComponentProps<APIEditorRouteParams> & {
theme?: EditorTheme;
apiName: string;
disabled?: boolean;
onRunClick: () => void;
responseDataTypes: { key: string; title: string }[];
responseDisplayFormat: { title: string; value: string };
};
const StatusCodeText = styled(BaseText)<PropsWithChildren<{ code: string }>>` const StatusCodeText = styled(BaseText)<PropsWithChildren<{ code: string }>>`
color: ${(props) => color: ${(props) =>
@ -313,31 +296,21 @@ export const NoResponse = (props: NoResponseProps) => (
function ApiResponseView(props: Props) { function ApiResponseView(props: Props) {
const { const {
actionResponse = EMPTY_RESPONSE,
currentActionConfig,
disabled, disabled,
match: { isRunning,
params: { apiId },
},
responseDataTypes, responseDataTypes,
responseDisplayFormat, responseDisplayFormat,
responses, theme = EditorTheme.LIGHT,
updateActionResponseDisplayFormat,
} = props; } = props;
let response: ActionResponse = EMPTY_RESPONSE; const hasFailed = actionResponse.statusCode
let isRunning = false; ? actionResponse.statusCode[0] !== "2"
let hasFailed = false; : false;
if (apiId && apiId in responses) {
response = responses[apiId] || EMPTY_RESPONSE;
isRunning = props.isRunning[apiId];
hasFailed = response.statusCode ? response.statusCode[0] !== "2" : false;
}
const actions: Action[] = useSelector((state: AppState) =>
state.entities.actions.map((action) => action.config),
);
const currentActionConfig: Action | undefined = actions.find(
(action) => action.id === apiId,
);
const panelRef: RefObject<HTMLDivElement> = useRef(null); const panelRef: RefObject<HTMLDivElement> = useRef(null);
const dispatch = useDispatch(); const dispatch = useDispatch();
const errorCount = useSelector(getErrorCount);
const onDebugClick = useCallback(() => { const onDebugClick = useCallback(() => {
AnalyticsUtil.logEvent("OPEN_DEBUGGER", { AnalyticsUtil.logEvent("OPEN_DEBUGGER", {
@ -353,12 +326,12 @@ function ApiResponseView(props: Props) {
}); });
}; };
const messages = response?.messages; const messages = actionResponse?.messages;
let responseHeaders = {}; let responseHeaders = {};
// if no headers are present in the response, use the default body text. // if no headers are present in the response, use the default body text.
if (response.headers) { if (actionResponse.headers) {
Object.entries(response.headers).forEach(([key, value]) => { Object.entries(actionResponse.headers).forEach(([key, value]) => {
if (isArray(value) && value.length < 2) if (isArray(value) && value.length < 2)
return (responseHeaders = { return (responseHeaders = {
...responseHeaders, ...responseHeaders,
@ -375,17 +348,19 @@ function ApiResponseView(props: Props) {
} }
const onResponseTabSelect = (tab: string) => { const onResponseTabSelect = (tab: string) => {
updateActionResponseDisplayFormat({ dispatch(
id: apiId ? apiId : "", setActionResponseDisplayFormat({
field: "responseDisplayFormat", id: currentActionConfig?.id || "",
value: tab, field: "responseDisplayFormat",
}); value: tab,
}),
);
}; };
let filteredResponseDataTypes: { key: string; title: string }[] = [ let filteredResponseDataTypes: { key: string; title: string }[] = [
...responseDataTypes, ...responseDataTypes,
]; ];
if (!!response.body && !isArray(response.body)) { if (!!actionResponse.body && !isArray(actionResponse.body)) {
filteredResponseDataTypes = responseDataTypes.filter( filteredResponseDataTypes = responseDataTypes.filter(
(item) => item.key !== API_RESPONSE_TYPE_OPTIONS.TABLE, (item) => item.key !== API_RESPONSE_TYPE_OPTIONS.TABLE,
); );
@ -403,7 +378,7 @@ function ApiResponseView(props: Props) {
title: dataType.title, title: dataType.title,
panelComponent: responseTabComponent( panelComponent: responseTabComponent(
dataType.key, dataType.key,
response?.body, actionResponse?.body,
responsePaneHeight, responsePaneHeight,
), ),
}; };
@ -444,12 +419,12 @@ function ApiResponseView(props: Props) {
}, []); }, []);
// get request timestamp formatted to human readable format. // get request timestamp formatted to human readable format.
const responseState = getUpdateTimestamp(response.request); const responseState = getUpdateTimestamp(actionResponse.request);
// action source for analytics. // action source for analytics.
const actionSource: SourceEntity = { const actionSource: SourceEntity = {
type: ENTITY_TYPE.ACTION, type: ENTITY_TYPE.ACTION,
name: currentActionConfig ? currentActionConfig.name : "API", name: currentActionConfig ? currentActionConfig.name : "API",
id: apiId ? apiId : "", id: currentActionConfig?.id || "",
}; };
const tabs = [ const tabs = [
{ {
@ -471,16 +446,18 @@ function ApiResponseView(props: Props) {
<ResponseTabErrorContent> <ResponseTabErrorContent>
<ResponseTabErrorDefaultMessage> <ResponseTabErrorDefaultMessage>
Your API failed to execute Your API failed to execute
{response.pluginErrorDetails && ":"} {actionResponse.pluginErrorDetails && ":"}
</ResponseTabErrorDefaultMessage> </ResponseTabErrorDefaultMessage>
{response.pluginErrorDetails && ( {actionResponse.pluginErrorDetails && (
<> <>
<div className="t--debugger-log-downstream-message"> <div className="t--debugger-log-downstream-message">
{response.pluginErrorDetails.downstreamErrorMessage} {actionResponse.pluginErrorDetails.downstreamErrorMessage}
</div> </div>
{response.pluginErrorDetails.downstreamErrorCode && ( {actionResponse.pluginErrorDetails.downstreamErrorCode && (
<LogAdditionalInfo <LogAdditionalInfo
text={response.pluginErrorDetails.downstreamErrorCode} text={
actionResponse.pluginErrorDetails.downstreamErrorCode
}
/> />
)} )}
</> </>
@ -488,11 +465,11 @@ function ApiResponseView(props: Props) {
<LogHelper <LogHelper
logType={LOG_TYPE.ACTION_EXECUTION_ERROR} logType={LOG_TYPE.ACTION_EXECUTION_ERROR}
name="PluginExecutionError" name="PluginExecutionError"
pluginErrorDetails={response.pluginErrorDetails} pluginErrorDetails={actionResponse.pluginErrorDetails}
source={actionSource} source={actionSource}
/> />
</ResponseTabErrorContent> </ResponseTabErrorContent>
{response.request && ( {actionResponse.request && (
<JsonWrapper <JsonWrapper
className="t--debugger-log-state" className="t--debugger-log-state"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
@ -503,7 +480,7 @@ function ApiResponseView(props: Props) {
</ResponseTabErrorContainer> </ResponseTabErrorContainer>
) : ( ) : (
<ResponseDataContainer> <ResponseDataContainer>
{isEmpty(response.statusCode) ? ( {isEmpty(actionResponse.statusCode) ? (
<NoResponse <NoResponse
isButtonDisabled={disabled} isButtonDisabled={disabled}
isQueryRunning={isRunning} isQueryRunning={isRunning}
@ -511,12 +488,13 @@ function ApiResponseView(props: Props) {
/> />
) : ( ) : (
<ResponseBodyContainer> <ResponseBodyContainer>
{isString(response?.body) && isHtml(response?.body) ? ( {isString(actionResponse?.body) &&
isHtml(actionResponse?.body) ? (
<ReadOnlyEditor <ReadOnlyEditor
folding folding
height={"100%"} height={"100%"}
input={{ input={{
value: response?.body, value: actionResponse?.body,
}} }}
/> />
) : responseTabs && ) : responseTabs &&
@ -536,7 +514,7 @@ function ApiResponseView(props: Props) {
/> />
{responseTabComponent( {responseTabComponent(
selectedControl || segmentedControlOptions[0]?.value, selectedControl || segmentedControlOptions[0]?.value,
response?.body, actionResponse?.body,
responsePaneHeight, responsePaneHeight,
)} )}
</SegmentedControlContainer> </SegmentedControlContainer>
@ -569,7 +547,7 @@ function ApiResponseView(props: Props) {
</Callout> </Callout>
)} )}
<ResponseDataContainer> <ResponseDataContainer>
{isEmpty(response.statusCode) ? ( {isEmpty(actionResponse.statusCode) ? (
<NoResponse <NoResponse
isButtonDisabled={disabled} isButtonDisabled={disabled}
isQueryRunning={isRunning} isQueryRunning={isRunning}
@ -593,7 +571,7 @@ function ApiResponseView(props: Props) {
{ {
key: DEBUGGER_TAB_KEYS.ERROR_TAB, key: DEBUGGER_TAB_KEYS.ERROR_TAB,
title: createMessage(DEBUGGER_ERRORS), title: createMessage(DEBUGGER_ERRORS),
count: props.errorCount, count: errorCount,
panelComponent: <ErrorLogs />, panelComponent: <ErrorLogs />,
}, },
{ {
@ -624,48 +602,49 @@ function ApiResponseView(props: Props) {
snapToHeight={ActionExecutionResizerHeight} snapToHeight={ActionExecutionResizerHeight}
/> />
{isRunning && ( {isRunning && (
<ActionExecutionInProgressView actionType="API" theme={props.theme} /> <ActionExecutionInProgressView actionType="API" theme={theme} />
)} )}
<TabbedViewWrapper> <TabbedViewWrapper>
{response.statusCode && ( {actionResponse.statusCode && (
<ResponseMetaWrapper> <ResponseMetaWrapper>
{response.statusCode && ( {actionResponse.statusCode && (
<Flex> <Flex>
<Text type={TextType.P3}>Status: </Text> <Text type={TextType.P3}>Status: </Text>
<StatusCodeText <StatusCodeText
accent="secondary" accent="secondary"
className="t--response-status-code" className="t--response-status-code"
code={response.statusCode.toString()} code={actionResponse.statusCode.toString()}
> >
{response.statusCode} {actionResponse.statusCode}
</StatusCodeText> </StatusCodeText>
</Flex> </Flex>
)} )}
<ResponseMetaInfo> <ResponseMetaInfo>
{response.duration && ( {actionResponse.duration && (
<Flex> <Flex>
<Text type={TextType.P3}>Time: </Text> <Text type={TextType.P3}>Time: </Text>
<Text type={TextType.H5}>{response.duration} ms</Text> <Text type={TextType.H5}>{actionResponse.duration} ms</Text>
</Flex> </Flex>
)} )}
{response.size && ( {actionResponse.size && (
<Flex> <Flex>
<Text type={TextType.P3}>Size: </Text> <Text type={TextType.P3}>Size: </Text>
<Text type={TextType.H5}> <Text type={TextType.H5}>
{formatBytes(parseInt(response.size))} {formatBytes(parseInt(actionResponse.size))}
</Text>
</Flex>
)}
{!isEmpty(response?.body) && Array.isArray(response?.body) && (
<Flex>
<Text type={TextType.P3}>Result: </Text>
<Text type={TextType.H5}>
{`${response?.body.length} Record${
response?.body.length > 1 ? "s" : ""
}`}
</Text> </Text>
</Flex> </Flex>
)} )}
{!isEmpty(actionResponse?.body) &&
Array.isArray(actionResponse?.body) && (
<Flex>
<Text type={TextType.P3}>Result: </Text>
<Text type={TextType.H5}>
{`${actionResponse?.body.length} Record${
actionResponse?.body.length > 1 ? "s" : ""
}`}
</Text>
</Flex>
)}
</ResponseMetaInfo> </ResponseMetaInfo>
</ResponseMetaWrapper> </ResponseMetaWrapper>
)} )}
@ -688,25 +667,4 @@ function ApiResponseView(props: Props) {
); );
} }
const mapStateToProps = (state: AppState): ReduxStateProps => { export default ApiResponseView;
return {
responses: getActionResponses(state),
isRunning: state.ui.apiPane.isRunning,
errorCount: state.ui.debugger.context.errorCount,
};
};
const mapDispatchToProps = (dispatch: any): ReduxDispatchProps => ({
updateActionResponseDisplayFormat: ({
field,
id,
value,
}: UpdateActionPropertyActionPayload) => {
dispatch(setActionResponseDisplayFormat({ id, field, value }));
},
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(withRouter(ApiResponseView));

View File

@ -0,0 +1 @@
export * from "ce/utils/getIsActionCreatedInApp";

View File

@ -8,7 +8,11 @@ import { GRAPHQL_HTTP_METHOD_OPTIONS } from "constants/ApiEditorConstants/GraphQ
import styled from "styled-components"; import styled from "styled-components";
import FormLabel from "components/editorComponents/FormLabel"; import FormLabel from "components/editorComponents/FormLabel";
import FormRow from "components/editorComponents/FormRow"; import FormRow from "components/editorComponents/FormRow";
import type { PaginationField, SuggestedWidget } from "api/ActionAPI"; import type {
ActionResponse,
PaginationField,
SuggestedWidget,
} from "api/ActionAPI";
import type { Action, PaginationType } from "entities/Action"; import type { Action, PaginationType } from "entities/Action";
import { isGraphqlPlugin } from "entities/Action"; import { isGraphqlPlugin } from "entities/Action";
import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray"; import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray";
@ -164,6 +168,7 @@ const MainContainer = styled.div`
/* padding: var(--ads-v2-spaces-7); */ /* padding: var(--ads-v2-spaces-7); */
`; `;
export interface CommonFormProps { export interface CommonFormProps {
actionResponse?: ActionResponse;
pluginId: string; pluginId: string;
onRunClick: (paginationField?: PaginationField) => void; onRunClick: (paginationField?: PaginationField) => void;
onDeleteClick: () => void; onDeleteClick: () => void;
@ -508,6 +513,7 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
actionConfigurationHeaders, actionConfigurationHeaders,
actionConfigurationParams, actionConfigurationParams,
actionName, actionName,
actionResponse,
autoGeneratedActionConfigHeaders, autoGeneratedActionConfigHeaders,
closeEditorLink, closeEditorLink,
currentActionDatasourceId, currentActionDatasourceId,
@ -729,8 +735,11 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
</TabbedViewContainer> </TabbedViewContainer>
{showDebugger && ( {showDebugger && (
<ApiResponseView <ApiResponseView
actionResponse={actionResponse}
apiName={actionName} apiName={actionName}
currentActionConfig={currentActionConfig}
disabled={!isExecutePermitted} disabled={!isExecutePermitted}
isRunning={isRunning}
onRunClick={onRunClick} onRunClick={onRunClick}
responseDataTypes={responseDataTypes} responseDataTypes={responseDataTypes}
responseDisplayFormat={responseDisplayFormat} responseDisplayFormat={responseDisplayFormat}

View File

@ -6,7 +6,6 @@ import classNames from "classnames";
import styled from "styled-components"; import styled from "styled-components";
import { API_EDITOR_FORM_NAME } from "@appsmith/constants/forms"; import { API_EDITOR_FORM_NAME } from "@appsmith/constants/forms";
import type { Action } from "entities/Action"; import type { Action } from "entities/Action";
import { EMPTY_RESPONSE } from "components/editorComponents/emptyResponse";
import type { AppState } from "@appsmith/reducers"; import type { AppState } from "@appsmith/reducers";
import { getApiName } from "selectors/formSelectors"; import { getApiName } from "selectors/formSelectors";
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig"; import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
@ -22,6 +21,7 @@ import { tailwindLayers } from "constants/Layers";
import VariableEditor from "./VariableEditor"; import VariableEditor from "./VariableEditor";
import Pagination from "./Pagination"; import Pagination from "./Pagination";
import { ApiEditorContext } from "../ApiEditorContext"; import { ApiEditorContext } from "../ApiEditorContext";
import { actionResponseDisplayDataFormats } from "pages/Editor/utils";
const ResizeableDiv = styled.div` const ResizeableDiv = styled.div`
display: flex; display: flex;
@ -235,37 +235,21 @@ export default connect(
let hasResponse = false; let hasResponse = false;
let suggestedWidgets; let suggestedWidgets;
if (apiId) { const actionResponse = getActionData(state, apiId);
const response = getActionData(state, apiId) || EMPTY_RESPONSE; if (actionResponse) {
hasResponse = hasResponse =
!isEmpty(response.statusCode) && response.statusCode[0] === "2"; !isEmpty(actionResponse.statusCode) &&
suggestedWidgets = response.suggestedWidgets; actionResponse.statusCode[0] === "2";
suggestedWidgets = actionResponse.suggestedWidgets;
} }
const actionData = getActionData(state, apiId); const actionData = getActionData(state, apiId);
let responseDisplayFormat: { title: string; value: string }; const { responseDataTypes, responseDisplayFormat } =
let responseDataTypes: { key: string; title: string }[]; actionResponseDisplayDataFormats(actionData);
if (!!actionData && actionData.responseDisplayFormat) {
responseDataTypes = actionData.dataTypes.map((data) => {
return {
key: data.dataType,
title: data.dataType,
};
});
responseDisplayFormat = {
title: actionData.responseDisplayFormat,
value: actionData.responseDisplayFormat,
};
} else {
responseDataTypes = [];
responseDisplayFormat = {
title: "",
value: "",
};
}
return { return {
actionName, actionName,
actionResponse,
apiId, apiId,
httpMethodFromForm, httpMethodFromForm,
actionConfigurationHeaders, actionConfigurationHeaders,

View File

@ -1,12 +1,18 @@
import React, { useContext } from "react"; import React, { useContext } from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import type { RouteComponentProps } from "react-router";
import type { InjectedFormProps } from "redux-form"; import type { InjectedFormProps } from "redux-form";
import { reduxForm, formValueSelector } from "redux-form"; import { reduxForm, formValueSelector } from "redux-form";
import { POST_BODY_FORMAT_OPTIONS } from "constants/ApiEditorConstants/CommonApiConstants"; import { POST_BODY_FORMAT_OPTIONS } from "constants/ApiEditorConstants/CommonApiConstants";
import styled from "styled-components"; import styled from "styled-components";
import FormLabel from "components/editorComponents/FormLabel"; import FormLabel from "components/editorComponents/FormLabel";
import FormRow from "components/editorComponents/FormRow"; import FormRow from "components/editorComponents/FormRow";
import type { PaginationField, BodyFormData, Property } from "api/ActionAPI"; import type {
PaginationField,
BodyFormData,
Property,
ActionResponse,
} from "api/ActionAPI";
import DynamicTextField from "components/editorComponents/form/fields/DynamicTextField"; import DynamicTextField from "components/editorComponents/form/fields/DynamicTextField";
import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray"; import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray";
import ApiResponseView from "components/editorComponents/ApiResponseView"; import ApiResponseView from "components/editorComponents/ApiResponseView";
@ -18,11 +24,12 @@ import type { PaginationType, Action } from "entities/Action";
import ActionNameEditor from "components/editorComponents/ActionNameEditor"; import ActionNameEditor from "components/editorComponents/ActionNameEditor";
import { NameWrapper } from "./CommonEditorForm"; import { NameWrapper } from "./CommonEditorForm";
import { BaseButton } from "components/designSystems/appsmith/BaseButton"; import { BaseButton } from "components/designSystems/appsmith/BaseButton";
import { getActionData } from "@appsmith/selectors/entitiesSelector"; import { getAction, getActionData } from "@appsmith/selectors/entitiesSelector";
import type { AppState } from "@appsmith/reducers"; import type { AppState } from "@appsmith/reducers";
import { Icon } from "design-system"; import { Icon } from "design-system";
import { showDebuggerFlag } from "selectors/debuggerSelectors"; import { showDebuggerFlag } from "selectors/debuggerSelectors";
import { ApiEditorContext } from "./ApiEditorContext"; import { ApiEditorContext } from "./ApiEditorContext";
import { actionResponseDisplayDataFormats } from "../utils";
const Form = styled.form` const Form = styled.form`
display: flex; display: flex;
@ -96,13 +103,21 @@ const TabbedViewContainer = styled.div`
padding-top: 12px; padding-top: 12px;
`; `;
interface APIFormProps { interface APIFormOwnProps {
onRunClick: (paginationField?: PaginationField) => void; apiId: string;
onDeleteClick: () => void; apiName: string;
isRunning: boolean;
isDeleting: boolean;
paginationType: PaginationType;
appName: string; appName: string;
isDeleting: boolean;
isRunning: boolean;
location: RouteComponentProps["location"];
onDeleteClick: () => void;
onRunClick: (paginationField?: PaginationField) => void;
paginationType: PaginationType;
}
interface APIFormProps {
actionData?: ActionResponse;
currentActionConfig?: Action;
templateId: string; templateId: string;
actionConfiguration?: any; actionConfiguration?: any;
actionConfigurationHeaders?: Property[]; actionConfigurationHeaders?: Property[];
@ -111,18 +126,15 @@ interface APIFormProps {
providerImage: string; providerImage: string;
providerURL: string; providerURL: string;
providerCredentialSteps: string; providerCredentialSteps: string;
location: {
pathname: string;
};
apiName: string;
apiId: string;
dispatch: any; dispatch: any;
responseDataTypes: { key: string; title: string }[]; responseDataTypes: { key: string; title: string }[];
responseDisplayFormat: { title: string; value: string }; responseDisplayFormat: { title: string; value: string };
showDebugger: boolean; showDebugger: boolean;
} }
type Props = APIFormProps & InjectedFormProps<Action, APIFormProps>; type Props = APIFormProps &
InjectedFormProps<Action, APIFormProps & APIFormOwnProps> &
APIFormOwnProps;
function RapidApiEditorForm(props: Props) { function RapidApiEditorForm(props: Props) {
const { const {
@ -276,7 +288,10 @@ function RapidApiEditorForm(props: Props) {
</TabbedViewContainer> </TabbedViewContainer>
{showDebugger && ( {showDebugger && (
<ApiResponseView <ApiResponseView
actionResponse={props.actionData}
apiName={props.apiName} apiName={props.apiName}
currentActionConfig={props.currentActionConfig}
isRunning={props.isRunning}
onRunClick={onRunClick} onRunClick={onRunClick}
responseDataTypes={responseDataTypes} responseDataTypes={responseDataTypes}
responseDisplayFormat={responseDisplayFormat} responseDisplayFormat={responseDisplayFormat}
@ -289,7 +304,7 @@ function RapidApiEditorForm(props: Props) {
const selector = formValueSelector(API_EDITOR_FORM_NAME); const selector = formValueSelector(API_EDITOR_FORM_NAME);
export default connect((state: AppState) => { export default connect((state: AppState, ownProps: APIFormOwnProps) => {
const displayFormat = selector(state, "displayFormat"); const displayFormat = selector(state, "displayFormat");
const providerImage = selector(state, "provider.imageUrl"); const providerImage = selector(state, "provider.imageUrl");
const providerURL = selector(state, "provider.url"); const providerURL = selector(state, "provider.url");
@ -317,29 +332,17 @@ export default connect((state: AppState) => {
`${actionConfigurationBodyFormData}`, `${actionConfigurationBodyFormData}`,
); );
} }
const currentActionConfig = getAction(state, ownProps.apiId);
const actionData = getActionData(state, actionConfiguration.id); const actionData = getActionData(state, actionConfiguration.id);
let responseDisplayFormat: { title: string; value: string }; const { responseDataTypes, responseDisplayFormat } =
let responseDataTypes: { key: string; title: string }[]; actionResponseDisplayDataFormats(actionData, {
if (!!actionData && actionData.responseDisplayFormat) {
responseDataTypes = actionData.dataTypes.map((data) => {
return {
key: data.dataType,
title: data.dataType,
};
});
responseDisplayFormat = {
title: actionData.responseDisplayFormat,
value: actionData.responseDisplayFormat,
};
} else {
responseDataTypes = [];
responseDisplayFormat = {
title: "JSON", title: "JSON",
value: "JSON", value: "JSON",
}; });
}
return { return {
actionData,
currentActionConfig,
displayFormat, displayFormat,
actionConfiguration, actionConfiguration,
actionConfigurationHeaders, actionConfigurationHeaders,
@ -353,7 +356,7 @@ export default connect((state: AppState) => {
providerCredentialSteps, providerCredentialSteps,
}; };
})( })(
reduxForm<Action, APIFormProps>({ reduxForm<Action, APIFormProps & APIFormOwnProps>({
form: API_EDITOR_FORM_NAME, form: API_EDITOR_FORM_NAME,
destroyOnUnmount: false, destroyOnUnmount: false,
})(RapidApiEditorForm), })(RapidApiEditorForm),

View File

@ -6,7 +6,6 @@ import styled from "styled-components";
import { API_EDITOR_FORM_NAME } from "@appsmith/constants/forms"; import { API_EDITOR_FORM_NAME } from "@appsmith/constants/forms";
import type { Action } from "entities/Action"; import type { Action } from "entities/Action";
import PostBodyData from "./PostBodyData"; import PostBodyData from "./PostBodyData";
import { EMPTY_RESPONSE } from "components/editorComponents/emptyResponse";
import type { AppState } from "@appsmith/reducers"; import type { AppState } from "@appsmith/reducers";
import { getApiName } from "selectors/formSelectors"; import { getApiName } from "selectors/formSelectors";
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig"; import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
@ -25,6 +24,7 @@ import CommonEditorForm from "./CommonEditorForm";
import Pagination from "./Pagination"; import Pagination from "./Pagination";
import { getCurrentEnvironmentId } from "@appsmith/selectors/environmentSelectors"; import { getCurrentEnvironmentId } from "@appsmith/selectors/environmentSelectors";
import { ApiEditorContext } from "./ApiEditorContext"; import { ApiEditorContext } from "./ApiEditorContext";
import { actionResponseDisplayDataFormats } from "../utils";
const NoBodyMessage = styled.div` const NoBodyMessage = styled.div`
margin-top: 20px; margin-top: 20px;
@ -162,39 +162,23 @@ export default connect((state: AppState, props: { pluginId: string }) => {
} }
const responses = getActionResponses(state); const responses = getActionResponses(state);
const actionResponse = responses[apiId];
let hasResponse = false; let hasResponse = false;
let suggestedWidgets; let suggestedWidgets;
if (apiId && apiId in responses) { if (actionResponse) {
const response = responses[apiId] || EMPTY_RESPONSE;
hasResponse = hasResponse =
!isEmpty(response.statusCode) && response.statusCode[0] === "2"; !isEmpty(actionResponse.statusCode) &&
suggestedWidgets = response.suggestedWidgets; actionResponse.statusCode[0] === "2";
suggestedWidgets = actionResponse.suggestedWidgets;
} }
const actionData = getActionData(state, apiId); const actionData = getActionData(state, apiId);
let responseDisplayFormat: { title: string; value: string }; const { responseDataTypes, responseDisplayFormat } =
let responseDataTypes: { key: string; title: string }[]; actionResponseDisplayDataFormats(actionData);
if (!!actionData && actionData.responseDisplayFormat) {
responseDataTypes = actionData.dataTypes.map((data) => {
return {
key: data.dataType,
title: data.dataType,
};
});
responseDisplayFormat = {
title: actionData.responseDisplayFormat,
value: actionData.responseDisplayFormat,
};
} else {
responseDataTypes = [];
responseDisplayFormat = {
title: "",
value: "",
};
}
return { return {
actionName, actionName,
actionResponse,
apiId, apiId,
httpMethodFromForm, httpMethodFromForm,
actionConfigurationHeaders, actionConfigurationHeaders,

View File

@ -228,10 +228,10 @@ class QueryEditor extends React.Component<Props> {
return ( return (
<QueryEditorForm <QueryEditorForm
actionResponse={responses[actionId]}
dataSources={dataSources} dataSources={dataSources}
datasourceId={this.props.datasourceId} datasourceId={this.props.datasourceId}
editorConfig={editorConfig} editorConfig={editorConfig}
executedQueryData={responses[actionId]}
formData={this.props.formData} formData={this.props.formData}
isDeleting={isDeleting} isDeleting={isDeleting}
isRunning={isRunning} isRunning={isRunning}

View File

@ -1,9 +1,8 @@
import { useContext, useEffect } from "react"; import { useContext, useEffect, useState } from "react";
import type { RefObject } from "react"; import React, { useCallback } from "react";
import React, { useCallback, useRef, useState } from "react";
import type { InjectedFormProps } from "redux-form"; import type { InjectedFormProps } from "redux-form";
import { Tag } from "@blueprintjs/core"; import { Tag } from "@blueprintjs/core";
import { isString, noop } from "lodash"; import { noop } from "lodash";
import type { Datasource } from "entities/Datasource"; import type { Datasource } from "entities/Datasource";
import { DatasourceStructureContext } from "entities/Datasource"; import { DatasourceStructureContext } from "entities/Datasource";
import { import {
@ -23,12 +22,9 @@ import DropdownField from "components/editorComponents/form/fields/DropdownField
import type { ControlProps } from "components/formControls/BaseControl"; import type { ControlProps } from "components/formControls/BaseControl";
import ActionSettings from "pages/Editor/ActionSettings"; import ActionSettings from "pages/Editor/ActionSettings";
import log from "loglevel"; import log from "loglevel";
import { Text, TextType } from "design-system-old";
import { import {
Button, Button,
Callout,
Icon, Icon,
SegmentedControl,
Spinner, Spinner,
Tab, Tab,
TabPanel, TabPanel,
@ -38,13 +34,8 @@ import {
} from "design-system"; } from "design-system";
import styled from "styled-components"; import styled from "styled-components";
import FormRow from "components/editorComponents/FormRow"; import FormRow from "components/editorComponents/FormRow";
import DebuggerLogs from "components/editorComponents/Debugger/DebuggerLogs"; import { ResizerCSS } from "components/editorComponents/Debugger/Resizer";
import ErrorLogs from "components/editorComponents/Debugger/Errors";
import Resizable, {
ResizerCSS,
} from "components/editorComponents/Debugger/Resizer";
import AnalyticsUtil from "utils/AnalyticsUtil"; import AnalyticsUtil from "utils/AnalyticsUtil";
import EntityDeps from "components/editorComponents/Debugger/EntityDependecies";
import { import {
checkIfSectionCanRender, checkIfSectionCanRender,
checkIfSectionIsEnabled, checkIfSectionIsEnabled,
@ -57,11 +48,8 @@ import {
ACTION_EDITOR_REFRESH, ACTION_EDITOR_REFRESH,
CREATE_NEW_DATASOURCE, CREATE_NEW_DATASOURCE,
createMessage, createMessage,
DEBUGGER_ERRORS,
DEBUGGER_LOGS,
DOCUMENTATION, DOCUMENTATION,
DOCUMENTATION_TOOLTIP, DOCUMENTATION_TOOLTIP,
INSPECT_ENTITY,
INVALID_FORM_CONFIGURATION, INVALID_FORM_CONFIGURATION,
NO_DATASOURCE_FOR_QUERY, NO_DATASOURCE_FOR_QUERY,
UNEXPECTED_ERROR, UNEXPECTED_ERROR,
@ -72,58 +60,27 @@ import { thinScrollbar } from "constants/DefaultTheme";
import ActionRightPane, { import ActionRightPane, {
useEntityDependencies, useEntityDependencies,
} from "components/editorComponents/ActionRightPane"; } from "components/editorComponents/ActionRightPane";
import type { import type { ActionResponse } from "api/ActionAPI";
ActionApiResponseReq,
PluginErrorDetails,
SuggestedWidget,
} from "api/ActionAPI";
import type { Plugin } from "api/PluginApi"; import type { Plugin } from "api/PluginApi";
import { UIComponentTypes } from "api/PluginApi"; import { UIComponentTypes } from "api/PluginApi";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import EntityBottomTabs from "components/editorComponents/EntityBottomTabs";
import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/helpers"; import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/helpers";
import { getErrorAsString } from "sagas/ActionExecution/errorUtils";
import type { UpdateActionPropertyActionPayload } from "actions/pluginActionActions";
import Guide from "pages/Editor/GuidedTour/Guide"; import Guide from "pages/Editor/GuidedTour/Guide";
import { inGuidedTour } from "selectors/onboardingSelectors"; import { inGuidedTour } from "selectors/onboardingSelectors";
import { EDITOR_TABS, SQL_DATASOURCES } from "constants/QueryEditorConstants"; import { EDITOR_TABS, SQL_DATASOURCES } from "constants/QueryEditorConstants";
import type { FormEvalOutput } from "reducers/evaluationReducers/formEvaluationReducer"; import type { FormEvalOutput } from "reducers/evaluationReducers/formEvaluationReducer";
import { isValidFormConfig } from "reducers/evaluationReducers/formEvaluationReducer"; import { isValidFormConfig } from "reducers/evaluationReducers/formEvaluationReducer";
import {
apiReactJsonProps,
NoResponse,
responseTabComponent,
ResponseTabErrorContainer,
ResponseTabErrorContent,
ResponseTabErrorDefaultMessage,
} from "components/editorComponents/ApiResponseView";
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
import { getQueryPaneConfigSelectedTabIndex } from "selectors/queryPaneSelectors"; import { getQueryPaneConfigSelectedTabIndex } from "selectors/queryPaneSelectors";
import { setQueryPaneConfigSelectedTabIndex } from "actions/queryPaneActions"; import { setQueryPaneConfigSelectedTabIndex } from "actions/queryPaneActions";
import { ActionExecutionResizerHeight } from "pages/Editor/APIEditor/constants"; import { ActionExecutionResizerHeight } from "pages/Editor/APIEditor/constants";
import { getCurrentAppWorkspace } from "@appsmith/selectors/workspaceSelectors"; import { getCurrentAppWorkspace } from "@appsmith/selectors/workspaceSelectors";
import {
setDebuggerSelectedTab,
setResponsePaneHeight,
showDebugger,
} from "actions/debuggerActions";
import { import {
getDebuggerSelectedTab, getDebuggerSelectedTab,
getErrorCount,
getResponsePaneHeight,
showDebuggerFlag, showDebuggerFlag,
} from "selectors/debuggerSelectors"; } from "selectors/debuggerSelectors";
import LogAdditionalInfo from "components/editorComponents/Debugger/ErrorLogs/components/LogAdditionalInfo";
import LogHelper from "components/editorComponents/Debugger/ErrorLogs/components/LogHelper";
import { JsonWrapper } from "components/editorComponents/Debugger/ErrorLogs/components/LogCollapseData";
import ReactJson from "react-json-view";
import { getUpdateTimestamp } from "components/editorComponents/Debugger/ErrorLogs/ErrorLogItem";
import LOG_TYPE from "entities/AppsmithConsole/logtype";
import type { SourceEntity } from "entities/AppsmithConsole"; import type { SourceEntity } from "entities/AppsmithConsole";
import { ENTITY_TYPE as SOURCE_ENTITY_TYPE } from "entities/AppsmithConsole"; import { ENTITY_TYPE as SOURCE_ENTITY_TYPE } from "entities/AppsmithConsole";
import { DocsLink, openDoc } from "../../../constants/DocumentationLinks"; import { DocsLink, openDoc } from "../../../constants/DocumentationLinks";
import ActionExecutionInProgressView from "components/editorComponents/ActionExecutionInProgressView";
import { CloseDebugger } from "components/editorComponents/Debugger/DebuggerTabs";
import { import {
getHasCreateDatasourcePermission, getHasCreateDatasourcePermission,
getHasExecuteActionPermission, getHasExecuteActionPermission,
@ -132,6 +89,8 @@ import {
import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag"; import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
import { QueryEditorContext } from "./QueryEditorContext"; import { QueryEditorContext } from "./QueryEditorContext";
import QueryResponseTabView from "./QueryResponseView";
import { setDebuggerSelectedTab, showDebugger } from "actions/debuggerActions";
const QueryFormContainer = styled.form` const QueryFormContainer = styled.form`
flex: 1; flex: 1;
@ -178,13 +137,6 @@ const SettingsWrapper = styled.div`
height: 100%; height: 100%;
`; `;
const ResultsCount = styled.div`
position: absolute;
right: ${(props) => props.theme.spaces[17] + 1}px;
top: 9px;
color: var(--ads-v2-color-fg);
`;
const FieldWrapper = styled.div` const FieldWrapper = styled.div`
margin-top: 15px; margin-top: 15px;
`; `;
@ -196,18 +148,6 @@ const SecondaryWrapper = styled.div`
overflow: hidden; overflow: hidden;
`; `;
const HelpSection = styled.div``;
const ResponseContentWrapper = styled.div<{ isError: boolean }>`
overflow-y: clip;
display: grid;
height: ${(props) => (props.isError ? "" : "100%")};
${HelpSection} {
margin-bottom: 10px;
}
`;
export const StyledFormRow = styled(FormRow)` export const StyledFormRow = styled(FormRow)`
padding: 0px var(--ads-v2-spaces-7) var(--ads-v2-spaces-5) padding: 0px var(--ads-v2-spaces-7) var(--ads-v2-spaces-5)
var(--ads-v2-spaces-7); var(--ads-v2-spaces-7);
@ -338,15 +278,7 @@ interface QueryFormProps {
isRunning: boolean; isRunning: boolean;
dataSources: Datasource[]; dataSources: Datasource[];
uiComponent: UIComponentTypes; uiComponent: UIComponentTypes;
executedQueryData?: { actionResponse?: ActionResponse;
body: any;
isExecutionSuccess?: boolean;
messages?: Array<string>;
suggestedWidgets?: SuggestedWidget[];
readableError?: string;
pluginErrorDetails?: PluginErrorDetails;
request?: ActionApiResponseReq;
};
runErrorMessage: string | undefined; runErrorMessage: string | undefined;
location: { location: {
state: any; state: any;
@ -357,11 +289,6 @@ interface QueryFormProps {
formData: SaaSAction | QueryAction; formData: SaaSAction | QueryAction;
responseDisplayFormat: { title: string; value: string }; responseDisplayFormat: { title: string; value: string };
responseDataTypes: { key: string; title: string }[]; responseDataTypes: { key: string; title: string }[];
updateActionResponseDisplayFormat: ({
field,
id,
value,
}: UpdateActionPropertyActionPayload) => void;
datasourceId: string; datasourceId: string;
showCloseEditor: boolean; showCloseEditor: boolean;
} }
@ -382,10 +309,10 @@ type Props = EditorJSONtoFormProps &
export function EditorJSONtoForm(props: Props) { export function EditorJSONtoForm(props: Props) {
const { const {
actionName, actionName,
actionResponse,
dataSources, dataSources,
documentationLink, documentationLink,
editorConfig, editorConfig,
executedQueryData,
formName, formName,
handleSubmit, handleSubmit,
isRunning, isRunning,
@ -397,7 +324,6 @@ export function EditorJSONtoForm(props: Props) {
runErrorMessage, runErrorMessage,
settingConfig, settingConfig,
uiComponent, uiComponent,
updateActionResponseDisplayFormat,
} = props; } = props;
const { const {
@ -408,16 +334,8 @@ export function EditorJSONtoForm(props: Props) {
saveActionName, saveActionName,
} = useContext(QueryEditorContext); } = useContext(QueryEditorContext);
let error = runErrorMessage;
let output: Record<string, any>[] | null = null;
let hintMessages: Array<string> = [];
const panelRef: RefObject<HTMLDivElement> = useRef(null);
const params = useParams<{ apiId?: string; queryId?: string }>(); const params = useParams<{ apiId?: string; queryId?: string }>();
// fetch the error count from the store. // fetch the error count from the store.
const errorCount = useSelector(getErrorCount);
const actions: Action[] = useSelector((state: AppState) => const actions: Action[] = useSelector((state: AppState) =>
state.entities.actions.map((action) => action.config), state.entities.actions.map((action) => action.config),
); );
@ -426,6 +344,8 @@ export function EditorJSONtoForm(props: Props) {
(action) => action.id === params.apiId || action.id === params.queryId, (action) => action.id === params.apiId || action.id === params.queryId,
); );
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled); const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const [showResponseOnFirstLoad, setShowResponseOnFirstLoad] =
useState<boolean>(false);
const isChangePermitted = getHasManageActionPermission( const isChangePermitted = getHasManageActionPermission(
isFeatureEnabled, isFeatureEnabled,
@ -440,9 +360,6 @@ export function EditorJSONtoForm(props: Props) {
(state: AppState) => getCurrentAppWorkspace(state).userPermissions ?? [], (state: AppState) => getCurrentAppWorkspace(state).userPermissions ?? [],
); );
const [showResponseOnFirstLoad, setShowResponseOnFirstLoad] =
useState<boolean>(false);
const canCreateDatasource = getHasCreateDatasourcePermission( const canCreateDatasource = getHasCreateDatasourcePermission(
isFeatureEnabled, isFeatureEnabled,
userWorkspacePermissions, userWorkspacePermissions,
@ -470,54 +387,24 @@ export function EditorJSONtoForm(props: Props) {
(!actionBody && SQL_DATASOURCES.includes(currentActionPluginName)) || (!actionBody && SQL_DATASOURCES.includes(currentActionPluginName)) ||
!isExecutePermitted; !isExecutePermitted;
// Query is executed even once during the session, show the response data. const dispatch = useDispatch();
if (executedQueryData) {
if (!executedQueryData.isExecutionSuccess) {
// Pass the error to be shown in the error tab
error = executedQueryData.readableError
? getErrorAsString(executedQueryData.readableError)
: getErrorAsString(executedQueryData.body);
} else if (isString(executedQueryData.body)) {
//reset error.
error = "";
try {
// Try to parse response as JSON array to be displayed in the Response tab
output = JSON.parse(executedQueryData.body);
} catch (e) {
// In case the string is not a JSON, wrap it in a response object
output = [
{
response: executedQueryData.body,
},
];
}
} else {
//reset error.
error = "";
output = executedQueryData.body;
}
if (executedQueryData.messages && executedQueryData.messages.length) {
//reset error.
error = "";
hintMessages = executedQueryData.messages;
}
}
// These useEffects are used to open the response tab by default for page load queries // These useEffects are used to open the response tab by default for page load queries
// as for page load queries, query response is available and can be shown in response tab // as for page load queries, query response is available and can be shown in response tab
useEffect(() => { useEffect(() => {
// output and responseDisplayFormat is present only when query has response available // actionResponse and responseDisplayFormat is present only when query has response available
if ( if (
responseDisplayFormat && responseDisplayFormat &&
!!responseDisplayFormat?.title && !!responseDisplayFormat?.title &&
output && actionResponse &&
actionResponse.isExecutionSuccess &&
!showResponseOnFirstLoad !showResponseOnFirstLoad
) { ) {
dispatch(showDebugger(true)); dispatch(showDebugger(true));
dispatch(setDebuggerSelectedTab(DEBUGGER_TAB_KEYS.RESPONSE_TAB)); dispatch(setDebuggerSelectedTab(DEBUGGER_TAB_KEYS.RESPONSE_TAB));
setShowResponseOnFirstLoad(true); setShowResponseOnFirstLoad(true);
} }
}, [responseDisplayFormat, output, showResponseOnFirstLoad]); }, [responseDisplayFormat, actionResponse, showResponseOnFirstLoad]);
// When multiple page load queries exist, we want to response tab by default for all of them // When multiple page load queries exist, we want to response tab by default for all of them
// Hence this useEffect will reset showResponseOnFirstLoad flag used to track whether to show response tab or not // Hence this useEffect will reset showResponseOnFirstLoad flag used to track whether to show response tab or not
@ -527,8 +414,6 @@ export function EditorJSONtoForm(props: Props) {
} }
}, [currentActionConfig?.id]); }, [currentActionConfig?.id]);
const dispatch = useDispatch();
const handleDocumentationClick = (e: React.MouseEvent) => { const handleDocumentationClick = (e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
openDoc(DocsLink.QUERY, plugin?.documentationLink, plugin?.name); openDoc(DocsLink.QUERY, plugin?.documentationLink, plugin?.name);
@ -684,63 +569,8 @@ export function EditorJSONtoForm(props: Props) {
}); });
}; };
// get the response pane height from the store.
const responsePaneHeight = useSelector(getResponsePaneHeight);
// set the response pane height on resize.
const setQueryResponsePaneHeight = useCallback((height: number) => {
dispatch(setResponsePaneHeight(height));
}, []);
const responseBodyTabs =
responseDataTypes &&
responseDataTypes.map((dataType, index) => {
return {
index: index,
key: dataType.key,
title: dataType.title,
panelComponent: responseTabComponent(
dataType.key,
output,
responsePaneHeight,
),
};
});
const segmentedControlOptions =
responseBodyTabs &&
responseBodyTabs.map((item) => {
return { value: item.key, label: item.title };
});
const [selectedControl, setSelectedControl] = useState(
segmentedControlOptions[0]?.value,
);
const onResponseTabSelect = (tabKey: string) => {
if (tabKey === DEBUGGER_TAB_KEYS.ERROR_TAB) {
AnalyticsUtil.logEvent("OPEN_DEBUGGER", {
source: "QUERY_PANE",
});
}
updateActionResponseDisplayFormat({
id: currentActionConfig?.id || "",
field: "responseDisplayFormat",
value: tabKey,
});
};
// onResponseTabSelect(selectedControl); // onResponseTabSelect(selectedControl);
const selectedTabIndex =
responseDataTypes &&
responseDataTypes.findIndex(
(dataType) => dataType.title === responseDisplayFormat?.title,
);
//Update request timestamp to human readable format.
const responseState =
executedQueryData && getUpdateTimestamp(executedQueryData.request);
// action source for analytics. // action source for analytics.
const actionSource: SourceEntity = { const actionSource: SourceEntity = {
type: SOURCE_ENTITY_TYPE.ACTION, type: SOURCE_ENTITY_TYPE.ACTION,
@ -748,128 +578,6 @@ export function EditorJSONtoForm(props: Props) {
id: currentActionConfig ? currentActionConfig.id : "", id: currentActionConfig ? currentActionConfig.id : "",
}; };
const responseTabs = [
{
key: "response",
title: "Response",
panelComponent: (
<ResponseContentWrapper isError={!!error}>
{error && (
<ResponseTabErrorContainer>
<ResponseTabErrorContent>
<ResponseTabErrorDefaultMessage>
Your query failed to execute
{executedQueryData &&
(executedQueryData.pluginErrorDetails ||
executedQueryData.body) &&
":"}
</ResponseTabErrorDefaultMessage>
{executedQueryData &&
(executedQueryData.pluginErrorDetails ? (
<>
<div data-testid="t--query-error">
{
executedQueryData.pluginErrorDetails
.downstreamErrorMessage
}
</div>
{executedQueryData.pluginErrorDetails
.downstreamErrorCode && (
<LogAdditionalInfo
text={
executedQueryData.pluginErrorDetails
.downstreamErrorCode
}
/>
)}
</>
) : (
executedQueryData.body && (
<div data-testid="t--query-error">
{executedQueryData.body}
</div>
)
))}
<LogHelper
logType={LOG_TYPE.ACTION_EXECUTION_ERROR}
name="PluginExecutionError"
pluginErrorDetails={
executedQueryData && executedQueryData.pluginErrorDetails
}
source={actionSource}
/>
</ResponseTabErrorContent>
{executedQueryData && executedQueryData.request && (
<JsonWrapper
className="t--debugger-log-state"
onClick={(e) => e.stopPropagation()}
>
<ReactJson src={responseState} {...apiReactJsonProps} />
</JsonWrapper>
)}
</ResponseTabErrorContainer>
)}
{hintMessages && hintMessages.length > 0 && (
<HelpSection>
{hintMessages.map((msg, index) => (
<Callout key={index} kind="warning">
{msg}
</Callout>
))}
</HelpSection>
)}
{currentActionConfig &&
output &&
responseBodyTabs &&
responseBodyTabs.length > 0 &&
selectedTabIndex !== -1 && (
<SegmentedControlContainer>
<SegmentedControl
data-testid="t--response-tab-segmented-control"
defaultValue={segmentedControlOptions[0]?.value}
isFullWidth={false}
onChange={(value) => {
setSelectedControl(value);
onResponseTabSelect(value);
}}
options={segmentedControlOptions}
value={selectedControl}
/>
{responseTabComponent(
selectedControl || segmentedControlOptions[0]?.value,
output,
responsePaneHeight,
)}
</SegmentedControlContainer>
)}
{!output && !error && (
<NoResponse
isButtonDisabled={!isExecutePermitted}
isQueryRunning={isRunning}
onRunClick={responseTabOnRunClick}
/>
)}
</ResponseContentWrapper>
),
},
{
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={actionName} />,
},
{
key: DEBUGGER_TAB_KEYS.INSPECT_TAB,
title: createMessage(INSPECT_ENTITY),
panelComponent: <EntityDeps />,
},
];
const { hasDependencies } = useEntityDependencies(props.actionName); const { hasDependencies } = useEntityDependencies(props.actionName);
const pluginImages = useSelector(getPluginImages); const pluginImages = useSelector(getPluginImages);
@ -907,18 +615,10 @@ export function EditorJSONtoForm(props: Props) {
const selectedResponseTab = useSelector(getDebuggerSelectedTab); const selectedResponseTab = useSelector(getDebuggerSelectedTab);
const setSelectedResponseTab = useCallback((tabKey: string) => {
dispatch(setDebuggerSelectedTab(tabKey));
}, []);
// close the debugger
//TODO: move this to a common place
const onClose = () => dispatch(showDebugger(false));
// here we check for normal conditions for opening action pane // here we check for normal conditions for opening action pane
// or if any of the flags are true, We should open the actionpane by default. // or if any of the flags are true, We should open the actionpane by default.
const shouldOpenActionPaneByDefault = const shouldOpenActionPaneByDefault =
((hasDependencies || !!output) && !guidedTourEnabled) || ((hasDependencies || !!actionResponse) && !guidedTourEnabled) ||
currentActionPluginName !== PluginName.SMTP; currentActionPluginName !== PluginName.SMTP;
// when switching between different redux forms, make sure this redux form has been initialized before rendering anything. // when switching between different redux forms, make sure this redux form has been initialized before rendering anything.
@ -1068,52 +768,18 @@ export function EditorJSONtoForm(props: Props) {
</TabContainerView> </TabContainerView>
{renderDebugger && {renderDebugger &&
selectedResponseTab !== DEBUGGER_TAB_KEYS.HEADER_TAB && ( selectedResponseTab !== DEBUGGER_TAB_KEYS.HEADER_TAB && (
<TabbedViewContainer <QueryResponseTabView
className="t--query-bottom-pane-container" actionName={actionName}
ref={panelRef} actionResponse={actionResponse}
> actionSource={actionSource}
<Resizable currentActionConfig={currentActionConfig}
initialHeight={responsePaneHeight} isExecutePermitted={isExecutePermitted}
onResizeComplete={(height: number) => isRunning={isRunning}
setQueryResponsePaneHeight(height) responseDataTypes={responseDataTypes}
} responseDisplayFormat={responseDisplayFormat}
openResizer={isRunning} responseTabOnRunClick={responseTabOnRunClick}
panelRef={panelRef} runErrorMessage={runErrorMessage}
snapToHeight={ActionExecutionResizerHeight} />
/>
{isRunning && (
<ActionExecutionInProgressView
actionType="query"
theme={EditorTheme.LIGHT}
/>
)}
{output && !!output.length && (
<ResultsCount>
<Text type={TextType.P3}>
Result:
<Text type={TextType.H5}>{` ${output.length} Record${
output.length > 1 ? "s" : ""
}`}</Text>
</Text>
</ResultsCount>
)}
<EntityBottomTabs
expandedHeight={`${ActionExecutionResizerHeight}px`}
onSelect={setSelectedResponseTab}
selectedTabKey={selectedResponseTab}
tabs={responseTabs}
/>
<CloseDebugger
className="close-debugger t--close-debugger"
isIconButton
kind="tertiary"
onClick={onClose}
size="md"
startIcon="close-modal"
/>
</TabbedViewContainer>
)} )}
</SecondaryWrapper> </SecondaryWrapper>
</div> </div>
@ -1125,9 +791,9 @@ export function EditorJSONtoForm(props: Props) {
context={DatasourceStructureContext.QUERY_EDITOR} context={DatasourceStructureContext.QUERY_EDITOR}
datasourceId={props.datasourceId} datasourceId={props.datasourceId}
hasConnections={hasDependencies} hasConnections={hasDependencies}
hasResponse={!!output} hasResponse={!!actionResponse}
pluginId={props.pluginId} pluginId={props.pluginId}
suggestedWidgets={executedQueryData?.suggestedWidgets} suggestedWidgets={actionResponse?.suggestedWidgets}
/> />
</SidebarWrapper> </SidebarWrapper>
</Wrapper> </Wrapper>

View File

@ -12,6 +12,7 @@ import {
import type { EditorJSONtoFormProps } from "./EditorJSONtoForm"; import type { EditorJSONtoFormProps } from "./EditorJSONtoForm";
import { EditorJSONtoForm } from "./EditorJSONtoForm"; import { EditorJSONtoForm } from "./EditorJSONtoForm";
import { getFormEvaluationState } from "selectors/formSelectors"; import { getFormEvaluationState } from "selectors/formSelectors";
import { actionResponseDisplayDataFormats } from "../utils";
const valueSelector = formValueSelector(QUERY_EDITOR_FORM_NAME); const valueSelector = formValueSelector(QUERY_EDITOR_FORM_NAME);
const mapStateToProps = (state: AppState, props: any) => { const mapStateToProps = (state: AppState, props: any) => {
@ -20,27 +21,8 @@ const mapStateToProps = (state: AppState, props: any) => {
const pluginId = valueSelector(state, "datasource.pluginId"); const pluginId = valueSelector(state, "datasource.pluginId");
const selectedDbId = valueSelector(state, "datasource.id"); const selectedDbId = valueSelector(state, "datasource.id");
const actionData = getActionData(state, actionId); const actionData = getActionData(state, actionId);
let responseDisplayFormat: { title: string; value: string }; const { responseDataTypes, responseDisplayFormat } =
let responseDataTypes: { key: string; title: string }[]; actionResponseDisplayDataFormats(actionData);
if (actionData && actionData.responseDisplayFormat) {
responseDataTypes = actionData.dataTypes.map((data) => {
return {
key: data.dataType,
title: data.dataType,
};
});
responseDisplayFormat = {
title: actionData.responseDisplayFormat,
value: actionData.responseDisplayFormat,
};
} else {
responseDataTypes = [];
responseDisplayFormat = {
title: "",
value: "",
};
}
const responseTypes = getPluginResponseTypes(state); const responseTypes = getPluginResponseTypes(state);
const documentationLinks = getPluginDocumentationLinks(state); const documentationLinks = getPluginDocumentationLinks(state);

View File

@ -0,0 +1,393 @@
import {
setDebuggerSelectedTab,
setResponsePaneHeight,
showDebugger,
} from "actions/debuggerActions";
import { CloseDebugger } from "components/editorComponents/Debugger/DebuggerTabs";
import EntityBottomTabs from "components/editorComponents/EntityBottomTabs";
import React, { useCallback, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import styled from "styled-components";
import { ActionExecutionResizerHeight } from "pages/Editor/APIEditor/constants";
import {
getDebuggerSelectedTab,
getErrorCount,
getResponsePaneHeight,
} from "selectors/debuggerSelectors";
import { Text, TextType } from "design-system-old";
import ActionExecutionInProgressView from "components/editorComponents/ActionExecutionInProgressView";
import Resizable, {
ResizerCSS,
} from "components/editorComponents/Debugger/Resizer";
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/helpers";
import {
DEBUGGER_ERRORS,
DEBUGGER_LOGS,
INSPECT_ENTITY,
createMessage,
} from "@appsmith/constants/messages";
import DebuggerLogs from "components/editorComponents/Debugger/DebuggerLogs";
import ErrorLogs from "components/editorComponents/Debugger/Errors";
import { Callout, SegmentedControl } from "design-system";
import {
NoResponse,
ResponseTabErrorContainer,
ResponseTabErrorContent,
ResponseTabErrorDefaultMessage,
apiReactJsonProps,
responseTabComponent,
} from "components/editorComponents/ApiResponseView";
import LogAdditionalInfo from "components/editorComponents/Debugger/ErrorLogs/components/LogAdditionalInfo";
import LogHelper from "components/editorComponents/Debugger/ErrorLogs/components/LogHelper";
import { JsonWrapper } from "components/editorComponents/Debugger/ErrorLogs/components/LogCollapseData";
import ReactJson from "react-json-view";
import EntityDeps from "components/editorComponents/Debugger/EntityDependecies";
import type { ActionResponse } from "api/ActionAPI";
import { getErrorAsString } from "sagas/ActionExecution/errorUtils";
import { isString } from "lodash";
import LOG_TYPE from "entities/AppsmithConsole/logtype";
import type { SourceEntity } from "entities/AppsmithConsole";
import { getUpdateTimestamp } from "components/editorComponents/Debugger/ErrorLogs/ErrorLogItem";
import type { Action } from "entities/Action";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { setActionResponseDisplayFormat } from "actions/pluginActionActions";
const HelpSection = styled.div``;
const ResponseContentWrapper = styled.div<{ isError: boolean }>`
overflow-y: clip;
display: grid;
height: ${(props) => (props.isError ? "" : "100%")};
${HelpSection} {
margin-bottom: 10px;
}
`;
const ResultsCount = styled.div`
position: absolute;
right: ${(props) => props.theme.spaces[17] + 1}px;
top: 9px;
color: var(--ads-v2-color-fg);
`;
export const TabbedViewContainer = styled.div`
${ResizerCSS};
height: ${ActionExecutionResizerHeight}px;
// Minimum height of bottom tabs as it can be resized
min-height: 36px;
width: 100%;
background-color: var(--ads-v2-color-bg);
border-top: 1px solid var(--ads-v2-color-border);
`;
export const SegmentedControlContainer = styled.div`
padding: 0 var(--ads-v2-spaces-7);
padding-top: var(--ads-v2-spaces-4);
display: flex;
flex-direction: column;
gap: var(--ads-v2-spaces-4);
overflow-y: clip;
overflow-x: scroll;
`;
interface QueryResponseViewProps {
actionSource: SourceEntity;
responseTabOnRunClick: () => void;
currentActionConfig?: Action;
isRunning: boolean;
actionName: string; // Check what and how to get
runErrorMessage?: string;
actionResponse?: ActionResponse;
responseDataTypes: { key: string; title: string }[];
responseDisplayFormat: { title: string; value: string };
isExecutePermitted: boolean;
}
function QueryResponseView({
actionName,
actionResponse,
actionSource,
currentActionConfig,
isExecutePermitted,
isRunning,
responseDataTypes,
responseDisplayFormat,
responseTabOnRunClick,
runErrorMessage,
}: QueryResponseViewProps) {
let output: Record<string, any>[] | null = null;
const responseBodyTabs =
responseDataTypes &&
responseDataTypes.map((dataType, index) => {
return {
index: index,
key: dataType.key,
title: dataType.title,
panelComponent: responseTabComponent(
dataType.key,
output,
responsePaneHeight,
),
};
});
const segmentedControlOptions =
responseBodyTabs &&
responseBodyTabs.map((item) => {
return { value: item.key, label: item.title };
});
const panelRef = useRef<HTMLDivElement>(null);
const dispatch = useDispatch();
const [selectedControl, setSelectedControl] = useState(
segmentedControlOptions[0]?.value,
);
const selectedResponseTab = useSelector(getDebuggerSelectedTab);
const responsePaneHeight = useSelector(getResponsePaneHeight);
const errorCount = useSelector(getErrorCount);
const onResponseTabSelect = (tabKey: string) => {
if (tabKey === DEBUGGER_TAB_KEYS.ERROR_TAB) {
AnalyticsUtil.logEvent("OPEN_DEBUGGER", {
source: "QUERY_PANE",
});
}
dispatch(
setActionResponseDisplayFormat({
id: currentActionConfig?.id || "",
field: "responseDisplayFormat",
value: tabKey,
}),
);
};
let error = runErrorMessage;
let hintMessages: Array<string> = [];
// Update request timestamp to human readable format.
const responseState =
actionResponse && getUpdateTimestamp(actionResponse.request);
const selectedTabIndex =
responseDataTypes &&
responseDataTypes.findIndex(
(dataType) => dataType.title === responseDisplayFormat?.title,
);
// Query is executed even once during the session, show the response data.
if (actionResponse) {
if (!actionResponse.isExecutionSuccess) {
// Pass the error to be shown in the error tab
error = actionResponse.readableError
? getErrorAsString(actionResponse.readableError)
: getErrorAsString(actionResponse.body);
} else if (isString(actionResponse.body)) {
//reset error.
error = "";
try {
// Try to parse response as JSON array to be displayed in the Response tab
output = JSON.parse(actionResponse.body);
} catch (e) {
// In case the string is not a JSON, wrap it in a response object
output = [
{
response: actionResponse.body,
},
];
}
} else {
//reset error.
error = "";
output = actionResponse.body as any;
}
if (actionResponse.messages && actionResponse.messages.length) {
//reset error.
error = "";
hintMessages = actionResponse.messages;
}
}
const setQueryResponsePaneHeight = useCallback((height: number) => {
dispatch(setResponsePaneHeight(height));
}, []);
const onClose = () => dispatch(showDebugger(false));
const setSelectedResponseTab = useCallback((tabKey: string) => {
dispatch(setDebuggerSelectedTab(tabKey));
}, []);
const responseTabs = [
{
key: "response",
title: "Response",
panelComponent: (
<ResponseContentWrapper isError={!!error}>
{error && (
<ResponseTabErrorContainer>
<ResponseTabErrorContent>
<ResponseTabErrorDefaultMessage>
Your query failed to execute
{actionResponse &&
(actionResponse.pluginErrorDetails ||
actionResponse.body) &&
":"}
</ResponseTabErrorDefaultMessage>
{actionResponse &&
(actionResponse.pluginErrorDetails ? (
<>
<div data-testid="t--query-error">
{
actionResponse.pluginErrorDetails
.downstreamErrorMessage
}
</div>
{actionResponse.pluginErrorDetails
.downstreamErrorCode && (
<LogAdditionalInfo
text={
actionResponse.pluginErrorDetails
.downstreamErrorCode
}
/>
)}
</>
) : (
actionResponse.body && (
<div data-testid="t--query-error">
{actionResponse.body}
</div>
)
))}
<LogHelper
logType={LOG_TYPE.ACTION_EXECUTION_ERROR}
name="PluginExecutionError"
pluginErrorDetails={
actionResponse && actionResponse.pluginErrorDetails
}
source={actionSource}
/>
</ResponseTabErrorContent>
{actionResponse && actionResponse.request && (
<JsonWrapper
className="t--debugger-log-state"
onClick={(e) => e.stopPropagation()}
>
<ReactJson src={responseState} {...apiReactJsonProps} />
</JsonWrapper>
)}
</ResponseTabErrorContainer>
)}
{hintMessages && hintMessages.length > 0 && (
<HelpSection>
{hintMessages.map((msg, index) => (
<Callout key={index} kind="warning">
{msg}
</Callout>
))}
</HelpSection>
)}
{currentActionConfig &&
output &&
responseBodyTabs &&
responseBodyTabs.length > 0 &&
selectedTabIndex !== -1 && (
<SegmentedControlContainer>
<SegmentedControl
data-testid="t--response-tab-segmented-control"
defaultValue={segmentedControlOptions[0]?.value}
isFullWidth={false}
onChange={(value) => {
setSelectedControl(value);
onResponseTabSelect(value);
}}
options={segmentedControlOptions}
value={selectedControl}
/>
{responseTabComponent(
selectedControl || segmentedControlOptions[0]?.value,
output,
responsePaneHeight,
)}
</SegmentedControlContainer>
)}
{!output && !error && (
<NoResponse
isButtonDisabled={!isExecutePermitted}
isQueryRunning={isRunning}
onRunClick={responseTabOnRunClick}
/>
)}
</ResponseContentWrapper>
),
},
{
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={actionName} />,
},
{
key: DEBUGGER_TAB_KEYS.INSPECT_TAB,
title: createMessage(INSPECT_ENTITY),
panelComponent: <EntityDeps />,
},
];
return (
<TabbedViewContainer
className="t--query-bottom-pane-container"
ref={panelRef}
>
<Resizable
initialHeight={responsePaneHeight}
onResizeComplete={(height: number) =>
setQueryResponsePaneHeight(height)
}
openResizer={isRunning}
panelRef={panelRef}
snapToHeight={ActionExecutionResizerHeight}
/>
{isRunning && (
<ActionExecutionInProgressView
actionType="query"
theme={EditorTheme.LIGHT}
/>
)}
{output && !!output.length && (
<ResultsCount>
<Text type={TextType.P3}>
Result:
<Text type={TextType.H5}>{` ${output.length} Record${
output.length > 1 ? "s" : ""
}`}</Text>
</Text>
</ResultsCount>
)}
<EntityBottomTabs
expandedHeight={`${ActionExecutionResizerHeight}px`}
onSelect={setSelectedResponseTab}
selectedTabKey={selectedResponseTab}
tabs={responseTabs}
/>
<CloseDebugger
className="close-debugger t--close-debugger"
isIconButton
kind="tertiary"
onClick={onClose}
size="md"
startIcon="close-modal"
/>
</TabbedViewContainer>
);
}
export default QueryResponseView;

View File

@ -18,6 +18,7 @@ import type { URLBuilderParams } from "@appsmith/entities/URLRedirect/URLAssembl
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { getCurrentPageId } from "selectors/editorSelectors"; import { getCurrentPageId } from "selectors/editorSelectors";
import type { WidgetCardProps } from "widgets/BaseWidget"; import type { WidgetCardProps } from "widgets/BaseWidget";
import type { ActionResponse } from "api/ActionAPI";
export const draggableElement = ( export const draggableElement = (
id: string, id: string,
@ -303,3 +304,35 @@ export const groupWidgetCardsByTags = (widgetCards: WidgetCardProps[]) => {
export const transformTextToSentenceCase = (s: string) => { export const transformTextToSentenceCase = (s: string) => {
return s.slice(0, 1).toUpperCase() + s.slice(1).toLowerCase(); return s.slice(0, 1).toUpperCase() + s.slice(1).toLowerCase();
}; };
export const actionResponseDisplayDataFormats = (
actionData?: ActionResponse,
defaultDisplayFormat: { title: string; value: string } = {
title: "",
value: "",
},
) => {
let responseDisplayFormat: { title: string; value: string };
let responseDataTypes: { key: string; title: string }[];
if (actionData && actionData.responseDisplayFormat) {
responseDataTypes = actionData.dataTypes.map((data) => {
return {
key: data.dataType,
title: data.dataType,
};
});
responseDisplayFormat = {
title: actionData.responseDisplayFormat,
value: actionData.responseDisplayFormat,
};
} else {
responseDataTypes = [];
responseDisplayFormat = defaultDisplayFormat;
}
return {
responseDataTypes,
responseDisplayFormat,
};
};

View File

@ -161,6 +161,7 @@ import {
getCurrentEnvironmentName, getCurrentEnvironmentName,
} from "@appsmith/selectors/environmentSelectors"; } from "@appsmith/selectors/environmentSelectors";
import { EVAL_WORKER_ACTIONS } from "@appsmith/workers/Evaluation/evalWorkerActions"; import { EVAL_WORKER_ACTIONS } from "@appsmith/workers/Evaluation/evalWorkerActions";
import { getIsActionCreatedInApp } from "@appsmith/utils/getIsActionCreatedInApp";
enum ActionResponseDataTypes { enum ActionResponseDataTypes {
BINARY = "BINARY", BINARY = "BINARY",
@ -560,7 +561,7 @@ export default function* executePluginActionTriggerSaga(
}); });
const executePluginActionResponse: ExecutePluginActionResponse = yield call( const executePluginActionResponse: ExecutePluginActionResponse = yield call(
executePluginActionSaga, executePluginActionSaga,
action.id, action,
pagination, pagination,
params, params,
); );
@ -733,11 +734,12 @@ interface RunActionError {
clientDefinedError?: boolean; clientDefinedError?: boolean;
} }
function* runActionSaga( export function* runActionSaga(
reduxAction: ReduxAction<{ reduxAction: ReduxAction<{
id: string; id: string;
paginationField: PaginationField; paginationField?: PaginationField;
skipOpeningDebugger: boolean; skipOpeningDebugger: boolean;
action?: Action;
}>, }>,
) { ) {
const actionId = reduxAction.payload.id; const actionId = reduxAction.payload.id;
@ -754,10 +756,12 @@ function* runActionSaga(
const currentEnvDetails: { id: string; name: string } = yield select( const currentEnvDetails: { id: string; name: string } = yield select(
getCurrentEnvironmentDetails, getCurrentEnvironmentDetails,
); );
const actionObject = shouldBeDefined<Action>( const actionObject =
yield select(getAction, actionId), reduxAction.payload.action ||
`action not found for id - ${actionId}`, shouldBeDefined<Action>(
); yield select(getAction, actionId),
`action not found for id - ${actionId}`,
);
const plugin: Plugin = yield select(getPlugin, actionObject?.pluginId); const plugin: Plugin = yield select(getPlugin, actionObject?.pluginId);
const datasource: Datasource = yield select( const datasource: Datasource = yield select(
getDatasource, getDatasource,
@ -787,7 +791,7 @@ function* runActionSaga(
}, },
}); });
const { id, paginationField } = reduxAction.payload; const { paginationField } = reduxAction.payload;
// open response tab in debugger on exection of action. // open response tab in debugger on exection of action.
if (!reduxAction.payload.skipOpeningDebugger) { if (!reduxAction.payload.skipOpeningDebugger) {
yield call(openDebugger); yield call(openDebugger);
@ -802,7 +806,7 @@ function* runActionSaga(
try { try {
const executePluginActionResponse: ExecutePluginActionResponse = yield call( const executePluginActionResponse: ExecutePluginActionResponse = yield call(
executePluginActionSaga, executePluginActionSaga,
id, actionObject,
paginationField, paginationField,
{}, {},
true, true,
@ -1119,7 +1123,7 @@ function* executePageLoadAction(pageAction: PageAction) {
try { try {
const executePluginActionResponse: ExecutePluginActionResponse = const executePluginActionResponse: ExecutePluginActionResponse =
yield call(executePluginActionSaga, pageAction); yield call(executePluginActionSaga, action);
payload = executePluginActionResponse.payload; payload = executePluginActionResponse.payload;
isError = executePluginActionResponse.isError; isError = executePluginActionResponse.isError;
} catch (e) { } catch (e) {
@ -1293,24 +1297,12 @@ interface ExecutePluginActionResponse {
* PluginActionExecutionError which needs to be handled by any saga that calls this. * PluginActionExecutionError which needs to be handled by any saga that calls this.
* */ * */
function* executePluginActionSaga( function* executePluginActionSaga(
actionOrActionId: PageAction | string, pluginAction: Action,
paginationField?: PaginationField, paginationField?: PaginationField,
params?: Record<string, unknown>, params?: Record<string, unknown>,
isUserInitiated?: boolean, isUserInitiated?: boolean,
) { ) {
let pluginAction; const actionId = pluginAction.id;
let actionId;
if (isString(actionOrActionId)) {
// @ts-expect-error: plugin Action can take many types
pluginAction = yield select(getAction, actionOrActionId);
actionId = actionOrActionId;
} else {
pluginAction = shouldBeDefined<Action>(
yield select(getAction, actionOrActionId.id),
`Action not found for id -> ${actionOrActionId.id}`,
);
actionId = actionOrActionId.id;
}
if (pluginAction.confirmBeforeExecute) { if (pluginAction.confirmBeforeExecute) {
const modalPayload = { const modalPayload = {
@ -1392,6 +1384,7 @@ function* executePluginActionSaga(
executePluginActionSuccess({ executePluginActionSuccess({
id: actionId, id: actionId,
response: payload, response: payload,
isActionCreatedInApp: getIsActionCreatedInApp(pluginAction),
}), }),
); );
@ -1454,6 +1447,7 @@ function* executePluginActionSaga(
executePluginActionSuccess({ executePluginActionSuccess({
id: actionId, id: actionId,
response: EMPTY_RESPONSE, response: EMPTY_RESPONSE,
isActionCreatedInApp: getIsActionCreatedInApp(pluginAction),
}), }),
); );
yield put( yield put(

View File

@ -15,3 +15,6 @@ export const getApiPaneConfigSelectedTabIndex = (state: AppState) =>
export const getApiRightPaneSelectedTab = (state: AppState) => export const getApiRightPaneSelectedTab = (state: AppState) =>
state.ui.apiPane.selectedRightPaneTab; state.ui.apiPane.selectedRightPaneTab;
export const getIsRunning = (state: AppState, apiId: string) =>
state.ui.apiPane.isRunning[apiId];