Prepare for Fabric native (#2050)

* Implement fabric native path

* Fix default values to work with current fabric clients

* Fix Fabric native mode

* Fix unit test

* export Fabric context

* Dynamically close Home tab for Mirrored databases in Fabric rather than conditional init (which doesn't work for Native)

* For Fabric native, don't show "Delete Database" in context menu and reading databases should return the database from the context.

* Update to V3 messaging

* For data plane operations, skip ARM for Fabric native. Refine the tests for fabric to make the distinction between mirrored key, mirrored AAD and native. Fix FabricUtil to strict compile.

* Add support for refreshing access tokens

* Buf fix: don't wait for refresh is async

* Fix format

* Fix strict compile issue

---------

Co-authored-by: Laurent Nguyen <languye@microsoft.com>
This commit is contained in:
Laurent Nguyen 2025-03-06 07:30:13 +01:00 committed by GitHub
parent 14c9874e5e
commit 083bccfda9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 831 additions and 351 deletions

View File

@ -1,13 +1,14 @@
import * as Cosmos from "@azure/cosmos"; import * as Cosmos from "@azure/cosmos";
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens"; import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
import { AuthorizationToken } from "Contracts/FabricMessageTypes"; 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 { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { PriorityLevel } from "../Common/Constants"; import { PriorityLevel } from "../Common/Constants";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import { Platform, configContext } from "../ConfigContext"; import { Platform, configContext } from "../ConfigContext";
import { updateUserContext, userContext } from "../UserContext"; import { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext";
import { isDataplaneRbacSupported } from "../Utils/APITypeUtils"; import { isDataplaneRbacSupported } from "../Utils/APITypeUtils";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils"; import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
@ -42,7 +43,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
return decodeURIComponent(headers.authorization); return decodeURIComponent(headers.authorization);
} }
if (configContext.platform === Platform.Fabric) { if (isFabricMirroredKey()) {
switch (requestInfo.resourceType) { switch (requestInfo.resourceType) {
case Cosmos.ResourceType.conflicts: case Cosmos.ResourceType.conflicts:
case Cosmos.ResourceType.container: case Cosmos.ResourceType.container:
@ -54,8 +55,13 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
// User resource tokens // User resource tokens
// TODO userContext.fabricContext.databaseConnectionInfo can be undefined // TODO userContext.fabricContext.databaseConnectionInfo can be undefined
headers[HttpHeaders.msDate] = new Date().toUTCString(); headers[HttpHeaders.msDate] = new Date().toUTCString();
const resourceTokens = userContext.fabricContext.databaseConnectionInfo.resourceTokens; const resourceTokens = (
checkDatabaseResourceTokensValidity(userContext.fabricContext.databaseConnectionInfo.resourceTokensTimestamp); 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); return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
case Cosmos.ResourceType.none: 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. // 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 // 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). // (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); return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId);
/* ************** TODO: Uncomment this code if we need to support these operations ************** /* ************** TODO: Uncomment this code if we need to support these operations **************

View File

@ -1,10 +1,10 @@
import { Platform, configContext } from "../ConfigContext"; import { isFabric } from "Platform/Fabric/FabricUtil";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less"); export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
export function updateStyles(): void { export function updateStyles(): void {
if (configContext.platform === Platform.Fabric) { if (isFabric()) {
StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh; StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh;
StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium; StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium;
StyleConstants.AccentLight = StyleConstants.FabricAccentLight; StyleConstants.AccentLight = StyleConstants.FabricAccentLight;

View File

@ -1,9 +1,10 @@
import { ContainerResponse } from "@azure/cosmos"; import { ContainerResponse } from "@azure/cosmos";
import { Queries } from "Common/Constants"; 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 { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { FabricArtifactInfo, userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
@ -16,15 +17,13 @@ import { handleError } from "../ErrorHandlingUtils";
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> { export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`); const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
if ( if (isFabricMirroredKey() && userContext.fabricContext?.databaseName === databaseId) {
configContext.platform === Platform.Fabric &&
userContext.fabricContext &&
userContext.fabricContext.databaseConnectionInfo.databaseId === databaseId
) {
const collections: DataModels.Collection[] = []; const collections: DataModels.Collection[] = [];
const promises: Promise<ContainerResponse>[] = []; const promises: Promise<ContainerResponse>[] = [];
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 // Dictionary key looks like this: dbs/SampleDB/colls/Container
const resourceIdObj = collectionResourceId.split("/"); const resourceIdObj = collectionResourceId.split("/");
const tokenDatabaseId = resourceIdObj[1]; const tokenDatabaseId = resourceIdObj[1];
@ -56,7 +55,8 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations && !userContext.features.enableSDKoperations &&
userContext.apiType !== "Tables" userContext.apiType !== "Tables" &&
!isFabric()
) { ) {
return await readCollectionsWithARM(databaseId); return await readCollectionsWithARM(databaseId);
} }

View File

@ -1,4 +1,4 @@
import { Platform, configContext } from "ConfigContext"; import { isFabric, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels"; import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@ -11,7 +11,7 @@ import { handleError } from "../ErrorHandlingUtils";
import { readOfferWithSDK } from "./readOfferWithSDK"; import { readOfferWithSDK } from "./readOfferWithSDK";
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => { export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
if (configContext.platform === Platform.Fabric) { if (isFabricMirroredKey()) {
// TODO This works, but is very slow, because it requests the token, so we skip for now // TODO This works, but is very slow, because it requests the token, so we skip for now
console.error("Skiping readDatabaseOffer for Fabric"); console.error("Skiping readDatabaseOffer for Fabric");
return undefined; return undefined;
@ -23,7 +23,8 @@ export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promis
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations && !userContext.features.enableSDKoperations &&
userContext.apiType !== "Tables" userContext.apiType !== "Tables" &&
!isFabric()
) { ) {
return await readDatabaseOfferWithARM(params.databaseId); return await readDatabaseOfferWithARM(params.databaseId);
} }

View File

@ -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 { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { FabricArtifactInfo, userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
@ -14,8 +15,13 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
let databases: DataModels.Database[]; let databases: DataModels.Database[];
const clearMessage = logConsoleProgress(`Querying databases`); const clearMessage = logConsoleProgress(`Querying databases`);
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.databaseConnectionInfo.resourceTokens) { if (
const tokensData = userContext.fabricContext.databaseConnectionInfo; 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<string>(); // databaseId const databaseIdsSet = new Set<string>(); // databaseId
@ -46,13 +52,28 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
})); }));
clearMessage(); clearMessage();
return databases; 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 { try {
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations && !userContext.features.enableSDKoperations &&
userContext.apiType !== "Tables" userContext.apiType !== "Tables" &&
!isFabric()
) { ) {
databases = await readDatabasesWithARM(); databases = await readDatabasesWithARM();
} else { } else {

View File

@ -4,6 +4,7 @@
export enum FabricMessageTypes { export enum FabricMessageTypes {
GetAuthorizationToken = "GetAuthorizationToken", GetAuthorizationToken = "GetAuthorizationToken",
GetAllResourceTokens = "GetAllResourceTokens", GetAllResourceTokens = "GetAllResourceTokens",
GetAccessToken = "GetAccessToken",
Ready = "Ready", Ready = "Ready",
} }

View File

@ -1,47 +1,9 @@
import { AuthorizationToken } from "Contracts/FabricMessageTypes"; import { AuthorizationToken } from "./FabricMessageTypes";
// This is the version of these messages // This is the version of these messages
export const FABRIC_RPC_VERSION = "2"; export const FABRIC_RPC_VERSION = "FabricMessageV3";
// Fabric to Data Explorer // 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 = export type FabricMessageV2 =
| { | {
type: "newContainer"; type: "newContainer";
@ -69,7 +31,7 @@ export type FabricMessageV2 =
message: { message: {
id: string; id: string;
error: string | undefined; error: string | undefined;
data: FabricDatabaseConnectionInfo | undefined; data: ResourceTokenInfo | undefined;
}; };
} }
| { | {
@ -79,17 +41,81 @@ export type FabricMessageV2 =
}; };
}; };
export type CosmosDBTokenResponse = { export type FabricMessageV3 =
token: string; | {
date: string; type: "newContainer";
}; databaseName: string;
}
| {
type: "initialize";
version: string;
id: string;
message: InitializeMessageV3<CosmosDbArtifactType>;
}
| {
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<T extends CosmosDbArtifactType> {
connectionId: string;
isVisible: boolean;
isReadOnly: boolean;
artifactType: T;
artifactConnectionInfo: ArtifactConnectionInfo[T];
}
export interface CosmosDBConnectionInfoResponse {
endpoint: string; endpoint: string;
databaseId: string; databaseId: string;
resourceTokens: { [resourceId: string]: string }; resourceTokens: Record<string, string> | undefined;
}; accessToken: string | undefined;
isReadOnly: boolean;
credentialType: "Key" | "OAuth2" | undefined;
}
export interface FabricDatabaseConnectionInfo extends CosmosDBConnectionInfoResponse { export interface ResourceTokenInfo extends CosmosDBConnectionInfoResponse {
resourceTokensTimestamp: number; resourceTokensTimestamp: number;
} }

View File

@ -1,5 +1,7 @@
import { configContext, Platform } from "ConfigContext";
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor"; import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
@ -19,7 +21,6 @@ import * as ViewModels from "../Contracts/ViewModels";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils"; import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { useSidePanel } from "../hooks/useSidePanel"; import { useSidePanel } from "../hooks/useSidePanel";
import { Platform, configContext } from "./../ConfigContext";
import Explorer from "./Explorer"; import Explorer from "./Explorer";
import { useNotebook } from "./Notebook/useNotebook"; import { useNotebook } from "./Notebook/useNotebook";
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane"; import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
@ -41,7 +42,7 @@ export interface DatabaseContextMenuButtonParams {
* New resource tree (in ReactJS) * New resource tree (in ReactJS)
*/ */
export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => { export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => {
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) { if (isFabric() && userContext.fabricContext?.isReadOnly) {
return undefined; 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({ items.push({
iconSrc: DeleteDatabaseIcon, iconSrc: DeleteDatabaseIcon,
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => { onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {

View File

@ -8,7 +8,7 @@ import { MessageTypes } from "Contracts/ExplorerContracts";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane"; import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { IGalleryItem } from "Juno/JunoClient"; 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 { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils"; import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils"; import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
@ -43,7 +43,7 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
import { useSidePanel } from "../hooks/useSidePanel"; import { useSidePanel } from "../hooks/useSidePanel";
import { useTabs } from "../hooks/useTabs"; import { ReactTabKind, useTabs } from "../hooks/useTabs";
import "./ComponentRegisterer"; import "./ComponentRegisterer";
import { DialogProps, useDialog } from "./Controls/Dialog"; import { DialogProps, useDialog } from "./Controls/Dialog";
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
@ -187,6 +187,10 @@ export default class Explorer {
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath); useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
} }
if (isFabricMirrored()) {
useTabs.getState().closeReactTab(ReactTabKind.Home);
}
this.refreshExplorer(); this.refreshExplorer();
} }
@ -347,8 +351,8 @@ export default class Explorer {
}; };
public onRefreshResourcesClick = async (): Promise<void> => { public onRefreshResourcesClick = async (): Promise<void> => {
if (configContext.platform === Platform.Fabric) { if (isFabricMirroredKey()) {
scheduleRefreshDatabaseResourceToken(true).then(() => this.refreshAllDatabases()); scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
return; return;
} }

View File

@ -6,12 +6,12 @@
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react"; import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import { useNotebook } from "Explorer/Notebook/useNotebook"; import { useNotebook } from "Explorer/Notebook/useNotebook";
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import * as React from "react"; import * as React from "react";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants"; import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants"; import { StyleConstants } from "../../../Common/StyleConstants";
import { Platform, configContext } from "../../../ConfigContext";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
@ -93,19 +93,18 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
); );
} }
const rootStyle = const rootStyle = isFabric()
configContext.platform === Platform.Fabric ? {
? { root: {
root: { backgroundColor: "transparent",
backgroundColor: "transparent", padding: "2px 8px 0px 8px",
padding: "2px 8px 0px 8px", },
}, }
} : {
: { root: {
root: { backgroundColor: backgroundColor,
backgroundColor: backgroundColor, },
}, };
};
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons); const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons); const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);

View File

@ -1,6 +1,6 @@
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled. // 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 { useDatabases } from "Explorer/useDatabases";
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
import React from "react"; import React from "react";
import { ActionContracts } from "../../Contracts/ExplorerContracts"; import { ActionContracts } from "../../Contracts/ExplorerContracts";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
@ -58,9 +58,9 @@ function openCollectionTab(
} }
if ( 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.SQLDocuments ||
action.tabKind === ActionContracts.TabKind.SQLQuery action.tabKind === ActionContracts.TabKind.SQLQuery

View File

@ -28,6 +28,7 @@ import {
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent"; import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import { useTeachingBubble } from "hooks/useTeachingBubble"; import { useTeachingBubble } from "hooks/useTeachingBubble";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import React from "react"; import React from "react";
import { CollectionCreation } from "Shared/Constants"; import { CollectionCreation } from "Shared/Constants";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
@ -284,150 +285,152 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)} )}
<div className="panelMainContent"> <div className="panelMainContent">
<Stack hidden={userContext.apiType === "Tables"}> {!(isFabricNative() && this.props.databaseId !== undefined) && (
<Stack horizontal> <Stack hidden={userContext.apiType === "Tables"}>
<span className="mandatoryStar">*&nbsp;</span> <Stack horizontal>
<Text className="panelTextBold" variant="small"> <span className="mandatoryStar">*&nbsp;</span>
Database {userContext.apiType === "Mongo" ? "name" : "id"} <Text className="panelTextBold" variant="small">
</Text> Database {userContext.apiType === "Mongo" ? "name" : "id"}
<TooltipHost </Text>
directionalHint={DirectionalHint.bottomLeftEdge} <TooltipHost
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName( directionalHint={DirectionalHint.bottomLeftEdge}
true, content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
).toLocaleLowerCase()}.`}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
true, true,
).toLocaleLowerCase()}.`} ).toLocaleLowerCase()}.`}
/> >
</TooltipHost> <Icon
</Stack> iconName="Info"
className="panelInfoIcon"
{configContext.platform !== Platform.Fabric && (
<Stack horizontal verticalAlign="center">
<div role="radiogroup">
<input
className="panelRadioBtn"
checked={this.state.createNewDatabase}
aria-label="Create new database"
aria-checked={this.state.createNewDatabase}
name="databaseType"
type="radio"
role="radio"
id="databaseCreateNew"
tabIndex={0} tabIndex={0}
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)} ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
true,
).toLocaleLowerCase()}.`}
/> />
<span className="panelRadioBtnLabel">Create new</span> </TooltipHost>
<input
className="panelRadioBtn"
checked={!this.state.createNewDatabase}
aria-label="Use existing database"
aria-checked={!this.state.createNewDatabase}
name="databaseType"
type="radio"
role="radio"
tabIndex={0}
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Use existing</span>
</div>
</Stack> </Stack>
)}
{this.state.createNewDatabase && ( {configContext.platform !== Platform.Fabric && (
<Stack className="panelGroupSpacing"> <Stack horizontal verticalAlign="center">
<input <div role="radiogroup">
name="newDatabaseId" <input
id="newDatabaseId" className="panelRadioBtn"
aria-required checked={this.state.createNewDatabase}
required aria-label="Create new database"
type="text" aria-checked={this.state.createNewDatabase}
autoComplete="off" name="databaseType"
pattern="[^/?#\\]*[^/?# \\]" type="radio"
title="May not end with space nor contain characters '\' '/' '#' '?'" role="radio"
placeholder="Type a new database id" id="databaseCreateNew"
size={40} tabIndex={0}
className="panelTextField" onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
aria-label="New database id, Type a new database id"
autoFocus
tabIndex={0}
value={this.state.newDatabaseId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
this.setState({ newDatabaseId: event.target.value })
}
/>
{!isServerlessAccount() && (
<Stack horizontal>
<Checkbox
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
checked={this.state.isSharedThroughputChecked}
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
this.setState({ isSharedThroughputChecked: isChecked })
}
/> />
<TooltipHost <span className="panelRadioBtnLabel">Create new</span>
directionalHint={DirectionalHint.bottomLeftEdge}
content={`Throughput configured at the database level will be shared across all ${getCollectionName( <input
true, className="panelRadioBtn"
).toLocaleLowerCase()} within the database.`} checked={!this.state.createNewDatabase}
> aria-label="Use existing database"
<Icon aria-checked={!this.state.createNewDatabase}
iconName="Info" name="databaseType"
className="panelInfoIcon" type="radio"
tabIndex={0} role="radio"
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName( tabIndex={0}
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Use existing</span>
</div>
</Stack>
)}
{this.state.createNewDatabase && (
<Stack className="panelGroupSpacing">
<input
name="newDatabaseId"
id="newDatabaseId"
aria-required
required
type="text"
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder="Type a new database id"
size={40}
className="panelTextField"
aria-label="New database id, Type a new database id"
autoFocus
tabIndex={0}
value={this.state.newDatabaseId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
this.setState({ newDatabaseId: event.target.value })
}
/>
{!isServerlessAccount() && (
<Stack horizontal>
<Checkbox
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
checked={this.state.isSharedThroughputChecked}
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
this.setState({ isSharedThroughputChecked: isChecked })
}
/>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
true, true,
).toLocaleLowerCase()} within the database.`} ).toLocaleLowerCase()} within the database.`}
/> >
</TooltipHost> <Icon
</Stack> iconName="Info"
)} className="panelInfoIcon"
tabIndex={0}
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
true,
).toLocaleLowerCase()} within the database.`}
/>
</TooltipHost>
</Stack>
)}
{!isServerlessAccount() && this.state.isSharedThroughputChecked && ( {!isServerlessAccount() && this.state.isSharedThroughputChecked && (
<ThroughputInput <ThroughputInput
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated} showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={true} isDatabase={true}
isSharded={this.state.isSharded} isSharded={this.state.isSharded}
isFreeTier={this.isFreeTierAccount()} isFreeTier={this.isFreeTierAccount()}
isQuickstart={this.props.isQuickstart} isQuickstart={this.props.isQuickstart}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)} setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)} setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
this.setState({ isThroughputCapExceeded }) this.setState({ isThroughputCapExceeded })
} }
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)} onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
/> />
)} )}
</Stack> </Stack>
)} )}
{!this.state.createNewDatabase && ( {!this.state.createNewDatabase && (
<Dropdown <Dropdown
ariaLabel="Choose an existing database" ariaLabel="Choose an existing database"
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }} styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
style={{ width: 300, fontSize: 12 }} style={{ width: 300, fontSize: 12 }}
placeholder="Choose an existing database" placeholder="Choose an existing database"
options={this.getDatabaseOptions()} options={this.getDatabaseOptions()}
onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) => onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) =>
this.setState({ selectedDatabaseId: database.key as string }) this.setState({ selectedDatabaseId: database.key as string })
} }
defaultSelectedKey={this.props.databaseId} defaultSelectedKey={this.props.databaseId}
responsiveMode={999} responsiveMode={999}
/> />
)} )}
<Separator className="panelSeparator" /> <Separator className="panelSeparator" />
</Stack> </Stack>
)}
<Stack> <Stack>
<Stack horizontal> <Stack horizontal>
@ -666,7 +669,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack> </Stack>
); );
})} })}
{userContext.apiType === "SQL" && ( {!isFabricNative() && userContext.apiType === "SQL" && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<DefaultButton <DefaultButton
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }} styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
@ -747,7 +750,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/> />
)} )}
{userContext.apiType === "SQL" && ( {!isFabricNative() && userContext.apiType === "SQL" && (
<Stack> <Stack>
<Stack horizontal> <Stack horizontal>
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
@ -937,7 +940,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</CollapsibleSectionComponent> </CollapsibleSectionComponent>
</Stack> </Stack>
)} )}
{userContext.apiType !== "Tables" && ( {!isFabricNative() && userContext.apiType !== "Tables" && (
<CollapsibleSectionComponent <CollapsibleSectionComponent
title="Advanced" title="Advanced"
isExpandedByDefault={false} isExpandedByDefault={false}
@ -1260,7 +1263,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
// } // }
private shouldShowCollectionThroughputInput(): boolean { private shouldShowCollectionThroughputInput(): boolean {
if (isServerlessAccount()) { if (isFabricNative() || isServerlessAccount()) {
return false; return false;
} }
@ -1286,7 +1289,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
private shouldShowAnalyticalStoreOptions(): boolean { private shouldShowAnalyticalStoreOptions(): boolean {
if (configContext.platform === Platform.Emulator) { if (isFabricNative() || configContext.platform === Platform.Emulator) {
return false; return false;
} }

View File

@ -1,5 +1,6 @@
import { import {
Button, Button,
makeStyles,
Menu, Menu,
MenuButton, MenuButton,
MenuButtonProps, MenuButtonProps,
@ -7,13 +8,12 @@ import {
MenuList, MenuList,
MenuPopover, MenuPopover,
MenuTrigger, MenuTrigger,
SplitButton,
makeStyles,
mergeClasses, mergeClasses,
shorthands, shorthands,
SplitButton,
} from "@fluentui/react-components"; } from "@fluentui/react-components";
import { Add16Regular, ArrowSync12Regular, ChevronLeft12Regular, ChevronRight12Regular } from "@fluentui/react-icons"; import { Add16Regular, ArrowSync12Regular, ChevronLeft12Regular, ChevronRight12Regular } from "@fluentui/react-icons";
import { Platform, configContext } from "ConfigContext"; import { configContext, Platform } from "ConfigContext";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { AddDatabasePanel } from "Explorer/Panes/AddDatabasePanel/AddDatabasePanel"; import { AddDatabasePanel } from "Explorer/Panes/AddDatabasePanel/AddDatabasePanel";
import { Tabs } from "Explorer/Tabs/Tabs"; import { Tabs } from "Explorer/Tabs/Tabs";
@ -21,6 +21,7 @@ import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/T
import { ResourceTree } from "Explorer/Tree/ResourceTree"; import { ResourceTree } from "Explorer/Tree/ResourceTree";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { KeyboardAction, KeyboardActionGroup, KeyboardActionHandler, useKeyboardActionGroup } from "KeyboardShortcuts"; import { KeyboardAction, KeyboardActionGroup, KeyboardActionHandler, useKeyboardActionGroup } from "KeyboardShortcuts";
import { isFabric, isFabricMirrored, isFabricNative } from "Platform/Fabric/FabricUtil";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils"; import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils";
import { Allotment, AllotmentHandle } from "allotment"; import { Allotment, AllotmentHandle } from "allotment";
@ -123,7 +124,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
const actions = useMemo<GlobalCommand[]>(() => { const actions = useMemo<GlobalCommand[]>(() => {
if ( if (
configContext.platform === Platform.Fabric || (isFabric() && userContext.fabricContext?.isReadOnly) ||
userContext.apiType === "Postgres" || userContext.apiType === "Postgres" ||
userContext.apiType === "VCoreMongo" userContext.apiType === "VCoreMongo"
) { ) {
@ -137,12 +138,15 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
id: "new_collection", id: "new_collection",
label: `New ${getCollectionName()}`, label: `New ${getCollectionName()}`,
icon: <Add16Regular />, icon: <Add16Regular />,
onClick: () => explorer.onNewCollectionClicked(), onClick: () => {
const databaseId = isFabricNative() ? userContext.fabricContext?.databaseName : undefined;
explorer.onNewCollectionClicked({ databaseId });
},
keyboardAction: KeyboardAction.NEW_COLLECTION, keyboardAction: KeyboardAction.NEW_COLLECTION,
}, },
]; ];
if (userContext.apiType !== "Tables") { if (configContext.platform !== Platform.Fabric && userContext.apiType !== "Tables") {
actions.push({ actions.push({
id: "new_database", id: "new_database",
label: `New ${getDatabaseName()}`, label: `New ${getDatabaseName()}`,
@ -288,7 +292,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
}, [setLoading]); }, [setLoading]);
const hasGlobalCommands = !( const hasGlobalCommands = !(
configContext.platform === Platform.Fabric || isFabricMirrored() ||
userContext.apiType === "Postgres" || userContext.apiType === "Postgres" ||
userContext.apiType === "VCoreMongo" userContext.apiType === "VCoreMongo"
); );

View File

@ -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<FabricHomeScreenButtonProps & { className: string }> = ({
title,
description,
icon,
className,
onClick,
}) => {
const styles = useStyles();
// TODO Make this a11y copmliant: aria-label for icon
return (
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick}>
<div className={styles.buttonUpperPart}>{icon}</div>
<div className={styles.buttonLowerPart}>
<div>{title}</div>
<div>{description}</div>
</div>
</div>
);
};
export const FabricHomeScreen: React.FC<SplashScreenProps> = (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: <DocumentAddRegular />,
onClick: () => {
const databaseId = isFabricNative() ? userContext.fabricContext?.databaseName : undefined;
props.explorer.onNewCollectionClicked({ databaseId });
},
},
{
title: "Sample data",
description: "Automatically load sample data in your database",
icon: <img src={CosmosDbBlackIcon} />,
},
{
title: "App development",
description: "Start here to use an SDK to build your apps",
icon: <LinkMultipleRegular />,
},
];
return (
<div className={styles.buttonsContainer}>
<FabricHomeScreenButton className={styles.one} {...buttons[0]} />
<FabricHomeScreenButton className={styles.two} {...buttons[1]} />
<FabricHomeScreenButton className={styles.three} {...buttons[2]} />
</div>
);
};
const title = "Build your database";
return (
<div className={styles.homeContainer}>
<div className={styles.title} role="heading" aria-label={title}>
{title}
</div>
{getSplashScreenButtons()}
<div className={styles.footer}>
Need help?{" "}
<Link href="https://cosmos.azure.com/docs" target="_blank">
Learn more <img src={LinkIcon} alt="Learn more" />
</Link>
</div>
</div>
);
};

View File

@ -2,6 +2,7 @@ import { FeedResponse, ItemDefinition, Resource } from "@azure/cosmos";
import { waitFor } from "@testing-library/react"; import { waitFor } from "@testing-library/react";
import { deleteDocuments } from "Common/dataAccess/deleteDocument"; import { deleteDocuments } from "Common/dataAccess/deleteDocument";
import { Platform, updateConfigContext } from "ConfigContext"; import { Platform, updateConfigContext } from "ConfigContext";
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
import { useDialog } from "Explorer/Controls/Dialog"; import { useDialog } from "Explorer/Controls/Dialog";
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact"; import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog"; import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
@ -341,10 +342,15 @@ describe("Documents tab (noSql API)", () => {
updateConfigContext({ platform: Platform.Fabric }); updateConfigContext({ platform: Platform.Fabric });
updateUserContext({ updateUserContext({
fabricContext: { fabricContext: {
connectionId: "test", databaseName: "database",
databaseConnectionInfo: undefined, artifactInfo: {
connectionId: "test",
resourceTokenInfo: undefined,
},
artifactType: CosmosDbArtifactType.MIRRORED_KEY,
isReadOnly: true, isReadOnly: true,
isVisible: true, isVisible: true,
fabricClientRpcVersion: "rpcVersion",
}, },
}); });

View File

@ -20,7 +20,6 @@ import {
import { queryDocuments } from "Common/dataAccess/queryDocuments"; import { queryDocuments } from "Common/dataAccess/queryDocuments";
import { readDocument } from "Common/dataAccess/readDocument"; import { readDocument } from "Common/dataAccess/readDocument";
import { updateDocument } from "Common/dataAccess/updateDocument"; import { updateDocument } from "Common/dataAccess/updateDocument";
import { Platform, configContext } from "ConfigContext";
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts"; import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
import { useDialog } from "Explorer/Controls/Dialog"; 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 { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { QueryConstants } from "Shared/Constants"; import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
@ -344,7 +344,7 @@ export const getTabsButtons = ({
onRevertExistingDocumentClick, onRevertExistingDocumentClick,
onDeleteExistingDocumentsClick, onDeleteExistingDocumentsClick,
}: ButtonsDependencies): CommandButtonComponentProps[] => { }: ButtonsDependencies): CommandButtonComponentProps[] => {
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) { if (isFabric() && userContext.fabricContext?.isReadOnly) {
// All the following buttons require write access // All the following buttons require write access
return []; return [];
} }
@ -2136,8 +2136,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
selectedColumnIds={selectedColumnIds} selectedColumnIds={selectedColumnIds}
columnDefinitions={columnDefinitions} columnDefinitions={columnDefinitions}
isRowSelectionDisabled={ isRowSelectionDisabled={
isBulkDeleteDisabled || isBulkDeleteDisabled || (isFabric() && userContext.fabricContext?.isReadOnly)
(configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly)
} }
onColumnSelectionChange={onColumnSelectionChange} onColumnSelectionChange={onColumnSelectionChange}
defaultColumnSelection={getInitialColumnSelection()} defaultColumnSelection={getInitialColumnSelection()}

View File

@ -2,6 +2,7 @@ import { CollectionTabKind } from "Contracts/ViewModels";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab"; import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab";
import { FabricHomeScreen } from "Explorer/SplashScreen/FabricHome";
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen"; import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
import { ConnectTab } from "Explorer/Tabs/ConnectTab"; import { ConnectTab } from "Explorer/Tabs/ConnectTab";
import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab"; import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
@ -9,6 +10,7 @@ import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab"; import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab"; import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { useTeachingBubble } from "hooks/useTeachingBubble"; import { useTeachingBubble } from "hooks/useTeachingBubble";
import ko from "knockout"; import ko from "knockout";
@ -271,7 +273,11 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
<ConnectTab /> <ConnectTab />
); );
case ReactTabKind.Home: case ReactTabKind.Home:
return <SplashScreen explorer={explorer} />; if (isFabricNative()) {
return <FabricHomeScreen explorer={explorer} />;
} else {
return <SplashScreen explorer={explorer} />;
}
case ReactTabKind.Quickstart: case ReactTabKind.Quickstart:
return userContext.apiType === "VCoreMongo" ? ( return userContext.apiType === "VCoreMongo" ? (
<VcoreMongoQuickstartTab explorer={explorer} /> <VcoreMongoQuickstartTab explorer={explorer} />

View File

@ -1,6 +1,7 @@
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos"; import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
import { useNotebook } from "Explorer/Notebook/useNotebook"; import { useNotebook } from "Explorer/Notebook/useNotebook";
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
import * as ko from "knockout"; import * as ko from "knockout";
import * as _ from "underscore"; import * as _ from "underscore";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
@ -34,7 +35,6 @@ import QueryTablesTab from "../Tabs/QueryTablesTab";
import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2"; import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
import { Platform, configContext } from "./../../ConfigContext";
import ConflictId from "./ConflictId"; import ConflictId from "./ConflictId";
import DocumentId from "./DocumentId"; import DocumentId from "./DocumentId";
import StoredProcedure from "./StoredProcedure"; import StoredProcedure from "./StoredProcedure";
@ -210,7 +210,7 @@ export default class Collection implements ViewModels.Collection {
}); });
const showScriptsMenus: boolean = const showScriptsMenus: boolean =
configContext.platform != Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin"); !isFabricMirrored() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
this.showStoredProcedures = ko.observable<boolean>(showScriptsMenus); this.showStoredProcedures = ko.observable<boolean>(showScriptsMenus);
this.showTriggers = ko.observable<boolean>(showScriptsMenus); this.showTriggers = ko.observable<boolean>(showScriptsMenus);
this.showUserDefinedFunctions = ko.observable<boolean>(showScriptsMenus); this.showUserDefinedFunctions = ko.observable<boolean>(showScriptsMenus);

View File

@ -1,7 +1,6 @@
import { Tree, TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from "@fluentui/react-components"; import { Tree, TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from "@fluentui/react-components";
import { Home16Regular } from "@fluentui/react-icons"; import { Home16Regular } from "@fluentui/react-icons";
import { AuthType } from "AuthType"; import { AuthType } from "AuthType";
import { Platform, configContext } from "ConfigContext";
import { useTreeStyles } from "Explorer/Controls/TreeComponent/Styles"; import { useTreeStyles } from "Explorer/Controls/TreeComponent/Styles";
import { TreeNode, TreeNodeComponent } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { TreeNode, TreeNodeComponent } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import { import {
@ -11,6 +10,7 @@ import {
} from "Explorer/Tree/treeNodeUtil"; } from "Explorer/Tree/treeNodeUtil";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
@ -76,23 +76,22 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
: []; : [];
}, [isSampleDataEnabled, sampleDataResourceTokenCollection]); }, [isSampleDataEnabled, sampleDataResourceTokenCollection]);
const headerNodes: TreeNode[] = const headerNodes: TreeNode[] = isFabricMirrored()
configContext.platform === Platform.Fabric ? []
? [] : [
: [ {
{ id: "home",
id: "home", iconSrc: <Home16Regular />,
iconSrc: <Home16Regular />, label: "Home",
label: "Home", isSelected: () =>
isSelected: () => useSelectedNode.getState().selectedNode === undefined &&
useSelectedNode.getState().selectedNode === undefined && useTabs.getState().activeReactTab === ReactTabKind.Home,
useTabs.getState().activeReactTab === ReactTabKind.Home, onClick: () => {
onClick: () => { useSelectedNode.getState().setSelectedNode(undefined);
useSelectedNode.getState().setSelectedNode(undefined); useTabs.getState().openAndActivateReactTab(ReactTabKind.Home);
useTabs.getState().openAndActivateReactTab(ReactTabKind.Home);
},
}, },
]; },
];
const rootNodes: TreeNode[] = useMemo(() => { const rootNodes: TreeNode[] = useMemo(() => {
if (sampleDataNodes.length > 0) { if (sampleDataNodes.length > 0) {

View File

@ -1,5 +1,6 @@
import { CapabilityNames } from "Common/Constants"; import { CapabilityNames } from "Common/Constants";
import { Platform, updateConfigContext } from "ConfigContext"; import { Platform, updateConfigContext } from "ConfigContext";
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
@ -16,7 +17,7 @@ import {
} from "Explorer/Tree/treeNodeUtil"; } from "Explorer/Tree/treeNodeUtil";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import { updateUserContext } from "UserContext"; import { FabricContext, updateUserContext } from "UserContext";
import PromiseSource from "Utils/PromiseSource"; import PromiseSource from "Utils/PromiseSource";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import { useTabs } from "hooks/useTabs"; import { useTabs } from "hooks/useTabs";
@ -551,7 +552,17 @@ describe("createDatabaseTreeNodes", () => {
}); });
it.each([ it.each([
["in Fabric", () => updateConfigContext({ platform: Platform.Fabric })], [
"in Fabric",
() => {
updateConfigContext({ platform: Platform.Fabric });
updateUserContext({
fabricContext: {
artifactType: CosmosDbArtifactType.MIRRORED_KEY,
} as FabricContext<CosmosDbArtifactType>,
});
},
],
[ [
"for Cassandra API", "for Cassandra API",
() => () =>

View File

@ -6,6 +6,7 @@ import StoredProcedure from "Explorer/Tree/StoredProcedure";
import Trigger from "Explorer/Tree/Trigger"; import Trigger from "Explorer/Tree/Trigger";
import UserDefinedFunction from "Explorer/Tree/UserDefinedFunction"; import UserDefinedFunction from "Explorer/Tree/UserDefinedFunction";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
import { getItemName } from "Utils/APITypeUtils"; import { getItemName } from "Utils/APITypeUtils";
import { isServerlessAccount } from "Utils/CapabilityUtils"; import { isServerlessAccount } from "Utils/CapabilityUtils";
import { useTabs } from "hooks/useTabs"; import { useTabs } from "hooks/useTabs";
@ -22,9 +23,7 @@ import { useNotebook } from "../Notebook/useNotebook";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
export const shouldShowScriptNodes = (): boolean => { export const shouldShowScriptNodes = (): boolean => {
return ( return !isFabricMirrored() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin")
);
}; };
const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />; const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />;

View File

@ -1,56 +1,112 @@
import { sendCachedDataMessage } from "Common/MessageHandler"; import { sendCachedDataMessage } from "Common/MessageHandler";
import { configContext, Platform } from "ConfigContext";
import { FabricMessageTypes } from "Contracts/FabricMessageTypes"; import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract"; import { CosmosDbArtifactType, ResourceTokenInfo } from "Contracts/FabricMessagesContract";
import { updateUserContext, userContext } from "UserContext"; import { FabricArtifactInfo, updateUserContext, userContext } from "UserContext";
import { logConsoleError } from "Utils/NotificationConsoleUtils"; import { logConsoleError } from "Utils/NotificationConsoleUtils";
const TOKEN_VALIDITY_MS = (3600 - 600) * 1000; // 1 hour minus 10 minutes to be safe const TOKEN_VALIDITY_MS = (3600 - 600) * 1000; // 1 hour minus 10 minutes to be safe
const DEBOUNCE_DELAY_MS = 1000 * 20; // 20 second 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 // Prevents multiple parallel requests during DEBOUNCE_DELAY_MS
let lastRequestTimestamp: number = undefined; let lastRequestTimestamp: number | undefined = undefined;
const requestDatabaseResourceTokens = async (): Promise<void> => { /**
* Request fabric token:
* - Mirrored key and AAD: Database Resource Tokens
* - Native: AAD token
* @returns
*/
const requestFabricToken = async (): Promise<void> => {
if (lastRequestTimestamp !== undefined && lastRequestTimestamp + DEBOUNCE_DELAY_MS > Date.now()) { if (lastRequestTimestamp !== undefined && lastRequestTimestamp + DEBOUNCE_DELAY_MS > Date.now()) {
return; return;
} }
lastRequestTimestamp = Date.now(); lastRequestTimestamp = Date.now();
try { try {
const fabricDatabaseConnectionInfo = await sendCachedDataMessage<FabricDatabaseConnectionInfo>( if (isFabricMirrored()) {
FabricMessageTypes.GetAllResourceTokens, await requestAndStoreDatabaseResourceTokens();
[], } else if (isFabricNative()) {
userContext.fabricContext.connectionId, await requestAndStoreAccessToken();
);
if (!userContext.databaseAccount.properties.documentEndpoint) {
userContext.databaseAccount.properties.documentEndpoint = fabricDatabaseConnectionInfo.endpoint;
} }
updateUserContext({ scheduleRefreshFabricToken();
fabricContext: {
...userContext.fabricContext,
databaseConnectionInfo: fabricDatabaseConnectionInfo,
isReadOnly: true,
},
databaseAccount: { ...userContext.databaseAccount },
});
scheduleRefreshDatabaseResourceToken();
} catch (error) { } catch (error) {
logConsoleError(error); logConsoleError(error as string);
throw error; throw error;
} finally { } finally {
lastRequestTimestamp = undefined; lastRequestTimestamp = undefined;
} }
}; };
const requestAndStoreDatabaseResourceTokens = async (): Promise<void> => {
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<ResourceTokenInfo>(
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<void> => {
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 * Check token validity and schedule a refresh if necessary
* @param tokenTimestamp * @param tokenTimestamp
* @returns * @returns
*/ */
export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Promise<void> => { export const scheduleRefreshFabricToken = (refreshNow?: boolean): Promise<void> => {
return new Promise((resolve) => { return new Promise((resolve) => {
if (timeoutId !== undefined) { if (timeoutId !== undefined) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
@ -59,7 +115,7 @@ export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Prom
timeoutId = setTimeout( timeoutId = setTimeout(
() => { () => {
requestDatabaseResourceTokens().then(resolve); requestFabricToken().then(resolve);
}, },
refreshNow ? 0 : TOKEN_VALIDITY_MS, refreshNow ? 0 : TOKEN_VALIDITY_MS,
); );
@ -68,6 +124,15 @@ export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Prom
export const checkDatabaseResourceTokensValidity = (tokenTimestamp: number): void => { export const checkDatabaseResourceTokensValidity = (tokenTimestamp: number): void => {
if (tokenTimestamp + TOKEN_VALIDITY_MS < Date.now()) { 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;

View File

@ -1,4 +1,4 @@
import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract"; import { CosmosDbArtifactType, ResourceTokenInfo } from "Contracts/FabricMessagesContract";
import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils"; import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor"; import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
@ -47,11 +47,21 @@ export interface VCoreMongoConnectionParams {
connectionString: string; connectionString: string;
} }
interface FabricContext { export interface FabricArtifactInfo {
connectionId: string; [CosmosDbArtifactType.MIRRORED_KEY]: {
databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined; connectionId: string;
resourceTokenInfo: ResourceTokenInfo | undefined;
};
[CosmosDbArtifactType.MIRRORED_AAD]: undefined;
[CosmosDbArtifactType.NATIVE]: undefined;
}
export interface FabricContext<T extends CosmosDbArtifactType> {
fabricClientRpcVersion: string;
isReadOnly: boolean; isReadOnly: boolean;
isVisible: boolean; isVisible: boolean;
databaseName: string;
artifactType: CosmosDbArtifactType;
artifactInfo: FabricArtifactInfo[T];
} }
export type AdminFeedbackControlPolicy = export type AdminFeedbackControlPolicy =
@ -70,7 +80,7 @@ export type AdminFeedbackPolicySettings = {
}; };
export interface UserContext { export interface UserContext {
readonly fabricContext?: FabricContext; readonly fabricContext?: FabricContext<CosmosDbArtifactType>;
readonly authType?: AuthType; readonly authType?: AuthType;
readonly masterKey?: string; readonly masterKey?: string;
readonly subscriptionId?: string; readonly subscriptionId?: string;

View File

@ -1,3 +1,4 @@
import { isFabric } from "Platform/Fabric/FabricUtil";
import { Platform, configContext } from "./../ConfigContext"; import { Platform, configContext } from "./../ConfigContext";
export const getDataExplorerWindow = (currentWindow: Window): Window | undefined => { export const getDataExplorerWindow = (currentWindow: Window): Window | undefined => {
@ -7,7 +8,7 @@ export const getDataExplorerWindow = (currentWindow: Window): Window | undefined
if (currentWindow.parent === currentWindow) { if (currentWindow.parent === currentWindow) {
return undefined; 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 // in Fabric data explorer is inside an extension iframe, so we have two parent iframes
return currentWindow; return currentWindow;
} }

View File

@ -2,11 +2,19 @@ import * as Constants from "Common/Constants";
import { createUri } from "Common/UrlUtility"; import { createUri } from "Common/UrlUtility";
import { DATA_EXPLORER_RPC_VERSION } from "Contracts/DataExplorerMessagesContract"; import { DATA_EXPLORER_RPC_VERSION } from "Contracts/DataExplorerMessagesContract";
import { FabricMessageTypes } from "Contracts/FabricMessageTypes"; 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 Explorer from "Explorer/Explorer";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane"; import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil"; import { isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil";
import { import {
AppStateComponentNames, AppStateComponentNames,
OPEN_TABS_SUBCOMPONENT_NAME, OPEN_TABS_SUBCOMPONENT_NAME,
@ -23,7 +31,7 @@ import { AccountKind, Flights } from "../Common/Constants";
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility"; import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import { handleCachedDataMessage, sendMessage, sendReadyMessage } from "../Common/MessageHandler"; 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 { ActionType, DataExplorerAction, TabKind } from "../Contracts/ActionContracts";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { DataExplorerInputsFrame } from "../Contracts/ViewModels"; import { DataExplorerInputsFrame } from "../Contracts/ViewModels";
@ -44,7 +52,7 @@ import {
} from "../Platform/Hosted/HostedUtils"; } from "../Platform/Hosted/HostedUtils";
import { extractFeatures } from "../Platform/Hosted/extractFeatures"; import { extractFeatures } from "../Platform/Hosted/extractFeatures";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext"; import { FabricArtifactInfo, Node, PortalEnv, updateUserContext, userContext } from "../UserContext";
import { import {
acquireMsalTokenForAccount, acquireMsalTokenForAccount,
acquireTokenWithMsal, acquireTokenWithMsal,
@ -104,7 +112,7 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
async function configureFabric(): Promise<Explorer> { async function configureFabric(): Promise<Explorer> {
// These are the versions of Fabric that Data Explorer supports. // 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 firstContainerOpened = false;
let explorer: Explorer; let explorer: Explorer;
@ -120,7 +128,7 @@ async function configureFabric(): Promise<Explorer> {
return; return;
} }
const data: FabricMessageV2 = event.data?.data; const data: FabricMessageV2 | FabricMessageV3 = event.data?.data;
if (!data) { if (!data) {
return; return;
} }
@ -129,38 +137,77 @@ async function configureFabric(): Promise<Explorer> {
case "initialize": { case "initialize": {
const fabricVersion = data.version; const fabricVersion = data.version;
if (!SUPPORTED_FABRIC_VERSIONS.includes(fabricVersion)) { 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}`); console.error(`Unsupported Fabric version: ${fabricVersion}`);
return; return;
} }
explorer = createExplorerFabric(data.message); if (fabricVersion === "2") {
await scheduleRefreshDatabaseResourceToken(true); // ----------------- TODO: Remove this when FabricMessageV2 is deprecated -----------------
resolve(explorer); const initializationMessage = data.message as {
await explorer.refreshAllDatabases(); connectionId: string;
if (userContext.fabricContext.isVisible) { isVisible: boolean;
firstContainerOpened = true; };
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
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<CosmosDbArtifactType>;
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; break;
} }
case "newContainer": case "newContainer":
explorer.onNewCollectionClicked(); explorer.onNewCollectionClicked();
break; break;
case "authorizationToken": case "authorizationToken":
case "allResourceTokens_v2": { case "allResourceTokens_v2":
case "accessToken": {
handleCachedDataMessage(data); handleCachedDataMessage(data);
break; break;
} }
case "explorerVisible": { case "explorerVisible": {
userContext.fabricContext.isVisible = data.message.visible; userContext.fabricContext.isVisible = data.message.visible;
if ( if (userContext.fabricContext.isVisible && !firstContainerOpened) {
userContext.fabricContext.isVisible && const { databaseName } = userContext.fabricContext;
!firstContainerOpened && if (databaseName !== undefined) {
userContext?.fabricContext?.databaseConnectionInfo?.databaseId !== undefined firstContainerOpened = true;
) { openFirstContainer(explorer, databaseName);
firstContainerOpened = true; }
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
} }
break; break;
} }
@ -420,13 +467,29 @@ function configureHostedWithResourceToken(config: ResourceToken): Explorer {
return 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({ updateUserContext({
fabricContext: { fabricContext: {
connectionId: params.connectionId, fabricClientRpcVersion,
databaseConnectionInfo: undefined,
isReadOnly: true, isReadOnly: true,
isVisible: params.isVisible ?? true, isVisible: params.isVisible ?? true,
databaseName: undefined,
artifactType: CosmosDbArtifactType.MIRRORED_KEY,
artifactInfo,
}, },
authType: AuthType.ConnectionString, authType: AuthType.ConnectionString,
databaseAccount: { databaseAccount: {
@ -440,11 +503,102 @@ function createExplorerFabric(params: { connectionId: string; isVisible: boolean
}, },
}, },
}); });
useTabs.getState().closeAllTabs();
const explorer = new Explorer(); const explorer = new Explorer();
return explorer; return explorer;
} }
/**
* Initialization for FabricMessageV3 and above
* @param params
* @returns
*/
const createExplorerFabric = (
params: InitializeMessageV3<CosmosDbArtifactType>,
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<CosmosDbArtifactType.NATIVE>;
// 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 { function configureWithEncryptedToken(config: EncryptedToken): Explorer {
const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind); const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind);
updateUserContext({ updateUserContext({

View File

@ -1,6 +1,7 @@
import { clamp } from "@fluentui/react"; import { clamp } from "@fluentui/react";
import { OpenTab } from "Contracts/ActionContracts"; import { OpenTab } from "Contracts/ActionContracts";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
import { import {
AppStateComponentNames, AppStateComponentNames,
OPEN_TABS_SUBCOMPONENT_NAME, OPEN_TABS_SUBCOMPONENT_NAME,
@ -11,7 +12,6 @@ import * as ViewModels from "../Contracts/ViewModels";
import { CollectionTabKind } from "../Contracts/ViewModels"; import { CollectionTabKind } from "../Contracts/ViewModels";
import NotebookTabV2 from "../Explorer/Tabs/NotebookV2Tab"; import NotebookTabV2 from "../Explorer/Tabs/NotebookV2Tab";
import TabsBase from "../Explorer/Tabs/TabsBase"; import TabsBase from "../Explorer/Tabs/TabsBase";
import { Platform, configContext } from "./../ConfigContext";
export interface TabsState { export interface TabsState {
openedTabs: TabsBase[]; openedTabs: TabsBase[];
@ -51,22 +51,11 @@ export enum ReactTabKind {
QueryCopilot, 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<TabsState> = create((set, get) => ({ export const useTabs: UseStore<TabsState> = create((set, get) => ({
openedTabs: [], openedTabs: [] as TabsBase[],
openedReactTabs: !isPlatformFabric ? [ReactTabKind.Home] : [], openedReactTabs: [ReactTabKind.Home],
activeTab: undefined, activeTab: undefined as TabsBase,
activeReactTab: !isPlatformFabric ? ReactTabKind.Home : undefined, activeReactTab: ReactTabKind.Home,
queryCopilotTabInitialInput: "", queryCopilotTabInitialInput: "",
isTabExecuting: false, isTabExecuting: false,
isQueryErrorThrown: false, isQueryErrorThrown: false,
@ -122,7 +111,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
} }
return true; return true;
}); });
if (updatedTabs.length === 0 && configContext.platform !== Platform.Fabric) { if (updatedTabs.length === 0 && !isFabricMirrored()) {
set({ activeTab: undefined, activeReactTab: undefined }); set({ activeTab: undefined, activeReactTab: undefined });
} }
@ -162,7 +151,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
} }
}); });
if (get().openedTabs.length === 0 && configContext.platform !== Platform.Fabric) { if (get().openedTabs.length === 0 && !isFabricMirrored()) {
set({ activeTab: undefined, activeReactTab: undefined }); set({ activeTab: undefined, activeReactTab: undefined });
} }
} }