Skip to content

Commit

Permalink
chore: Init Plugin Action Response (#36485)
Browse files Browse the repository at this point in the history
## 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: c3b5b4b
> <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 -->
  • Loading branch information
hetunandu committed Sep 25, 2024
1 parent 20fa8de commit 5ee7f83
Show file tree
Hide file tree
Showing 31 changed files with 906 additions and 504 deletions.
6 changes: 4 additions & 2 deletions app/client/src/PluginActionEditor/PluginActionContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions app/client/src/PluginActionEditor/PluginActionEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { identifyEntityFromPath } from "../navigation/FocusEntity";
import { useSelector } from "react-redux";
import {
getActionByBaseId,
getActionResponses,
getDatasource,
getEditorConfig,
getPlugin,
Expand Down Expand Up @@ -39,6 +40,8 @@ const PluginActionEditor = (props: ChildrenProps) => {

const editorConfig = useSelector((state) => getEditorConfig(state, pluginId));

const actionResponses = useSelector(getActionResponses);

if (!isEditorInitialized) {
return (
<CenteredWrapper>
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 5ee7f83

Please sign in to comment.