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 { 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 **************

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

View File

@ -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<DataModels.Collection[]> {
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<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
const resourceIdObj = collectionResourceId.split("/");
const tokenDatabaseId = resourceIdObj[1];
@ -56,7 +55,8 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
if (
userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations &&
userContext.apiType !== "Tables"
userContext.apiType !== "Tables" &&
!isFabric()
) {
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 { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";
@ -11,7 +11,7 @@ import { handleError } from "../ErrorHandlingUtils";
import { readOfferWithSDK } from "./readOfferWithSDK";
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
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);
}

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 * 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<DataModels.Database[]> {
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<string>(); // databaseId
@ -46,13 +52,28 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
}));
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 {

View File

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

View File

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

View File

@ -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<HTMLElement>) => {

View File

@ -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<void> => {
if (configContext.platform === Platform.Fabric) {
scheduleRefreshDatabaseResourceToken(true).then(() => this.refreshAllDatabases());
if (isFabricMirroredKey()) {
scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
return;
}

View File

@ -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<Props> = ({ 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);

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.
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

View File

@ -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<AddCollectionPanelProps,
)}
<div className="panelMainContent">
<Stack hidden={userContext.apiType === "Tables"}>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Database {userContext.apiType === "Mongo" ? "name" : "id"}
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
true,
).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(
{!(isFabricNative() && this.props.databaseId !== undefined) && (
<Stack hidden={userContext.apiType === "Tables"}>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Database {userContext.apiType === "Mongo" ? "name" : "id"}
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
true,
).toLocaleLowerCase()}.`}
/>
</TooltipHost>
</Stack>
{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"
>
<Icon
iconName="Info"
className="panelInfoIcon"
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>
<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>
</TooltipHost>
</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 })
}
{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}
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
/>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
true,
).toLocaleLowerCase()} within the database.`}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
<span className="panelRadioBtnLabel">Create new</span>
<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>
)}
{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,
).toLocaleLowerCase()} within the database.`}
/>
</TooltipHost>
</Stack>
)}
>
<Icon
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 && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={true}
isSharded={this.state.isSharded}
isFreeTier={this.isFreeTierAccount()}
isQuickstart={this.props.isQuickstart}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
this.setState({ isThroughputCapExceeded })
}
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
/>
)}
</Stack>
)}
{!this.state.createNewDatabase && (
<Dropdown
ariaLabel="Choose an existing database"
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
style={{ width: 300, fontSize: 12 }}
placeholder="Choose an existing database"
options={this.getDatabaseOptions()}
onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) =>
this.setState({ selectedDatabaseId: database.key as string })
}
defaultSelectedKey={this.props.databaseId}
responsiveMode={999}
/>
)}
<Separator className="panelSeparator" />
</Stack>
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={true}
isSharded={this.state.isSharded}
isFreeTier={this.isFreeTierAccount()}
isQuickstart={this.props.isQuickstart}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
this.setState({ isThroughputCapExceeded })
}
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
/>
)}
</Stack>
)}
{!this.state.createNewDatabase && (
<Dropdown
ariaLabel="Choose an existing database"
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
style={{ width: 300, fontSize: 12 }}
placeholder="Choose an existing database"
options={this.getDatabaseOptions()}
onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) =>
this.setState({ selectedDatabaseId: database.key as string })
}
defaultSelectedKey={this.props.databaseId}
responsiveMode={999}
/>
)}
<Separator className="panelSeparator" />
</Stack>
)}
<Stack>
<Stack horizontal>
@ -666,7 +669,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack>
);
})}
{userContext.apiType === "SQL" && (
{!isFabricNative() && userContext.apiType === "SQL" && (
<Stack className="panelGroupSpacing">
<DefaultButton
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 horizontal>
<Text className="panelTextBold" variant="small">
@ -937,7 +940,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</CollapsibleSectionComponent>
</Stack>
)}
{userContext.apiType !== "Tables" && (
{!isFabricNative() && userContext.apiType !== "Tables" && (
<CollapsibleSectionComponent
title="Advanced"
isExpandedByDefault={false}
@ -1260,7 +1263,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
// }
private shouldShowCollectionThroughputInput(): boolean {
if (isServerlessAccount()) {
if (isFabricNative() || isServerlessAccount()) {
return false;
}
@ -1286,7 +1289,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
private shouldShowAnalyticalStoreOptions(): boolean {
if (configContext.platform === Platform.Emulator) {
if (isFabricNative() || configContext.platform === Platform.Emulator) {
return false;
}

View File

@ -1,5 +1,6 @@
import {
Button,
makeStyles,
Menu,
MenuButton,
MenuButtonProps,
@ -7,13 +8,12 @@ import {
MenuList,
MenuPopover,
MenuTrigger,
SplitButton,
makeStyles,
mergeClasses,
shorthands,
SplitButton,
} from "@fluentui/react-components";
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 { AddDatabasePanel } from "Explorer/Panes/AddDatabasePanel/AddDatabasePanel";
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 { useDatabases } from "Explorer/useDatabases";
import { KeyboardAction, KeyboardActionGroup, KeyboardActionHandler, useKeyboardActionGroup } from "KeyboardShortcuts";
import { isFabric, isFabricMirrored, isFabricNative } from "Platform/Fabric/FabricUtil";
import { userContext } from "UserContext";
import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils";
import { Allotment, AllotmentHandle } from "allotment";
@ -123,7 +124,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
const actions = useMemo<GlobalCommand[]>(() => {
if (
configContext.platform === Platform.Fabric ||
(isFabric() && userContext.fabricContext?.isReadOnly) ||
userContext.apiType === "Postgres" ||
userContext.apiType === "VCoreMongo"
) {
@ -137,12 +138,15 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
id: "new_collection",
label: `New ${getCollectionName()}`,
icon: <Add16Regular />,
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<SidebarProps> = ({ explorer }) => {
}, [setLoading]);
const hasGlobalCommands = !(
configContext.platform === Platform.Fabric ||
isFabricMirrored() ||
userContext.apiType === "Postgres" ||
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 { 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",
},
});

View File

@ -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<IDocumentsTabCompone
selectedColumnIds={selectedColumnIds}
columnDefinitions={columnDefinitions}
isRowSelectionDisabled={
isBulkDeleteDisabled ||
(configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly)
isBulkDeleteDisabled || (isFabric() && userContext.fabricContext?.isReadOnly)
}
onColumnSelectionChange={onColumnSelectionChange}
defaultColumnSelection={getInitialColumnSelection()}

View File

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

View File

@ -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<boolean>(showScriptsMenus);
this.showTriggers = 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 { 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<ResourceTreeProps> = ({ explorer }: Resource
: [];
}, [isSampleDataEnabled, sampleDataResourceTokenCollection]);
const headerNodes: TreeNode[] =
configContext.platform === Platform.Fabric
? []
: [
{
id: "home",
iconSrc: <Home16Regular />,
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: <Home16Regular />,
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) {

View File

@ -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<CosmosDbArtifactType>,
});
},
],
[
"for Cassandra API",
() =>

View File

@ -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 = <DatabaseRegular fontSize={16} />;

View File

@ -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<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()) {
return;
}
lastRequestTimestamp = Date.now();
try {
const fabricDatabaseConnectionInfo = await sendCachedDataMessage<FabricDatabaseConnectionInfo>(
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<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
* @param tokenTimestamp
* @returns
*/
export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Promise<void> => {
export const scheduleRefreshFabricToken = (refreshNow?: boolean): Promise<void> => {
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;

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 { 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<T extends CosmosDbArtifactType> {
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<CosmosDbArtifactType>;
readonly authType?: AuthType;
readonly masterKey?: string;
readonly subscriptionId?: string;

View File

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

View File

@ -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<Explorer> {
// 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<Explorer> {
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<Explorer> {
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<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;
}
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<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 {
const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind);
updateUserContext({

View File

@ -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<TabsState> = 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<TabsState> = 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<TabsState> = create((set, get) => ({
}
});
if (get().openedTabs.length === 0 && configContext.platform !== Platform.Fabric) {
if (get().openedTabs.length === 0 && !isFabricMirrored()) {
set({ activeTab: undefined, activeReactTab: undefined });
}
}