From 0e36569b0687b83bcd5ef2cd00586d274f2ac5f9 Mon Sep 17 00:00:00 2001 From: Ayangade Adeoluwa <37867493+Irongade@users.noreply.github.com> Date: Fri, 12 Aug 2022 08:19:17 +0100 Subject: [PATCH] feat: Abort Query Execution (#15775) * Snaps response view to certain height and adds abort query execution feature * make raw string a constant * Add cypress tests * Remove theme variable * move raw strings to constants in messages.ts file * Fix test case description * Comment out query abort action execution * cy fix * Fix abortion of parallel action execution like onPageLoad Co-authored-by: Aishwarya UR --- .../Abort_Action_Execution_spec.js | 47 +++++++++++++ .../cypress/locators/commonlocators.json | 5 +- app/client/cypress/support/ApiCommands.js | 4 ++ app/client/cypress/support/commands.js | 11 ++++ app/client/cypress/support/queryCommands.js | 6 ++ app/client/src/api/ActionAPI.tsx | 3 + app/client/src/ce/constants/messages.ts | 12 ++++ .../editorComponents/ApiResponseView.tsx | 55 ++++++++++++++-- .../Debugger/Resizer/index.tsx | 19 ++++++ .../Editor/QueryEditor/EditorJSONtoForm.tsx | 66 +++++++++++++++---- 10 files changed, 210 insertions(+), 18 deletions(-) create mode 100644 app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ActionExecution/Abort_Action_Execution_spec.js diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ActionExecution/Abort_Action_Execution_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ActionExecution/Abort_Action_Execution_spec.js new file mode 100644 index 0000000000..f3c90dcc73 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ActionExecution/Abort_Action_Execution_spec.js @@ -0,0 +1,47 @@ +const commonlocators = require("../../../../locators/commonlocators.json"); +const queryLocators = require("../../../../locators/QueryEditor.json"); +const datasource = require("../../../../locators/DatasourcesEditor.json"); +const formControls = require("../../../../locators/FormControl.json"); + +const testApiUrl = "https://jsonplaceholder.typicode.com/photos"; + +const ERROR_ACTION_EXECUTE_FAIL = (actionName) => + `${actionName} action returned an error response`; + +describe("Abort Action Execution", function() { + it("Cancel Request button should abort API action execution", function() { + cy.createAndFillApi(testApiUrl, ""); + cy.RunAPIWithoutWaitingForResolution(); + cy.get(commonlocators.cancelActionExecution).click(); + cy.VerifyErrorMsgPresence(ERROR_ACTION_EXECUTE_FAIL("Api1")); + }); + + // Queries were resolving quicker than we could cancel them + // Commenting this out till we can find a query that resolves slow enough for us to cancel its execution. + + // it("Cancel Request button should abort Query action execution", function() { + // cy.NavigateToDatasourceEditor(); + // cy.get(datasource.MongoDB).click(); + // cy.fillMongoDatasourceForm(); + // cy.testSaveDatasource(); + // cy.get("@createDatasource").then( + // (httpResponse) => httpResponse.response.body.data.name, + // ); + // cy.NavigateToQueryEditor(); + // cy.NavigateToActiveTab(); + // cy.get(queryLocators.createQuery) + // .last() + // .click(); + // cy.get(queryLocators.queryNameField).type("AbortQuery"); + // cy.ValidateAndSelectDropdownOption( + // formControls.commandDropdown, + // "Find Document(s)", + // ); + + // cy.typeValueNValidate("friends", formControls.mongoCollection); + // cy.typeValueNValidate("300", formControls.mongoFindLimit); + // cy.RunQueryWithoutWaitingForResolution(); + // cy.get(commonlocators.cancelActionExecution).click(); + // cy.VerifyErrorMsgPresence(ERROR_ACTION_EXECUTE_FAIL("AbortQuery")); + // }); +}); diff --git a/app/client/cypress/locators/commonlocators.json b/app/client/cypress/locators/commonlocators.json index 674b118d35..f477258e47 100644 --- a/app/client/cypress/locators/commonlocators.json +++ b/app/client/cypress/locators/commonlocators.json @@ -183,5 +183,6 @@ "selectThemeBackBtn": ".t--theme-select-back-btn", "themeAppBorderRadiusBtn": ".t--theme-appBorderRadius", "codeEditorWrapper": ".unfocused-code-editor", - "textWidgetContainer": ".t--text-widget-container" -} \ No newline at end of file + "textWidgetContainer": ".t--text-widget-container", + "cancelActionExecution": ".t--cancel-action-button" +} diff --git a/app/client/cypress/support/ApiCommands.js b/app/client/cypress/support/ApiCommands.js index 6bfe5efd0a..599f7e3035 100644 --- a/app/client/cypress/support/ApiCommands.js +++ b/app/client/cypress/support/ApiCommands.js @@ -113,6 +113,10 @@ Cypress.Commands.add("RunAPI", () => { cy.wait("@postExecute"); }); +Cypress.Commands.add("RunAPIWithoutWaitingForResolution", () => { + cy.get(ApiEditor.ApiRunBtn).click({ force: true }); +}); + Cypress.Commands.add("SaveAndRunAPI", () => { cy.WaitAutoSave(); cy.RunAPI(); diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index a3d16aa507..8992abf1ff 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -1536,6 +1536,17 @@ Cypress.Commands.add("VerifyErrorMsgAbsence", (errorMsgToVerifyAbsence) => { ).should("not.exist"); }); +Cypress.Commands.add("VerifyErrorMsgPresence", (errorMsgToVerifyAbsence) => { + // Give this element 10 seconds to appear + //cy.wait(10000) + cy.xpath( + "//div[@class='Toastify']//span[contains(text(),'" + + errorMsgToVerifyAbsence + + "')]", + { timeout: 0 }, + ).should("exist"); +}); + Cypress.Commands.add("setQueryTimeout", (timeout) => { cy.get(queryLocators.settings).click(); cy.xpath(queryLocators.queryTimeout) diff --git a/app/client/cypress/support/queryCommands.js b/app/client/cypress/support/queryCommands.js index 3105d9d103..3fd2c906dd 100644 --- a/app/client/cypress/support/queryCommands.js +++ b/app/client/cypress/support/queryCommands.js @@ -97,6 +97,12 @@ Cypress.Commands.add("onlyQueryRun", () => { .wait(1000); }); +Cypress.Commands.add("RunQueryWithoutWaitingForResolution", () => { + cy.xpath(queryEditor.runQuery) + .last() + .click({ force: true }); +}); + Cypress.Commands.add("hoverAndClick", () => { cy.xpath(apiwidget.popover) .last() diff --git a/app/client/src/api/ActionAPI.tsx b/app/client/src/api/ActionAPI.tsx index cea0163bf9..3eb4f1775a 100644 --- a/app/client/src/api/ActionAPI.tsx +++ b/app/client/src/api/ActionAPI.tsx @@ -117,6 +117,7 @@ class ActionAPI extends API { static url = "v1/actions"; static apiUpdateCancelTokenSource: CancelTokenSource; static queryUpdateCancelTokenSource: CancelTokenSource; + static abortActionExecutionTokenSource: CancelTokenSource; static createAction( apiConfig: Partial, @@ -169,12 +170,14 @@ class ActionAPI extends API { executeAction: FormData, timeout?: number, ): AxiosPromise { + ActionAPI.abortActionExecutionTokenSource = axios.CancelToken.source(); return API.post(ActionAPI.url + "/execute", executeAction, undefined, { timeout: timeout || DEFAULT_EXECUTE_ACTION_TIMEOUT_MS, headers: { accept: "application/json", "Content-Type": "multipart/form-data", }, + cancelToken: ActionAPI.abortActionExecutionTokenSource.token, }); } diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 755a8d2a0c..01ef59e294 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -254,6 +254,16 @@ export const OAUTH_2_0 = () => "OAuth 2.0"; export const ENABLE = () => "ENABLE"; export const UPGRADE = () => "UPGRADE"; export const EDIT = () => "EDIT"; +export const UNEXPECTED_ERROR = () => "An unexpected error occurred"; +export const EXPECTED_ERROR = () => "An error occurred"; +export const NO_DATASOURCE_FOR_QUERY = () => + `Seems like you don’t have any Datasources to create a query`; +export const ACTION_EDITOR_REFRESH = () => "Refresh"; +export const INVALID_FORM_CONFIGURATION = () => "Invalid form configuration"; +export const ACTION_RUN_BUTTON_MESSAGE_FIRST_HALF = () => "🙌 Click on"; +export const ACTION_RUN_BUTTON_MESSAGE_SECOND_HALF = () => + "after adding your query"; +export const CREATE_NEW_DATASOURCE = () => "Create new datasource"; export const ERROR_EVAL_ERROR_GENERIC = () => `Unexpected error occurred while evaluating the application`; @@ -913,6 +923,8 @@ export const API_EDITOR_TAB_TITLES = { AUTHENTICATION: () => "Authentication", SETTINGS: () => "Settings", }; +export const ACTION_EXECUTION_MESSAGE = (actionType: string) => + `Sending the ${actionType} request`; export const WELCOME_FORM_HEADER = () => "Let us get to know you better!"; export const WELCOME_FORM_FULL_NAME = () => "Full Name"; diff --git a/app/client/src/components/editorComponents/ApiResponseView.tsx b/app/client/src/components/editorComponents/ApiResponseView.tsx index 1711f7b017..9744bdef01 100644 --- a/app/client/src/components/editorComponents/ApiResponseView.tsx +++ b/app/client/src/components/editorComponents/ApiResponseView.tsx @@ -19,6 +19,7 @@ import { EMPTY_RESPONSE_FIRST_HALF, EMPTY_RESPONSE_LAST_HALF, INSPECT_ENTITY, + ACTION_EXECUTION_MESSAGE, } from "@appsmith/constants/messages"; import { Text, TextType } from "design-system"; import { Text as BlueprintText } from "@blueprintjs/core"; @@ -32,7 +33,7 @@ import Resizer, { ResizerCSS } from "./Debugger/Resizer"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { DebugButton } from "./Debugger/DebugCTA"; import EntityDeps from "./Debugger/EntityDependecies"; -import Button, { Size } from "components/ads/Button"; +import Button, { Size, Category } from "components/ads/Button"; import EntityBottomTabs from "./EntityBottomTabs"; import { DEBUGGER_TAB_KEYS } from "./Debugger/helpers"; import { setCurrentTab } from "actions/debuggerActions"; @@ -43,6 +44,7 @@ import { UpdateActionPropertyActionPayload, } from "actions/pluginActionActions"; import { isHtml } from "./utils"; +import ActionAPI from "api/ActionAPI"; type TextStyleProps = { accent: "primary" | "secondary" | "error"; @@ -105,7 +107,7 @@ const TabbedViewWrapper = styled.div` } `; -const SectionDivider = styled.div` +export const SectionDivider = styled.div` height: 1px; width: 100%; background: ${(props) => props.theme.colors.apiPane.dividerBg}; @@ -174,6 +176,23 @@ const ResponseBodyContainer = styled.div` display: grid; `; +export const CancelRequestButton = styled(Button)` + margin-top: 10px; +`; + +export const LoadingOverlayContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: transparent; + position: relative; + z-index: 20; + width: 100%; + height: 100%; + margin-top: 5px; +`; + interface ReduxStateProps { responses: Record; isRunning: Record; @@ -237,6 +256,8 @@ const ResponseDataContainer = styled.div` `; export const TableCellHeight = 39; +// When action execution is triggered, open response container to height specified by this variable. +export const ActionExecutionResizerHeight = 307; export const responseTabComponent = ( responseType: string, @@ -271,6 +292,10 @@ export const responseTabComponent = ( }[responseType]; }; +export const handleCancelActionExecution = () => { + ActionAPI.abortActionExecutionTokenSource.cancel(); +}; + function ApiResponseView(props: Props) { const { match: { @@ -506,17 +531,37 @@ function ApiResponseView(props: Props) { return ( // TableCellHeight in this case is the height of one table cell in pixels. setTableBodyHeightHeight(height - TableCellHeight) } + snapToHeight={ActionExecutionResizerHeight} /> {isRunning && ( - - Sending Request - + <> + + +
+ + {createMessage(ACTION_EXECUTION_MESSAGE, "API")} + + { + handleCancelActionExecution(); + }} + size={Size.medium} + tag="button" + text="Cancel Request" + type="button" + /> +
+
+ )} {response.statusCode && ( diff --git a/app/client/src/components/editorComponents/Debugger/Resizer/index.tsx b/app/client/src/components/editorComponents/Debugger/Resizer/index.tsx index a5b2030049..b8e2ed31de 100644 --- a/app/client/src/components/editorComponents/Debugger/Resizer/index.tsx +++ b/app/client/src/components/editorComponents/Debugger/Resizer/index.tsx @@ -21,6 +21,8 @@ const Top = styled.div` type ResizerProps = { panelRef: RefObject; setContainerDimensions?: (height: number) => void; + snapToHeight?: number; + openResizer?: boolean; }; function Resizer(props: ResizerProps) { @@ -51,6 +53,23 @@ function Resizer(props: ResizerProps) { handleResize(0); }, []); + useEffect(() => { + // if the resizer is configured to open and the user is not actively controlling it + // snap the resizer to a specific height as specified by the snapToHeight prop. + if (props.openResizer && !mouseDown) { + const panel = props.panelRef.current; + if (!panel) return; + + const { height } = panel.getBoundingClientRect(); + + if (props?.snapToHeight && height < props?.snapToHeight) { + panel.style.height = `${props?.snapToHeight}px`; + props.setContainerDimensions && + props.setContainerDimensions(props?.snapToHeight); + } + } + }, [props?.openResizer]); + useEffect(() => { const handleMouseMove = (e: MouseEvent) => { e.preventDefault(); diff --git a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx index 9382dc184e..afc2cd8ac5 100644 --- a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx +++ b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx @@ -56,12 +56,21 @@ import { DOCUMENTATION, DOCUMENTATION_TOOLTIP, INSPECT_ENTITY, + ACTION_EXECUTION_MESSAGE, + UNEXPECTED_ERROR, + NO_DATASOURCE_FOR_QUERY, + ACTION_EDITOR_REFRESH, + EXPECTED_ERROR, + INVALID_FORM_CONFIGURATION, + ACTION_RUN_BUTTON_MESSAGE_FIRST_HALF, + ACTION_RUN_BUTTON_MESSAGE_SECOND_HALF, + CREATE_NEW_DATASOURCE, } from "@appsmith/constants/messages"; import { useParams } from "react-router"; import { AppState } from "reducers"; import { ExplorerURLParams } from "../Explorer/helpers"; import MoreActionsMenu from "../Explorer/Actions/MoreActionsMenu"; -import Button, { Size } from "components/ads/Button"; +import Button, { Size, Category } from "components/ads/Button"; import { thinScrollbar } from "constants/DefaultTheme"; import ActionRightPane, { useEntityDependencies, @@ -90,7 +99,14 @@ import { responseTabComponent, InlineButton, TableCellHeight, + SectionDivider, + CancelRequestButton, + LoadingOverlayContainer, + handleCancelActionExecution, + ActionExecutionResizerHeight, } from "components/editorComponents/ApiResponseView"; +import LoadingOverlayScreen from "components/editorComponents/LoadingOverlayScreen"; +import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig"; const QueryFormContainer = styled.form` flex: 1; @@ -517,7 +533,7 @@ export function EditorJSONtoForm(props: Props) { {props.children} onCreateDatasourceClick()}> - Create new datasource + {createMessage(CREATE_NEW_DATASOURCE)} ); @@ -594,7 +610,9 @@ export function EditorJSONtoForm(props: Props) { Sentry.captureException(e); return ( <> - Invalid form configuration + + {createMessage(INVALID_FORM_CONFIGURATION)} + window.location.reload()} round > - Refresh + {createMessage(ACTION_EDITOR_REFRESH)} ); @@ -753,7 +771,7 @@ export function EditorJSONtoForm(props: Props) { - An error occurred + {createMessage(EXPECTED_ERROR)} - 🙌 Click on + {createMessage(ACTION_RUN_BUTTON_MESSAGE_FIRST_HALF)} - after adding your query + {createMessage(ACTION_RUN_BUTTON_MESSAGE_SECOND_HALF)} )} @@ -953,7 +971,7 @@ export function EditorJSONtoForm(props: Props) { ) : ( <> - An unexpected error occurred + {createMessage(UNEXPECTED_ERROR)} window.location.reload()} round > - Refresh + {createMessage(ACTION_EDITOR_REFRESH)} )} {dataSources.length === 0 && (

- Seems like you don’t have any Datasources to - create a query + {createMessage(NO_DATASOURCE_FOR_QUERY)}

// TableCellHeight in this case is the height of one table cell in pixels. setTableBodyHeightHeight(height - TableCellHeight) } + snapToHeight={ActionExecutionResizerHeight} /> + + {isRunning && ( + <> + + +
+ + {createMessage(ACTION_EXECUTION_MESSAGE, "Query")} + + { + handleCancelActionExecution(); + }} + size={Size.medium} + tag="button" + text="Cancel Request" + type="button" + /> +
+
+ + )} + {output && !!output.length && (