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/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index 79ed76434..cf34b2279 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -1,13 +1,15 @@ 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"; import { EmulatorMasterKey, HttpHeaders } from "./Constants"; @@ -18,7 +20,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} `, @@ -41,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: @@ -53,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: @@ -65,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/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/readCollections.ts b/src/Common/dataAccess/readCollections.ts index 570df8d60..39e241cda 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) { - // 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; } @@ -23,7 +24,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/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/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 39377f500..3786b5453 100644 --- a/src/Explorer/ContextMenuButtonFactory.tsx +++ b/src/Explorer/ContextMenuButtonFactory.tsx @@ -1,11 +1,13 @@ import { MaterializedViewsLabels } from "Common/Constants"; import { isMaterializedViewsEnabled } from "Common/DatabaseAccountUtility"; +import { configContext, Platform } from "ConfigContext"; import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { AddMaterializedViewPanel, AddMaterializedViewPanelProps, } from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel"; 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"; @@ -25,7 +27,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"; @@ -47,7 +48,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; } @@ -59,7 +60,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) => { @@ -151,7 +152,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/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/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 96bc0c9ed..e62bfe5b3 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/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)} />
-
= (
= (
= ({ 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/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/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
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/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx index 860154cc2..7d1870a74 100644 --- a/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx @@ -42,6 +42,7 @@ import { } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; 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"; @@ -264,150 +265,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} + /> + )} + + + )} @@ -643,7 +646,7 @@ export class AddCollectionPanel extends React.Component ); })} - {userContext.apiType === "SQL" && ( + {!isFabricNative() && userContext.apiType === "SQL" && ( )} - {userContext.apiType === "SQL" && ( + {!isFabricNative() && userContext.apiType === "SQL" && ( {UniqueKeysHeader()} {this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => { @@ -894,7 +897,7 @@ export class AddCollectionPanel extends React.Component )} - {userContext.apiType !== "Tables" && ( + {!isFabricNative() && userContext.apiType !== "Tables" && ( = ({ 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/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={() => { diff --git a/src/Explorer/Sidebar.tsx b/src/Explorer/Sidebar.tsx index 2ad443efa..6eb38c27f 100644 --- a/src/Explorer/Sidebar.tsx +++ b/src/Explorer/Sidebar.tsx @@ -1,5 +1,6 @@ import { Button, + makeStyles, Menu, MenuButton, MenuButtonProps, @@ -7,15 +8,14 @@ import { MenuList, MenuPopover, MenuTrigger, - SplitButton, - makeStyles, mergeClasses, shorthands, + SplitButton, } from "@fluentui/react-components"; import { Add16Regular, ArrowSync12Regular, ChevronLeft12Regular, ChevronRight12Regular } from "@fluentui/react-icons"; import { MaterializedViewsLabels } from "Common/Constants"; import { isMaterializedViewsEnabled } from "Common/DatabaseAccountUtility"; -import { Platform, configContext } from "ConfigContext"; +import { configContext, Platform } from "ConfigContext"; import Explorer from "Explorer/Explorer"; import { AddDatabasePanel } from "Explorer/Panes/AddDatabasePanel/AddDatabasePanel"; import { @@ -27,6 +27,7 @@ import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/T import { ResourceTree } from "Explorer/Tree/ResourceTree"; import { useDatabases } from "Explorer/useDatabases"; import { KeyboardAction, KeyboardActionGroup, KeyboardActionHandler, useKeyboardActionGroup } from "KeyboardShortcuts"; +import { isFabric, isFabricMirrored, isFabricNative } from "Platform/Fabric/FabricUtil"; import { userContext } from "UserContext"; import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils"; import { Allotment, AllotmentHandle } from "allotment"; @@ -129,7 +130,7 @@ const GlobalCommands: React.FC = ({ explorer }) => { const actions = useMemo(() => { if ( - configContext.platform === Platform.Fabric || + (isFabric() && userContext.fabricContext?.isReadOnly) || userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo" ) { @@ -143,12 +144,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()}`, @@ -313,7 +317,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 9dd723762..bc3af39f1 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"; @@ -214,7 +214,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/__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 239b6144d..f9298a428 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, UserContext } from "UserContext"; import PromiseSource from "Utils/PromiseSource"; import { useSidePanel } from "hooks/useSidePanel"; import { useTabs } from "hooks/useTabs"; @@ -360,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, @@ -373,6 +395,7 @@ describe("createDatabaseTreeNodes", () => { { name: CapabilityNames.EnableServerless, description: "" }, ], }, + { fabricContext: undefined }, ], [ "the Mongo API, with Notebooks and Phoenix features, on Emulator", @@ -381,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. @@ -551,7 +579,18 @@ describe("createDatabaseTreeNodes", () => { }); it.each([ - ["in Fabric", () => updateConfigContext({ platform: Platform.Fabric })], + [ + "in Fabric", + () => { + updateConfigContext({ platform: Platform.Fabric }); + updateUserContext({ + fabricContext: { + artifactType: CosmosDbArtifactType.MIRRORED_KEY, + isReadOnly: true, + } as FabricContext, + }); + }, + ], [ "for Cassandra API", () => diff --git a/src/Explorer/Tree/treeNodeUtil.tsx b/src/Explorer/Tree/treeNodeUtil.tsx index 533008ab3..88c38ff28 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/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..ef4cb614c 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 - example = "example", - sqlx = "sqlx", - graphapicompute = "graphapicompute", - materializedviewsbuilder = "materializedviewsbuilder", + // NOTE: text and casing of the enum's value must match the corresponding file in Localization\en\ + example = "SelfServeExample", + sqlx = "SqlX", + graphapicompute = "GraphAPICompute", + materializedviewsbuilder = "MaterializedViewsBuilder", } /** 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/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/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) { 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 2d12f3af4..2e29d1363 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -2,17 +2,26 @@ 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, 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"; @@ -22,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"; @@ -43,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, @@ -103,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; @@ -119,7 +128,7 @@ async function configureFabric(): Promise { return; } - const data: FabricMessageV2 = event.data?.data; + const data: FabricMessageV2 | FabricMessageV3 = event.data?.data; if (!data) { return; } @@ -128,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; } @@ -299,7 +347,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( @@ -419,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: { @@ -439,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({ @@ -552,7 +707,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( 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 }); } }