Merge branch 'master' of https://github.com/Azure/cosmos-explorer into feature/materialized-views

This commit is contained in:
Asier Isayas 2025-03-10 14:09:47 -04:00
commit 69cf523274
47 changed files with 1148 additions and 443 deletions

View File

@ -61,6 +61,8 @@
@GalleryBackgroundColor: #fdfdfd; @GalleryBackgroundColor: #fdfdfd;
@LinkColor: #2d6da4;
//Icons //Icons
@InfoIconColor: #0072c6; @InfoIconColor: #0072c6;
@WarningIconColor: #db7500; @WarningIconColor: #db7500;

View File

@ -1,13 +1,15 @@
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 { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils"; import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
import { EmulatorMasterKey, HttpHeaders } from "./Constants"; import { EmulatorMasterKey, HttpHeaders } from "./Constants";
@ -18,7 +20,7 @@ const _global = typeof self === "undefined" ? window : self;
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
const { verb, resourceId, resourceType, headers } = requestInfo; const { verb, resourceId, resourceType, headers } = requestInfo;
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL"; const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType);
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) { if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
Logger.logInfo( Logger.logInfo(
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `, `AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
@ -41,7 +43,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
return decodeURIComponent(headers.authorization); 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:
@ -53,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:
@ -65,7 +72,9 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
// For now, these operations aren't used, so fetching the authorization token is commented out. // 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,4 +1,5 @@
import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos"; import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos";
import { 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 { useDatabases } from "../../Explorer/useDatabases"; import { useDatabases } from "../../Explorer/useDatabases";
@ -24,7 +25,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
); );
try { try {
let collection: DataModels.Collection; let collection: DataModels.Collection;
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) { if (!isFabricNative() && userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
if (params.createNewDatabase) { if (params.createNewDatabase) {
const createDatabaseParams: DataModels.CreateDatabaseParams = { const createDatabaseParams: DataModels.CreateDatabaseParams = {
autoPilotMaxThroughput: params.autoPilotMaxThroughput, autoPilotMaxThroughput: params.autoPilotMaxThroughput,

View File

@ -1,3 +1,4 @@
import { isFabric } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { deleteCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { deleteCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
@ -12,7 +13,7 @@ import { handleError } from "../ErrorHandlingUtils";
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> { export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`); const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
try { try {
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) { if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) {
await deleteCollectionWithARM(databaseId, collectionId); await deleteCollectionWithARM(databaseId, collectionId);
} else { } else {
await client().database(databaseId).container(collectionId).delete(); await client().database(databaseId).container(collectionId).delete();

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, isFabricNative } 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,8 +11,9 @@ 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() || isFabricNative()) {
// TODO This works, but is very slow, because it requests the token, so we skip for now // For Fabric Mirroring, it is slow, because it requests the token and we don't need it.
// For Fabric Native, it is not supported.
console.error("Skiping readDatabaseOffer for Fabric"); console.error("Skiping readDatabaseOffer for Fabric");
return undefined; return undefined;
} }
@ -23,7 +24,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

@ -1,4 +1,5 @@
import { ContainerDefinition, RequestOptions } from "@azure/cosmos"; import { ContainerDefinition, RequestOptions } from "@azure/cosmos";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { Collection } from "../../Contracts/DataModels"; import { Collection } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@ -36,7 +37,8 @@ export async function updateCollection(
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations && !userContext.features.enableSDKoperations &&
userContext.apiType !== "Tables" userContext.apiType !== "Tables" &&
!isFabric()
) { ) {
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection); collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
} else { } else {

View File

@ -1,4 +1,5 @@
import { OfferDefinition, RequestOptions } from "@azure/cosmos"; import { OfferDefinition, RequestOptions } from "@azure/cosmos";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { Offer, SDKOfferDefinition, ThroughputBucket, UpdateOfferParams } from "../../Contracts/DataModels"; import { Offer, SDKOfferDefinition, ThroughputBucket, UpdateOfferParams } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@ -56,7 +57,7 @@ export const updateOffer = async (params: UpdateOfferParams): Promise<Offer> =>
const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`); const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`);
try { try {
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) { if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) {
if (params.collectionId) { if (params.collectionId) {
updatedOffer = await updateCollectionOfferWithARM(params); updatedOffer = await updateCollectionOfferWithARM(params);
} else if (userContext.apiType === "Tables") { } else if (userContext.apiType === "Tables") {

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,11 +1,13 @@
import { MaterializedViewsLabels } from "Common/Constants"; import { MaterializedViewsLabels } from "Common/Constants";
import { isMaterializedViewsEnabled } from "Common/DatabaseAccountUtility"; import { isMaterializedViewsEnabled } from "Common/DatabaseAccountUtility";
import { configContext, Platform } from "ConfigContext";
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import { import {
AddMaterializedViewPanel, AddMaterializedViewPanel,
AddMaterializedViewPanelProps, AddMaterializedViewPanelProps,
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel"; } from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
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";
@ -25,7 +27,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";
@ -47,7 +48,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;
} }
@ -59,7 +60,7 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
}, },
]; ];
if (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations) { if (!isFabricNative() && (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations)) {
items.push({ items.push({
iconSrc: DeleteDatabaseIcon, iconSrc: DeleteDatabaseIcon,
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => { onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
@ -151,7 +152,7 @@ export const createCollectionContextMenuButton = (
}); });
} }
if (configContext.platform !== Platform.Fabric) { if (!isFabric() || (isFabric() && !userContext.fabricContext?.isReadOnly)) {
items.push({ items.push({
iconSrc: DeleteCollectionIcon, iconSrc: DeleteCollectionIcon,
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => { onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {

View File

@ -35,12 +35,20 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
setIsThroughputCapExceeded, setIsThroughputCapExceeded,
onCostAcknowledgeChange, onCostAcknowledgeChange,
}: ThroughputInputProps) => { }: ThroughputInputProps) => {
const defaultThroughput: number = let defaultThroughput: number;
const workloadType: Constants.WorkloadType = getWorkloadType();
if (
isFreeTier || isFreeTier ||
isQuickstart || isQuickstart ||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(getWorkloadType()) [Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType)
? AutoPilotUtils.autoPilotThroughput1K ) {
: AutoPilotUtils.autoPilotThroughput4K; defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
} else if (workloadType === Constants.WorkloadType.Production) {
defaultThroughput = AutoPilotUtils.autoPilotThroughput10K;
} else {
defaultThroughput = AutoPilotUtils.autoPilotThroughput4K;
}
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true); const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
const [throughput, setThroughput] = useState<number>(defaultThroughput); const [throughput, setThroughput] = useState<number>(defaultThroughput);

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

@ -14,10 +14,6 @@
.flex-direction(@direction: row); .flex-direction(@direction: row);
padding: 4px 5px; padding: 4px 5px;
label {
padding: 0px;
}
.valueCol { .valueCol {
flex-grow: 1; flex-grow: 1;
padding-right: 5px; padding-right: 5px;
@ -63,6 +59,10 @@
height: 100%; height: 100%;
} }
.customTrashIcon {
padding-top: 33px;
}
.rightPaneTrashIconImg { .rightPaneTrashIconImg {
vertical-align: top; vertical-align: top;
} }

View File

@ -142,10 +142,11 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
<div className="labelCol"> <div className="labelCol">
<TextField <TextField
className="edgeInput" className="edgeInput"
label={index === 0 && "Key"}
type="text" type="text"
id="propertyKeyNewVertexPane" id="propertyKeyNewVertexPane"
componentRef={input} componentRef={input}
aria-required="true" required
placeholder="Key" placeholder="Key"
autoComplete="off" autoComplete="off"
aria-label={`Enter value for propery ${index + 1}`} aria-label={`Enter value for propery ${index + 1}`}
@ -153,11 +154,11 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onKeyChange(event, index)} onChange={(event: React.ChangeEvent<HTMLInputElement>) => onKeyChange(event, index)}
/> />
</div> </div>
<span className="mandatoryStar">*&nbsp;</span>
<div className="valueCol"> <div className="valueCol">
<TextField <TextField
className="edgeInput" className="edgeInput"
label={index === 0 && "Value"}
type="text" type="text"
placeholder="Value" placeholder="Value"
autoComplete="off" autoComplete="off"
@ -169,6 +170,8 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
<div> <div>
<Dropdown <Dropdown
role="combobox" role="combobox"
label={index === 0 && "Type"}
ariaLabel="Type"
placeholder="Select an option" placeholder="Select an option"
defaultSelectedKey={data.values[0].type} defaultSelectedKey={data.values[0].type}
style={{ width: 100 }} style={{ width: 100 }}
@ -181,7 +184,7 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
</div> </div>
<div className="actionCol"> <div className="actionCol">
<div <div
className="rightPaneTrashIcon rightPaneBtns" className={`rightPaneTrashIcon rightPaneBtns ${index === 0 && "customTrashIcon"}`}
tabIndex={0} tabIndex={0}
role="button" role="button"
aria-label={`Delete ${data.key}`} aria-label={`Delete ${data.key}`}

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

@ -37,21 +37,25 @@ describe("CommandBarComponentButtonFactory tests", () => {
expect(enableAzureSynapseLinkBtn).toBeDefined(); expect(enableAzureSynapseLinkBtn).toBeDefined();
}); });
it("Button should not be visible for Tables API", () => { // TODO: Now that Tables API supports dataplane RBAC, calling createStaticCommandBarButtons will enable the
updateUserContext({ // Entra ID Login button, which causes this test to fail due to "Invalid hook call.". This seems to be
databaseAccount: { // unsupported in jest and needs to be tested with react-hooks-testing-library.
properties: { //
capabilities: [{ name: "EnableTable" }], // it("Button should not be visible for Tables API", () => {
}, // updateUserContext({
} as DatabaseAccount, // databaseAccount: {
}); // properties: {
// capabilities: [{ name: "EnableTable" }],
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); // },
const enableAzureSynapseLinkBtn = buttons.find( // } as DatabaseAccount,
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel, // });
); //
expect(enableAzureSynapseLinkBtn).toBeUndefined(); // const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
}); // const enableAzureSynapseLinkBtn = buttons.find(
// (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
// );
// expect(enableAzureSynapseLinkBtn).toBeUndefined();
//});
it("Button should not be visible for Cassandra API", () => { it("Button should not be visible for Cassandra API", () => {
updateUserContext({ updateUserContext({

View File

@ -1,4 +1,5 @@
import { KeyboardAction } from "KeyboardShortcuts"; import { KeyboardAction } from "KeyboardShortcuts";
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
import * as React from "react"; import * as React from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg"; import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
@ -61,7 +62,7 @@ export function createStaticCommandBarButtons(
} }
} }
if (userContext.apiType === "SQL") { if (isDataplaneRbacSupported(userContext.apiType)) {
const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined); const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled); const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated); const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);

View File

@ -109,15 +109,15 @@ export class NotificationConsoleComponent extends React.Component<
<div className="statusBar"> <div className="statusBar">
<span className="dataTypeIcons"> <span className="dataTypeIcons">
<span className="notificationConsoleHeaderIconWithData"> <span className="notificationConsoleHeaderIconWithData">
<img src={LoadingIcon} alt="in progress items" /> <img src={LoadingIcon} alt="In progress items" />
<span className="numInProgress">{numInProgress}</span> <span className="numInProgress">{numInProgress}</span>
</span> </span>
<span className="notificationConsoleHeaderIconWithData"> <span className="notificationConsoleHeaderIconWithData">
<img src={ErrorBlackIcon} alt="error items" /> <img src={ErrorBlackIcon} alt="Error items" />
<span className="numErroredItems">{numErroredItems}</span> <span className="numErroredItems">{numErroredItems}</span>
</span> </span>
<span className="notificationConsoleHeaderIconWithData"> <span className="notificationConsoleHeaderIconWithData">
<img src={infoBubbleIcon} alt="info items" /> <img src={infoBubbleIcon} alt="Info items" />
<span className="numInfoItems">{numInfoItems}</span> <span className="numInfoItems">{numInfoItems}</span>
</span> </span>
</span> </span>
@ -134,12 +134,12 @@ export class NotificationConsoleComponent extends React.Component<
data-test="NotificationConsole/ExpandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton"
role="button" role="button"
tabIndex={0} tabIndex={0}
aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")} aria-label="Console"
aria-expanded={!this.props.isConsoleExpanded} aria-expanded={this.props.isConsoleExpanded}
> >
<img <img
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon} src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
alt={this.props.isConsoleExpanded ? "ChevronDownIcon" : "ChevronUpIcon"} alt={this.props.isConsoleExpanded ? "Collapse icon" : "Expand icon"}
/> />
</div> </div>
</div> </div>

View File

@ -21,7 +21,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
className="notificationConsoleHeaderIconWithData" className="notificationConsoleHeaderIconWithData"
> >
<img <img
alt="in progress items" alt="In progress items"
src={{}} src={{}}
/> />
<span <span
@ -34,7 +34,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
className="notificationConsoleHeaderIconWithData" className="notificationConsoleHeaderIconWithData"
> >
<img <img
alt="error items" alt="Error items"
src={{}} src={{}}
/> />
<span <span
@ -47,7 +47,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
className="notificationConsoleHeaderIconWithData" className="notificationConsoleHeaderIconWithData"
> >
<img <img
alt="info items" alt="Info items"
src={{}} src={{}}
/> />
<span <span
@ -71,15 +71,15 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
</span> </span>
</div> </div>
<div <div
aria-expanded={true} aria-expanded={false}
aria-label="console button collapsed" aria-label="Console"
className="expandCollapseButton" className="expandCollapseButton"
data-test="NotificationConsole/ExpandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton"
role="button" role="button"
tabIndex={0} tabIndex={0}
> >
<img <img
alt="ChevronUpIcon" alt="Expand icon"
src="" src=""
/> />
</div> </div>
@ -192,7 +192,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
className="notificationConsoleHeaderIconWithData" className="notificationConsoleHeaderIconWithData"
> >
<img <img
alt="in progress items" alt="In progress items"
src={{}} src={{}}
/> />
<span <span
@ -205,7 +205,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
className="notificationConsoleHeaderIconWithData" className="notificationConsoleHeaderIconWithData"
> >
<img <img
alt="error items" alt="Error items"
src={{}} src={{}}
/> />
<span <span
@ -218,7 +218,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
className="notificationConsoleHeaderIconWithData" className="notificationConsoleHeaderIconWithData"
> >
<img <img
alt="info items" alt="Info items"
src={{}} src={{}}
/> />
<span <span
@ -244,15 +244,15 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
</span> </span>
</div> </div>
<div <div
aria-expanded={true} aria-expanded={false}
aria-label="console button collapsed" aria-label="Console"
className="expandCollapseButton" className="expandCollapseButton"
data-test="NotificationConsole/ExpandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton"
role="button" role="button"
tabIndex={0} tabIndex={0}
> >
<img <img
alt="ChevronUpIcon" alt="Expand icon"
src="" src=""
/> />
</div> </div>

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

@ -42,6 +42,7 @@ import {
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
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";
@ -264,150 +265,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={isFreeTierAccount() && !isFirstResourceCreated} showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={true} isDatabase={true}
isSharded={this.state.isSharded} isSharded={this.state.isSharded}
isFreeTier={isFreeTierAccount()} isFreeTier={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>
@ -643,7 +646,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 } }}
@ -724,7 +727,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/> />
)} )}
{userContext.apiType === "SQL" && ( {!isFabricNative() && userContext.apiType === "SQL" && (
<Stack> <Stack>
{UniqueKeysHeader()} {UniqueKeysHeader()}
{this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => { {this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => {
@ -894,7 +897,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}
@ -1128,7 +1131,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
// } // }
private shouldShowCollectionThroughputInput(): boolean { private shouldShowCollectionThroughputInput(): boolean {
if (isServerlessAccount()) { if (isFabricNative() || isServerlessAccount()) {
return false; return false;
} }

View File

@ -3,6 +3,7 @@ import * as Constants from "Common/Constants";
import { configContext, Platform } from "ConfigContext"; import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels"; import * as DataModels from "Contracts/DataModels";
import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent"; import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import React from "react"; import React from "react";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
@ -84,7 +85,7 @@ export function UniqueKeysHeader(): JSX.Element {
} }
export function shouldShowAnalyticalStoreOptions(): boolean { export function shouldShowAnalyticalStoreOptions(): boolean {
if (configContext.platform === Platform.Emulator) { if (isFabricNative() || configContext.platform === Platform.Emulator) {
return false; return false;
} }

View File

@ -94,6 +94,7 @@
padding-left: @MediumSpace; padding-left: @MediumSpace;
.paneErrorLink { .paneErrorLink {
color: @LinkColor;
cursor: pointer; cursor: pointer;
font-size: @mediumFontSize; font-size: @mediumFontSize;
} }

View File

@ -32,6 +32,7 @@ import {
} from "Shared/StorageUtility"; } from "Shared/StorageUtility";
import * as StringUtility from "Shared/StringUtility"; import * as StringUtility from "Shared/StringUtility";
import { updateUserContext, userContext } from "UserContext"; import { updateUserContext, userContext } from "UserContext";
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils"; import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils"; import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
@ -183,7 +184,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin" && !isEmulator; const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin" && !isEmulator;
const shouldShowParallelismOption = userContext.apiType !== "Gremlin" && !isEmulator; const shouldShowParallelismOption = userContext.apiType !== "Gremlin" && !isEmulator;
const showEnableEntraIdRbac = const showEnableEntraIdRbac =
userContext.apiType === "SQL" && isDataplaneRbacSupported(userContext.apiType) &&
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
configContext.platform !== Platform.Fabric && configContext.platform !== Platform.Fabric &&
!isEmulator; !isEmulator;

View File

@ -393,8 +393,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
}, },
}} }}
disabled={isGeneratingQuery} disabled={isGeneratingQuery}
autoComplete="list" autoComplete="off"
aria-expanded={showSamplePrompts}
placeholder="Ask a question in natural language and well generate the query for you." placeholder="Ask a question in natural language and well generate the query for you."
aria-labelledby="copilot-textfield-label" aria-labelledby="copilot-textfield-label"
onRenderSuffix={() => { onRenderSuffix={() => {

View File

@ -1,5 +1,6 @@
import { import {
Button, Button,
makeStyles,
Menu, Menu,
MenuButton, MenuButton,
MenuButtonProps, MenuButtonProps,
@ -7,15 +8,14 @@ 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 { MaterializedViewsLabels } from "Common/Constants"; import { MaterializedViewsLabels } from "Common/Constants";
import { isMaterializedViewsEnabled } from "Common/DatabaseAccountUtility"; import { isMaterializedViewsEnabled } from "Common/DatabaseAccountUtility";
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 { import {
@ -27,6 +27,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";
@ -129,7 +130,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"
) { ) {
@ -143,12 +144,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()}`,
@ -313,7 +317,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";
@ -214,7 +214,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

@ -740,7 +740,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
] ]
`; `;
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric 1`] = ` exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric non read-only 1`] = `
[ [
{ {
"children": [ "children": [
@ -753,6 +753,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"label": "New SQL Query", "label": "New SQL Query",
"onClick": [Function], "onClick": [Function],
}, },
{
"iconSrc": {},
"label": "Delete Container",
"onClick": [Function],
"styleClass": "deleteCollectionMenuItem",
},
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <DocumentMultipleRegular
fontSize={16} fontSize={16}
@ -774,6 +780,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"label": "New SQL Query", "label": "New SQL Query",
"onClick": [Function], "onClick": [Function],
}, },
{
"iconSrc": {},
"label": "Delete Container",
"onClick": [Function],
"styleClass": "deleteCollectionMenuItem",
},
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <DocumentMultipleRegular
fontSize={16} fontSize={16}
@ -822,6 +834,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"label": "New SQL Query", "label": "New SQL Query",
"onClick": [Function], "onClick": [Function],
}, },
{
"iconSrc": {},
"label": "Delete Container",
"onClick": [Function],
"styleClass": "deleteCollectionMenuItem",
},
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <DocumentMultipleRegular
fontSize={16} fontSize={16}
@ -870,6 +888,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"label": "New SQL Query", "label": "New SQL Query",
"onClick": [Function], "onClick": [Function],
}, },
{
"iconSrc": {},
"label": "Delete Container",
"onClick": [Function],
"styleClass": "deleteCollectionMenuItem",
},
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <DocumentMultipleRegular
fontSize={16} fontSize={16}
@ -915,6 +939,145 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
] ]
`; `;
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric read-only 1`] = `
[
{
"children": [
{
"children": undefined,
"className": "collectionNode",
"contextMenu": [
{
"iconSrc": {},
"label": "New SQL Query",
"onClick": [Function],
},
],
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
"isSelected": [Function],
"label": "standardCollection",
"onClick": [Function],
"onCollapsed": [Function],
"onContextMenuOpen": [Function],
"onExpanded": [Function],
},
{
"children": undefined,
"className": "collectionNode",
"contextMenu": [
{
"iconSrc": {},
"label": "New SQL Query",
"onClick": [Function],
},
],
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
"isSelected": [Function],
"label": "conflictsCollection",
"onClick": [Function],
"onCollapsed": [Function],
"onContextMenuOpen": [Function],
"onExpanded": [Function],
},
],
"className": "databaseNode",
"contextMenu": undefined,
"iconSrc": <DatabaseRegular
fontSize={16}
/>,
"isExpanded": true,
"isSelected": [Function],
"label": "standardDb",
"onCollapsed": [Function],
"onContextMenuOpen": [Function],
"onExpanded": [Function],
},
{
"children": [
{
"children": undefined,
"className": "collectionNode",
"contextMenu": [
{
"iconSrc": {},
"label": "New SQL Query",
"onClick": [Function],
},
],
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
"isSelected": [Function],
"label": "sampleItemsCollection",
"onClick": [Function],
"onCollapsed": [Function],
"onContextMenuOpen": [Function],
"onExpanded": [Function],
},
],
"className": "databaseNode",
"contextMenu": undefined,
"iconSrc": <DatabaseRegular
fontSize={16}
/>,
"isExpanded": true,
"isSelected": [Function],
"label": "sharedDatabase",
"onCollapsed": [Function],
"onContextMenuOpen": [Function],
"onExpanded": [Function],
},
{
"children": [
{
"children": undefined,
"className": "collectionNode",
"contextMenu": [
{
"iconSrc": {},
"label": "New SQL Query",
"onClick": [Function],
},
],
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
"isSelected": [Function],
"label": "schemaCollection",
"onClick": [Function],
"onCollapsed": [Function],
"onContextMenuOpen": [Function],
"onExpanded": [Function],
},
{
"className": "loadMoreNode",
"label": "load more",
"onClick": [Function],
},
],
"className": "databaseNode",
"contextMenu": undefined,
"iconSrc": <DatabaseRegular
fontSize={16}
/>,
"isExpanded": true,
"isSelected": [Function],
"label": "giganticDatabase",
"onCollapsed": [Function],
"onContextMenuOpen": [Function],
"onExpanded": [Function],
},
]
`;
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Portal 1`] = ` exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Portal 1`] = `
[ [
{ {
@ -972,7 +1135,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
}, },
], ],
"isSelected": [Function], "isSelected": [Function],
"label": "mockSproc3", "label": "mockSproc4",
"onClick": [Function], "onClick": [Function],
}, },
], ],
@ -990,7 +1153,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
}, },
], ],
"isSelected": [Function], "isSelected": [Function],
"label": "mockUdf3", "label": "mockUdf4",
"onClick": [Function], "onClick": [Function],
}, },
], ],
@ -1008,7 +1171,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
}, },
], ],
"isSelected": [Function], "isSelected": [Function],
"label": "mockTrigger3", "label": "mockTrigger4",
"onClick": [Function], "onClick": [Function],
}, },
], ],

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, UserContext } 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";
@ -360,9 +361,30 @@ describe("createDatabaseTreeNodes", () => {
}); });
}); });
it.each<[string, Platform, boolean, Partial<DataModels.DatabaseAccountExtendedProperties>]>([ it.each<[string, Platform, boolean, Partial<DataModels.DatabaseAccountExtendedProperties>, Partial<UserContext>]>([
["the SQL API, on Fabric", Platform.Fabric, false, { capabilities: [], enableMultipleWriteLocations: true }], [
["the SQL API, on Portal", Platform.Portal, false, { capabilities: [], enableMultipleWriteLocations: true }], "the SQL API, on Fabric read-only",
Platform.Fabric,
false,
{ capabilities: [], enableMultipleWriteLocations: true },
{ fabricContext: { isReadOnly: true } as FabricContext<CosmosDbArtifactType> },
],
[
"the SQL API, on Fabric non read-only",
Platform.Fabric,
false,
{ capabilities: [], enableMultipleWriteLocations: true },
{ fabricContext: { isReadOnly: false } as FabricContext<CosmosDbArtifactType> },
],
[
"the SQL API, on Portal",
Platform.Portal,
false,
{ capabilities: [], enableMultipleWriteLocations: true },
{
fabricContext: undefined,
},
],
[ [
"the Cassandra API, serverless, on Hosted", "the Cassandra API, serverless, on Hosted",
Platform.Hosted, Platform.Hosted,
@ -373,6 +395,7 @@ describe("createDatabaseTreeNodes", () => {
{ name: CapabilityNames.EnableServerless, description: "" }, { name: CapabilityNames.EnableServerless, description: "" },
], ],
}, },
{ fabricContext: undefined },
], ],
[ [
"the Mongo API, with Notebooks and Phoenix features, on Emulator", "the Mongo API, with Notebooks and Phoenix features, on Emulator",
@ -381,26 +404,31 @@ describe("createDatabaseTreeNodes", () => {
{ {
capabilities: [{ name: CapabilityNames.EnableMongo, description: "" }], capabilities: [{ name: CapabilityNames.EnableMongo, description: "" }],
}, },
{ fabricContext: undefined },
], ],
])("generates the correct tree structure for %s", (_, platform, isNotebookEnabled, dbAccountProperties) => { ])(
useNotebook.setState({ isPhoenixFeatures: isNotebookEnabled }); "generates the correct tree structure for %s",
updateConfigContext({ platform }); (_, platform, isNotebookEnabled, dbAccountProperties, userContext) => {
updateUserContext({ useNotebook.setState({ isPhoenixFeatures: isNotebookEnabled });
databaseAccount: { updateConfigContext({ platform });
properties: { updateUserContext({
enableMultipleWriteLocations: true, ...userContext,
...dbAccountProperties, databaseAccount: {
}, properties: {
} as unknown as DataModels.DatabaseAccount, enableMultipleWriteLocations: true,
}); ...dbAccountProperties,
const nodes = createDatabaseTreeNodes( },
explorer, } as unknown as DataModels.DatabaseAccount,
isNotebookEnabled, });
useDatabases.getState().databases, const nodes = createDatabaseTreeNodes(
refreshActiveTab, explorer,
); isNotebookEnabled,
expect(nodes).toMatchSnapshot(); useDatabases.getState().databases,
}); refreshActiveTab,
);
expect(nodes).toMatchSnapshot();
},
);
// The above tests focused on the tree structure. The below tests focus on some core behaviors of the nodes. // The above tests focused on the tree structure. The below tests focus on some core behaviors of the nodes.
// They are not exhaustive, because exhaustive tests here require a lot of mocking and can become very brittle. // They are not exhaustive, because exhaustive tests here require a lot of mocking and can become very brittle.
@ -551,7 +579,18 @@ describe("createDatabaseTreeNodes", () => {
}); });
it.each([ it.each([
["in Fabric", () => updateConfigContext({ platform: Platform.Fabric })], [
"in Fabric",
() => {
updateConfigContext({ platform: Platform.Fabric });
updateUserContext({
fabricContext: {
artifactType: CosmosDbArtifactType.MIRRORED_KEY,
isReadOnly: true,
} 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

@ -41,13 +41,13 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
case SelfServeType.example: { case SelfServeType.example: {
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample"); const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
const selfServeExample = new SelfServeExample.default(); const selfServeExample = new SelfServeExample.default();
await loadTranslations(selfServeExample.constructor.name); await loadTranslations(selfServeType);
return selfServeExample.toSelfServeDescriptor(); return selfServeExample.toSelfServeDescriptor();
} }
case SelfServeType.sqlx: { case SelfServeType.sqlx: {
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX"); const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
const sqlX = new SqlX.default(); const sqlX = new SqlX.default();
await loadTranslations(sqlX.constructor.name); await loadTranslations(selfServeType);
return sqlX.toSelfServeDescriptor(); return sqlX.toSelfServeDescriptor();
} }
case SelfServeType.graphapicompute: { case SelfServeType.graphapicompute: {
@ -55,7 +55,7 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
/* webpackChunkName: "GraphAPICompute" */ "./GraphAPICompute/GraphAPICompute" /* webpackChunkName: "GraphAPICompute" */ "./GraphAPICompute/GraphAPICompute"
); );
const graphAPICompute = new GraphAPICompute.default(); const graphAPICompute = new GraphAPICompute.default();
await loadTranslations(graphAPICompute.constructor.name); await loadTranslations(selfServeType);
return graphAPICompute.toSelfServeDescriptor(); return graphAPICompute.toSelfServeDescriptor();
} }
case SelfServeType.materializedviewsbuilder: { case SelfServeType.materializedviewsbuilder: {
@ -63,7 +63,7 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
/* webpackChunkName: "MaterializedViewsBuilder" */ "./MaterializedViewsBuilder/MaterializedViewsBuilder" /* webpackChunkName: "MaterializedViewsBuilder" */ "./MaterializedViewsBuilder/MaterializedViewsBuilder"
); );
const materializedViewsBuilder = new MaterializedViewsBuilder.default(); const materializedViewsBuilder = new MaterializedViewsBuilder.default();
await loadTranslations(materializedViewsBuilder.constructor.name); await loadTranslations(selfServeType);
return materializedViewsBuilder.toSelfServeDescriptor(); return materializedViewsBuilder.toSelfServeDescriptor();
} }
default: default:
@ -103,7 +103,7 @@ const handleMessage = async (event: MessageEvent): Promise<void> => {
const urlSearchParams = new URLSearchParams(window.location.search); const urlSearchParams = new URLSearchParams(window.location.search);
const selfServeTypeText = urlSearchParams.get("selfServeType") || inputs.selfServeType; const selfServeTypeText = urlSearchParams.get("selfServeType") || inputs.selfServeType;
const selfServeType = SelfServeType[selfServeTypeText?.toLowerCase() as keyof typeof SelfServeType]; const selfServeType = SelfServeType[selfServeTypeText.toLocaleLowerCase() as keyof typeof SelfServeType];
if ( if (
!inputs.subscriptionId || !inputs.subscriptionId ||
!inputs.resourceGroup || !inputs.resourceGroup ||

View File

@ -29,10 +29,11 @@ export enum SelfServeType {
// Unsupported self serve type passed as feature flag // Unsupported self serve type passed as feature flag
invalid = "invalid", invalid = "invalid",
// Add your self serve types here // Add your self serve types here
example = "example", // NOTE: text and casing of the enum's value must match the corresponding file in Localization\en\
sqlx = "sqlx", example = "SelfServeExample",
graphapicompute = "graphapicompute", sqlx = "SqlX",
materializedviewsbuilder = "materializedviewsbuilder", graphapicompute = "GraphAPICompute",
materializedviewsbuilder = "MaterializedViewsBuilder",
} }
/** /**

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

@ -89,3 +89,7 @@ export const getItemName = (): string => {
return "Items"; return "Items";
} }
}; };
export const isDataplaneRbacSupported = (apiType: string): boolean => {
return apiType === "SQL" || apiType === "Tables";
};

View File

@ -1,6 +1,7 @@
export const autoPilotThroughput1K = 1000; export const autoPilotThroughput1K = 1000;
export const autoPilotIncrementStep = 1000; export const autoPilotIncrementStep = 1000;
export const autoPilotThroughput4K = 4000; export const autoPilotThroughput4K = 4000;
export const autoPilotThroughput10K = 10000;
export function isValidAutoPilotThroughput(maxThroughput: number): boolean { export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
if (!maxThroughput) { if (!maxThroughput) {

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,17 +2,26 @@ 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,
readSubComponentState, readSubComponentState,
} from "Shared/AppStatePersistenceUtility"; } from "Shared/AppStatePersistenceUtility";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
import { logConsoleError } from "Utils/NotificationConsoleUtils"; import { logConsoleError } from "Utils/NotificationConsoleUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
@ -22,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";
@ -43,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,
@ -103,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;
@ -119,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;
} }
@ -128,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;
} }
@ -299,7 +347,7 @@ async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
); );
if (!userContext.features.enableAadDataPlane) { if (!userContext.features.enableAadDataPlane) {
Logger.logInfo(`AAD Feature flag is not enabled for account ${account.name}`, "Explorer/configureHostedWithAAD"); Logger.logInfo(`AAD Feature flag is not enabled for account ${account.name}`, "Explorer/configureHostedWithAAD");
if (userContext.apiType === "SQL") { if (isDataplaneRbacSupported(userContext.apiType)) {
if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) {
const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled);
Logger.logInfo( Logger.logInfo(
@ -419,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: {
@ -439,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({
@ -552,7 +707,7 @@ async function configurePortal(): Promise<Explorer> {
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext; const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
let dataPlaneRbacEnabled; let dataPlaneRbacEnabled;
if (userContext.apiType === "SQL") { if (isDataplaneRbacSupported(userContext.apiType)) {
if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) {
const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled);
Logger.logInfo( Logger.logInfo(

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