From a91858242e259f62a6525308f01bf5f8c7a50fb6 Mon Sep 17 00:00:00 2001 From: "Craig Boger (from Dev Box)" Date: Thu, 2 Jan 2025 17:22:54 -0800 Subject: [PATCH] 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). --- src/Common/Constants.ts | 4 + src/Common/CosmosClient.ts | 14 +++- .../Panes/SettingsPane/SettingsPane.tsx | 77 ++++++++++++------- src/Shared/StorageUtility.ts | 2 +- src/UserContext.ts | 1 + src/Utils/AuthorizationUtils.ts | 9 +-- 6 files changed, 71 insertions(+), 36 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 11f399ba4..5031a3786 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -232,6 +232,10 @@ export class SavedQueries { public static readonly PartitionKeyProperty: string = "id"; } +export class RegionSelectionOptions { + public static readonly Global: string = "Global"; +} + export class DocumentsGridMetrics { public static DocumentsPerPage: number = 100; public static IndividualRowHeight: number = 34; diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index 94a7959d6..bb800eacc 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -119,7 +119,11 @@ export const endpoint = () => { const location = _global.parent ? _global.parent.location : _global.location; return configContext.EMULATOR_ENDPOINT || location.origin; } - return userContext.endpoint || userContext?.databaseAccount?.properties?.documentEndpoint; + return ( + userContext.endpoint || + userContext.selectedRegionalEndpoint || + userContext?.databaseAccount?.properties?.documentEndpoint + ); }; export async function getTokenFromAuthService( @@ -253,13 +257,15 @@ export function client(): Cosmos.CosmosClient { const currentWriteRegion = await client.getWriteEndpoint(); console.log(`Current write endpoint: ${JSON.stringify(currentWriteRegion)}`); - console.log(`Current userContext endpoint: ${JSON.stringify(userContext?.endpoint)}`); + console.log( + `Current userContext.selectedRegionalEndpoint: ${JSON.stringify(userContext?.selectedRegionalEndpoint)}`, + ); } const options: Cosmos.CosmosClientOptions = { - endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called + // endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called // endpoint: "https://test-craig-nosql-westus3.documents.azure.com:443/", - // endpoint: "https://test-craig-nosql-eastus2.documents.azure.com:443/", + endpoint: "https://test-craig-nosql-eastus2.documents.azure.com:443/", key: userContext.dataPlaneRbacEnabled ? "" : userContext.masterKey, tokenProvider, userAgentSuffix: "Azure Portal", diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index a5b1f7abb..72a20d3e4 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -144,10 +144,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ ? LocalStorageUtility.getEntryString(StorageKey.IsGraphAutoVizDisabled) : "false", ); - const [readRegion, setReadRegion] = useState( - LocalStorageUtility.hasItem(StorageKey.ReadRegion) - ? LocalStorageUtility.getEntryString(StorageKey.ReadRegion) - : userContext?.databaseAccount?.properties?.readLocations?.[0]?.locationName, + const [selectedRegion, setSelectedRegion] = useState( + LocalStorageUtility.hasItem(StorageKey.SelectedRegion) + ? LocalStorageUtility.getEntryString(StorageKey.SelectedRegion) + : Constants.RegionSelectionOptions.Global, ); const [retryAttempts, setRetryAttempts] = useState( LocalStorageUtility.hasItem(StorageKey.RetryAttempts) @@ -186,10 +186,44 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin"; const shouldShowParallelismOption = userContext.apiType !== "Gremlin"; const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled(); - const readRegionOptions = userContext?.databaseAccount?.properties?.readLocations?.map((location) => ({ - key: location.locationName, - text: location.locationName, - })); + + const uniqueAccountRegions = new Set(); + const regionOptions: IDropdownOption[] = []; + regionOptions.push({ + key: Constants.RegionSelectionOptions.Global, + text: `${Constants.RegionSelectionOptions.Global} (Default)`, + data: { + endpoint: userContext?.databaseAccount?.properties?.documentEndpoint, + writeEnabled: true, + }, + }); + userContext?.databaseAccount?.properties?.writeLocations?.forEach((loc) => { + if (!uniqueAccountRegions.has(loc.locationName)) { + uniqueAccountRegions.add(loc.locationName); + regionOptions.push({ + key: loc.locationName, + text: `${loc.locationName} (Read/Write)`, + data: { + endpoint: loc.documentEndpoint, + writeEnabled: true, + }, + }); + } + }); + userContext?.databaseAccount?.properties?.readLocations?.forEach((loc) => { + if (!uniqueAccountRegions.has(loc.locationName)) { + uniqueAccountRegions.add(loc.locationName); + regionOptions.push({ + key: loc.locationName, + text: `${loc.locationName} (Read)`, + data: { + endpoint: loc.documentEndpoint, + writeEnabled: false, + }, + }); + } + }); + const shouldShowCopilotSampleDBOption = userContext.apiType === "SQL" && useQueryCopilot.getState().copilotEnabled && @@ -273,20 +307,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ } } - // Check if region selection has been updated. Update database account in user context accordingly. - const updatedDatabaseAccount = { - ...userContext.databaseAccount, - properties: { - ...userContext.databaseAccount.properties, - documentEndpoint: userContext?.databaseAccount?.properties?.readLocations?.find( - (loc) => loc.locationName === readRegion, - )?.documentEndpoint, - }, - }; + // TODO: Check if region selection has been updated. Update database account in user context accordingly. updateUserContext({ - databaseAccount: updatedDatabaseAccount, + selectedRegionalEndpoint: regionOptions.find((option) => option.key === selectedRegion)?.data?.endpoint, hasCosmosClientRegionSettingChanged: true, }); + // TODO: If Global selected, then clear out region selection, but keep change variable enabled. console.log( `userContext?.databaseAccount?.properties?.documentEndpoint details: ${JSON.stringify( userContext?.databaseAccount?.properties?.documentEndpoint, @@ -295,7 +321,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled); LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled); - LocalStorageUtility.setEntryString(StorageKey.ReadRegion, readRegion); + LocalStorageUtility.setEntryString(StorageKey.SelectedRegion, selectedRegion); LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts); LocalStorageUtility.setEntryNumber(StorageKey.RetryInterval, retryInterval); LocalStorageUtility.setEntryNumber(StorageKey.MaxWaitTimeInSeconds, MaxWaitTimeInSeconds); @@ -443,9 +469,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ setDefaultQueryResultsView(option.key as SplitterDirection); }; - const handleOnReadRegionOptionChange = (ev: React.FormEvent, option: IDropdownOption): void => { - // TODO: Region validation? - setReadRegion(option.text); + const handleOnSelectedRegionOptionChange = (ev: React.FormEvent, option: IDropdownOption): void => { + setSelectedRegion(option.key as string); }; const handleOnQueryRetryAttemptsSpinButtonChange = (ev: React.MouseEvent, newValue?: string): void => { @@ -723,9 +748,9 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ option.key === selectedRegion)?.text} + onChange={handleOnSelectedRegionOptionChange} + options={regionOptions} styles={{ root: { marginBottom: "10px" } }} /> diff --git a/src/Shared/StorageUtility.ts b/src/Shared/StorageUtility.ts index f77acc8bc..7ecacb34e 100644 --- a/src/Shared/StorageUtility.ts +++ b/src/Shared/StorageUtility.ts @@ -11,7 +11,7 @@ export enum StorageKey { RUThreshold, QueryTimeoutEnabled, QueryTimeout, - ReadRegion, + SelectedRegion, WriteRegion, RetryAttempts, RetryInterval, diff --git a/src/UserContext.ts b/src/UserContext.ts index d30951780..07e0dabab 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -101,6 +101,7 @@ export interface UserContext { readonly isReplica?: boolean; collectionCreationDefaults: CollectionCreationDefaults; sampleDataConnectionInfo?: ParsedResourceTokenConnectionString; + readonly selectedRegionalEndpoint?: string; readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams; readonly feedbackPolicies?: AdminFeedbackPolicySettings; readonly dataPlaneRbacEnabled?: boolean; diff --git a/src/Utils/AuthorizationUtils.ts b/src/Utils/AuthorizationUtils.ts index ad9f41947..d2ef4e8ff 100644 --- a/src/Utils/AuthorizationUtils.ts +++ b/src/Utils/AuthorizationUtils.ts @@ -73,11 +73,10 @@ export async function acquireMsalTokenForAccount( if (userContext.databaseAccount.properties?.documentEndpoint === undefined) { throw new Error("Database account has no document endpoint defined"); } - // const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace( - // /\/+$/, - // "/.default", - // ); - const hrefEndpoint = new URL("https://test-craig-nosql.documents.azure.com").href.replace(/\/+$/, "/.default"); + const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace( + /\/+$/, + "/.default", + ); const msalInstance = await getMsalInstance(); const knownAccounts = msalInstance.getAllAccounts(); // If user_hint is provided, we will try to use it to find the account.