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:
parent
a5733da363
commit
0e36569b06
|
|
@ -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"));
|
||||
// });
|
||||
});
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 don’t 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}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user