diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil.ts b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil.ts index f24f19eb4..56729da59 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil.ts +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil.ts @@ -3,17 +3,11 @@ import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent"; import { AppStateComponentNames, - deleteState, - loadState, - saveState, - saveStateDebounced, + deleteSubComponentState, + readSubComponentState, + saveSubComponentState, } from "Shared/AppStatePersistenceUtility"; -import { userContext } from "UserContext"; 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 { ColumnSizes = "ColumnSizes", @@ -30,84 +24,22 @@ export type TabDivider = { leftPaneWidthPercent: number }; export type ColumnsSelection = { selectedColumnIds: string[]; columnDefinitions: ColumnDefinition[] }; export type ColumnSort = { columnId: string; direction: "ascending" | "descending" }; -/** - * - * @param subComponentName - * @param collection - * @param defaultValue Will be returned if persisted state is not found - * @returns - */ -export const readSubComponentState = ( +// Wrap the ...SubComponentState functions for type safety + +export const readDocumentsTabSubComponentState = ( subComponentName: SubComponentName, 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; - } +): T => readSubComponentState(AppStateComponentNames.DocumentsTab, subComponentName, collection, 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 = ( +export const saveDocumentsTabSubComponentState = ( subComponentName: SubComponentName, 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; - } +): void => saveSubComponentState(AppStateComponentNames.DocumentsTab, subComponentName, collection, state, debounce); - (debounce ? saveStateDebounced : saveState)( - { - componentName: componentName, - subComponentName, - 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(), - }); -}; +export const deleteDocumentsTabSubComponentState = ( + subComponentName: SubComponentName, + collection: ViewModels.CollectionBase, +) => deleteSubComponentState(AppStateComponentNames.DocumentsTab, subComponentName, collection); diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx index 8bcb35aa1..29399ab14 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -35,8 +35,8 @@ import { FilterHistory, SubComponentName, TabDivider, - readSubComponentState, - saveSubComponentState, + readDocumentsTabSubComponentState, + saveDocumentsTabSubComponentState, } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil"; import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper"; import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil"; @@ -619,7 +619,7 @@ export const DocumentsTabComponent: React.FunctionComponent(() => - readSubComponentState(SubComponentName.MainTabDivider, _collection, { + readDocumentsTabSubComponentState(SubComponentName.MainTabDivider, _collection, { leftPaneWidthPercent: 35, }), ); @@ -634,7 +634,7 @@ export const DocumentsTabComponent: React.FunctionComponent(() => - readSubComponentState(SubComponentName.FilterHistory, _collection, [] as FilterHistory), + readDocumentsTabSubComponentState(SubComponentName.FilterHistory, _collection, [] as FilterHistory), ); // For progress bar for bulk delete (noSql) @@ -804,7 +804,7 @@ export const DocumentsTabComponent: React.FunctionComponent(() => { - const persistedColumnsSelection = readSubComponentState( + const persistedColumnsSelection = readDocumentsTabSubComponentState( SubComponentName.ColumnsSelection, _collection, undefined, @@ -1714,7 +1714,7 @@ export const DocumentsTabComponent: React.FunctionComponent to garantee uniqueness const [columnDefinitions, setColumnDefinitions] = useState(() => { - const persistedColumnsSelection = readSubComponentState( + const persistedColumnsSelection = readDocumentsTabSubComponentState( SubComponentName.ColumnsSelection, _collection, undefined, @@ -2025,7 +2025,7 @@ export const DocumentsTabComponent: React.FunctionComponent(SubComponentName.FilterHistory, _collection, lastFilterContents); + saveDocumentsTabSubComponentState(SubComponentName.FilterHistory, _collection, lastFilterContents); }; const refreshDocumentsGrid = useCallback( @@ -2086,7 +2086,7 @@ export const DocumentsTabComponent: React.FunctionComponent(SubComponentName.ColumnsSelection, _collection, { + saveDocumentsTabSubComponentState(SubComponentName.ColumnsSelection, _collection, { selectedColumnIds: newSelectedColumnIds, columnDefinitions, }); @@ -2214,7 +2214,7 @@ export const DocumentsTabComponent: React.FunctionComponent { tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]); - saveSubComponentState(SubComponentName.MainTabDivider, _collection, tabStateData); + saveDocumentsTabSubComponentState(SubComponentName.MainTabDivider, _collection, tabStateData); setTabStateData(tabStateData); }} > @@ -2316,17 +2316,15 @@ export const DocumentsTabComponent: React.FunctionComponent )} - {bulkDeleteProcess.hasBeenThrottled && ( - - - Warning - {get429WarningMessageNoSql()}{" "} - - Learn More - - - - )} + + + Warning + {get429WarningMessageNoSql()}{" "} + + Learn More + + + )} diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx index 730e2c1a6..e71502634 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx @@ -42,9 +42,9 @@ import { TableColumnSelectionPane } from "Explorer/Panes/TableColumnSelectionPan import { ColumnSizesMap, ColumnSort, - deleteSubComponentState, - readSubComponentState, - saveSubComponentState, + deleteDocumentsTabSubComponentState, + readDocumentsTabSubComponentState, + saveDocumentsTabSubComponentState, SubComponentName, } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil"; import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; @@ -116,7 +116,11 @@ export const DocumentsTableComponent: React.FC = const styles = useDocumentsTabStyles(); const [columnSizingOptions, setColumnSizingOptions] = React.useState(() => { - const columnSizesMap: ColumnSizesMap = readSubComponentState(SubComponentName.ColumnSizes, collection, {}); + const columnSizesMap: ColumnSizesMap = readDocumentsTabSubComponentState( + SubComponentName.ColumnSizes, + collection, + {}, + ); const columnSizesPx: TableColumnSizingOptions = {}; selectedColumnIds.forEach((columnId) => { if ( @@ -140,7 +144,7 @@ export const DocumentsTableComponent: React.FC = sortDirection: "ascending" | "descending"; sortColumn: TableColumnId | undefined; }>(() => { - const sort = readSubComponentState(SubComponentName.ColumnSort, collection, undefined); + const sort = readDocumentsTabSubComponentState(SubComponentName.ColumnSort, collection, undefined); if (!sort) { return { @@ -172,7 +176,12 @@ export const DocumentsTableComponent: React.FC = return acc; }, {} as ColumnSizesMap); - saveSubComponentState(SubComponentName.ColumnSizes, collection, persistentSizes, true); + saveDocumentsTabSubComponentState( + SubComponentName.ColumnSizes, + collection, + persistentSizes, + true, + ); return newSizingOptions; }); @@ -184,11 +193,14 @@ export const DocumentsTableComponent: React.FC = setColumnSort(event, columnId, direction); if (columnId === undefined || direction === undefined) { - deleteSubComponentState(SubComponentName.ColumnSort, collection); + deleteDocumentsTabSubComponentState(SubComponentName.ColumnSort, collection); return; } - saveSubComponentState(SubComponentName.ColumnSort, collection, { columnId, direction }); + saveDocumentsTabSubComponentState(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 diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.test.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.test.tsx index f671ad628..3a81f76ab 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.test.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.test.tsx @@ -34,6 +34,7 @@ jest.mock("Shared/AppStatePersistenceUtility", () => ({ AppStateComponentNames: { QueryCopilot: "QueryCopilot", }, + readSubComponentState: jest.fn(), })); describe("QueryTabComponent", () => { diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index 5f66836e4..1c347e26e 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -13,6 +13,11 @@ import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/Query import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar"; 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 { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil"; import { useSelectedNode } from "Explorer/useSelectedNode"; @@ -118,6 +123,7 @@ interface IQueryTabStates { queryResultsView: SplitterDirection; errors?: QueryError[]; modelMarkers?: monaco.editor.IMarkerData[]; + queryViewSizePercent: number; } export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any => { @@ -165,7 +171,7 @@ class QueryTabComponentImpl extends React.Component(SubComponentName.QueryViewSizePercent, props.collection, 50); + } + + private _getDefaultQUeryResultsViewDirection(props: QueryTabComponentImplProps): SplitterDirection { + const defaultQueryResultsView = getDefaultQueryResultsView(); + return readQueryTabSubComponentState( + 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(SubComponentName.QueryText, props.collection, defaultText); + } + private _queryCopilotActive(): boolean { if (this.props.copilotEnabled) { return readCopilotToggleStatus(userContext.databaseAccount); @@ -569,6 +595,13 @@ class QueryTabComponentImpl extends React.Component( + SubComponentName.SplitterDirection, + this.props.collection, + direction, + ); + // We'll need to refresh the context buttons to update the selected state of the view buttons setTimeout(() => { useCommandBar.getState().setContextButtons(this.getTabsButtons()); @@ -623,6 +656,8 @@ class QueryTabComponentImpl extends React.Component 0; useCommandBar.getState().setContextButtons(this.getTabsButtons()); + + saveQueryTabSubComponentState(SubComponentName.QueryText, this.props.collection, newContent, true); } public onSelectedContent(selectedContent: string, selection: monaco.Selection): void { @@ -704,8 +739,21 @@ class QueryTabComponentImpl extends React.Component )} {/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */} - - + { + const queryViewSizePercent = (100 * sizes[0]) / (sizes[0] + sizes[1]); + saveQueryTabSubComponentState( + SubComponentName.QueryViewSizePercent, + this.props.collection, + queryViewSizePercent, + true, + ); + this.setState({ queryViewSizePercent }); + }} + > + ( + subComponentName: SubComponentName, + collection: ViewModels.CollectionBase, + defaultValue: T, +): T => readSubComponentState(AppStateComponentNames.QueryTab, subComponentName, collection, defaultValue); + +export const saveQueryTabSubComponentState = ( + subComponentName: SubComponentName, + collection: ViewModels.CollectionBase, + state: T, + debounce?: boolean, +): void => saveSubComponentState(AppStateComponentNames.QueryTab, subComponentName, collection, state, debounce); + +export const deleteQueryTabSubComponentState = ( + subComponentName: SubComponentName, + collection: ViewModels.CollectionBase, +) => deleteSubComponentState(AppStateComponentNames.QueryTab, subComponentName, collection); diff --git a/src/Shared/AppStatePersistenceUtility.ts b/src/Shared/AppStatePersistenceUtility.ts index b48258d4f..2652a86c3 100644 --- a/src/Shared/AppStatePersistenceUtility.ts +++ b/src/Shared/AppStatePersistenceUtility.ts @@ -1,10 +1,15 @@ 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. export enum AppStateComponentNames { DocumentsTab = "DocumentsTab", MostRecentActivity = "MostRecentActivity", QueryCopilot = "QueryCopilot", + QueryTab = "QueryTab", } export const PATH_SEPARATOR = "/"; // export for testing purposes @@ -112,3 +117,93 @@ export const createKeyFromPath = (path: StorePath): string => { export const deleteAllStates = (): void => { 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 = ( + 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 = ( + 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(), + }); +};