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 <aishwarya@appsmith.com>
This commit is contained in:
Ayangade Adeoluwa 2022-08-12 08:19:17 +01:00 committed by GitHub
parent a5733da363
commit 0e36569b06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 210 additions and 18 deletions

View File

@ -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"));
// });
});

View File

@ -183,5 +183,6 @@
"selectThemeBackBtn": ".t--theme-select-back-btn",
"themeAppBorderRadiusBtn": ".t--theme-appBorderRadius",
"codeEditorWrapper": ".unfocused-code-editor",
"textWidgetContainer": ".t--text-widget-container"
}
"textWidgetContainer": ".t--text-widget-container",
"cancelActionExecution": ".t--cancel-action-button"
}

View File

@ -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();

View File

@ -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)

View File

@ -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()

View File

@ -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<Action>,
@ -169,12 +170,14 @@ class ActionAPI extends API {
executeAction: FormData,
timeout?: number,
): AxiosPromise<ActionExecutionResponse> {
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,
});
}

View File

@ -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 dont 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";

View File

@ -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<string, ActionResponse | undefined>;
isRunning: Record<string, boolean>;
@ -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 (
<ResponseContainer ref={panelRef}>
<Resizer
openResizer={isRunning}
panelRef={panelRef}
setContainerDimensions={(height: number) =>
// TableCellHeight in this case is the height of one table cell in pixels.
setTableBodyHeightHeight(height - TableCellHeight)
}
snapToHeight={ActionExecutionResizerHeight}
/>
<SectionDivider />
{isRunning && (
<LoadingOverlayScreen theme={props.theme}>
Sending Request
</LoadingOverlayScreen>
<>
<LoadingOverlayScreen theme={props.theme} />
<LoadingOverlayContainer>
<div>
<Text textAlign={"center"} type={TextType.P1}>
{createMessage(ACTION_EXECUTION_MESSAGE, "API")}
</Text>
<CancelRequestButton
category={Category.tertiary}
className={`t--cancel-action-button`}
onClick={() => {
handleCancelActionExecution();
}}
size={Size.medium}
tag="button"
text="Cancel Request"
type="button"
/>
</div>
</LoadingOverlayContainer>
</>
)}
<TabbedViewWrapper>
{response.statusCode && (

View File

@ -21,6 +21,8 @@ const Top = styled.div`
type ResizerProps = {
panelRef: RefObject<HTMLDivElement>;
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();

View File

@ -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) {
<components.MenuList {...props}>{props.children}</components.MenuList>
<CreateDatasource onClick={() => onCreateDatasourceClick()}>
<Icon className="createIcon" icon="plus" iconSize={11} />
Create new datasource
{createMessage(CREATE_NEW_DATASOURCE)}
</CreateDatasource>
</>
);
@ -594,7 +610,9 @@ export function EditorJSONtoForm(props: Props) {
Sentry.captureException(e);
return (
<>
<ErrorMessage>Invalid form configuration</ErrorMessage>
<ErrorMessage>
{createMessage(INVALID_FORM_CONFIGURATION)}
</ErrorMessage>
<Tag
intent="warning"
interactive
@ -602,7 +620,7 @@ export function EditorJSONtoForm(props: Props) {
onClick={() => window.location.reload()}
round
>
Refresh
{createMessage(ACTION_EDITOR_REFRESH)}
</Tag>
</>
);
@ -753,7 +771,7 @@ export function EditorJSONtoForm(props: Props) {
<ErrorContainer>
<AdsIcon keepColors name="warning-triangle" />
<Text style={{ color: "#F22B2B" }} type={TextType.H3}>
An error occurred
{createMessage(EXPECTED_ERROR)}
</Text>
<ErrorDescriptionText
@ -804,7 +822,7 @@ export function EditorJSONtoForm(props: Props) {
<NoResponseContainer>
<AdsIcon name="no-response" />
<Text type={TextType.P1}>
🙌 Click on
{createMessage(ACTION_RUN_BUTTON_MESSAGE_FIRST_HALF)}
<InlineButton
isLoading={isRunning}
onClick={responeTabOnRunClick}
@ -813,7 +831,7 @@ export function EditorJSONtoForm(props: Props) {
text="Run"
type="button"
/>
after adding your query
{createMessage(ACTION_RUN_BUTTON_MESSAGE_SECOND_HALF)}
</Text>
</NoResponseContainer>
)}
@ -953,7 +971,7 @@ export function EditorJSONtoForm(props: Props) {
) : (
<>
<ErrorMessage>
An unexpected error occurred
{createMessage(UNEXPECTED_ERROR)}
</ErrorMessage>
<Tag
intent="warning"
@ -962,15 +980,14 @@ export function EditorJSONtoForm(props: Props) {
onClick={() => window.location.reload()}
round
>
Refresh
{createMessage(ACTION_EDITOR_REFRESH)}
</Tag>
</>
)}
{dataSources.length === 0 && (
<NoDataSourceContainer>
<p className="font18">
Seems like you dont have any Datasources to
create a query
{createMessage(NO_DATASOURCE_FOR_QUERY)}
</p>
<EditorButton
filled
@ -1003,12 +1020,39 @@ export function EditorJSONtoForm(props: Props) {
<TabbedViewContainer ref={panelRef}>
<Resizable
openResizer={isRunning}
panelRef={panelRef}
setContainerDimensions={(height: number) =>
// TableCellHeight in this case is the height of one table cell in pixels.
setTableBodyHeightHeight(height - TableCellHeight)
}
snapToHeight={ActionExecutionResizerHeight}
/>
<SectionDivider />
{isRunning && (
<>
<LoadingOverlayScreen theme={EditorTheme.LIGHT} />
<LoadingOverlayContainer>
<div>
<Text textAlign={"center"} type={TextType.P1}>
{createMessage(ACTION_EXECUTION_MESSAGE, "Query")}
</Text>
<CancelRequestButton
category={Category.tertiary}
className={`t--cancel-action-button`}
onClick={() => {
handleCancelActionExecution();
}}
size={Size.medium}
tag="button"
text="Cancel Request"
type="button"
/>
</div>
</LoadingOverlayContainer>
</>
)}
{output && !!output.length && (
<ResultsCount>
<Text type={TextType.P3}>