Cancel query timeout (#1651)

* cancel query option

* query timeout

* run prettier

* removed comments

* fixed npm run compile errors

* fixed tests

* fixed unit test  errors

* fixed unit test  errors

* fixed unit test  errors

* fixed unit test  errors

* fixed unit test  errors

* increased min timeout

* added automatican cancel query option

* added react string format

* npm run format

* added unless automatic cancellation has been enabled

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
This commit is contained in:
Asier Isayas
2023-10-19 13:21:39 -04:00
committed by GitHub
parent 9b032ecae4
commit 94158504a8
9 changed files with 278 additions and 19 deletions

View File

@@ -98,7 +98,7 @@
<button
class="filterbtnstyle queryButton"
data-bind="
click: refreshDocumentsGrid,
click: refreshDocumentsGrid.bind($data, true),
enable: applyFilterButton.enabled"
aria-label="Apply filter"
tabindex="0"
@@ -176,7 +176,7 @@
<img
class="refreshcol"
src="/refresh-cosmos.svg"
data-bind="click: refreshDocumentsGrid"
data-bind="click: refreshDocumentsGrid.bind($data, false)"
alt="Refresh documents"
tabindex="0"
/>
@@ -209,7 +209,10 @@
</table>
</div>
<div class="loadMore">
<a role="button" data-bind="click: loadNextPage, event: { keypress: onLoadMoreKeyInput }" tabindex="0"
<a
role="button"
data-bind="click: loadNextPage.bind($data, false), event: { keypress: onLoadMoreKeyInput }"
tabindex="0"
>Load more</a
>
</div>

View File

@@ -2,6 +2,9 @@ import { extractPartitionKey, ItemDefinition, PartitionKeyDefinition, QueryItera
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import * as ko from "knockout";
import Q from "q";
import { format } from "react-string-format";
import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
import DiscardIcon from "../../../images/discard.svg";
import NewDocumentIcon from "../../../images/NewDocument.svg";
@@ -79,6 +82,7 @@ export default class DocumentsTab extends TabsBase {
private _resourceTokenPartitionKey: string;
private _isQueryCopilotSampleContainer: boolean;
private queryAbortController: AbortController;
private cancelQueryTimeoutID: NodeJS.Timeout;
constructor(options: ViewModels.DocumentsTabOptions) {
super(options);
@@ -350,11 +354,11 @@ export default class DocumentsTab extends TabsBase {
* Query first page of documents
* Select and query first document and display content
*/
private async autoPopulateContent() {
private async autoPopulateContent(applyFilterButtonPressed?: boolean) {
// reset iterator
this._documentsIterator = this.createIterator();
// load documents
await this.loadNextPage();
await this.loadNextPage(applyFilterButtonPressed);
// Select first document and load content
if (this.documentIds().length > 0) {
@@ -391,12 +395,14 @@ export default class DocumentsTab extends TabsBase {
return true;
};
public async refreshDocumentsGrid(): Promise<void> {
public async refreshDocumentsGrid(applyFilterButtonPressed?: boolean): Promise<void> {
// clear documents grid
this.documentIds([]);
try {
await this.autoPopulateContent();
// reset iterator
this._documentsIterator = this.createIterator();
// load documents
await this.autoPopulateContent(applyFilterButtonPressed);
// collapse filter
this.appliedFilter(this.filterContent());
this.isFilterExpanded(false);
@@ -733,9 +739,35 @@ export default class DocumentsTab extends TabsBase {
this.initDocumentEditor(documentId, content);
}
public loadNextPage(): Q.Promise<any> {
public loadNextPage(applyFilterButtonClicked?: boolean): Q.Promise<any> {
this.isExecuting(true);
this.isExecutionError(false);
let automaticallyCancelQueryAfterTimeout: boolean;
if (applyFilterButtonClicked && this.queryTimeoutEnabled()) {
const queryTimeout: number = LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout);
automaticallyCancelQueryAfterTimeout = LocalStorageUtility.getEntryBoolean(
StorageKey.AutomaticallyCancelQueryAfterTimeout,
);
const cancelQueryTimeoutID: NodeJS.Timeout = setTimeout(() => {
if (this.isExecuting()) {
if (automaticallyCancelQueryAfterTimeout) {
this.queryAbortController.abort();
} else {
useDialog
.getState()
.showOkCancelModalDialog(
QueryConstants.CancelQueryTitle,
format(QueryConstants.CancelQuerySubTextTemplate, QueryConstants.CancelQueryTimeoutThresholdReached),
"Yes",
() => this.queryAbortController.abort(),
"No",
undefined,
);
}
}
}, queryTimeout);
this.cancelQueryTimeoutID = cancelQueryTimeoutID;
}
return this._loadNextPageInternal()
.then(
(documentsIdsResponse = []) => {
@@ -791,7 +823,15 @@ export default class DocumentsTab extends TabsBase {
}
},
)
.finally(() => this.isExecuting(false));
.finally(() => {
this.isExecuting(false);
if (applyFilterButtonClicked && this.queryTimeoutEnabled()) {
clearTimeout(this.cancelQueryTimeoutID);
if (!automaticallyCancelQueryAfterTimeout) {
useDialog.getState().closeDialog();
}
}
});
}
public onLoadMoreKeyInput = (source: any, event: KeyboardEvent): void => {
@@ -969,4 +1009,8 @@ export default class DocumentsTab extends TabsBase {
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
};
}
private queryTimeoutEnabled(): boolean {
return !this.isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled);
}
}

View File

@@ -1,12 +1,16 @@
import { FeedOptions } from "@azure/cosmos";
import { useDialog } from "Explorer/Controls/Dialog";
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import React, { Fragment } from "react";
import SplitterLayout from "react-splitter-layout";
import "react-splitter-layout/lib/index.css";
import { format } from "react-string-format";
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
@@ -80,6 +84,7 @@ interface IQueryTabStates {
isExecuting: boolean;
showCopilotSidebar: boolean;
queryCopilotGeneratedQuery: string;
cancelQueryTimeoutID: NodeJS.Timeout;
}
export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> {
@@ -107,13 +112,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
isExecuting: false,
showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar,
queryCopilotGeneratedQuery: useQueryCopilot.getState().query,
cancelQueryTimeoutID: undefined,
};
this.isCloseClicked = false;
this.splitterId = this.props.tabId + "_splitter";
this.queryEditorId = `queryeditor${this.props.tabId}`;
this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB;
this.isCopilotTabActive = QueryCopilotSampleDatabaseId === this.props.collection.databaseId;
this.executeQueryButton = {
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
visible: true,
@@ -250,6 +255,34 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
this.setState({
isExecuting: true,
});
let automaticallyCancelQueryAfterTimeout: boolean;
if (this.queryTimeoutEnabled()) {
const queryTimeout: number = LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout);
automaticallyCancelQueryAfterTimeout = LocalStorageUtility.getEntryBoolean(
StorageKey.AutomaticallyCancelQueryAfterTimeout,
);
const cancelQueryTimeoutID: NodeJS.Timeout = setTimeout(() => {
if (this.state.isExecuting) {
if (automaticallyCancelQueryAfterTimeout) {
this.queryAbortController.abort();
} else {
useDialog
.getState()
.showOkCancelModalDialog(
QueryConstants.CancelQueryTitle,
format(QueryConstants.CancelQuerySubTextTemplate, QueryConstants.CancelQueryTimeoutThresholdReached),
"Yes",
() => this.queryAbortController.abort(),
"No",
undefined,
);
}
}
}, queryTimeout);
this.setState({
cancelQueryTimeoutID,
});
}
useCommandBar.getState().setContextButtons(this.getTabsButtons());
try {
@@ -273,7 +306,14 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
this.props.tabsBaseInstance.isExecuting(false);
this.setState({
isExecuting: false,
cancelQueryTimeoutID: undefined,
});
if (this.queryTimeoutEnabled()) {
clearTimeout(this.state.cancelQueryTimeoutID);
if (!automaticallyCancelQueryAfterTimeout) {
useDialog.getState().closeDialog();
}
}
this.togglesOnFocus();
useCommandBar.getState().setContextButtons(this.getTabsButtons());
}
@@ -405,6 +445,10 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
return this.state.sqlQueryEditorContent;
}
private queryTimeoutEnabled(): boolean {
return !this.isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled);
}
private unsubscribeCopilotSidebar: () => void;
componentDidMount(): void {