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 }); } }