diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil.ts b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil.ts new file mode 100644 index 000000000..507d96d7c --- /dev/null +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil.ts @@ -0,0 +1,74 @@ +// Definitions of State data + +import { TableColumnSizingOptions } from "@fluentui/react-components"; +import { loadState, saveStateDebounced } from "Shared/AppStatePersistenceUtility"; +import { userContext } from "UserContext"; + +// Component states +export interface DocumentsTabStateData { + leftPaneWidthPercent: number; +} + +const defaultState: DocumentsTabStateData = { + leftPaneWidthPercent: 35, +}; + +const ComponentName = "DocumentsTab"; + +export const readDocumentsTabState = (): DocumentsTabStateData => { + const state = loadState({ componentName: ComponentName }); + return (state as DocumentsTabStateData) || defaultState; +}; + +export const saveDocumentsTabState = (state: DocumentsTabStateData): void => { + saveStateDebounced({ componentName: ComponentName }, state); +}; + +type ColumnSizesMap = { [columnId: string]: WidthDefinition }; +type WidthDefinition = { idealWidth?: number; minWidth?: number }; + +const defaultSize: WidthDefinition = { + idealWidth: 200, + minWidth: 50, +}; + +const ColumnSizesSubComponentName = "ColumnSizes"; +export const readColumnSizes = ( + databaseName: string, + containerName: string, + columnIds: string[], +): TableColumnSizingOptions => { + const globalAccountName = userContext.databaseAccount?.name; + // TODO what if databaseAccount doesn't exist? + + const state = loadState({ + globalAccountName, + databaseName, + containerName, + componentName: ComponentName, + subComponentName: ColumnSizesSubComponentName, + }) as ColumnSizesMap; + + const columnSizesPx: ColumnSizesMap = {}; + columnIds.forEach((columnId) => { + columnSizesPx[columnId] = (state && state[columnId]) || defaultSize; + }); + + return columnSizesPx; +}; + +export const saveColumnSizes = (databaseName: string, containerName: string, columnSizesMap: ColumnSizesMap): void => { + const globalAccountName = userContext.databaseAccount?.name; + // TODO what if databaseAccount doesn't exist? + + saveStateDebounced( + { + componentName: ComponentName, + subComponentName: ColumnSizesSubComponentName, + globalAccountName, + databaseName, + containerName, + }, + columnSizesMap, + ); +}; diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx index b243968b6..9268f1b96 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -21,6 +21,11 @@ import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; import Explorer from "Explorer/Explorer"; import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities"; +import { + DocumentsTabStateData, + readDocumentsTabState, + saveDocumentsTabState, +} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil"; import { getPlatformTheme } from "Explorer/Theme/ThemeUtil"; import { useSelectedNode } from "Explorer/useSelectedNode"; import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; @@ -480,6 +485,9 @@ export const DocumentsTabComponent: React.FunctionComponent(() => readDocumentsTabState()); + const isQueryCopilotSampleContainer = _collection?.isSampleCollection && _collection?.databaseId === QueryCopilotSampleDatabaseId && @@ -1772,7 +1780,13 @@ export const DocumentsTabComponent: React.FunctionComponent doesn't like to be a flex child */}
- + { + tabStateData.leftPaneWidthPercent = Math.min(100, Math.max(0, Math.round(100 * preSize) / 100)); + saveDocumentsTabState(tabStateData); + setTabStateData({ ...tabStateData }); + }} + >
{tableItems.length > 0 && ( { @@ -65,30 +68,26 @@ export const DocumentsTableComponent: React.FC = size, columnHeaders, isSelectionDisabled, + collection, }: IDocumentsTableComponentProps) => { - const initialSizingOptions: TableColumnSizingOptions = { - id: { - idealWidth: 280, - minWidth: 50, - }, - }; - columnHeaders.partitionKeyHeaders.forEach((pkHeader) => { - initialSizingOptions[pkHeader] = { - idealWidth: 200, - minWidth: 50, - }; - }); - - const [columnSizingOptions, setColumnSizingOptions] = React.useState(initialSizingOptions); + const [columnSizingOptions, setColumnSizingOptions] = React.useState(() => + readColumnSizes(collection.databaseId, collection.id(), ["id"].concat(columnHeaders.partitionKeyHeaders)), + ); const onColumnResize = React.useCallback((_, { columnId, width }) => { - setColumnSizingOptions((state) => ({ - ...state, - [columnId]: { - ...state[columnId], - idealWidth: width, - }, - })); + setColumnSizingOptions((state) => { + const newSizingOptions = { + ...state, + [columnId]: { + ...state[columnId], + idealWidth: width, + }, + }; + + saveColumnSizes(collection.databaseId, collection.id(), newSizingOptions); + + return newSizingOptions; + }); }, []); // Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes diff --git a/src/Shared/AppStatePersistenceUtility.ts b/src/Shared/AppStatePersistenceUtility.ts new file mode 100644 index 000000000..b596c7f1a --- /dev/null +++ b/src/Shared/AppStatePersistenceUtility.ts @@ -0,0 +1,90 @@ +import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; + +// The component name whose state is being saved. Component name must not include special characters. +export type ComponentName = "DocumentsTab" | "DocumentsTab.columnSizes"; + +const SCHEMA_VERSION = 1; +export interface StateData { + schemaVersion: number; + timestamp: number; + data: unknown; +} + +type StorePath = { + componentName: string; + subComponentName?: string; + globalAccountName?: string; + databaseName?: string; + containerName?: string; +}; + +// Load and save state data +export const loadState = (path: StorePath): unknown => { + const appState = + LocalStorageUtility.getEntryObject(StorageKey.AppState) || ({} as ApplicationState); + const key = createKeyFromPath(path); + return appState[key]?.data; +}; +export const saveState = (path: StorePath, state: unknown): void => { + // Retrieve state object + const appState = + LocalStorageUtility.getEntryObject(StorageKey.AppState) || ({} as ApplicationState); + const key = createKeyFromPath(path); + appState[key] = { + schemaVersion: SCHEMA_VERSION, + timestamp: Date.now(), + data: state, + }; + + // TODO Add logic to clean up old state data based on timestamp + + LocalStorageUtility.setEntryObject(StorageKey.AppState, appState); +}; + +// This is for high-frequency state changes +let timeoutId: NodeJS.Timeout | undefined; +export const saveStateDebounced = (path: StorePath, state: unknown, debounceDelayMs = 1000): void => { + if (timeoutId) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(() => saveState(path, state), debounceDelayMs); +}; + +// Internal stored state +// interface ApplicationState { +// data: GlobalStateData; +// globalAccounts: { +// [globalAccountName: string]: { +// data: GlobalAccountStateData; +// databases: { +// [databaseName: string]: { +// data: DatabaseStateData; +// containers: { +// data: ContainerStateData; +// [containerName: string]: { +// [componentName: string]: BaseStateData; +// }; +// }; +// }; +// }; +// }; +// }; +// } + +interface ApplicationState { + [statePath: string]: StateData; +} + +/** + * /componentName/globalAccountName/databaseName/containerName/ + * Any of the path segments can be "" except componentName + * @param path + */ +const createKeyFromPath = (path: StorePath): string => { + let key = `/${path.componentName}`; + ["subComponentName", "globalAccountName", "databaseName", "containerName"].forEach((segment) => { + const segmentValue = (path as any)[segment]; + key += `/${segmentValue !== undefined ? segmentValue : ""}`; + }); + return key; +}; diff --git a/src/Shared/LocalStorageUtility.ts b/src/Shared/LocalStorageUtility.ts index 9fc2f4f7c..097f45877 100644 --- a/src/Shared/LocalStorageUtility.ts +++ b/src/Shared/LocalStorageUtility.ts @@ -20,3 +20,14 @@ export const setEntryNumber = (key: StorageKey, value: number): void => export const setEntryBoolean = (key: StorageKey, value: boolean): void => localStorage.setItem(StorageKey[key], value.toString()); + +export const setEntryObject = (key: StorageKey, value: unknown): void => { + localStorage.setItem(StorageKey[key], JSON.stringify(value)); +}; +export const getEntryObject = (key: StorageKey): T | null => { + const item = localStorage.getItem(StorageKey[key]); + if (item) { + return JSON.parse(item) as T; + } + return null; +}; diff --git a/src/Shared/StorageUtility.ts b/src/Shared/StorageUtility.ts index f2ca1f20b..952bcd9ac 100644 --- a/src/Shared/StorageUtility.ts +++ b/src/Shared/StorageUtility.ts @@ -30,6 +30,7 @@ export enum StorageKey { VisitedAccounts, PriorityLevel, DefaultQueryResultsView, + AppState, } export const hasRUThresholdBeenConfigured = (): boolean => {