mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2024-11-27 16:07:01 +00:00
Persist and restore query text, tab position and splitter direction in QueryTabComponent (#1993)
* Save query text, tab splitter direction and position in QueryTabComponent * Fix unit tests
This commit is contained in:
parent
808faa9fa5
commit
d562fc0f40
@ -3,17 +3,11 @@
|
|||||||
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
||||||
import {
|
import {
|
||||||
AppStateComponentNames,
|
AppStateComponentNames,
|
||||||
deleteState,
|
deleteSubComponentState,
|
||||||
loadState,
|
readSubComponentState,
|
||||||
saveState,
|
saveSubComponentState,
|
||||||
saveStateDebounced,
|
|
||||||
} from "Shared/AppStatePersistenceUtility";
|
} from "Shared/AppStatePersistenceUtility";
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
|
|
||||||
const componentName = AppStateComponentNames.DocumentsTab;
|
|
||||||
|
|
||||||
export enum SubComponentName {
|
export enum SubComponentName {
|
||||||
ColumnSizes = "ColumnSizes",
|
ColumnSizes = "ColumnSizes",
|
||||||
@ -30,84 +24,22 @@ export type TabDivider = { leftPaneWidthPercent: number };
|
|||||||
export type ColumnsSelection = { selectedColumnIds: string[]; columnDefinitions: ColumnDefinition[] };
|
export type ColumnsSelection = { selectedColumnIds: string[]; columnDefinitions: ColumnDefinition[] };
|
||||||
export type ColumnSort = { columnId: string; direction: "ascending" | "descending" };
|
export type ColumnSort = { columnId: string; direction: "ascending" | "descending" };
|
||||||
|
|
||||||
/**
|
// Wrap the ...SubComponentState functions for type safety
|
||||||
*
|
|
||||||
* @param subComponentName
|
export const readDocumentsTabSubComponentState = <T>(
|
||||||
* @param collection
|
|
||||||
* @param defaultValue Will be returned if persisted state is not found
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const readSubComponentState = <T>(
|
|
||||||
subComponentName: SubComponentName,
|
subComponentName: SubComponentName,
|
||||||
collection: ViewModels.CollectionBase,
|
collection: ViewModels.CollectionBase,
|
||||||
defaultValue: T,
|
defaultValue: T,
|
||||||
): T => {
|
): T => readSubComponentState<T>(AppStateComponentNames.DocumentsTab, subComponentName, collection, defaultValue);
|
||||||
const globalAccountName = userContext.databaseAccount?.name;
|
|
||||||
if (!globalAccountName) {
|
|
||||||
const message = "Database account name not found in userContext";
|
|
||||||
console.error(message);
|
|
||||||
TelemetryProcessor.traceFailure(Action.ReadPersistedTabState, { message, componentName });
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = loadState({
|
export const saveDocumentsTabSubComponentState = <T>(
|
||||||
componentName: componentName,
|
|
||||||
subComponentName,
|
|
||||||
globalAccountName,
|
|
||||||
databaseName: collection.databaseId,
|
|
||||||
containerName: collection.id(),
|
|
||||||
}) as T;
|
|
||||||
|
|
||||||
return state || defaultValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param subComponentName
|
|
||||||
* @param collection
|
|
||||||
* @param state State to save
|
|
||||||
* @param debounce true for high-frequency calls (e.g mouse drag events)
|
|
||||||
*/
|
|
||||||
export const saveSubComponentState = <T>(
|
|
||||||
subComponentName: SubComponentName,
|
subComponentName: SubComponentName,
|
||||||
collection: ViewModels.CollectionBase,
|
collection: ViewModels.CollectionBase,
|
||||||
state: T,
|
state: T,
|
||||||
debounce?: boolean,
|
debounce?: boolean,
|
||||||
): void => {
|
): void => saveSubComponentState<T>(AppStateComponentNames.DocumentsTab, subComponentName, collection, state, debounce);
|
||||||
const globalAccountName = userContext.databaseAccount?.name;
|
|
||||||
if (!globalAccountName) {
|
|
||||||
const message = "Database account name not found in userContext";
|
|
||||||
console.error(message);
|
|
||||||
TelemetryProcessor.traceFailure(Action.SavePersistedTabState, { message, componentName });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(debounce ? saveStateDebounced : saveState)(
|
export const deleteDocumentsTabSubComponentState = (
|
||||||
{
|
subComponentName: SubComponentName,
|
||||||
componentName: componentName,
|
collection: ViewModels.CollectionBase,
|
||||||
subComponentName,
|
) => deleteSubComponentState(AppStateComponentNames.DocumentsTab, subComponentName, collection);
|
||||||
globalAccountName,
|
|
||||||
databaseName: collection.databaseId,
|
|
||||||
containerName: collection.id(),
|
|
||||||
},
|
|
||||||
state,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteSubComponentState = (subComponentName: SubComponentName, collection: ViewModels.CollectionBase) => {
|
|
||||||
const globalAccountName = userContext.databaseAccount?.name;
|
|
||||||
if (!globalAccountName) {
|
|
||||||
const message = "Database account name not found in userContext";
|
|
||||||
console.error(message);
|
|
||||||
TelemetryProcessor.traceFailure(Action.DeletePersistedTabState, { message, componentName });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteState({
|
|
||||||
componentName: componentName,
|
|
||||||
subComponentName,
|
|
||||||
globalAccountName,
|
|
||||||
databaseName: collection.databaseId,
|
|
||||||
containerName: collection.id(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
@ -35,8 +35,8 @@ import {
|
|||||||
FilterHistory,
|
FilterHistory,
|
||||||
SubComponentName,
|
SubComponentName,
|
||||||
TabDivider,
|
TabDivider,
|
||||||
readSubComponentState,
|
readDocumentsTabSubComponentState,
|
||||||
saveSubComponentState,
|
saveDocumentsTabSubComponentState,
|
||||||
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
||||||
import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
||||||
import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
||||||
@ -619,7 +619,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
// State
|
// State
|
||||||
const [tabStateData, setTabStateData] = useState<TabDivider>(() =>
|
const [tabStateData, setTabStateData] = useState<TabDivider>(() =>
|
||||||
readSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
|
readDocumentsTabSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
|
||||||
leftPaneWidthPercent: 35,
|
leftPaneWidthPercent: 35,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -634,7 +634,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
// User's filter history
|
// User's filter history
|
||||||
const [lastFilterContents, setLastFilterContents] = useState<FilterHistory>(() =>
|
const [lastFilterContents, setLastFilterContents] = useState<FilterHistory>(() =>
|
||||||
readSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory),
|
readDocumentsTabSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory),
|
||||||
);
|
);
|
||||||
|
|
||||||
// For progress bar for bulk delete (noSql)
|
// For progress bar for bulk delete (noSql)
|
||||||
@ -804,7 +804,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [selectedColumnIds, setSelectedColumnIds] = useState<string[]>(() => {
|
const [selectedColumnIds, setSelectedColumnIds] = useState<string[]>(() => {
|
||||||
const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
|
const persistedColumnsSelection = readDocumentsTabSubComponentState<ColumnsSelection>(
|
||||||
SubComponentName.ColumnsSelection,
|
SubComponentName.ColumnsSelection,
|
||||||
_collection,
|
_collection,
|
||||||
undefined,
|
undefined,
|
||||||
@ -1714,7 +1714,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
// Column definition is a map<id, ColumnDefinition> to garantee uniqueness
|
// Column definition is a map<id, ColumnDefinition> to garantee uniqueness
|
||||||
const [columnDefinitions, setColumnDefinitions] = useState<ColumnDefinition[]>(() => {
|
const [columnDefinitions, setColumnDefinitions] = useState<ColumnDefinition[]>(() => {
|
||||||
const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
|
const persistedColumnsSelection = readDocumentsTabSubComponentState<ColumnsSelection>(
|
||||||
SubComponentName.ColumnsSelection,
|
SubComponentName.ColumnsSelection,
|
||||||
_collection,
|
_collection,
|
||||||
undefined,
|
undefined,
|
||||||
@ -2025,7 +2025,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
const limitedLastFilterContents = lastFilterContents.slice(0, MAX_FILTER_HISTORY_COUNT);
|
const limitedLastFilterContents = lastFilterContents.slice(0, MAX_FILTER_HISTORY_COUNT);
|
||||||
|
|
||||||
setLastFilterContents(limitedLastFilterContents);
|
setLastFilterContents(limitedLastFilterContents);
|
||||||
saveSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, lastFilterContents);
|
saveDocumentsTabSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, lastFilterContents);
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshDocumentsGrid = useCallback(
|
const refreshDocumentsGrid = useCallback(
|
||||||
@ -2086,7 +2086,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
setSelectedColumnIds(newSelectedColumnIds);
|
setSelectedColumnIds(newSelectedColumnIds);
|
||||||
|
|
||||||
saveSubComponentState<ColumnsSelection>(SubComponentName.ColumnsSelection, _collection, {
|
saveDocumentsTabSubComponentState<ColumnsSelection>(SubComponentName.ColumnsSelection, _collection, {
|
||||||
selectedColumnIds: newSelectedColumnIds,
|
selectedColumnIds: newSelectedColumnIds,
|
||||||
columnDefinitions,
|
columnDefinitions,
|
||||||
});
|
});
|
||||||
@ -2214,7 +2214,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
<Allotment
|
<Allotment
|
||||||
onDragEnd={(sizes: number[]) => {
|
onDragEnd={(sizes: number[]) => {
|
||||||
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
||||||
saveSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData);
|
saveDocumentsTabSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData);
|
||||||
setTabStateData(tabStateData);
|
setTabStateData(tabStateData);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -2316,7 +2316,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
</MessageBarBody>
|
</MessageBarBody>
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
)}
|
)}
|
||||||
{bulkDeleteProcess.hasBeenThrottled && (
|
|
||||||
<MessageBar intent="warning">
|
<MessageBar intent="warning">
|
||||||
<MessageBarBody>
|
<MessageBarBody>
|
||||||
<MessageBarTitle>Warning</MessageBarTitle>
|
<MessageBarTitle>Warning</MessageBarTitle>
|
||||||
@ -2326,7 +2325,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
</Link>
|
</Link>
|
||||||
</MessageBarBody>
|
</MessageBarBody>
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</ProgressModalDialog>
|
</ProgressModalDialog>
|
||||||
)}
|
)}
|
||||||
|
@ -42,9 +42,9 @@ import { TableColumnSelectionPane } from "Explorer/Panes/TableColumnSelectionPan
|
|||||||
import {
|
import {
|
||||||
ColumnSizesMap,
|
ColumnSizesMap,
|
||||||
ColumnSort,
|
ColumnSort,
|
||||||
deleteSubComponentState,
|
deleteDocumentsTabSubComponentState,
|
||||||
readSubComponentState,
|
readDocumentsTabSubComponentState,
|
||||||
saveSubComponentState,
|
saveDocumentsTabSubComponentState,
|
||||||
SubComponentName,
|
SubComponentName,
|
||||||
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
||||||
import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||||
@ -116,7 +116,11 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
const styles = useDocumentsTabStyles();
|
const styles = useDocumentsTabStyles();
|
||||||
|
|
||||||
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(() => {
|
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(() => {
|
||||||
const columnSizesMap: ColumnSizesMap = readSubComponentState(SubComponentName.ColumnSizes, collection, {});
|
const columnSizesMap: ColumnSizesMap = readDocumentsTabSubComponentState(
|
||||||
|
SubComponentName.ColumnSizes,
|
||||||
|
collection,
|
||||||
|
{},
|
||||||
|
);
|
||||||
const columnSizesPx: TableColumnSizingOptions = {};
|
const columnSizesPx: TableColumnSizingOptions = {};
|
||||||
selectedColumnIds.forEach((columnId) => {
|
selectedColumnIds.forEach((columnId) => {
|
||||||
if (
|
if (
|
||||||
@ -140,7 +144,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
sortDirection: "ascending" | "descending";
|
sortDirection: "ascending" | "descending";
|
||||||
sortColumn: TableColumnId | undefined;
|
sortColumn: TableColumnId | undefined;
|
||||||
}>(() => {
|
}>(() => {
|
||||||
const sort = readSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, undefined);
|
const sort = readDocumentsTabSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, undefined);
|
||||||
|
|
||||||
if (!sort) {
|
if (!sort) {
|
||||||
return {
|
return {
|
||||||
@ -172,7 +176,12 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
return acc;
|
return acc;
|
||||||
}, {} as ColumnSizesMap);
|
}, {} as ColumnSizesMap);
|
||||||
|
|
||||||
saveSubComponentState<ColumnSizesMap>(SubComponentName.ColumnSizes, collection, persistentSizes, true);
|
saveDocumentsTabSubComponentState<ColumnSizesMap>(
|
||||||
|
SubComponentName.ColumnSizes,
|
||||||
|
collection,
|
||||||
|
persistentSizes,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
return newSizingOptions;
|
return newSizingOptions;
|
||||||
});
|
});
|
||||||
@ -184,11 +193,14 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
setColumnSort(event, columnId, direction);
|
setColumnSort(event, columnId, direction);
|
||||||
|
|
||||||
if (columnId === undefined || direction === undefined) {
|
if (columnId === undefined || direction === undefined) {
|
||||||
deleteSubComponentState(SubComponentName.ColumnSort, collection);
|
deleteDocumentsTabSubComponentState(SubComponentName.ColumnSort, collection);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, { columnId, direction });
|
saveDocumentsTabSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, {
|
||||||
|
columnId,
|
||||||
|
direction,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes
|
// Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes
|
||||||
|
@ -34,6 +34,7 @@ jest.mock("Shared/AppStatePersistenceUtility", () => ({
|
|||||||
AppStateComponentNames: {
|
AppStateComponentNames: {
|
||||||
QueryCopilot: "QueryCopilot",
|
QueryCopilot: "QueryCopilot",
|
||||||
},
|
},
|
||||||
|
readSubComponentState: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("QueryTabComponent", () => {
|
describe("QueryTabComponent", () => {
|
||||||
|
@ -13,6 +13,11 @@ import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/Query
|
|||||||
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
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 {
|
||||||
|
SubComponentName,
|
||||||
|
readQueryTabSubComponentState,
|
||||||
|
saveQueryTabSubComponentState,
|
||||||
|
} from "Explorer/Tabs/QueryTab/QueryTabStateUtil";
|
||||||
import { QueryTabStyles, useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
|
import { QueryTabStyles, useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
|
||||||
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
@ -118,6 +123,7 @@ interface IQueryTabStates {
|
|||||||
queryResultsView: SplitterDirection;
|
queryResultsView: SplitterDirection;
|
||||||
errors?: QueryError[];
|
errors?: QueryError[];
|
||||||
modelMarkers?: monaco.editor.IMarkerData[];
|
modelMarkers?: monaco.editor.IMarkerData[];
|
||||||
|
queryViewSizePercent: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any => {
|
export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any => {
|
||||||
@ -165,7 +171,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
toggleState: ToggleState.Result,
|
toggleState: ToggleState.Result,
|
||||||
sqlQueryEditorContent: props.isPreferredApiMongoDB ? "{}" : props.queryText || "SELECT * FROM c",
|
sqlQueryEditorContent: this._getDefaultQueryEditorContent(props),
|
||||||
selectedContent: "",
|
selectedContent: "",
|
||||||
queryResults: undefined,
|
queryResults: undefined,
|
||||||
errors: [],
|
errors: [],
|
||||||
@ -176,7 +182,8 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
cancelQueryTimeoutID: undefined,
|
cancelQueryTimeoutID: undefined,
|
||||||
copilotActive: this._queryCopilotActive(),
|
copilotActive: this._queryCopilotActive(),
|
||||||
currentTabActive: true,
|
currentTabActive: true,
|
||||||
queryResultsView: getDefaultQueryResultsView(),
|
queryResultsView: this._getDefaultQUeryResultsViewDirection(props),
|
||||||
|
queryViewSizePercent: this._getQueryViewSizePercent(props),
|
||||||
};
|
};
|
||||||
this.isCloseClicked = false;
|
this.isCloseClicked = false;
|
||||||
this.splitterId = this.props.tabId + "_splitter";
|
this.splitterId = this.props.tabId + "_splitter";
|
||||||
@ -207,6 +214,25 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _getQueryViewSizePercent(props: QueryTabComponentImplProps): number {
|
||||||
|
return readQueryTabSubComponentState<number>(SubComponentName.QueryViewSizePercent, props.collection, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getDefaultQUeryResultsViewDirection(props: QueryTabComponentImplProps): SplitterDirection {
|
||||||
|
const defaultQueryResultsView = getDefaultQueryResultsView();
|
||||||
|
return readQueryTabSubComponentState<SplitterDirection>(
|
||||||
|
SubComponentName.SplitterDirection,
|
||||||
|
props.collection,
|
||||||
|
defaultQueryResultsView,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getDefaultQueryEditorContent(props: QueryTabComponentImplProps): string {
|
||||||
|
const defaultText = props.isPreferredApiMongoDB ? "{}" : props.queryText || "SELECT * FROM c";
|
||||||
|
// Retrieve from app state if available
|
||||||
|
return readQueryTabSubComponentState<string>(SubComponentName.QueryText, props.collection, defaultText);
|
||||||
|
}
|
||||||
|
|
||||||
private _queryCopilotActive(): boolean {
|
private _queryCopilotActive(): boolean {
|
||||||
if (this.props.copilotEnabled) {
|
if (this.props.copilotEnabled) {
|
||||||
return readCopilotToggleStatus(userContext.databaseAccount);
|
return readCopilotToggleStatus(userContext.databaseAccount);
|
||||||
@ -569,6 +595,13 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
private _setViewLayout(direction: SplitterDirection): void {
|
private _setViewLayout(direction: SplitterDirection): void {
|
||||||
this.setState({ queryResultsView: direction });
|
this.setState({ queryResultsView: direction });
|
||||||
|
|
||||||
|
// Store to local storage
|
||||||
|
saveQueryTabSubComponentState<SplitterDirection>(
|
||||||
|
SubComponentName.SplitterDirection,
|
||||||
|
this.props.collection,
|
||||||
|
direction,
|
||||||
|
);
|
||||||
|
|
||||||
// We'll need to refresh the context buttons to update the selected state of the view buttons
|
// We'll need to refresh the context buttons to update the selected state of the view buttons
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||||
@ -623,6 +656,8 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
this.saveQueryButton.enabled = newContent.length > 0;
|
this.saveQueryButton.enabled = newContent.length > 0;
|
||||||
|
|
||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||||
|
|
||||||
|
saveQueryTabSubComponentState<string>(SubComponentName.QueryText, this.props.collection, newContent, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSelectedContent(selectedContent: string, selection: monaco.Selection): void {
|
public onSelectedContent(selectedContent: string, selection: monaco.Selection): void {
|
||||||
@ -704,8 +739,21 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
></QueryCopilotPromptbar>
|
></QueryCopilotPromptbar>
|
||||||
)}
|
)}
|
||||||
{/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */}
|
{/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */}
|
||||||
<Allotment key={vertical.toString()} vertical={vertical}>
|
<Allotment
|
||||||
<Allotment.Pane data-test="QueryTab/EditorPane">
|
key={vertical.toString()}
|
||||||
|
vertical={vertical}
|
||||||
|
onDragEnd={(sizes: number[]) => {
|
||||||
|
const queryViewSizePercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
||||||
|
saveQueryTabSubComponentState<number>(
|
||||||
|
SubComponentName.QueryViewSizePercent,
|
||||||
|
this.props.collection,
|
||||||
|
queryViewSizePercent,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
this.setState({ queryViewSizePercent });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Allotment.Pane data-test="QueryTab/EditorPane" preferredSize={`${this.state.queryViewSizePercent}%`}>
|
||||||
<EditorReact
|
<EditorReact
|
||||||
ref={this.queryEditor}
|
ref={this.queryEditor}
|
||||||
className={this.props.styles.queryEditor}
|
className={this.props.styles.queryEditor}
|
||||||
|
37
src/Explorer/Tabs/QueryTab/QueryTabStateUtil.ts
Normal file
37
src/Explorer/Tabs/QueryTab/QueryTabStateUtil.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// Definitions of State data
|
||||||
|
|
||||||
|
import {
|
||||||
|
AppStateComponentNames,
|
||||||
|
deleteSubComponentState,
|
||||||
|
readSubComponentState,
|
||||||
|
saveSubComponentState,
|
||||||
|
} from "Shared/AppStatePersistenceUtility";
|
||||||
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
|
||||||
|
export enum SubComponentName {
|
||||||
|
SplitterDirection = "SplitterDirection",
|
||||||
|
QueryViewSizePercent = "QueryViewSizePercent",
|
||||||
|
QueryText = "QueryText",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QueryViewSizePercent = number;
|
||||||
|
export type QueryText = string;
|
||||||
|
|
||||||
|
// Wrap the ...SubComponentState functions for type safety
|
||||||
|
export const readQueryTabSubComponentState = <T>(
|
||||||
|
subComponentName: SubComponentName,
|
||||||
|
collection: ViewModels.CollectionBase,
|
||||||
|
defaultValue: T,
|
||||||
|
): T => readSubComponentState<T>(AppStateComponentNames.QueryTab, subComponentName, collection, defaultValue);
|
||||||
|
|
||||||
|
export const saveQueryTabSubComponentState = <T>(
|
||||||
|
subComponentName: SubComponentName,
|
||||||
|
collection: ViewModels.CollectionBase,
|
||||||
|
state: T,
|
||||||
|
debounce?: boolean,
|
||||||
|
): void => saveSubComponentState<T>(AppStateComponentNames.QueryTab, subComponentName, collection, state, debounce);
|
||||||
|
|
||||||
|
export const deleteQueryTabSubComponentState = (
|
||||||
|
subComponentName: SubComponentName,
|
||||||
|
collection: ViewModels.CollectionBase,
|
||||||
|
) => deleteSubComponentState(AppStateComponentNames.QueryTab, subComponentName, collection);
|
@ -1,10 +1,15 @@
|
|||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
|
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
|
||||||
// The component name whose state is being saved. Component name must not include special characters.
|
// The component name whose state is being saved. Component name must not include special characters.
|
||||||
export enum AppStateComponentNames {
|
export enum AppStateComponentNames {
|
||||||
DocumentsTab = "DocumentsTab",
|
DocumentsTab = "DocumentsTab",
|
||||||
MostRecentActivity = "MostRecentActivity",
|
MostRecentActivity = "MostRecentActivity",
|
||||||
QueryCopilot = "QueryCopilot",
|
QueryCopilot = "QueryCopilot",
|
||||||
|
QueryTab = "QueryTab",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PATH_SEPARATOR = "/"; // export for testing purposes
|
export const PATH_SEPARATOR = "/"; // export for testing purposes
|
||||||
@ -112,3 +117,93 @@ export const createKeyFromPath = (path: StorePath): string => {
|
|||||||
export const deleteAllStates = (): void => {
|
export const deleteAllStates = (): void => {
|
||||||
LocalStorageUtility.removeEntry(StorageKey.AppState);
|
LocalStorageUtility.removeEntry(StorageKey.AppState);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Convenience functions
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param subComponentName
|
||||||
|
* @param collection
|
||||||
|
* @param defaultValue Will be returned if persisted state is not found
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const readSubComponentState = <T>(
|
||||||
|
componentName: AppStateComponentNames,
|
||||||
|
subComponentName: string,
|
||||||
|
collection: ViewModels.CollectionBase,
|
||||||
|
defaultValue: T,
|
||||||
|
): T => {
|
||||||
|
const globalAccountName = userContext.databaseAccount?.name;
|
||||||
|
if (!globalAccountName) {
|
||||||
|
const message = "Database account name not found in userContext";
|
||||||
|
console.error(message);
|
||||||
|
TelemetryProcessor.traceFailure(Action.ReadPersistedTabState, { message, componentName });
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = loadState({
|
||||||
|
componentName: componentName,
|
||||||
|
subComponentName,
|
||||||
|
globalAccountName,
|
||||||
|
databaseName: collection.databaseId,
|
||||||
|
containerName: collection.id(),
|
||||||
|
}) as T;
|
||||||
|
|
||||||
|
return state || defaultValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param subComponentName
|
||||||
|
* @param collection
|
||||||
|
* @param state State to save
|
||||||
|
* @param debounce true for high-frequency calls (e.g mouse drag events)
|
||||||
|
*/
|
||||||
|
export const saveSubComponentState = <T>(
|
||||||
|
componentName: AppStateComponentNames,
|
||||||
|
subComponentName: string,
|
||||||
|
collection: ViewModels.CollectionBase,
|
||||||
|
state: T,
|
||||||
|
debounce?: boolean,
|
||||||
|
): void => {
|
||||||
|
const globalAccountName = userContext.databaseAccount?.name;
|
||||||
|
if (!globalAccountName) {
|
||||||
|
const message = "Database account name not found in userContext";
|
||||||
|
console.error(message);
|
||||||
|
TelemetryProcessor.traceFailure(Action.SavePersistedTabState, { message, componentName });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(debounce ? saveStateDebounced : saveState)(
|
||||||
|
{
|
||||||
|
componentName: componentName,
|
||||||
|
subComponentName,
|
||||||
|
globalAccountName,
|
||||||
|
databaseName: collection.databaseId,
|
||||||
|
containerName: collection.id(),
|
||||||
|
},
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteSubComponentState = (
|
||||||
|
componentName: AppStateComponentNames,
|
||||||
|
subComponentName: string,
|
||||||
|
collection: ViewModels.CollectionBase,
|
||||||
|
) => {
|
||||||
|
const globalAccountName = userContext.databaseAccount?.name;
|
||||||
|
if (!globalAccountName) {
|
||||||
|
const message = "Database account name not found in userContext";
|
||||||
|
console.error(message);
|
||||||
|
TelemetryProcessor.traceFailure(Action.DeletePersistedTabState, { message, componentName });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteState({
|
||||||
|
componentName: componentName,
|
||||||
|
subComponentName,
|
||||||
|
globalAccountName,
|
||||||
|
databaseName: collection.databaseId,
|
||||||
|
containerName: collection.id(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user