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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 278 additions and 19 deletions

6
package-lock.json generated
View File

@ -22133,6 +22133,12 @@
"resolved": "https://registry.npmjs.org/react-splitter-layout/-/react-splitter-layout-4.0.0.tgz", "resolved": "https://registry.npmjs.org/react-splitter-layout/-/react-splitter-layout-4.0.0.tgz",
"integrity": "sha512-SLqOjBOxRuizWUa83w6q5/u9cDWa9/yj9Iko9V9JFN8x+cqIXiDlUFWSx+icz3IIgvsN/oRIw3za5/32RjIwrA==" "integrity": "sha512-SLqOjBOxRuizWUa83w6q5/u9cDWa9/yj9Iko9V9JFN8x+cqIXiDlUFWSx+icz3IIgvsN/oRIw3za5/32RjIwrA=="
}, },
"react-string-format": {
"version": "1.0.1",
"resolved": "https://msazure.pkgs.visualstudio.com/_packaging/AzurePortal/npm/registry/react-string-format/-/react-string-format-1.0.1.tgz",
"integrity": "sha1-JyQaRZHqURInBBx64HC3FJBh3AA=",
"license": "MIT"
},
"react-syntax-highlighter": { "react-syntax-highlighter": {
"version": "12.2.1", "version": "12.2.1",
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-12.2.1.tgz", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-12.2.1.tgz",

View File

@ -92,6 +92,7 @@
"react-notification-system": "0.2.17", "react-notification-system": "0.2.17",
"react-redux": "7.1.3", "react-redux": "7.1.3",
"react-splitter-layout": "4.0.0", "react-splitter-layout": "4.0.0",
"react-string-format": "1.0.1",
"react-youtube": "9.0.1", "react-youtube": "9.0.1",
"redux": "4.0.4", "redux": "4.0.4",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",

View File

@ -1,4 +1,13 @@
import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "@fluentui/react"; import {
Checkbox,
ChoiceGroup,
IChoiceGroupOption,
ISpinButtonStyles,
IToggleStyles,
Position,
SpinButton,
Toggle,
} from "@fluentui/react";
import * as Constants from "Common/Constants"; import * as Constants from "Common/Constants";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip"; import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { configContext } from "ConfigContext"; import { configContext } from "ConfigContext";
@ -6,10 +15,10 @@ import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import * as StringUtility from "Shared/StringUtility"; import * as StringUtility from "Shared/StringUtility";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { logConsoleInfo } from "Utils/NotificationConsoleUtils"; import { logConsoleInfo } from "Utils/NotificationConsoleUtils";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, MouseEvent, useState } from "react";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils"; import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export const SettingsPane: FunctionComponent = () => { export const SettingsPane: FunctionComponent = () => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel); const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
@ -19,6 +28,13 @@ export const SettingsPane: FunctionComponent = () => {
? Constants.Queries.UnlimitedPageOption ? Constants.Queries.UnlimitedPageOption
: Constants.Queries.CustomPageOption, : Constants.Queries.CustomPageOption,
); );
const [queryTimeoutEnabled, setQueryTimeoutEnabled] = useState<boolean>(
LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
);
const [queryTimeout, setQueryTimeout] = useState<number>(LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout));
const [automaticallyCancelQueryAfterTimeout, setAutomaticallyCancelQueryAfterTimeout] = useState<boolean>(
LocalStorageUtility.getEntryBoolean(StorageKey.AutomaticallyCancelQueryAfterTimeout),
);
const [customItemPerPage, setCustomItemPerPage] = useState<number>( const [customItemPerPage, setCustomItemPerPage] = useState<number>(
LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage) || 0, LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage) || 0,
); );
@ -53,7 +69,7 @@ export const SettingsPane: FunctionComponent = () => {
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin"; const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
const shouldShowParallelismOption = userContext.apiType !== "Gremlin"; const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled(); const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
const handlerOnSubmit = (e: MouseEvent<HTMLButtonElement>) => { const handlerOnSubmit = () => {
setIsExecuting(true); setIsExecuting(true);
LocalStorageUtility.setEntryNumber( LocalStorageUtility.setEntryNumber(
@ -61,6 +77,7 @@ export const SettingsPane: FunctionComponent = () => {
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage, isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage,
); );
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage); LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString()); LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString());
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString()); LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism); LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
@ -73,6 +90,14 @@ export const SettingsPane: FunctionComponent = () => {
); );
} }
if (queryTimeoutEnabled) {
LocalStorageUtility.setEntryNumber(StorageKey.QueryTimeout, queryTimeout);
LocalStorageUtility.setEntryBoolean(
StorageKey.AutomaticallyCancelQueryAfterTimeout,
automaticallyCancelQueryAfterTimeout,
);
}
setIsExecuting(false); setIsExecuting(false);
logConsoleInfo( logConsoleInfo(
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`, `Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`,
@ -97,7 +122,6 @@ export const SettingsPane: FunctionComponent = () => {
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`, `Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`,
); );
closeSidePanel(); closeSidePanel();
e.preventDefault();
}; };
const isCustomPageOptionSelected = () => { const isCustomPageOptionSelected = () => {
@ -112,7 +136,7 @@ export const SettingsPane: FunctionComponent = () => {
formError: "", formError: "",
isExecuting, isExecuting,
submitButtonText: "Apply", submitButtonText: "Apply",
onSubmit: () => handlerOnSubmit(undefined), onSubmit: () => handlerOnSubmit(),
}; };
const pageOptionList: IChoiceGroupOption[] = [ const pageOptionList: IChoiceGroupOption[] = [
{ key: Constants.Queries.CustomPageOption, text: "Custom" }, { key: Constants.Queries.CustomPageOption, text: "Custom" },
@ -140,6 +164,21 @@ export const SettingsPane: FunctionComponent = () => {
setPageOption(option.key); setPageOption(option.key);
}; };
const handleOnQueryTimeoutToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
setQueryTimeoutEnabled(checked);
};
const handleOnAutomaticallyCancelQueryToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
setAutomaticallyCancelQueryAfterTimeout(checked);
};
const handleOnQueryTimeoutSpinButtonChange = (ev: React.MouseEvent<HTMLElement>, newValue?: string): void => {
const queryTimeout = Number(newValue);
if (!isNaN(queryTimeout)) {
setQueryTimeout(queryTimeout);
}
};
const choiceButtonStyles = { const choiceButtonStyles = {
root: { root: {
clear: "both", clear: "both",
@ -161,6 +200,35 @@ export const SettingsPane: FunctionComponent = () => {
}, },
], ],
}; };
const queryTimeoutToggleStyles: IToggleStyles = {
label: {
fontSize: 12,
fontWeight: 400,
display: "block",
},
root: {},
container: {},
pill: {},
thumb: {},
text: {},
};
const queryTimeoutSpinButtonStyles: ISpinButtonStyles = {
label: {
fontSize: 12,
fontWeight: 400,
},
root: {
paddingBottom: 10,
},
labelWrapper: {},
icon: {},
spinButtonWrapper: {},
input: {},
arrowButtonsContainer: {},
};
return ( return (
<RightPaneForm {...genericPaneProps}> <RightPaneForm {...genericPaneProps}>
<div className="paneMainContent"> <div className="paneMainContent">
@ -211,6 +279,50 @@ export const SettingsPane: FunctionComponent = () => {
</div> </div>
</div> </div>
)} )}
{userContext.apiType === "SQL" && (
<div className="settingsSection">
<div className="settingsSectionPart">
<div>
<legend id="queryTimeoutLabel" className="settingsSectionLabel legendLabel">
Query Timeout
</legend>
<InfoTooltip>
When a query reaches a specified time limit, a popup with an option to cancel the query will show
unless automatic cancellation has been enabled
</InfoTooltip>
</div>
<div>
<Toggle
styles={queryTimeoutToggleStyles}
label="Enable query timeout"
onChange={handleOnQueryTimeoutToggleChange}
defaultChecked={queryTimeoutEnabled}
/>
</div>
{queryTimeoutEnabled && (
<div>
<SpinButton
label="Query timeout (ms)"
labelPosition={Position.top}
defaultValue={(queryTimeout || 5000).toString()}
min={100}
step={1000}
onChange={handleOnQueryTimeoutSpinButtonChange}
incrementButtonAriaLabel="Increase value by 1000"
decrementButtonAriaLabel="Decrease value by 1000"
styles={queryTimeoutSpinButtonStyles}
/>
<Toggle
label="Automatically cancel query after timeout"
styles={queryTimeoutToggleStyles}
onChange={handleOnAutomaticallyCancelQueryToggleChange}
defaultChecked={automaticallyCancelQueryAfterTimeout}
/>
</div>
)}
</div>
</div>
)}
<div className="settingsSection"> <div className="settingsSection">
<div className="settingsSectionPart"> <div className="settingsSectionPart">
<div className="settingsSectionLabel"> <div className="settingsSectionLabel">

