mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-28 21:32:05 +00:00
* Add standin region selection to settings menu. * Retrieve read and write regions from user context and populate dropdown menu. Update local storage value. Need to now connect with updating read region of primary cosmos client. * Change to only selecting region for cosmos client. Not setting up separate read and write clients. * Add read and write endpoint logging to cosmos client. * Pass changing endpoint from settings menu to client. Encountered token issues using new endpoint in client. * Rough implementation of region selection of endpoint for cosmos client. Still need to: 1 - Use separate context var to track selected region. Directly updating database account context throws off token generation by acquireMSALTokenForAccount 2 - Remove href overrides in acquireMSALTokenForAccount. * Update region selection to include global endpoint and generate a unique list of read and write endpoints. Need to continue with clearing out selected endpoint when global is selected again. Write operations stall when read region is selected even though 403 returned when region rejects operation. Need to limit feature availablility to nosql, table, gremlin (maybe). * Update cosmos client to fix bug. Clients continuously generate after changing RBAC setting. * Swapping back to default endpoint value. * Rebase on client refresh bug fix. * Enable region selection for NoSql, Table, Gremlin * Add logic to reset regional endpoint when global is selected. * Fix state changing when selecting region or resetting to global. * Rough implementation of configuring regional endpoint when DE is loaded in portal or hosted with AAD/Entra auth. * Ininitial attempt at adding error handling, but still having issues with errors caught at proxy plugin. * Added rough error handling in local requestPlugin used in local environments. Passes new error to calling code. Might need to add specific error handling for request plugin to the handleError class. * Change how request plugin returns error so existing error handling utility can process and present error. * Only enable region selection for nosql accounts. * Limit region selection to portal and hosted AAD auth. SQL accounts only. Could possibly enable on table and gremlin later. * Update error handling to account for generic error code. * Refactor error code extraction. * Update test snapshots and remove unneeded logging. * Change error handling to use only the message rather than casting to any. * Clean up debug logging in cosmos client. * Remove unused storage keys. * Use endpoint instead of region name to track selected region. Prevents having to do endpoint lookups. * Add initial button state update depending on region selection. Need to update with the API and react to user context changes. * Disable CRUD buttons when read region selected. * Default to write enabled in react. * Disable query saving when read region is selected. * Patch clientWidth error on conflicts tab. * Resolve merge conflicts from rebase. * Make sure proxy endpoints return in all cases. * Remove excess client logging and match main for ConflictsTab. * Cleaning up logging and fixing endpoint discovery bug. * Fix formatting. * Reformatting if statements with preferred formatting. * Migrate region selection to local persistence. Fixes account swapping bug. TODO: Inspect better way to reset interface elements when deleteAllStates is called. Need to react to regional endpoint being reset. * Relocate resetting interface context to helper function. * Remove legacy state storage for regional endpoint selection. * Laurent suggestion updates.
220 lines
6.6 KiB
TypeScript
220 lines
6.6 KiB
TypeScript
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",
|
|
DataExplorerAction = "DataExplorerAction",
|
|
SelectedRegionalEndpoint = "SelectedRegionalEndpoint",
|
|
}
|
|
|
|
// Subcomponent for DataExplorerAction
|
|
export const OPEN_TABS_SUBCOMPONENT_NAME = "OpenTabs";
|
|
|
|
export const PATH_SEPARATOR = "/"; // export for testing purposes
|
|
const SCHEMA_VERSION = 1;
|
|
|
|
// Export for testing purposes
|
|
export const MAX_ENTRY_NB = 100_000; // Limit number of entries to 100k
|
|
|
|
export interface StateData {
|
|
schemaVersion: number;
|
|
timestamp: number;
|
|
data: unknown;
|
|
}
|
|
|
|
// Export for testing purposes
|
|
export type StorePath = {
|
|
componentName: AppStateComponentNames;
|
|
subComponentName?: string;
|
|
globalAccountName?: string;
|
|
databaseName?: string;
|
|
containerName?: string;
|
|
};
|
|
|
|
// Load and save state data
|
|
export const loadState = (path: StorePath): unknown => {
|
|
const appState =
|
|
LocalStorageUtility.getEntryObject<ApplicationState>(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<ApplicationState>(StorageKey.AppState) || ({} as ApplicationState);
|
|
const key = createKeyFromPath(path);
|
|
appState[key] = {
|
|
schemaVersion: SCHEMA_VERSION,
|
|
timestamp: Date.now(),
|
|
data: state,
|
|
};
|
|
|
|
if (Object.keys(appState).length > MAX_ENTRY_NB) {
|
|
// Remove the oldest entry
|
|
const oldestKey = Object.keys(appState).reduce((oldest, current) =>
|
|
appState[current].timestamp < appState[oldest].timestamp ? current : oldest,
|
|
);
|
|
delete appState[oldestKey];
|
|
}
|
|
|
|
LocalStorageUtility.setEntryObject(StorageKey.AppState, appState);
|
|
};
|
|
|
|
export const deleteState = (path: StorePath): void => {
|
|
// Retrieve state object
|
|
const appState =
|
|
LocalStorageUtility.getEntryObject<ApplicationState>(StorageKey.AppState) || ({} as ApplicationState);
|
|
const key = createKeyFromPath(path);
|
|
delete appState[key];
|
|
LocalStorageUtility.setEntryObject(StorageKey.AppState, appState);
|
|
};
|
|
|
|
export const hasState = (path: StorePath): boolean => {
|
|
return loadState(path) !== undefined;
|
|
};
|
|
|
|
// This is for high-frequency state changes
|
|
// Keep track of timeouts per path
|
|
const pathToTimeoutIdMap = new Map<string, NodeJS.Timeout>();
|
|
export const saveStateDebounced = (path: StorePath, state: unknown, debounceDelayMs = 1000): void => {
|
|
const key = createKeyFromPath(path);
|
|
const timeoutId = pathToTimeoutIdMap.get(key);
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
pathToTimeoutIdMap.set(
|
|
key,
|
|
setTimeout(() => saveState(path, state), debounceDelayMs),
|
|
);
|
|
};
|
|
|
|
interface ApplicationState {
|
|
[statePath: string]: StateData;
|
|
}
|
|
|
|
const orderedPathSegments: (keyof StorePath)[] = [
|
|
"subComponentName",
|
|
"globalAccountName",
|
|
"databaseName",
|
|
"containerName",
|
|
];
|
|
|
|
/**
|
|
* /componentName/subComponentName/globalAccountName/databaseName/containerName/
|
|
* Any of the path segments can be "" except componentName
|
|
* Export for testing purposes
|
|
* @param path
|
|
*/
|
|
export const createKeyFromPath = (path: StorePath): string => {
|
|
let key = `${PATH_SEPARATOR}${encodeURIComponent(path.componentName)}`; // ComponentName is always there
|
|
orderedPathSegments.forEach((segment) => {
|
|
const segmentValue = path[segment as keyof StorePath];
|
|
key += `${PATH_SEPARATOR}${segmentValue !== undefined ? encodeURIComponent(segmentValue) : ""}`;
|
|
});
|
|
return key;
|
|
};
|
|
|
|
/**
|
|
* Remove the entire app state key from local storage
|
|
*/
|
|
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 = <T>(
|
|
componentName: AppStateComponentNames,
|
|
subComponentName: string,
|
|
collection: ViewModels.CollectionBase | undefined,
|
|
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 ? collection.databaseId : "",
|
|
containerName: collection ? 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 | undefined,
|
|
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 ? collection.databaseId : "",
|
|
containerName: collection ? 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(),
|
|
});
|
|
};
|