From 0170c9e1cccf9ba4f6452f576b09d9346872b8d2 Mon Sep 17 00:00:00 2001 From: SATYA SB <107645008+satya07sb@users.noreply.github.com> Date: Fri, 14 Feb 2025 11:53:01 +0530 Subject: [PATCH 01/11] [accessibility-3739182]:[Visual Requirement - Azure Cosmos DB - Add Row]: Ensures the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds. (#2054) Co-authored-by: Satyapriya Bai --- less/Common/Constants.less | 2 ++ src/Explorer/Panes/PanelComponent.less | 1 + 2 files changed, 3 insertions(+) diff --git a/less/Common/Constants.less b/less/Common/Constants.less index 8c3a66b99..222b13c32 100644 --- a/less/Common/Constants.less +++ b/less/Common/Constants.less @@ -61,6 +61,8 @@ @GalleryBackgroundColor: #fdfdfd; +@LinkColor: #2d6da4; + //Icons @InfoIconColor: #0072c6; @WarningIconColor: #db7500; diff --git a/src/Explorer/Panes/PanelComponent.less b/src/Explorer/Panes/PanelComponent.less index 3a0ab9b3c..998201a09 100644 --- a/src/Explorer/Panes/PanelComponent.less +++ b/src/Explorer/Panes/PanelComponent.less @@ -94,6 +94,7 @@ padding-left: @MediumSpace; .paneErrorLink { + color: @LinkColor; cursor: pointer; font-size: @mediumFontSize; } From d7923db10893f84164c0baec40bc1f3634f0851c Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Tue, 18 Feb 2025 09:29:53 -0800 Subject: [PATCH 02/11] Add Tables as an API type that supports dataplane RBAC. (#2056) --- src/Common/CosmosClient.ts | 3 ++- src/Explorer/Panes/SettingsPane/SettingsPane.tsx | 3 ++- src/Utils/APITypeUtils.ts | 4 ++++ src/hooks/useKnockoutExplorer.ts | 3 ++- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index 79ed76434..79e41dda4 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -8,6 +8,7 @@ import { PriorityLevel } from "../Common/Constants"; import * as Logger from "../Common/Logger"; import { Platform, configContext } from "../ConfigContext"; import { updateUserContext, userContext } from "../UserContext"; +import { isDataplaneRbacSupported } from "../Utils/APITypeUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils"; import { EmulatorMasterKey, HttpHeaders } from "./Constants"; @@ -18,7 +19,7 @@ const _global = typeof self === "undefined" ? window : self; export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { const { verb, resourceId, resourceType, headers } = requestInfo; - const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL"; + const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType); if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) { Logger.logInfo( `AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `, diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index a19c89be2..1626f09d1 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -32,6 +32,7 @@ import { } from "Shared/StorageUtility"; import * as StringUtility from "Shared/StringUtility"; import { updateUserContext, userContext } from "UserContext"; +import { isDataplaneRbacSupported } from "Utils/APITypeUtils"; import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils"; import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils"; import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils"; @@ -183,7 +184,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin" && !isEmulator; const shouldShowParallelismOption = userContext.apiType !== "Gremlin" && !isEmulator; const showEnableEntraIdRbac = - userContext.apiType === "SQL" && + isDataplaneRbacSupported(userContext.apiType) && userContext.authType === AuthType.AAD && configContext.platform !== Platform.Fabric && !isEmulator; diff --git a/src/Utils/APITypeUtils.ts b/src/Utils/APITypeUtils.ts index b25ca1c29..aa88ecf66 100644 --- a/src/Utils/APITypeUtils.ts +++ b/src/Utils/APITypeUtils.ts @@ -89,3 +89,7 @@ export const getItemName = (): string => { return "Items"; } }; + +export const isDataplaneRbacSupported = (apiType: string): boolean => { + return apiType === "SQL" || apiType === "Tables"; +}; diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 2d12f3af4..70a772559 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -13,6 +13,7 @@ import { readSubComponentState, } from "Shared/AppStatePersistenceUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; +import { isDataplaneRbacSupported } from "Utils/APITypeUtils"; import { logConsoleError } from "Utils/NotificationConsoleUtils"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import { ReactTabKind, useTabs } from "hooks/useTabs"; @@ -299,7 +300,7 @@ async function configureHostedWithAAD(config: AAD): Promise { ); if (!userContext.features.enableAadDataPlane) { Logger.logInfo(`AAD Feature flag is not enabled for account ${account.name}`, "Explorer/configureHostedWithAAD"); - if (userContext.apiType === "SQL") { + if (isDataplaneRbacSupported(userContext.apiType)) { if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); Logger.logInfo( From 4ac41031e6179b5b044aa8b49eeb0d2d76e38a63 Mon Sep 17 00:00:00 2001 From: vchske Date: Tue, 18 Feb 2025 09:59:51 -0800 Subject: [PATCH 03/11] Fixing SelfServeType enum to work in MPAC (#2057) --- src/SelfServe/SelfServe.tsx | 10 +++++----- src/SelfServe/SelfServeUtils.tsx | 7 ++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/SelfServe/SelfServe.tsx b/src/SelfServe/SelfServe.tsx index 932951b34..ee1949746 100644 --- a/src/SelfServe/SelfServe.tsx +++ b/src/SelfServe/SelfServe.tsx @@ -41,13 +41,13 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise => { const urlSearchParams = new URLSearchParams(window.location.search); const selfServeTypeText = urlSearchParams.get("selfServeType") || inputs.selfServeType; - const selfServeType = SelfServeType[selfServeTypeText?.toLowerCase() as keyof typeof SelfServeType]; + const selfServeType = SelfServeType[selfServeTypeText.toLocaleLowerCase() as keyof typeof SelfServeType]; if ( !inputs.subscriptionId || !inputs.resourceGroup || diff --git a/src/SelfServe/SelfServeUtils.tsx b/src/SelfServe/SelfServeUtils.tsx index 0a5ffa4d3..f5b144915 100644 --- a/src/SelfServe/SelfServeUtils.tsx +++ b/src/SelfServe/SelfServeUtils.tsx @@ -29,10 +29,11 @@ export enum SelfServeType { // Unsupported self serve type passed as feature flag invalid = "invalid", // Add your self serve types here + // NOTE: text and casing of the enum's value must match the corresponding file in Localization\en\ example = "example", - sqlx = "sqlx", - graphapicompute = "graphapicompute", - materializedviewsbuilder = "materializedviewsbuilder", + sqlx = "SqlX", + graphapicompute = "GraphAPICompute", + materializedviewsbuilder = "MaterializedViewsBuilder", } /** From 8da078579e95a4865aae447c67781467ced8432e Mon Sep 17 00:00:00 2001 From: SATYA SB <107645008+satya07sb@users.noreply.github.com> Date: Wed, 19 Feb 2025 11:25:44 +0530 Subject: [PATCH 04/11] [accessibility-3739618]:[Screen Reader - Azure Cosmos DB- Data Explorer - Graphs]: Screen Reader announces both expanded and collapsed information simultaneously for expand/collapse button in bottom notification region under 'Data Explorer' pane. (#2048) Co-authored-by: Satyapriya Bai --- .../NotificationConsoleComponent.tsx | 12 +++++----- ...NotificationConsoleComponent.test.tsx.snap | 24 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx index e86f08320..1821fd14c 100644 --- a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx +++ b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx @@ -109,15 +109,15 @@ export class NotificationConsoleComponent extends React.Component<
- in progress items + In progress items {numInProgress} - error items + Error items {numErroredItems} - info items + Info items {numInfoItems} @@ -134,12 +134,12 @@ export class NotificationConsoleComponent extends React.Component< data-test="NotificationConsole/ExpandCollapseButton" role="button" tabIndex={0} - aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")} - aria-expanded={!this.props.isConsoleExpanded} + aria-label="Console" + aria-expanded={this.props.isConsoleExpanded} > {this.props.isConsoleExpanded
diff --git a/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap b/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap index 57267f1e0..268b110b0 100644 --- a/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap +++ b/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap @@ -21,7 +21,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = ` className="notificationConsoleHeaderIconWithData" > in progress items error items info items
ChevronUpIcon
@@ -192,7 +192,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = ` className="notificationConsoleHeaderIconWithData" > in progress items error items info items
ChevronUpIcon
From 3fcbdf61529bf247264cd76e86fadbb46b37a147 Mon Sep 17 00:00:00 2001 From: SATYA SB <107645008+satya07sb@users.noreply.github.com> Date: Wed, 19 Feb 2025 11:26:15 +0530 Subject: [PATCH 05/11] [accessibility-3739790-3739677]:[Forms and Validation - Azure Cosmos DB- Data Explorer - New Vertex]: Visual Label is not defined for Key, Value and Type input fields under 'New Vertex' pane. (#2040) Co-authored-by: Satyapriya Bai --- .../Graph/NewVertexComponent/NewVertexComponent.less | 8 ++++---- .../Graph/NewVertexComponent/NewVertexComponent.tsx | 9 ++++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.less b/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.less index 7f214d22d..6993cbe39 100644 --- a/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.less +++ b/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.less @@ -14,10 +14,6 @@ .flex-direction(@direction: row); padding: 4px 5px; - label { - padding: 0px; - } - .valueCol { flex-grow: 1; padding-right: 5px; @@ -63,6 +59,10 @@ height: 100%; } + .customTrashIcon { + padding-top: 33px; + } + .rightPaneTrashIconImg { vertical-align: top; } diff --git a/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx b/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx index 6b20cfcb0..8701bfb28 100644 --- a/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx +++ b/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx @@ -142,10 +142,11 @@ export const NewVertexComponent: FunctionComponent = (
= ( onChange={(event: React.ChangeEvent) => onKeyChange(event, index)} />
-
= (
= (
Date: Thu, 20 Feb 2025 07:06:25 -0800 Subject: [PATCH 06/11] Change value of the example SelfServeType enum to match name of (#2062) localization file. --- src/SelfServe/SelfServeUtils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SelfServe/SelfServeUtils.tsx b/src/SelfServe/SelfServeUtils.tsx index f5b144915..ef4cb614c 100644 --- a/src/SelfServe/SelfServeUtils.tsx +++ b/src/SelfServe/SelfServeUtils.tsx @@ -30,7 +30,7 @@ export enum SelfServeType { invalid = "invalid", // Add your self serve types here // NOTE: text and casing of the enum's value must match the corresponding file in Localization\en\ - example = "example", + example = "SelfServeExample", sqlx = "SqlX", graphapicompute = "GraphAPICompute", materializedviewsbuilder = "MaterializedViewsBuilder", From a04eaff6be10893541cb16211805c8f003a6ff5c Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Thu, 20 Feb 2025 08:15:53 -0800 Subject: [PATCH 07/11] Add Tables to missing api type checks for dataplane RBAC. (#2060) * Add Tables to missing api type checks for dataplane RBAC. * Comment out test that is broken due to invalid hook call error. --- .../CommandBarComponentButtonFactory.test.ts | 34 +++++++++++-------- .../CommandBarComponentButtonFactory.tsx | 3 +- src/hooks/useKnockoutExplorer.ts | 2 +- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts index 0a4a805d2..8ac604ea2 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts @@ -37,21 +37,25 @@ describe("CommandBarComponentButtonFactory tests", () => { expect(enableAzureSynapseLinkBtn).toBeDefined(); }); - it("Button should not be visible for Tables API", () => { - updateUserContext({ - databaseAccount: { - properties: { - capabilities: [{ name: "EnableTable" }], - }, - } as DatabaseAccount, - }); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const enableAzureSynapseLinkBtn = buttons.find( - (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel, - ); - expect(enableAzureSynapseLinkBtn).toBeUndefined(); - }); + // TODO: Now that Tables API supports dataplane RBAC, calling createStaticCommandBarButtons will enable the + // Entra ID Login button, which causes this test to fail due to "Invalid hook call.". This seems to be + // unsupported in jest and needs to be tested with react-hooks-testing-library. + // + // it("Button should not be visible for Tables API", () => { + // updateUserContext({ + // databaseAccount: { + // properties: { + // capabilities: [{ name: "EnableTable" }], + // }, + // } as DatabaseAccount, + // }); + // + // const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); + // const enableAzureSynapseLinkBtn = buttons.find( + // (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel, + // ); + // expect(enableAzureSynapseLinkBtn).toBeUndefined(); + //}); it("Button should not be visible for Cassandra API", () => { updateUserContext({ diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 8c374a4c1..49c513a0a 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -1,4 +1,5 @@ import { KeyboardAction } from "KeyboardShortcuts"; +import { isDataplaneRbacSupported } from "Utils/APITypeUtils"; import * as React from "react"; import { useEffect, useState } from "react"; import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg"; @@ -61,7 +62,7 @@ export function createStaticCommandBarButtons( } } - if (userContext.apiType === "SQL") { + if (isDataplaneRbacSupported(userContext.apiType)) { const [loginButtonProps, setLoginButtonProps] = useState(undefined); const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled); const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated); diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 70a772559..3c59baadf 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -553,7 +553,7 @@ async function configurePortal(): Promise { const { databaseAccount: account, subscriptionId, resourceGroup } = userContext; let dataPlaneRbacEnabled; - if (userContext.apiType === "SQL") { + if (isDataplaneRbacSupported(userContext.apiType)) { if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); Logger.logInfo( From 14c9874e5ea92983f8d58503529999bdf9db151c Mon Sep 17 00:00:00 2001 From: SATYA SB <107645008+satya07sb@users.noreply.github.com> Date: Tue, 25 Feb 2025 13:35:59 +0530 Subject: [PATCH 08/11] [accessibility-3560325]:[Programmatic access - Cosmos DB Query Copilot - Query Faster with Copilot>Enable Query Advisor]: Element's role present under 'Sample Query1' tab does not support its ARIA attributes. (#2059) Co-authored-by: Satyapriya Bai --- src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx index e4a7b35de..1582a4335 100644 --- a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx +++ b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx @@ -393,8 +393,7 @@ export const QueryCopilotPromptbar: React.FC = ({ }, }} disabled={isGeneratingQuery} - autoComplete="list" - aria-expanded={showSamplePrompts} + autoComplete="off" placeholder="Ask a question in natural language and we’ll generate the query for you." aria-labelledby="copilot-textfield-label" onRenderSuffix={() => { From 083bccfda9f6886d0bfec8a622e1d1cc2365e03c Mon Sep 17 00:00:00 2001 From: Laurent Nguyen Date: Thu, 6 Mar 2025 07:30:13 +0100 Subject: [PATCH 09/11] Prepare for Fabric native (#2050) * Implement fabric native path * Fix default values to work with current fabric clients * Fix Fabric native mode * Fix unit test * export Fabric context * Dynamically close Home tab for Mirrored databases in Fabric rather than conditional init (which doesn't work for Native) * For Fabric native, don't show "Delete Database" in context menu and reading databases should return the database from the context. * Update to V3 messaging * For data plane operations, skip ARM for Fabric native. Refine the tests for fabric to make the distinction between mirrored key, mirrored AAD and native. Fix FabricUtil to strict compile. * Add support for refreshing access tokens * Buf fix: don't wait for refresh is async * Fix format * Fix strict compile issue --------- Co-authored-by: Laurent Nguyen --- src/Common/CosmosClient.ts | 20 +- src/Common/StyleConstants.ts | 4 +- src/Common/dataAccess/readCollections.ts | 18 +- src/Common/dataAccess/readDatabaseOffer.ts | 7 +- src/Common/dataAccess/readDatabases.ts | 31 +- src/Contracts/FabricMessageTypes.ts | 1 + src/Contracts/FabricMessagesContract.ts | 124 +++++--- src/Explorer/ContextMenuButtonFactory.tsx | 7 +- src/Explorer/Explorer.tsx | 12 +- .../CommandBar/CommandBarComponentAdapter.tsx | 27 +- src/Explorer/OpenActions/OpenActions.tsx | 6 +- src/Explorer/Panes/AddCollectionPanel.tsx | 281 +++++++++--------- src/Explorer/Sidebar.tsx | 18 +- src/Explorer/SplashScreen/FabricHome.tsx | 173 +++++++++++ .../DocumentsTabV2/DocumentsTabV2.test.tsx | 10 +- .../Tabs/DocumentsTabV2/DocumentsTabV2.tsx | 7 +- src/Explorer/Tabs/Tabs.tsx | 8 +- src/Explorer/Tree/Collection.ts | 4 +- src/Explorer/Tree/ResourceTree.tsx | 33 +- src/Explorer/Tree/treeNodeUtil.test.ts | 15 +- src/Explorer/Tree/treeNodeUtil.tsx | 5 +- src/Platform/Fabric/FabricUtil.ts | 117 ++++++-- src/UserContext.ts | 20 +- src/Utils/WindowUtils.ts | 3 +- src/hooks/useKnockoutExplorer.ts | 206 +++++++++++-- src/hooks/useTabs.ts | 25 +- 26 files changed, 831 insertions(+), 351 deletions(-) create mode 100644 src/Explorer/SplashScreen/FabricHome.tsx diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index 79e41dda4..cf34b2279 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -1,13 +1,14 @@ import * as Cosmos from "@azure/cosmos"; import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens"; +import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract"; import { AuthorizationToken } from "Contracts/FabricMessageTypes"; -import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil"; +import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { AuthType } from "../AuthType"; import { PriorityLevel } from "../Common/Constants"; import * as Logger from "../Common/Logger"; import { Platform, configContext } from "../ConfigContext"; -import { updateUserContext, userContext } from "../UserContext"; +import { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext"; import { isDataplaneRbacSupported } from "../Utils/APITypeUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils"; @@ -42,7 +43,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { return decodeURIComponent(headers.authorization); } - if (configContext.platform === Platform.Fabric) { + if (isFabricMirroredKey()) { switch (requestInfo.resourceType) { case Cosmos.ResourceType.conflicts: case Cosmos.ResourceType.container: @@ -54,8 +55,13 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { // User resource tokens // TODO userContext.fabricContext.databaseConnectionInfo can be undefined headers[HttpHeaders.msDate] = new Date().toUTCString(); - const resourceTokens = userContext.fabricContext.databaseConnectionInfo.resourceTokens; - checkDatabaseResourceTokensValidity(userContext.fabricContext.databaseConnectionInfo.resourceTokensTimestamp); + const resourceTokens = ( + userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY] + ).resourceTokenInfo.resourceTokens; + checkDatabaseResourceTokensValidity( + (userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]) + .resourceTokenInfo.resourceTokensTimestamp, + ); return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId); case Cosmos.ResourceType.none: @@ -66,7 +72,9 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { // For now, these operations aren't used, so fetching the authorization token is commented out. // This provider must return a real token to pass validation by the client, so we return the cached resource token // (which is a valid token, but won't work for these operations). - const resourceTokens2 = userContext.fabricContext.databaseConnectionInfo.resourceTokens; + const resourceTokens2 = ( + userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY] + ).resourceTokenInfo.resourceTokens; return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId); /* ************** TODO: Uncomment this code if we need to support these operations ************** diff --git a/src/Common/StyleConstants.ts b/src/Common/StyleConstants.ts index 81742d8ce..ca1205339 100644 --- a/src/Common/StyleConstants.ts +++ b/src/Common/StyleConstants.ts @@ -1,10 +1,10 @@ -import { Platform, configContext } from "../ConfigContext"; +import { isFabric } from "Platform/Fabric/FabricUtil"; // eslint-disable-next-line @typescript-eslint/no-var-requires export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less"); export function updateStyles(): void { - if (configContext.platform === Platform.Fabric) { + if (isFabric()) { StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh; StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium; StyleConstants.AccentLight = StyleConstants.FabricAccentLight; diff --git a/src/Common/dataAccess/readCollections.ts b/src/Common/dataAccess/readCollections.ts index 4098b3fe8..ecb67c876 100644 --- a/src/Common/dataAccess/readCollections.ts +++ b/src/Common/dataAccess/readCollections.ts @@ -1,9 +1,10 @@ import { ContainerResponse } from "@azure/cosmos"; import { Queries } from "Common/Constants"; -import { Platform, configContext } from "ConfigContext"; +import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract"; +import { isFabric, isFabricMirroredKey } from "Platform/Fabric/FabricUtil"; import { AuthType } from "../../AuthType"; import * as DataModels from "../../Contracts/DataModels"; -import { userContext } from "../../UserContext"; +import { FabricArtifactInfo, userContext } from "../../UserContext"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; @@ -16,15 +17,13 @@ import { handleError } from "../ErrorHandlingUtils"; export async function readCollections(databaseId: string): Promise { const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`); - if ( - configContext.platform === Platform.Fabric && - userContext.fabricContext && - userContext.fabricContext.databaseConnectionInfo.databaseId === databaseId - ) { + if (isFabricMirroredKey() && userContext.fabricContext?.databaseName === databaseId) { const collections: DataModels.Collection[] = []; const promises: Promise[] = []; - for (const collectionResourceId in userContext.fabricContext.databaseConnectionInfo.resourceTokens) { + for (const collectionResourceId in ( + userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY] + ).resourceTokenInfo.resourceTokens) { // Dictionary key looks like this: dbs/SampleDB/colls/Container const resourceIdObj = collectionResourceId.split("/"); const tokenDatabaseId = resourceIdObj[1]; @@ -56,7 +55,8 @@ export async function readCollections(databaseId: string): Promise => { - if (configContext.platform === Platform.Fabric) { + if (isFabricMirroredKey()) { // TODO This works, but is very slow, because it requests the token, so we skip for now console.error("Skiping readDatabaseOffer for Fabric"); return undefined; @@ -23,7 +23,8 @@ export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promis if ( userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && - userContext.apiType !== "Tables" + userContext.apiType !== "Tables" && + !isFabric() ) { return await readDatabaseOfferWithARM(params.databaseId); } diff --git a/src/Common/dataAccess/readDatabases.ts b/src/Common/dataAccess/readDatabases.ts index 9cc0b0641..66ea1e76f 100644 --- a/src/Common/dataAccess/readDatabases.ts +++ b/src/Common/dataAccess/readDatabases.ts @@ -1,7 +1,8 @@ -import { Platform, configContext } from "ConfigContext"; +import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract"; +import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil"; import { AuthType } from "../../AuthType"; import * as DataModels from "../../Contracts/DataModels"; -import { userContext } from "../../UserContext"; +import { FabricArtifactInfo, userContext } from "../../UserContext"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; @@ -14,8 +15,13 @@ export async function readDatabases(): Promise { let databases: DataModels.Database[]; const clearMessage = logConsoleProgress(`Querying databases`); - if (configContext.platform === Platform.Fabric && userContext.fabricContext?.databaseConnectionInfo.resourceTokens) { - const tokensData = userContext.fabricContext.databaseConnectionInfo; + if ( + isFabricMirroredKey() && + (userContext.fabricContext?.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]).resourceTokenInfo + .resourceTokens + ) { + const tokensData = (userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]) + .resourceTokenInfo; const databaseIdsSet = new Set(); // databaseId @@ -46,13 +52,28 @@ export async function readDatabases(): Promise { })); clearMessage(); return databases; + } else if (isFabricNative() && userContext.fabricContext?.databaseName) { + const databaseId = userContext.fabricContext.databaseName; + databases = [ + { + _rid: "", + _self: "", + _etag: "", + _ts: 0, + id: databaseId, + collections: [], + }, + ]; + clearMessage(); + return databases; } try { if ( userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && - userContext.apiType !== "Tables" + userContext.apiType !== "Tables" && + !isFabric() ) { databases = await readDatabasesWithARM(); } else { diff --git a/src/Contracts/FabricMessageTypes.ts b/src/Contracts/FabricMessageTypes.ts index aa374472d..1d4576391 100644 --- a/src/Contracts/FabricMessageTypes.ts +++ b/src/Contracts/FabricMessageTypes.ts @@ -4,6 +4,7 @@ export enum FabricMessageTypes { GetAuthorizationToken = "GetAuthorizationToken", GetAllResourceTokens = "GetAllResourceTokens", + GetAccessToken = "GetAccessToken", Ready = "Ready", } diff --git a/src/Contracts/FabricMessagesContract.ts b/src/Contracts/FabricMessagesContract.ts index dcf5a5a50..2cc99c578 100644 --- a/src/Contracts/FabricMessagesContract.ts +++ b/src/Contracts/FabricMessagesContract.ts @@ -1,47 +1,9 @@ -import { AuthorizationToken } from "Contracts/FabricMessageTypes"; +import { AuthorizationToken } from "./FabricMessageTypes"; // This is the version of these messages -export const FABRIC_RPC_VERSION = "2"; +export const FABRIC_RPC_VERSION = "FabricMessageV3"; // Fabric to Data Explorer - -// TODO Deprecated. Remove this section once DE is updated -export type FabricMessageV1 = - | { - type: "newContainer"; - databaseName: string; - } - | { - type: "initialize"; - message: { - endpoint: string | undefined; - databaseId: string | undefined; - resourceTokens: unknown | undefined; - resourceTokensTimestamp: number | undefined; - error: string | undefined; - }; - } - | { - type: "authorizationToken"; - message: { - id: string; - error: string | undefined; - data: AuthorizationToken | undefined; - }; - } - | { - type: "allResourceTokens"; - message: { - id: string; - error: string | undefined; - endpoint: string | undefined; - databaseId: string | undefined; - resourceTokens: unknown | undefined; - resourceTokensTimestamp: number | undefined; - }; - }; -// ----------------------------- - export type FabricMessageV2 = | { type: "newContainer"; @@ -69,7 +31,7 @@ export type FabricMessageV2 = message: { id: string; error: string | undefined; - data: FabricDatabaseConnectionInfo | undefined; + data: ResourceTokenInfo | undefined; }; } | { @@ -79,17 +41,81 @@ export type FabricMessageV2 = }; }; -export type CosmosDBTokenResponse = { - token: string; - date: string; -}; +export type FabricMessageV3 = + | { + type: "newContainer"; + databaseName: string; + } + | { + type: "initialize"; + version: string; + id: string; + message: InitializeMessageV3; + } + | { + type: "authorizationToken"; + message: { + id: string; + error: string | undefined; + data: AuthorizationToken | undefined; + }; + } + | { + type: "allResourceTokens_v2"; + message: { + id: string; + error: string | undefined; + data: ResourceTokenInfo | undefined; + }; + } + | { + type: "explorerVisible"; + message: { + visible: boolean; + }; + } + | { + type: "accessToken"; + message: { + id: string; + error: string | undefined; + data: { accessToken: string }; + }; + }; -export type CosmosDBConnectionInfoResponse = { +export enum CosmosDbArtifactType { + MIRRORED_KEY = "MIRRORED_KEY", + MIRRORED_AAD = "MIRRORED_AAD", + NATIVE = "NATIVE", +} +export interface ArtifactConnectionInfo { + [CosmosDbArtifactType.MIRRORED_KEY]: { connectionId: string }; + [CosmosDbArtifactType.MIRRORED_AAD]: AccessTokenConnectionInfo; + [CosmosDbArtifactType.NATIVE]: AccessTokenConnectionInfo; +} + +export interface AccessTokenConnectionInfo { + accessToken: string; + databaseName: string; + accountEndpoint: string; +} + +export interface InitializeMessageV3 { + connectionId: string; + isVisible: boolean; + isReadOnly: boolean; + artifactType: T; + artifactConnectionInfo: ArtifactConnectionInfo[T]; +} +export interface CosmosDBConnectionInfoResponse { endpoint: string; databaseId: string; - resourceTokens: { [resourceId: string]: string }; -}; + resourceTokens: Record | undefined; + accessToken: string | undefined; + isReadOnly: boolean; + credentialType: "Key" | "OAuth2" | undefined; +} -export interface FabricDatabaseConnectionInfo extends CosmosDBConnectionInfoResponse { +export interface ResourceTokenInfo extends CosmosDBConnectionInfoResponse { resourceTokensTimestamp: number; } diff --git a/src/Explorer/ContextMenuButtonFactory.tsx b/src/Explorer/ContextMenuButtonFactory.tsx index 8946c1e18..890e2f86f 100644 --- a/src/Explorer/ContextMenuButtonFactory.tsx +++ b/src/Explorer/ContextMenuButtonFactory.tsx @@ -1,5 +1,7 @@ +import { configContext, Platform } from "ConfigContext"; import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { useDatabases } from "Explorer/useDatabases"; +import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil"; import { Action } from "Shared/Telemetry/TelemetryConstants"; import { traceOpen } from "Shared/Telemetry/TelemetryProcessor"; import { ReactTabKind, useTabs } from "hooks/useTabs"; @@ -19,7 +21,6 @@ import * as ViewModels from "../Contracts/ViewModels"; import { userContext } from "../UserContext"; import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils"; import { useSidePanel } from "../hooks/useSidePanel"; -import { Platform, configContext } from "./../ConfigContext"; import Explorer from "./Explorer"; import { useNotebook } from "./Notebook/useNotebook"; import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane"; @@ -41,7 +42,7 @@ export interface DatabaseContextMenuButtonParams { * New resource tree (in ReactJS) */ export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => { - if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) { + if (isFabric() && userContext.fabricContext?.isReadOnly) { return undefined; } @@ -53,7 +54,7 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin }, ]; - if (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations) { + if (!isFabricNative() && (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations)) { items.push({ iconSrc: DeleteDatabaseIcon, onClick: (lastFocusedElement?: React.RefObject) => { diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index fdef1076e..ace0aaffe 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -8,7 +8,7 @@ import { MessageTypes } from "Contracts/ExplorerContracts"; import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane"; import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { IGalleryItem } from "Juno/JunoClient"; -import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil"; +import { isFabricMirrored, isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils"; import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils"; @@ -43,7 +43,7 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import { useSidePanel } from "../hooks/useSidePanel"; -import { useTabs } from "../hooks/useTabs"; +import { ReactTabKind, useTabs } from "../hooks/useTabs"; import "./ComponentRegisterer"; import { DialogProps, useDialog } from "./Controls/Dialog"; import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent"; @@ -187,6 +187,10 @@ export default class Explorer { useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath); } + if (isFabricMirrored()) { + useTabs.getState().closeReactTab(ReactTabKind.Home); + } + this.refreshExplorer(); } @@ -347,8 +351,8 @@ export default class Explorer { }; public onRefreshResourcesClick = async (): Promise => { - if (configContext.platform === Platform.Fabric) { - scheduleRefreshDatabaseResourceToken(true).then(() => this.refreshAllDatabases()); + if (isFabricMirroredKey()) { + scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases()); return; } diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index 9a5f222a3..eb596c0f7 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -6,12 +6,12 @@ import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react"; import { useNotebook } from "Explorer/Notebook/useNotebook"; import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; +import { isFabric } from "Platform/Fabric/FabricUtil"; import { userContext } from "UserContext"; import * as React from "react"; import create, { UseStore } from "zustand"; import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants"; import { StyleConstants } from "../../../Common/StyleConstants"; -import { Platform, configContext } from "../../../ConfigContext"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import Explorer from "../../Explorer"; import { useSelectedNode } from "../../useSelectedNode"; @@ -93,19 +93,18 @@ export const CommandBar: React.FC = ({ container }: Props) => { ); } - const rootStyle = - configContext.platform === Platform.Fabric - ? { - root: { - backgroundColor: "transparent", - padding: "2px 8px 0px 8px", - }, - } - : { - root: { - backgroundColor: backgroundColor, - }, - }; + const rootStyle = isFabric() + ? { + root: { + backgroundColor: "transparent", + padding: "2px 8px 0px 8px", + }, + } + : { + root: { + backgroundColor: backgroundColor, + }, + }; const allButtons = staticButtons.concat(contextButtons).concat(controlButtons); const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons); diff --git a/src/Explorer/OpenActions/OpenActions.tsx b/src/Explorer/OpenActions/OpenActions.tsx index 53fb896b5..382f05dc2 100644 --- a/src/Explorer/OpenActions/OpenActions.tsx +++ b/src/Explorer/OpenActions/OpenActions.tsx @@ -1,6 +1,6 @@ // TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled. -import { configContext, Platform } from "ConfigContext"; import { useDatabases } from "Explorer/useDatabases"; +import { isFabricMirrored } from "Platform/Fabric/FabricUtil"; import React from "react"; import { ActionContracts } from "../../Contracts/ExplorerContracts"; import * as ViewModels from "../../Contracts/ViewModels"; @@ -58,9 +58,9 @@ function openCollectionTab( } if ( - configContext.platform === Platform.Fabric && + isFabricMirrored() && !( - // whitelist the tab kinds that are allowed to be opened in Fabric + // whitelist the tab kinds that are allowed to be opened in Fabric mirrored ( action.tabKind === ActionContracts.TabKind.SQLDocuments || action.tabKind === ActionContracts.TabKind.SQLQuery diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index 61cca9f74..d4e39a441 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -28,6 +28,7 @@ import { import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent"; import { useSidePanel } from "hooks/useSidePanel"; import { useTeachingBubble } from "hooks/useTeachingBubble"; +import { isFabricNative } from "Platform/Fabric/FabricUtil"; import React from "react"; import { CollectionCreation } from "Shared/Constants"; import { Action } from "Shared/Telemetry/TelemetryConstants"; @@ -284,150 +285,152 @@ export class AddCollectionPanel extends React.Component - + )} + {!this.state.createNewDatabase && ( + , database: IDropdownOption) => + this.setState({ selectedDatabaseId: database.key as string }) + } + defaultSelectedKey={this.props.databaseId} + responsiveMode={999} + /> + )} + + + )} @@ -666,7 +669,7 @@ export class AddCollectionPanel extends React.Component ); })} - {userContext.apiType === "SQL" && ( + {!isFabricNative() && userContext.apiType === "SQL" && ( )} - {userContext.apiType === "SQL" && ( + {!isFabricNative() && userContext.apiType === "SQL" && ( @@ -937,7 +940,7 @@ export class AddCollectionPanel extends React.Component )} - {userContext.apiType !== "Tables" && ( + {!isFabricNative() && userContext.apiType !== "Tables" && ( = ({ explorer }) => { const actions = useMemo(() => { if ( - configContext.platform === Platform.Fabric || + (isFabric() && userContext.fabricContext?.isReadOnly) || userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo" ) { @@ -137,12 +138,15 @@ const GlobalCommands: React.FC = ({ explorer }) => { id: "new_collection", label: `New ${getCollectionName()}`, icon: , - onClick: () => explorer.onNewCollectionClicked(), + onClick: () => { + const databaseId = isFabricNative() ? userContext.fabricContext?.databaseName : undefined; + explorer.onNewCollectionClicked({ databaseId }); + }, keyboardAction: KeyboardAction.NEW_COLLECTION, }, ]; - if (userContext.apiType !== "Tables") { + if (configContext.platform !== Platform.Fabric && userContext.apiType !== "Tables") { actions.push({ id: "new_database", label: `New ${getDatabaseName()}`, @@ -288,7 +292,7 @@ export const SidebarContainer: React.FC = ({ explorer }) => { }, [setLoading]); const hasGlobalCommands = !( - configContext.platform === Platform.Fabric || + isFabricMirrored() || userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo" ); diff --git a/src/Explorer/SplashScreen/FabricHome.tsx b/src/Explorer/SplashScreen/FabricHome.tsx new file mode 100644 index 000000000..a923dd928 --- /dev/null +++ b/src/Explorer/SplashScreen/FabricHome.tsx @@ -0,0 +1,173 @@ +/** + * Accordion top class + */ +import { Link, makeStyles, tokens } from "@fluentui/react-components"; +import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons"; +import { isFabricNative } from "Platform/Fabric/FabricUtil"; +import * as React from "react"; +import { userContext } from "UserContext"; +import CosmosDbBlackIcon from "../../../images/CosmosDB_black.svg"; +import LinkIcon from "../../../images/Link_blue.svg"; +import Explorer from "../Explorer"; + +export interface SplashScreenProps { + explorer: Explorer; +} + +const useStyles = makeStyles({ + homeContainer: { + width: "100%", + alignContent: "center", + }, + title: { + textAlign: "center", + fontSize: "20px", + fontWeight: "bold", + }, + buttonsContainer: { + width: "584px", + margin: "auto", + display: "grid", + padding: "16px", + gridTemplateColumns: "repeat(3, 1fr)", + gap: "10px", + gridAutoRows: "minmax(184px, auto)", + }, + one: { + gridColumn: "1 / 3", + gridRow: "1 / 3", + "& svg": { + width: "48px", + height: "48px", + margin: "auto", + }, + }, + two: { + gridColumn: "3", + gridRow: "1", + "& img": { + width: "32px", + height: "32px", + margin: "auto", + }, + }, + three: { + gridColumn: "3", + gridRow: "2", + "& svg": { + width: "32px", + height: "32px", + margin: "auto", + }, + }, + buttonContainer: { + height: "100%", + display: "flex", + flexDirection: "column", + border: "1px solid #e0e0e0", + cursor: "pointer", + "&:hover": { + backgroundColor: tokens.colorNeutralBackground1Hover, + "border-color": tokens.colorNeutralStroke1Hover, + }, + }, + buttonUpperPart: { + textAlign: "center", + flexGrow: 1, + display: "flex", + backgroundColor: "#e3f7ef", + }, + buttonLowerPart: { + borderTop: "1px solid #e0e0e0", + height: "76px", + padding: "8px", + "> div:nth-child(1)": { + fontWeight: "bold", + }, + display: "flex", + flexDirection: "column", + justifyContent: "center", + }, + footer: { + textAlign: "center", + }, +}); + +interface FabricHomeScreenButtonProps { + title: string; + description: string; + icon: JSX.Element; + onClick?: () => void; +} + +const FabricHomeScreenButton: React.FC = ({ + title, + description, + icon, + className, + onClick, +}) => { + const styles = useStyles(); + + // TODO Make this a11y copmliant: aria-label for icon + return ( +
+
{icon}
+
+
{title}
+
{description}
+
+
+ ); +}; + +export const FabricHomeScreen: React.FC = (props: SplashScreenProps) => { + const styles = useStyles(); + const getSplashScreenButtons = (): JSX.Element => { + const buttons: FabricHomeScreenButtonProps[] = [ + { + title: "New container", + description: "Create a destination container to store your data", + icon: , + onClick: () => { + const databaseId = isFabricNative() ? userContext.fabricContext?.databaseName : undefined; + props.explorer.onNewCollectionClicked({ databaseId }); + }, + }, + { + title: "Sample data", + description: "Automatically load sample data in your database", + icon: , + }, + { + title: "App development", + description: "Start here to use an SDK to build your apps", + icon: , + }, + ]; + + return ( +
+ + + +
+ ); + }; + + const title = "Build your database"; + return ( +
+
+ {title} +
+ {getSplashScreenButtons()} +
+ Need help?{" "} + + Learn more Learn more + +
+
+ ); +}; diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx index b0c6514ef..6bb014011 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx @@ -2,6 +2,7 @@ import { FeedResponse, ItemDefinition, Resource } from "@azure/cosmos"; import { waitFor } from "@testing-library/react"; import { deleteDocuments } from "Common/dataAccess/deleteDocument"; import { Platform, updateConfigContext } from "ConfigContext"; +import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract"; import { useDialog } from "Explorer/Controls/Dialog"; import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact"; import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog"; @@ -341,10 +342,15 @@ describe("Documents tab (noSql API)", () => { updateConfigContext({ platform: Platform.Fabric }); updateUserContext({ fabricContext: { - connectionId: "test", - databaseConnectionInfo: undefined, + databaseName: "database", + artifactInfo: { + connectionId: "test", + resourceTokenInfo: undefined, + }, + artifactType: CosmosDbArtifactType.MIRRORED_KEY, isReadOnly: true, isVisible: true, + fabricClientRpcVersion: "rpcVersion", }, }); diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx index 2cef0f663..4e7a79909 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -20,7 +20,6 @@ import { import { queryDocuments } from "Common/dataAccess/queryDocuments"; import { readDocument } from "Common/dataAccess/readDocument"; import { updateDocument } from "Common/dataAccess/updateDocument"; -import { Platform, configContext } from "ConfigContext"; import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts"; import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent"; import { useDialog } from "Explorer/Controls/Dialog"; @@ -43,6 +42,7 @@ import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper"; import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil"; import { useSelectedNode } from "Explorer/useSelectedNode"; import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; +import { isFabric } from "Platform/Fabric/FabricUtil"; import { QueryConstants } from "Shared/Constants"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { Action } from "Shared/Telemetry/TelemetryConstants"; @@ -344,7 +344,7 @@ export const getTabsButtons = ({ onRevertExistingDocumentClick, onDeleteExistingDocumentsClick, }: ButtonsDependencies): CommandButtonComponentProps[] => { - if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) { + if (isFabric() && userContext.fabricContext?.isReadOnly) { // All the following buttons require write access return []; } @@ -2136,8 +2136,7 @@ export const DocumentsTabComponent: React.FunctionComponent ); case ReactTabKind.Home: - return ; + if (isFabricNative()) { + return ; + } else { + return ; + } case ReactTabKind.Quickstart: return userContext.apiType === "VCoreMongo" ? ( diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 2e08562e4..c692d0747 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -1,6 +1,7 @@ import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos"; import { useNotebook } from "Explorer/Notebook/useNotebook"; import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; +import { isFabricMirrored } from "Platform/Fabric/FabricUtil"; import * as ko from "knockout"; import * as _ from "underscore"; import * as Constants from "../../Common/Constants"; @@ -34,7 +35,6 @@ import QueryTablesTab from "../Tabs/QueryTablesTab"; import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; -import { Platform, configContext } from "./../../ConfigContext"; import ConflictId from "./ConflictId"; import DocumentId from "./DocumentId"; import StoredProcedure from "./StoredProcedure"; @@ -210,7 +210,7 @@ export default class Collection implements ViewModels.Collection { }); const showScriptsMenus: boolean = - configContext.platform != Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin"); + !isFabricMirrored() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin"); this.showStoredProcedures = ko.observable(showScriptsMenus); this.showTriggers = ko.observable(showScriptsMenus); this.showUserDefinedFunctions = ko.observable(showScriptsMenus); diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index 71ccd82bb..10b8316c0 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -1,7 +1,6 @@ import { Tree, TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from "@fluentui/react-components"; import { Home16Regular } from "@fluentui/react-icons"; import { AuthType } from "AuthType"; -import { Platform, configContext } from "ConfigContext"; import { useTreeStyles } from "Explorer/Controls/TreeComponent/Styles"; import { TreeNode, TreeNodeComponent } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { @@ -11,6 +10,7 @@ import { } from "Explorer/Tree/treeNodeUtil"; import { useDatabases } from "Explorer/useDatabases"; import { useSelectedNode } from "Explorer/useSelectedNode"; +import { isFabricMirrored } from "Platform/Fabric/FabricUtil"; import { userContext } from "UserContext"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import { ReactTabKind, useTabs } from "hooks/useTabs"; @@ -76,23 +76,22 @@ export const ResourceTree: React.FC = ({ explorer }: Resource : []; }, [isSampleDataEnabled, sampleDataResourceTokenCollection]); - const headerNodes: TreeNode[] = - configContext.platform === Platform.Fabric - ? [] - : [ - { - id: "home", - iconSrc: , - label: "Home", - isSelected: () => - useSelectedNode.getState().selectedNode === undefined && - useTabs.getState().activeReactTab === ReactTabKind.Home, - onClick: () => { - useSelectedNode.getState().setSelectedNode(undefined); - useTabs.getState().openAndActivateReactTab(ReactTabKind.Home); - }, + const headerNodes: TreeNode[] = isFabricMirrored() + ? [] + : [ + { + id: "home", + iconSrc: , + label: "Home", + isSelected: () => + useSelectedNode.getState().selectedNode === undefined && + useTabs.getState().activeReactTab === ReactTabKind.Home, + onClick: () => { + useSelectedNode.getState().setSelectedNode(undefined); + useTabs.getState().openAndActivateReactTab(ReactTabKind.Home); }, - ]; + }, + ]; const rootNodes: TreeNode[] = useMemo(() => { if (sampleDataNodes.length > 0) { diff --git a/src/Explorer/Tree/treeNodeUtil.test.ts b/src/Explorer/Tree/treeNodeUtil.test.ts index 239b6144d..2c7af8021 100644 --- a/src/Explorer/Tree/treeNodeUtil.test.ts +++ b/src/Explorer/Tree/treeNodeUtil.test.ts @@ -1,5 +1,6 @@ import { CapabilityNames } from "Common/Constants"; import { Platform, updateConfigContext } from "ConfigContext"; +import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract"; import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import Explorer from "Explorer/Explorer"; import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; @@ -16,7 +17,7 @@ import { } from "Explorer/Tree/treeNodeUtil"; import { useDatabases } from "Explorer/useDatabases"; import { useSelectedNode } from "Explorer/useSelectedNode"; -import { updateUserContext } from "UserContext"; +import { FabricContext, updateUserContext } from "UserContext"; import PromiseSource from "Utils/PromiseSource"; import { useSidePanel } from "hooks/useSidePanel"; import { useTabs } from "hooks/useTabs"; @@ -551,7 +552,17 @@ describe("createDatabaseTreeNodes", () => { }); it.each([ - ["in Fabric", () => updateConfigContext({ platform: Platform.Fabric })], + [ + "in Fabric", + () => { + updateConfigContext({ platform: Platform.Fabric }); + updateUserContext({ + fabricContext: { + artifactType: CosmosDbArtifactType.MIRRORED_KEY, + } as FabricContext, + }); + }, + ], [ "for Cassandra API", () => diff --git a/src/Explorer/Tree/treeNodeUtil.tsx b/src/Explorer/Tree/treeNodeUtil.tsx index 8e6c94559..60fe9079b 100644 --- a/src/Explorer/Tree/treeNodeUtil.tsx +++ b/src/Explorer/Tree/treeNodeUtil.tsx @@ -6,6 +6,7 @@ import StoredProcedure from "Explorer/Tree/StoredProcedure"; import Trigger from "Explorer/Tree/Trigger"; import UserDefinedFunction from "Explorer/Tree/UserDefinedFunction"; import { useDatabases } from "Explorer/useDatabases"; +import { isFabricMirrored } from "Platform/Fabric/FabricUtil"; import { getItemName } from "Utils/APITypeUtils"; import { isServerlessAccount } from "Utils/CapabilityUtils"; import { useTabs } from "hooks/useTabs"; @@ -22,9 +23,7 @@ import { useNotebook } from "../Notebook/useNotebook"; import { useSelectedNode } from "../useSelectedNode"; export const shouldShowScriptNodes = (): boolean => { - return ( - configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") - ); + return !isFabricMirrored() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin"); }; const TreeDatabaseIcon = ; diff --git a/src/Platform/Fabric/FabricUtil.ts b/src/Platform/Fabric/FabricUtil.ts index 26ba859ff..a9a653dd7 100644 --- a/src/Platform/Fabric/FabricUtil.ts +++ b/src/Platform/Fabric/FabricUtil.ts @@ -1,56 +1,112 @@ import { sendCachedDataMessage } from "Common/MessageHandler"; +import { configContext, Platform } from "ConfigContext"; import { FabricMessageTypes } from "Contracts/FabricMessageTypes"; -import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract"; -import { updateUserContext, userContext } from "UserContext"; +import { CosmosDbArtifactType, ResourceTokenInfo } from "Contracts/FabricMessagesContract"; +import { FabricArtifactInfo, updateUserContext, userContext } from "UserContext"; import { logConsoleError } from "Utils/NotificationConsoleUtils"; const TOKEN_VALIDITY_MS = (3600 - 600) * 1000; // 1 hour minus 10 minutes to be safe const DEBOUNCE_DELAY_MS = 1000 * 20; // 20 second -let timeoutId: NodeJS.Timeout; +let timeoutId: NodeJS.Timeout | undefined; // Prevents multiple parallel requests during DEBOUNCE_DELAY_MS -let lastRequestTimestamp: number = undefined; +let lastRequestTimestamp: number | undefined = undefined; -const requestDatabaseResourceTokens = async (): Promise => { +/** + * Request fabric token: + * - Mirrored key and AAD: Database Resource Tokens + * - Native: AAD token + * @returns + */ +const requestFabricToken = async (): Promise => { if (lastRequestTimestamp !== undefined && lastRequestTimestamp + DEBOUNCE_DELAY_MS > Date.now()) { return; } lastRequestTimestamp = Date.now(); try { - const fabricDatabaseConnectionInfo = await sendCachedDataMessage( - FabricMessageTypes.GetAllResourceTokens, - [], - userContext.fabricContext.connectionId, - ); - - if (!userContext.databaseAccount.properties.documentEndpoint) { - userContext.databaseAccount.properties.documentEndpoint = fabricDatabaseConnectionInfo.endpoint; + if (isFabricMirrored()) { + await requestAndStoreDatabaseResourceTokens(); + } else if (isFabricNative()) { + await requestAndStoreAccessToken(); } - updateUserContext({ - fabricContext: { - ...userContext.fabricContext, - databaseConnectionInfo: fabricDatabaseConnectionInfo, - isReadOnly: true, - }, - databaseAccount: { ...userContext.databaseAccount }, - }); - scheduleRefreshDatabaseResourceToken(); + scheduleRefreshFabricToken(); } catch (error) { - logConsoleError(error); + logConsoleError(error as string); throw error; } finally { lastRequestTimestamp = undefined; } }; +const requestAndStoreDatabaseResourceTokens = async (): Promise => { + if (!userContext.fabricContext || !userContext.databaseAccount) { + // This should not happen + logConsoleError("Fabric context or database account is missing: cannot request tokens"); + return; + } + + const resourceTokenInfo = await sendCachedDataMessage( + FabricMessageTypes.GetAllResourceTokens, + [], + userContext.fabricContext.artifactInfo?.connectionId, + ); + + if (!userContext.databaseAccount.properties.documentEndpoint) { + userContext.databaseAccount.properties.documentEndpoint = resourceTokenInfo.endpoint; + } + + if (resourceTokenInfo.credentialType === "OAuth2") { + // Mirrored AAD + updateUserContext({ + fabricContext: { + ...userContext.fabricContext, + databaseName: resourceTokenInfo.databaseId, + artifactInfo: undefined, + isReadOnly: resourceTokenInfo.isReadOnly ?? userContext.fabricContext.isReadOnly, + }, + databaseAccount: { ...userContext.databaseAccount }, + aadToken: resourceTokenInfo.accessToken, + }); + } else { + // TODO: In Fabric contract V2, credentialType is undefined. For V3, it is "Key". Check for "Key" when V3 is supported for Fabric Mirroring Key + // Mirrored key + updateUserContext({ + fabricContext: { + ...userContext.fabricContext, + databaseName: resourceTokenInfo.databaseId, + artifactInfo: { + ...(userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]), + resourceTokenInfo, + }, + isReadOnly: resourceTokenInfo.isReadOnly ?? userContext.fabricContext.isReadOnly, + }, + databaseAccount: { ...userContext.databaseAccount }, + }); + } +}; + +const requestAndStoreAccessToken = async (): Promise => { + if (!userContext.fabricContext || !userContext.databaseAccount) { + // This should not happen + logConsoleError("Fabric context or database account is missing: cannot request tokens"); + return; + } + + const accessTokenInfo = await sendCachedDataMessage<{ accessToken: string }>(FabricMessageTypes.GetAccessToken, []); + + updateUserContext({ + aadToken: accessTokenInfo.accessToken, + }); +}; + /** * Check token validity and schedule a refresh if necessary * @param tokenTimestamp * @returns */ -export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Promise => { +export const scheduleRefreshFabricToken = (refreshNow?: boolean): Promise => { return new Promise((resolve) => { if (timeoutId !== undefined) { clearTimeout(timeoutId); @@ -59,7 +115,7 @@ export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Prom timeoutId = setTimeout( () => { - requestDatabaseResourceTokens().then(resolve); + requestFabricToken().then(resolve); }, refreshNow ? 0 : TOKEN_VALIDITY_MS, ); @@ -68,6 +124,15 @@ export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Prom export const checkDatabaseResourceTokensValidity = (tokenTimestamp: number): void => { if (tokenTimestamp + TOKEN_VALIDITY_MS < Date.now()) { - scheduleRefreshDatabaseResourceToken(true); + scheduleRefreshFabricToken(true); } }; + +export const isFabric = (): boolean => configContext.platform === Platform.Fabric; +export const isFabricMirroredKey = (): boolean => + isFabric() && userContext.fabricContext?.artifactType === CosmosDbArtifactType.MIRRORED_KEY; +export const isFabricMirroredAAD = (): boolean => + isFabric() && userContext.fabricContext?.artifactType === CosmosDbArtifactType.MIRRORED_AAD; +export const isFabricMirrored = (): boolean => isFabricMirroredKey() || isFabricMirroredAAD(); +export const isFabricNative = (): boolean => + isFabric() && userContext.fabricContext?.artifactType === CosmosDbArtifactType.NATIVE; diff --git a/src/UserContext.ts b/src/UserContext.ts index d30951780..6569d5e18 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -1,4 +1,4 @@ -import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract"; +import { CosmosDbArtifactType, ResourceTokenInfo } from "Contracts/FabricMessagesContract"; import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils"; import { Action } from "Shared/Telemetry/TelemetryConstants"; import { traceOpen } from "Shared/Telemetry/TelemetryProcessor"; @@ -47,11 +47,21 @@ export interface VCoreMongoConnectionParams { connectionString: string; } -interface FabricContext { - connectionId: string; - databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined; +export interface FabricArtifactInfo { + [CosmosDbArtifactType.MIRRORED_KEY]: { + connectionId: string; + resourceTokenInfo: ResourceTokenInfo | undefined; + }; + [CosmosDbArtifactType.MIRRORED_AAD]: undefined; + [CosmosDbArtifactType.NATIVE]: undefined; +} +export interface FabricContext { + fabricClientRpcVersion: string; isReadOnly: boolean; isVisible: boolean; + databaseName: string; + artifactType: CosmosDbArtifactType; + artifactInfo: FabricArtifactInfo[T]; } export type AdminFeedbackControlPolicy = @@ -70,7 +80,7 @@ export type AdminFeedbackPolicySettings = { }; export interface UserContext { - readonly fabricContext?: FabricContext; + readonly fabricContext?: FabricContext; readonly authType?: AuthType; readonly masterKey?: string; readonly subscriptionId?: string; diff --git a/src/Utils/WindowUtils.ts b/src/Utils/WindowUtils.ts index 8776796f7..b437d17fa 100644 --- a/src/Utils/WindowUtils.ts +++ b/src/Utils/WindowUtils.ts @@ -1,3 +1,4 @@ +import { isFabric } from "Platform/Fabric/FabricUtil"; import { Platform, configContext } from "./../ConfigContext"; export const getDataExplorerWindow = (currentWindow: Window): Window | undefined => { @@ -7,7 +8,7 @@ export const getDataExplorerWindow = (currentWindow: Window): Window | undefined if (currentWindow.parent === currentWindow) { return undefined; } - if (configContext.platform === Platform.Fabric && currentWindow.parent.parent === currentWindow.top) { + if (isFabric() && currentWindow.parent.parent === currentWindow.top) { // in Fabric data explorer is inside an extension iframe, so we have two parent iframes return currentWindow; } diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 3c59baadf..2e29d1363 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -2,11 +2,19 @@ import * as Constants from "Common/Constants"; import { createUri } from "Common/UrlUtility"; import { DATA_EXPLORER_RPC_VERSION } from "Contracts/DataExplorerMessagesContract"; import { FabricMessageTypes } from "Contracts/FabricMessageTypes"; -import { FABRIC_RPC_VERSION, FabricMessageV2 } from "Contracts/FabricMessagesContract"; +import { + ArtifactConnectionInfo, + CosmosDbArtifactType, + FABRIC_RPC_VERSION, + FabricMessageV2, + FabricMessageV3, + InitializeMessageV3, +} from "Contracts/FabricMessagesContract"; +import { useDialog } from "Explorer/Controls/Dialog"; import Explorer from "Explorer/Explorer"; import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane"; import { useSelectedNode } from "Explorer/useSelectedNode"; -import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil"; +import { isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil"; import { AppStateComponentNames, OPEN_TABS_SUBCOMPONENT_NAME, @@ -23,7 +31,7 @@ import { AccountKind, Flights } from "../Common/Constants"; import { normalizeArmEndpoint } from "../Common/EnvironmentUtility"; import * as Logger from "../Common/Logger"; import { handleCachedDataMessage, sendMessage, sendReadyMessage } from "../Common/MessageHandler"; -import { Platform, configContext, updateConfigContext } from "../ConfigContext"; +import { configContext, Platform, updateConfigContext } from "../ConfigContext"; import { ActionType, DataExplorerAction, TabKind } from "../Contracts/ActionContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts"; import { DataExplorerInputsFrame } from "../Contracts/ViewModels"; @@ -44,7 +52,7 @@ import { } from "../Platform/Hosted/HostedUtils"; import { extractFeatures } from "../Platform/Hosted/extractFeatures"; import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; -import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext"; +import { FabricArtifactInfo, Node, PortalEnv, updateUserContext, userContext } from "../UserContext"; import { acquireMsalTokenForAccount, acquireTokenWithMsal, @@ -104,7 +112,7 @@ export function useKnockoutExplorer(platform: Platform): Explorer { async function configureFabric(): Promise { // These are the versions of Fabric that Data Explorer supports. - const SUPPORTED_FABRIC_VERSIONS = [FABRIC_RPC_VERSION]; + const SUPPORTED_FABRIC_VERSIONS = ["2", FABRIC_RPC_VERSION]; let firstContainerOpened = false; let explorer: Explorer; @@ -120,7 +128,7 @@ async function configureFabric(): Promise { return; } - const data: FabricMessageV2 = event.data?.data; + const data: FabricMessageV2 | FabricMessageV3 = event.data?.data; if (!data) { return; } @@ -129,38 +137,77 @@ async function configureFabric(): Promise { case "initialize": { const fabricVersion = data.version; if (!SUPPORTED_FABRIC_VERSIONS.includes(fabricVersion)) { - // TODO Surface error to user + // TODO Surface error to user and log to telemetry + useDialog + .getState() + .showOkModalDialog("Unsupported Fabric version", `Unsupported Fabric version: ${fabricVersion}`); + Logger.logError(`Unsupported Fabric version: ${fabricVersion}`, "Explorer/configureFabric"); console.error(`Unsupported Fabric version: ${fabricVersion}`); return; } - explorer = createExplorerFabric(data.message); - await scheduleRefreshDatabaseResourceToken(true); - resolve(explorer); - await explorer.refreshAllDatabases(); - if (userContext.fabricContext.isVisible) { - firstContainerOpened = true; - openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId); + if (fabricVersion === "2") { + // ----------------- TODO: Remove this when FabricMessageV2 is deprecated ----------------- + const initializationMessage = data.message as { + connectionId: string; + isVisible: boolean; + }; + + explorer = createExplorerFabricLegacy(initializationMessage, data.version); + await scheduleRefreshFabricToken(true); + resolve(explorer); + await explorer.refreshAllDatabases(); + if (userContext.fabricContext.isVisible) { + firstContainerOpened = true; + openFirstContainer(explorer, userContext.fabricContext.databaseName); + } + // ----------------------------------------------------------------------------------------- + } else if (fabricVersion === FABRIC_RPC_VERSION) { + const initializationMessage = data.message as InitializeMessageV3; + explorer = createExplorerFabric(initializationMessage, data.version); + + if (initializationMessage.artifactType === CosmosDbArtifactType.MIRRORED_KEY) { + // Do not show Home tab for Mirrored + useTabs.getState().closeReactTab(ReactTabKind.Home); + } + + // All tokens used in fabric expire, so schedule a refresh + // For Mirrored key, we need the token right away to get the database and containers list. + if (isFabricMirroredKey()) { + await scheduleRefreshFabricToken(true); + } else { + scheduleRefreshFabricToken(false); + } + + resolve(explorer); + await explorer.refreshAllDatabases(); + + const { databaseName } = userContext.fabricContext; + if (userContext.fabricContext.isVisible && databaseName) { + firstContainerOpened = true; + openFirstContainer(explorer, databaseName); + } } + break; } case "newContainer": explorer.onNewCollectionClicked(); break; case "authorizationToken": - case "allResourceTokens_v2": { + case "allResourceTokens_v2": + case "accessToken": { handleCachedDataMessage(data); break; } case "explorerVisible": { userContext.fabricContext.isVisible = data.message.visible; - if ( - userContext.fabricContext.isVisible && - !firstContainerOpened && - userContext?.fabricContext?.databaseConnectionInfo?.databaseId !== undefined - ) { - firstContainerOpened = true; - openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId); + if (userContext.fabricContext.isVisible && !firstContainerOpened) { + const { databaseName } = userContext.fabricContext; + if (databaseName !== undefined) { + firstContainerOpened = true; + openFirstContainer(explorer, databaseName); + } } break; } @@ -420,13 +467,29 @@ function configureHostedWithResourceToken(config: ResourceToken): Explorer { return explorer; } -function createExplorerFabric(params: { connectionId: string; isVisible: boolean }): Explorer { +/** + * Initialization for FabricMessageV2 + * TODO: delete when FabricMessageV2 is deprecated + * @param params + * @returns + */ +function createExplorerFabricLegacy( + params: { connectionId: string; isVisible: boolean }, + fabricClientRpcVersion: string, +): Explorer { + const artifactInfo: FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY] = { + connectionId: params.connectionId, + resourceTokenInfo: undefined, + }; + updateUserContext({ fabricContext: { - connectionId: params.connectionId, - databaseConnectionInfo: undefined, + fabricClientRpcVersion, isReadOnly: true, isVisible: params.isVisible ?? true, + databaseName: undefined, + artifactType: CosmosDbArtifactType.MIRRORED_KEY, + artifactInfo, }, authType: AuthType.ConnectionString, databaseAccount: { @@ -440,11 +503,102 @@ function createExplorerFabric(params: { connectionId: string; isVisible: boolean }, }, }); - useTabs.getState().closeAllTabs(); const explorer = new Explorer(); return explorer; } +/** + * Initialization for FabricMessageV3 and above + * @param params + * @returns + */ +const createExplorerFabric = ( + params: InitializeMessageV3, + fabricClientRpcVersion: string, +): Explorer => { + updateUserContext({ + fabricContext: { + fabricClientRpcVersion, + databaseName: undefined, + isVisible: params.isVisible, + isReadOnly: params.isReadOnly, + artifactType: params.artifactType, + artifactInfo: undefined, + }, + }); + + if (params.artifactType === CosmosDbArtifactType.MIRRORED_KEY) { + updateUserContext({ + authType: AuthType.ConnectionString, // TODO: will need its own type + databaseAccount: { + id: "", + location: "", + type: "", + name: "Mounted", // TODO: not used? + kind: AccountKind.Default, + properties: { + documentEndpoint: undefined, + }, + }, + fabricContext: { + ...userContext.fabricContext, + artifactInfo: { + connectionId: (params.artifactConnectionInfo as ArtifactConnectionInfo[CosmosDbArtifactType.MIRRORED_KEY]) + .connectionId, + resourceTokenInfo: undefined, + }, + }, + }); + } else if (params.artifactType === CosmosDbArtifactType.MIRRORED_AAD) { + updateUserContext({ + databaseAccount: { + id: "", + location: "", + type: "", + name: "Mounted", // TODO: not used? + kind: AccountKind.Default, + properties: { + documentEndpoint: undefined, + }, + }, + authType: AuthType.AAD, + dataPlaneRbacEnabled: true, + aadToken: undefined, + masterKey: undefined, + fabricContext: { + ...userContext.fabricContext, + artifactInfo: undefined, + }, + }); + } else if (params.artifactType === CosmosDbArtifactType.NATIVE) { + const nativeParams = params as InitializeMessageV3; + // Make it behave like Hosted/AAD/RBAC + updateUserContext({ + databaseAccount: { + id: "", + location: "", + type: "", + name: "Native", // TODO: not used? + kind: AccountKind.Default, + properties: { + documentEndpoint: nativeParams.artifactConnectionInfo.accountEndpoint, + }, + }, + authType: AuthType.AAD, + dataPlaneRbacEnabled: true, + aadToken: nativeParams.artifactConnectionInfo.accessToken, + masterKey: undefined, + fabricContext: { + ...userContext.fabricContext, + databaseName: nativeParams.artifactConnectionInfo.databaseName, + }, + }); + } + + const explorer = new Explorer(); + return explorer; +}; + function configureWithEncryptedToken(config: EncryptedToken): Explorer { const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind); updateUserContext({ diff --git a/src/hooks/useTabs.ts b/src/hooks/useTabs.ts index f29f34f72..8b7051a52 100644 --- a/src/hooks/useTabs.ts +++ b/src/hooks/useTabs.ts @@ -1,6 +1,7 @@ import { clamp } from "@fluentui/react"; import { OpenTab } from "Contracts/ActionContracts"; import { useSelectedNode } from "Explorer/useSelectedNode"; +import { isFabricMirrored } from "Platform/Fabric/FabricUtil"; import { AppStateComponentNames, OPEN_TABS_SUBCOMPONENT_NAME, @@ -11,7 +12,6 @@ import * as ViewModels from "../Contracts/ViewModels"; import { CollectionTabKind } from "../Contracts/ViewModels"; import NotebookTabV2 from "../Explorer/Tabs/NotebookV2Tab"; import TabsBase from "../Explorer/Tabs/TabsBase"; -import { Platform, configContext } from "./../ConfigContext"; export interface TabsState { openedTabs: TabsBase[]; @@ -51,22 +51,11 @@ export enum ReactTabKind { QueryCopilot, } -// HACK: using this const when the configuration context is not initialized yet. -// Since Fabric is always setting the url param, use that instead of the regular config. -const isPlatformFabric = (() => { - const params = new URLSearchParams(window.location.search); - if (params.has("platform")) { - const platform = params.get("platform"); - return platform === Platform.Fabric; - } - return false; -})(); - export const useTabs: UseStore = create((set, get) => ({ - openedTabs: [], - openedReactTabs: !isPlatformFabric ? [ReactTabKind.Home] : [], - activeTab: undefined, - activeReactTab: !isPlatformFabric ? ReactTabKind.Home : undefined, + openedTabs: [] as TabsBase[], + openedReactTabs: [ReactTabKind.Home], + activeTab: undefined as TabsBase, + activeReactTab: ReactTabKind.Home, queryCopilotTabInitialInput: "", isTabExecuting: false, isQueryErrorThrown: false, @@ -122,7 +111,7 @@ export const useTabs: UseStore = create((set, get) => ({ } return true; }); - if (updatedTabs.length === 0 && configContext.platform !== Platform.Fabric) { + if (updatedTabs.length === 0 && !isFabricMirrored()) { set({ activeTab: undefined, activeReactTab: undefined }); } @@ -162,7 +151,7 @@ export const useTabs: UseStore = create((set, get) => ({ } }); - if (get().openedTabs.length === 0 && configContext.platform !== Platform.Fabric) { + if (get().openedTabs.length === 0 && !isFabricMirrored()) { set({ activeTab: undefined, activeReactTab: undefined }); } } From 15293031075ec4503525e05424eab5e5ec0192c7 Mon Sep 17 00:00:00 2001 From: Laurent Nguyen Date: Fri, 7 Mar 2025 07:10:45 +0100 Subject: [PATCH 10/11] Fabric native: use SDK not ARM for update offers/collections. Enable Delete Container context menu item in resource tree (#2069) * For all control plane operations, do not use ARM for Fabric. Enable "delete container" for fabric native. * Fix unit test * Fix tre note tests with proper fabric config. Add new fabric non-readonly test. --- src/Common/dataAccess/createCollection.ts | 3 +- src/Common/dataAccess/deleteCollection.ts | 3 +- src/Common/dataAccess/readDatabaseOffer.ts | 7 +- src/Common/dataAccess/updateCollection.ts | 4 +- src/Common/dataAccess/updateOffer.ts | 3 +- src/Explorer/ContextMenuButtonFactory.tsx | 2 +- .../__snapshots__/treeNodeUtil.test.ts.snap | 171 +++++++++++++++++- src/Explorer/Tree/treeNodeUtil.test.ts | 74 +++++--- 8 files changed, 232 insertions(+), 35 deletions(-) diff --git a/src/Common/dataAccess/createCollection.ts b/src/Common/dataAccess/createCollection.ts index b5afd70a3..78ef4f488 100644 --- a/src/Common/dataAccess/createCollection.ts +++ b/src/Common/dataAccess/createCollection.ts @@ -1,4 +1,5 @@ import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos"; +import { isFabricNative } from "Platform/Fabric/FabricUtil"; import { AuthType } from "../../AuthType"; import * as DataModels from "../../Contracts/DataModels"; import { useDatabases } from "../../Explorer/useDatabases"; @@ -24,7 +25,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams ); try { let collection: DataModels.Collection; - if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) { + if (!isFabricNative() && userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) { if (params.createNewDatabase) { const createDatabaseParams: DataModels.CreateDatabaseParams = { autoPilotMaxThroughput: params.autoPilotMaxThroughput, diff --git a/src/Common/dataAccess/deleteCollection.ts b/src/Common/dataAccess/deleteCollection.ts index f83126dd1..e786e5ea7 100644 --- a/src/Common/dataAccess/deleteCollection.ts +++ b/src/Common/dataAccess/deleteCollection.ts @@ -1,3 +1,4 @@ +import { isFabric } from "Platform/Fabric/FabricUtil"; import { AuthType } from "../../AuthType"; import { userContext } from "../../UserContext"; import { deleteCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; @@ -12,7 +13,7 @@ import { handleError } from "../ErrorHandlingUtils"; export async function deleteCollection(databaseId: string, collectionId: string): Promise { const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`); try { - if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) { + if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) { await deleteCollectionWithARM(databaseId, collectionId); } else { await client().database(databaseId).container(collectionId).delete(); diff --git a/src/Common/dataAccess/readDatabaseOffer.ts b/src/Common/dataAccess/readDatabaseOffer.ts index c0c695ad1..ba8019367 100644 --- a/src/Common/dataAccess/readDatabaseOffer.ts +++ b/src/Common/dataAccess/readDatabaseOffer.ts @@ -1,4 +1,4 @@ -import { isFabric, isFabricMirroredKey } from "Platform/Fabric/FabricUtil"; +import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil"; import { AuthType } from "../../AuthType"; import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels"; import { userContext } from "../../UserContext"; @@ -11,8 +11,9 @@ import { handleError } from "../ErrorHandlingUtils"; import { readOfferWithSDK } from "./readOfferWithSDK"; export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise => { - if (isFabricMirroredKey()) { - // TODO This works, but is very slow, because it requests the token, so we skip for now + if (isFabricMirroredKey() || isFabricNative()) { + // For Fabric Mirroring, it is slow, because it requests the token and we don't need it. + // For Fabric Native, it is not supported. console.error("Skiping readDatabaseOffer for Fabric"); return undefined; } diff --git a/src/Common/dataAccess/updateCollection.ts b/src/Common/dataAccess/updateCollection.ts index 15515f5b5..263960663 100644 --- a/src/Common/dataAccess/updateCollection.ts +++ b/src/Common/dataAccess/updateCollection.ts @@ -1,4 +1,5 @@ import { ContainerDefinition, RequestOptions } from "@azure/cosmos"; +import { isFabric } from "Platform/Fabric/FabricUtil"; import { AuthType } from "../../AuthType"; import { Collection } from "../../Contracts/DataModels"; import { userContext } from "../../UserContext"; @@ -36,7 +37,8 @@ export async function updateCollection( if ( userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && - userContext.apiType !== "Tables" + userContext.apiType !== "Tables" && + !isFabric() ) { collection = await updateCollectionWithARM(databaseId, collectionId, newCollection); } else { diff --git a/src/Common/dataAccess/updateOffer.ts b/src/Common/dataAccess/updateOffer.ts index 4d26ca68d..cd20fcd4c 100644 --- a/src/Common/dataAccess/updateOffer.ts +++ b/src/Common/dataAccess/updateOffer.ts @@ -1,4 +1,5 @@ import { OfferDefinition, RequestOptions } from "@azure/cosmos"; +import { isFabric } from "Platform/Fabric/FabricUtil"; import { AuthType } from "../../AuthType"; import { Offer, SDKOfferDefinition, ThroughputBucket, UpdateOfferParams } from "../../Contracts/DataModels"; import { userContext } from "../../UserContext"; @@ -56,7 +57,7 @@ export const updateOffer = async (params: UpdateOfferParams): Promise => const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`); try { - if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) { + if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) { if (params.collectionId) { updatedOffer = await updateCollectionOfferWithARM(params); } else if (userContext.apiType === "Tables") { diff --git a/src/Explorer/ContextMenuButtonFactory.tsx b/src/Explorer/ContextMenuButtonFactory.tsx index 890e2f86f..8108cbbb2 100644 --- a/src/Explorer/ContextMenuButtonFactory.tsx +++ b/src/Explorer/ContextMenuButtonFactory.tsx @@ -146,7 +146,7 @@ export const createCollectionContextMenuButton = ( }); } - if (configContext.platform !== Platform.Fabric) { + if (!isFabric() || (isFabric() && !userContext.fabricContext?.isReadOnly)) { items.push({ iconSrc: DeleteCollectionIcon, onClick: (lastFocusedElement?: React.RefObject) => { diff --git a/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap b/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap index 787443041..9814a72fb 100644 --- a/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap +++ b/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap @@ -740,7 +740,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo ] `; -exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric 1`] = ` +exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric non read-only 1`] = ` [ { "children": [ @@ -753,6 +753,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "label": "New SQL Query", "onClick": [Function], }, + { + "iconSrc": {}, + "label": "Delete Container", + "onClick": [Function], + "styleClass": "deleteCollectionMenuItem", + }, ], "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "standardCollection", + "onClick": [Function], + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, + { + "children": undefined, + "className": "collectionNode", + "contextMenu": [ + { + "iconSrc": {}, + "label": "New SQL Query", + "onClick": [Function], + }, + ], + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "conflictsCollection", + "onClick": [Function], + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, + ], + "className": "databaseNode", + "contextMenu": undefined, + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "standardDb", + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, + { + "children": [ + { + "children": undefined, + "className": "collectionNode", + "contextMenu": [ + { + "iconSrc": {}, + "label": "New SQL Query", + "onClick": [Function], + }, + ], + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "sampleItemsCollection", + "onClick": [Function], + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, + ], + "className": "databaseNode", + "contextMenu": undefined, + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "sharedDatabase", + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, + { + "children": [ + { + "children": undefined, + "className": "collectionNode", + "contextMenu": [ + { + "iconSrc": {}, + "label": "New SQL Query", + "onClick": [Function], + }, + ], + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "schemaCollection", + "onClick": [Function], + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, + { + "className": "loadMoreNode", + "label": "load more", + "onClick": [Function], + }, + ], + "className": "databaseNode", + "contextMenu": undefined, + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "giganticDatabase", + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, +] +`; + exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Portal 1`] = ` [ { @@ -972,7 +1135,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ }, ], "isSelected": [Function], - "label": "mockSproc3", + "label": "mockSproc4", "onClick": [Function], }, ], @@ -990,7 +1153,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ }, ], "isSelected": [Function], - "label": "mockUdf3", + "label": "mockUdf4", "onClick": [Function], }, ], @@ -1008,7 +1171,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ }, ], "isSelected": [Function], - "label": "mockTrigger3", + "label": "mockTrigger4", "onClick": [Function], }, ], diff --git a/src/Explorer/Tree/treeNodeUtil.test.ts b/src/Explorer/Tree/treeNodeUtil.test.ts index 2c7af8021..f9298a428 100644 --- a/src/Explorer/Tree/treeNodeUtil.test.ts +++ b/src/Explorer/Tree/treeNodeUtil.test.ts @@ -17,7 +17,7 @@ import { } from "Explorer/Tree/treeNodeUtil"; import { useDatabases } from "Explorer/useDatabases"; import { useSelectedNode } from "Explorer/useSelectedNode"; -import { FabricContext, updateUserContext } from "UserContext"; +import { FabricContext, updateUserContext, UserContext } from "UserContext"; import PromiseSource from "Utils/PromiseSource"; import { useSidePanel } from "hooks/useSidePanel"; import { useTabs } from "hooks/useTabs"; @@ -361,9 +361,30 @@ describe("createDatabaseTreeNodes", () => { }); }); - it.each<[string, Platform, boolean, Partial]>([ - ["the SQL API, on Fabric", Platform.Fabric, false, { capabilities: [], enableMultipleWriteLocations: true }], - ["the SQL API, on Portal", Platform.Portal, false, { capabilities: [], enableMultipleWriteLocations: true }], + it.each<[string, Platform, boolean, Partial, Partial]>([ + [ + "the SQL API, on Fabric read-only", + Platform.Fabric, + false, + { capabilities: [], enableMultipleWriteLocations: true }, + { fabricContext: { isReadOnly: true } as FabricContext }, + ], + [ + "the SQL API, on Fabric non read-only", + Platform.Fabric, + false, + { capabilities: [], enableMultipleWriteLocations: true }, + { fabricContext: { isReadOnly: false } as FabricContext }, + ], + [ + "the SQL API, on Portal", + Platform.Portal, + false, + { capabilities: [], enableMultipleWriteLocations: true }, + { + fabricContext: undefined, + }, + ], [ "the Cassandra API, serverless, on Hosted", Platform.Hosted, @@ -374,6 +395,7 @@ describe("createDatabaseTreeNodes", () => { { name: CapabilityNames.EnableServerless, description: "" }, ], }, + { fabricContext: undefined }, ], [ "the Mongo API, with Notebooks and Phoenix features, on Emulator", @@ -382,26 +404,31 @@ describe("createDatabaseTreeNodes", () => { { capabilities: [{ name: CapabilityNames.EnableMongo, description: "" }], }, + { fabricContext: undefined }, ], - ])("generates the correct tree structure for %s", (_, platform, isNotebookEnabled, dbAccountProperties) => { - useNotebook.setState({ isPhoenixFeatures: isNotebookEnabled }); - updateConfigContext({ platform }); - updateUserContext({ - databaseAccount: { - properties: { - enableMultipleWriteLocations: true, - ...dbAccountProperties, - }, - } as unknown as DataModels.DatabaseAccount, - }); - const nodes = createDatabaseTreeNodes( - explorer, - isNotebookEnabled, - useDatabases.getState().databases, - refreshActiveTab, - ); - expect(nodes).toMatchSnapshot(); - }); + ])( + "generates the correct tree structure for %s", + (_, platform, isNotebookEnabled, dbAccountProperties, userContext) => { + useNotebook.setState({ isPhoenixFeatures: isNotebookEnabled }); + updateConfigContext({ platform }); + updateUserContext({ + ...userContext, + databaseAccount: { + properties: { + enableMultipleWriteLocations: true, + ...dbAccountProperties, + }, + } as unknown as DataModels.DatabaseAccount, + }); + const nodes = createDatabaseTreeNodes( + explorer, + isNotebookEnabled, + useDatabases.getState().databases, + refreshActiveTab, + ); + expect(nodes).toMatchSnapshot(); + }, + ); // The above tests focused on the tree structure. The below tests focus on some core behaviors of the nodes. // They are not exhaustive, because exhaustive tests here require a lot of mocking and can become very brittle. @@ -559,6 +586,7 @@ describe("createDatabaseTreeNodes", () => { updateUserContext({ fabricContext: { artifactType: CosmosDbArtifactType.MIRRORED_KEY, + isReadOnly: true, } as FabricContext, }); }, From b5d7423849ec9b52f72c35651729b61607f85b6c Mon Sep 17 00:00:00 2001 From: asier-isayas Date: Mon, 10 Mar 2025 11:35:17 -0400 Subject: [PATCH 11/11] Set default RU throughput for Production workload accounts to be 10k (#2070) * assign default throughput based on workload type * combined common logic * fix unit tests * add tests * update tests * npm run format * Set default RU throughput for Production workload accounts to be 10k * remove unused method * refactor --------- Co-authored-by: Asier Isayas --- .../Controls/ThroughputInput/ThroughputInput.tsx | 16 ++++++++++++---- src/Utils/AutoPilotUtils.ts | 1 + 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx index 64676bbc3..9fb5b28f9 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx @@ -35,12 +35,20 @@ export const ThroughputInput: FunctionComponent = ({ setIsThroughputCapExceeded, onCostAcknowledgeChange, }: ThroughputInputProps) => { - const defaultThroughput: number = + let defaultThroughput: number; + const workloadType: Constants.WorkloadType = getWorkloadType(); + + if ( isFreeTier || isQuickstart || - [Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(getWorkloadType()) - ? AutoPilotUtils.autoPilotThroughput1K - : AutoPilotUtils.autoPilotThroughput4K; + [Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType) + ) { + defaultThroughput = AutoPilotUtils.autoPilotThroughput1K; + } else if (workloadType === Constants.WorkloadType.Production) { + defaultThroughput = AutoPilotUtils.autoPilotThroughput10K; + } else { + defaultThroughput = AutoPilotUtils.autoPilotThroughput4K; + } const [isAutoscaleSelected, setIsAutoScaleSelected] = useState(true); const [throughput, setThroughput] = useState(defaultThroughput); diff --git a/src/Utils/AutoPilotUtils.ts b/src/Utils/AutoPilotUtils.ts index 57d1bf5f2..c2ed7c61e 100644 --- a/src/Utils/AutoPilotUtils.ts +++ b/src/Utils/AutoPilotUtils.ts @@ -1,6 +1,7 @@ export const autoPilotThroughput1K = 1000; export const autoPilotIncrementStep = 1000; export const autoPilotThroughput4K = 4000; +export const autoPilotThroughput10K = 10000; export function isValidAutoPilotThroughput(maxThroughput: number): boolean { if (!maxThroughput) {