View File

@ -97,6 +97,46 @@ exports[`Settings Pane should render Default properly 1`] = `
</div> </div>
</div> </div>
</div> </div>
<div
className="settingsSection"
>
<div
className="settingsSectionPart"
>
<div>
<legend
className="settingsSectionLabel legendLabel"
id="queryTimeoutLabel"
>
Query Timeout
</legend>
<InfoTooltip>
When a query reaches a specified time limit, a popup with an option to cancel the query will show unless automatic cancellation has been enabled
</InfoTooltip>
</div>
<div>
<StyledToggleBase
defaultChecked={false}
label="Enable query timeout"
onChange={[Function]}
styles={
Object {
"container": Object {},
"label": Object {
"display": "block",
"fontSize": 12,
"fontWeight": 400,
},
"pill": Object {},
"root": Object {},
"text": Object {},
"thumb": Object {},
}
}
/>
</div>
</div>
</div>
<div <div
className="settingsSection" className="settingsSection"
> >

View File

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

View File

@ -2,6 +2,9 @@ import { extractPartitionKey, ItemDefinition, PartitionKeyDefinition, QueryItera
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities"; import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import * as ko from "knockout"; import * as ko from "knockout";
import Q from "q"; 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 DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
import DiscardIcon from "../../../images/discard.svg"; import DiscardIcon from "../../../images/discard.svg";
import NewDocumentIcon from "../../../images/NewDocument.svg"; import NewDocumentIcon from "../../../images/NewDocument.svg";
@ -79,6 +82,7 @@ export default class DocumentsTab extends TabsBase {
private _resourceTokenPartitionKey: string; private _resourceTokenPartitionKey: string;
private _isQueryCopilotSampleContainer: boolean; private _isQueryCopilotSampleContainer: boolean;
private queryAbortController: AbortController; private queryAbortController: AbortController;
private cancelQueryTimeoutID: NodeJS.Timeout;
constructor(options: ViewModels.DocumentsTabOptions) { constructor(options: ViewModels.DocumentsTabOptions) {
super(options); super(options);
@ -350,11 +354,11 @@ export default class DocumentsTab extends TabsBase {
* Query first page of documents * Query first page of documents
* Select and query first document and display content * Select and query first document and display content
*/ */
private async autoPopulateContent() { private async autoPopulateContent(applyFilterButtonPressed?: boolean) {
// reset iterator // reset iterator
this._documentsIterator = this.createIterator(); this._documentsIterator = this.createIterator();
// load documents // load documents
await this.loadNextPage(); await this.loadNextPage(applyFilterButtonPressed);
// Select first document and load content // Select first document and load content
if (this.documentIds().length > 0) { if (this.documentIds().length > 0) {
@ -391,12 +395,14 @@ export default class DocumentsTab extends TabsBase {
return true; return true;
}; };
public async refreshDocumentsGrid(): Promise<void> { public async refreshDocumentsGrid(applyFilterButtonPressed?: boolean): Promise<void> {
// clear documents grid // clear documents grid
this.documentIds([]); this.documentIds([]);
try { try {
await this.autoPopulateContent(); // reset iterator
this._documentsIterator = this.createIterator();
// load documents
await this.autoPopulateContent(applyFilterButtonPressed);
// collapse filter // collapse filter
this.appliedFilter(this.filterContent()); this.appliedFilter(this.filterContent());
this.isFilterExpanded(false); this.isFilterExpanded(false);
@ -733,9 +739,35 @@ export default class DocumentsTab extends TabsBase {
this.initDocumentEditor(documentId, content); this.initDocumentEditor(documentId, content);
} }
public loadNextPage(): Q.Promise<any> { public loadNextPage(applyFilterButtonClicked?: boolean): Q.Promise<any> {
this.isExecuting(true); this.isExecuting(true);
this.isExecutionError(false); 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() return this._loadNextPageInternal()
.then( .then(
(documentsIdsResponse = []) => { (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 => { public onLoadMoreKeyInput = (source: any, event: KeyboardEvent): void => {
@ -969,4 +1009,8 @@ export default class DocumentsTab extends TabsBase {
useSelectedNode.getState().isQueryCopilotCollectionSelected(), 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 { FeedOptions } from "@azure/cosmos";
import { useDialog } from "Explorer/Controls/Dialog";
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults"; import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar"; import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection"; 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 { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import SplitterLayout from "react-splitter-layout"; import SplitterLayout from "react-splitter-layout";
import "react-splitter-layout/lib/index.css"; import "react-splitter-layout/lib/index.css";
import { format } from "react-string-format";
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg"; import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
import CancelQueryIcon from "../../../../images/Entity_cancel.svg"; import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
@ -80,6 +84,7 @@ interface IQueryTabStates {
isExecuting: boolean; isExecuting: boolean;
showCopilotSidebar: boolean; showCopilotSidebar: boolean;
queryCopilotGeneratedQuery: string; queryCopilotGeneratedQuery: string;
cancelQueryTimeoutID: NodeJS.Timeout;
} }
export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> { export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> {
@ -107,13 +112,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
isExecuting: false, isExecuting: false,
showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar, showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar,
queryCopilotGeneratedQuery: useQueryCopilot.getState().query, queryCopilotGeneratedQuery: useQueryCopilot.getState().query,
cancelQueryTimeoutID: undefined,
}; };
this.isCloseClicked = false; this.isCloseClicked = false;
this.splitterId = this.props.tabId + "_splitter"; this.splitterId = this.props.tabId + "_splitter";
this.queryEditorId = `queryeditor${this.props.tabId}`; this.queryEditorId = `queryeditor${this.props.tabId}`;
this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB; this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB;
this.isCopilotTabActive = QueryCopilotSampleDatabaseId === this.props.collection.databaseId; this.isCopilotTabActive = QueryCopilotSampleDatabaseId === this.props.collection.databaseId;
this.executeQueryButton = { this.executeQueryButton = {
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0, enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
visible: true, visible: true,
@ -250,6 +255,34 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
this.setState({ this.setState({
isExecuting: true, 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()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
try { try {
@ -273,7 +306,14 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
this.props.tabsBaseInstance.isExecuting(false); this.props.tabsBaseInstance.isExecuting(false);
this.setState({ this.setState({
isExecuting: false, isExecuting: false,
cancelQueryTimeoutID: undefined,
}); });
if (this.queryTimeoutEnabled()) {
clearTimeout(this.state.cancelQueryTimeoutID);
if (!automaticallyCancelQueryAfterTimeout) {
useDialog.getState().closeDialog();
}
}
this.togglesOnFocus(); this.togglesOnFocus();
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
} }
@ -405,6 +445,10 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
return this.state.sqlQueryEditorContent; return this.state.sqlQueryEditorContent;
} }
private queryTimeoutEnabled(): boolean {
return !this.isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled);
}
private unsubscribeCopilotSidebar: () => void; private unsubscribeCopilotSidebar: () => void;
componentDidMount(): void { componentDidMount(): void {

View File

@ -208,3 +208,9 @@ export class FreeTierLimits {
public static RU: number = 1000; public static RU: number = 1000;
public static Storage: number = 25; public static Storage: number = 25;
} }
export class QueryConstants {
public static readonly CancelQueryTitle: string = "Cancel query";
public static readonly CancelQuerySubTextTemplate: string = "{0} Do you want to cancel this query?";
public static readonly CancelQueryTimeoutThresholdReached: string = "The query timeout threshold has been reached.";
}

View File

@ -4,6 +4,9 @@ import * as SessionStorageUtility from "./SessionStorageUtility";
export { LocalStorageUtility, SessionStorageUtility }; export { LocalStorageUtility, SessionStorageUtility };
export enum StorageKey { export enum StorageKey {
ActualItemPerPage, ActualItemPerPage,
QueryTimeoutEnabled,
QueryTimeout,
AutomaticallyCancelQueryAfterTimeout,
ContainerPaginationEnabled, ContainerPaginationEnabled,
CustomItemPerPage, CustomItemPerPage,
DatabaseAccountId, DatabaseAccountId,