diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index f7a4fbfc5..be703e73c 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -1,7 +1,7 @@ import * as Cosmos from "@azure/cosmos"; import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens"; import { AuthorizationToken } from "Contracts/FabricMessageTypes"; -import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil"; +import { checkDatabaseResourceTokensValidity, isFabricMirrored } from "Platform/Fabric/FabricUtil"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils"; import { AuthType } from "../AuthType"; @@ -42,7 +42,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { return decodeURIComponent(headers.authorization); } - if (configContext.platform === Platform.Fabric) { + if (isFabricMirrored()) { switch (requestInfo.resourceType) { case Cosmos.ResourceType.conflicts: case Cosmos.ResourceType.container: @@ -54,8 +54,8 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { // User resource tokens // TODO userContext.fabricContext.databaseConnectionInfo can be undefined headers[HttpHeaders.msDate] = new Date().toUTCString(); - const resourceTokens = userContext.fabricContext.databaseConnectionInfo.resourceTokens; - checkDatabaseResourceTokensValidity(userContext.fabricContext.databaseConnectionInfo.resourceTokensTimestamp); + const resourceTokens = userContext.fabricContext.mirroredConnectionInfo.resourceTokens; + checkDatabaseResourceTokensValidity(userContext.fabricContext.mirroredConnectionInfo.resourceTokensTimestamp); return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId); case Cosmos.ResourceType.none: @@ -66,7 +66,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { // For now, these operations aren't used, so fetching the authorization token is commented out. // This provider must return a real token to pass validation by the client, so we return the cached resource token // (which is a valid token, but won't work for these operations). - const resourceTokens2 = userContext.fabricContext.databaseConnectionInfo.resourceTokens; + const resourceTokens2 = userContext.fabricContext.mirroredConnectionInfo.resourceTokens; return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId); /* ************** TODO: Uncomment this code if we need to support these operations ************** diff --git a/src/Common/dataAccess/readCollections.ts b/src/Common/dataAccess/readCollections.ts index 4098b3fe8..33f7586bc 100644 --- a/src/Common/dataAccess/readCollections.ts +++ b/src/Common/dataAccess/readCollections.ts @@ -1,6 +1,6 @@ import { ContainerResponse } from "@azure/cosmos"; import { Queries } from "Common/Constants"; -import { Platform, configContext } from "ConfigContext"; +import { isFabricMirrored } from "Platform/Fabric/FabricUtil"; import { AuthType } from "../../AuthType"; import * as DataModels from "../../Contracts/DataModels"; import { userContext } from "../../UserContext"; @@ -16,15 +16,11 @@ import { handleError } from "../ErrorHandlingUtils"; export async function readCollections(databaseId: string): Promise { const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`); - if ( - configContext.platform === Platform.Fabric && - userContext.fabricContext && - userContext.fabricContext.databaseConnectionInfo.databaseId === databaseId - ) { + if (isFabricMirrored() && userContext.fabricContext?.mirroredConnectionInfo.databaseId === databaseId) { const collections: DataModels.Collection[] = []; const promises: Promise[] = []; - for (const collectionResourceId in userContext.fabricContext.databaseConnectionInfo.resourceTokens) { + for (const collectionResourceId in userContext.fabricContext.mirroredConnectionInfo.resourceTokens) { // Dictionary key looks like this: dbs/SampleDB/colls/Container const resourceIdObj = collectionResourceId.split("/"); const tokenDatabaseId = resourceIdObj[1]; diff --git a/src/Common/dataAccess/readDatabaseOffer.ts b/src/Common/dataAccess/readDatabaseOffer.ts index 2f990d9a3..8eeb49742 100644 --- a/src/Common/dataAccess/readDatabaseOffer.ts +++ b/src/Common/dataAccess/readDatabaseOffer.ts @@ -1,4 +1,4 @@ -import { Platform, configContext } from "ConfigContext"; +import { isFabricMirrored } from "Platform/Fabric/FabricUtil"; import { AuthType } from "../../AuthType"; import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels"; import { userContext } from "../../UserContext"; @@ -11,7 +11,7 @@ import { handleError } from "../ErrorHandlingUtils"; import { readOfferWithSDK } from "./readOfferWithSDK"; export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise => { - if (configContext.platform === Platform.Fabric) { + if (isFabricMirrored()) { // TODO This works, but is very slow, because it requests the token, so we skip for now console.error("Skiping readDatabaseOffer for Fabric"); return undefined; diff --git a/src/Common/dataAccess/readDatabases.ts b/src/Common/dataAccess/readDatabases.ts index 9cc0b0641..199cf6eb4 100644 --- a/src/Common/dataAccess/readDatabases.ts +++ b/src/Common/dataAccess/readDatabases.ts @@ -1,4 +1,4 @@ -import { Platform, configContext } from "ConfigContext"; +import { isFabricMirrored } from "Platform/Fabric/FabricUtil"; import { AuthType } from "../../AuthType"; import * as DataModels from "../../Contracts/DataModels"; import { userContext } from "../../UserContext"; @@ -14,8 +14,8 @@ export async function readDatabases(): Promise { let databases: DataModels.Database[]; const clearMessage = logConsoleProgress(`Querying databases`); - if (configContext.platform === Platform.Fabric && userContext.fabricContext?.databaseConnectionInfo.resourceTokens) { - const tokensData = userContext.fabricContext.databaseConnectionInfo; + if (isFabricMirrored() && userContext.fabricContext?.mirroredConnectionInfo.resourceTokens) { + const tokensData = userContext.fabricContext.mirroredConnectionInfo; const databaseIdsSet = new Set(); // databaseId diff --git a/src/Contracts/FabricMessagesContract.ts b/src/Contracts/FabricMessagesContract.ts index dcf5a5a50..7594972de 100644 --- a/src/Contracts/FabricMessagesContract.ts +++ b/src/Contracts/FabricMessagesContract.ts @@ -1,47 +1,9 @@ -import { AuthorizationToken } from "Contracts/FabricMessageTypes"; +import { AuthorizationToken } from "./FabricMessageTypes"; // This is the version of these messages export const FABRIC_RPC_VERSION = "2"; // Fabric to Data Explorer - -// TODO Deprecated. Remove this section once DE is updated -export type FabricMessageV1 = - | { - type: "newContainer"; - databaseName: string; - } - | { - type: "initialize"; - message: { - endpoint: string | undefined; - databaseId: string | undefined; - resourceTokens: unknown | undefined; - resourceTokensTimestamp: number | undefined; - error: string | undefined; - }; - } - | { - type: "authorizationToken"; - message: { - id: string; - error: string | undefined; - data: AuthorizationToken | undefined; - }; - } - | { - type: "allResourceTokens"; - message: { - id: string; - error: string | undefined; - endpoint: string | undefined; - databaseId: string | undefined; - resourceTokens: unknown | undefined; - resourceTokensTimestamp: number | undefined; - }; - }; -// ----------------------------- - export type FabricMessageV2 = | { type: "newContainer"; @@ -54,6 +16,11 @@ export type FabricMessageV2 = message: { connectionId: string; isVisible: boolean; + isReadOnly: boolean; + artifactType: CosmosDbArtifactType; + + // For Native artifacts + nativeConnectionInfo?: FabricNativeDatabaseConnectionInfo; }; } | { @@ -69,7 +36,7 @@ export type FabricMessageV2 = message: { id: string; error: string | undefined; - data: FabricDatabaseConnectionInfo | undefined; + data: FabricMirroredDatabaseConnectionInfo | undefined; }; } | { @@ -79,17 +46,29 @@ export type FabricMessageV2 = }; }; -export type CosmosDBTokenResponse = { +export enum CosmosDbArtifactType { + MIRRORED = "MIRRORED", + NATIVE = "NATIVE", +} + +export interface FabricNativeDatabaseConnectionInfo { + accessToken: string; + accountName: string; + databaseName: string; + connectionString: string; +} + +export interface CosmosDBTokenResponse { token: string; date: string; -}; +} -export type CosmosDBConnectionInfoResponse = { +export interface CosmosDBConnectionInfoResponse { endpoint: string; databaseId: string; - resourceTokens: { [resourceId: string]: string }; -}; + resourceTokens: Record; +} -export interface FabricDatabaseConnectionInfo extends CosmosDBConnectionInfoResponse { +export interface FabricMirroredDatabaseConnectionInfo extends CosmosDBConnectionInfoResponse { resourceTokensTimestamp: number; } diff --git a/src/Explorer/ContextMenuButtonFactory.tsx b/src/Explorer/ContextMenuButtonFactory.tsx index 8946c1e18..f91d2fbe6 100644 --- a/src/Explorer/ContextMenuButtonFactory.tsx +++ b/src/Explorer/ContextMenuButtonFactory.tsx @@ -1,5 +1,7 @@ +import { configContext, Platform } from "ConfigContext"; import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { useDatabases } from "Explorer/useDatabases"; +import { isFabricMirrored } from "Platform/Fabric/FabricUtil"; import { Action } from "Shared/Telemetry/TelemetryConstants"; import { traceOpen } from "Shared/Telemetry/TelemetryProcessor"; import { ReactTabKind, useTabs } from "hooks/useTabs"; @@ -19,7 +21,6 @@ import * as ViewModels from "../Contracts/ViewModels"; import { userContext } from "../UserContext"; import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils"; import { useSidePanel } from "../hooks/useSidePanel"; -import { Platform, configContext } from "./../ConfigContext"; import Explorer from "./Explorer"; import { useNotebook } from "./Notebook/useNotebook"; import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane"; @@ -41,7 +42,7 @@ export interface DatabaseContextMenuButtonParams { * New resource tree (in ReactJS) */ export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => { - if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) { + if (isFabricMirrored() && userContext.fabricContext?.isReadOnly) { return undefined; } diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index dea7f7e95..c23069022 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -8,7 +8,7 @@ import { MessageTypes } from "Contracts/ExplorerContracts"; import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane"; import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { IGalleryItem } from "Juno/JunoClient"; -import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil"; +import { isFabricMirrored, scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils"; import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils"; @@ -378,7 +378,7 @@ export default class Explorer { }; public onRefreshResourcesClick = async (): Promise => { - if (configContext.platform === Platform.Fabric) { + if (isFabricMirrored()) { scheduleRefreshDatabaseResourceToken(true).then(() => this.refreshAllDatabases()); return; } diff --git a/src/Explorer/OpenActions/OpenActions.tsx b/src/Explorer/OpenActions/OpenActions.tsx index 53fb896b5..382f05dc2 100644 --- a/src/Explorer/OpenActions/OpenActions.tsx +++ b/src/Explorer/OpenActions/OpenActions.tsx @@ -1,6 +1,6 @@ // TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled. -import { configContext, Platform } from "ConfigContext"; import { useDatabases } from "Explorer/useDatabases"; +import { isFabricMirrored } from "Platform/Fabric/FabricUtil"; import React from "react"; import { ActionContracts } from "../../Contracts/ExplorerContracts"; import * as ViewModels from "../../Contracts/ViewModels"; @@ -58,9 +58,9 @@ function openCollectionTab( } if ( - configContext.platform === Platform.Fabric && + isFabricMirrored() && !( - // whitelist the tab kinds that are allowed to be opened in Fabric + // whitelist the tab kinds that are allowed to be opened in Fabric mirrored ( action.tabKind === ActionContracts.TabKind.SQLDocuments || action.tabKind === ActionContracts.TabKind.SQLQuery diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index 523da6619..4d127aa60 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -28,6 +28,7 @@ import { import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent"; import { useSidePanel } from "hooks/useSidePanel"; import { useTeachingBubble } from "hooks/useTeachingBubble"; +import { isFabricNative } from "Platform/Fabric/FabricUtil"; import React from "react"; import { CollectionCreation } from "Shared/Constants"; import { Action } from "Shared/Telemetry/TelemetryConstants"; @@ -284,150 +285,152 @@ export class AddCollectionPanel extends React.Component - + )} + {!this.state.createNewDatabase && ( + , database: IDropdownOption) => + this.setState({ selectedDatabaseId: database.key as string }) + } + defaultSelectedKey={this.props.databaseId} + responsiveMode={999} + /> + )} + + + )} @@ -666,7 +669,7 @@ export class AddCollectionPanel extends React.Component ); })} - {userContext.apiType === "SQL" && ( + {!isFabricNative() && userContext.apiType === "SQL" && ( )} - {userContext.apiType === "SQL" && ( + {!isFabricNative() && userContext.apiType === "SQL" && ( @@ -936,7 +939,7 @@ export class AddCollectionPanel extends React.Component )} - {userContext.apiType !== "Tables" && ( + {!isFabricNative() && userContext.apiType !== "Tables" && ( = ({ explorer }) => { const [globalCommandButton, setGlobalCommandButton] = useState(null); const actions = useMemo(() => { - if ( - configContext.platform === Platform.Fabric || - userContext.apiType === "Postgres" || - userContext.apiType === "VCoreMongo" - ) { + if (isFabricMirrored() || userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") { // No Global Commands for these API types. // In fact, no sidebar for Postgres or VCoreMongo at all, but just in case, we check here anyway. return []; @@ -135,12 +132,17 @@ const GlobalCommands: React.FC = ({ explorer }) => { id: "new_collection", label: `New ${getCollectionName()}`, icon: , - onClick: () => explorer.onNewCollectionClicked(), + onClick: () => { + const databaseId = isFabricNative() + ? userContext.fabricContext?.nativeConnectionInfo?.databaseName + : undefined; + explorer.onNewCollectionClicked({ databaseId }); + }, keyboardAction: KeyboardAction.NEW_COLLECTION, }, ]; - if (userContext.apiType !== "Tables") { + if (configContext.platform !== Platform.Fabric && userContext.apiType !== "Tables") { actions.push({ id: "new_database", label: `New ${getDatabaseName()}`, @@ -276,7 +278,7 @@ export const SidebarContainer: React.FC = ({ explorer }) => { }, [setLoading]); const hasGlobalCommands = !( - configContext.platform === Platform.Fabric || + isFabricMirrored() || userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo" ); diff --git a/src/Explorer/SplashScreen/FabricHome.tsx b/src/Explorer/SplashScreen/FabricHome.tsx new file mode 100644 index 000000000..72eca2d9f --- /dev/null +++ b/src/Explorer/SplashScreen/FabricHome.tsx @@ -0,0 +1,175 @@ +/** + * Accordion top class + */ +import { Link, makeStyles, tokens } from "@fluentui/react-components"; +import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons"; +import { isFabricNative } from "Platform/Fabric/FabricUtil"; +import * as React from "react"; +import { userContext } from "UserContext"; +import CosmosDbBlackIcon from "../../../images/CosmosDB_black.svg"; +import LinkIcon from "../../../images/Link_blue.svg"; +import Explorer from "../Explorer"; + +export interface SplashScreenProps { + explorer: Explorer; +} + +const useStyles = makeStyles({ + homeContainer: { + width: "100%", + alignContent: "center", + }, + title: { + textAlign: "center", + fontSize: "20px", + fontWeight: "bold", + }, + buttonsContainer: { + width: "584px", + margin: "auto", + display: "grid", + padding: "16px", + gridTemplateColumns: "repeat(3, 1fr)", + gap: "10px", + gridAutoRows: "minmax(184px, auto)", + }, + one: { + gridColumn: "1 / 3", + gridRow: "1 / 3", + "& svg": { + width: "48px", + height: "48px", + margin: "auto", + }, + }, + two: { + gridColumn: "3", + gridRow: "1", + "& img": { + width: "32px", + height: "32px", + margin: "auto", + }, + }, + three: { + gridColumn: "3", + gridRow: "2", + "& svg": { + width: "32px", + height: "32px", + margin: "auto", + }, + }, + buttonContainer: { + height: "100%", + display: "flex", + flexDirection: "column", + border: "1px solid #e0e0e0", + cursor: "pointer", + "&:hover": { + backgroundColor: tokens.colorNeutralBackground1Hover, + "border-color": tokens.colorNeutralStroke1Hover, + }, + }, + buttonUpperPart: { + textAlign: "center", + flexGrow: 1, + display: "flex", + backgroundColor: "#e3f7ef", + }, + buttonLowerPart: { + borderTop: "1px solid #e0e0e0", + height: "76px", + padding: "8px", + "> div:nth-child(1)": { + fontWeight: "bold", + }, + display: "flex", + flexDirection: "column", + justifyContent: "center", + }, + footer: { + textAlign: "center", + }, +}); + +interface FabricHomeScreenButtonProps { + title: string; + description: string; + icon: JSX.Element; + onClick?: () => void; +} + +const FabricHomeScreenButton: React.FC = ({ + title, + description, + icon, + className, + onClick, +}) => { + const styles = useStyles(); + + // TODO Make this a11y copmliant: aria-label for icon + return ( +
+
{icon}
+
+
{title}
+
{description}
+
+
+ ); +}; + +export const FabricHomeScreen: React.FC = (props: SplashScreenProps) => { + const styles = useStyles(); + const getSplashScreenButtons = (): JSX.Element => { + const buttons: FabricHomeScreenButtonProps[] = [ + { + title: "New container", + description: "Create a destination container to store your data", + icon: , + onClick: () => { + const databaseId = isFabricNative() + ? userContext.fabricContext?.nativeConnectionInfo?.databaseName + : undefined; + props.explorer.onNewCollectionClicked({ databaseId }); + }, + }, + { + title: "Sample data", + description: "Automatically load sample data in your database", + icon: , + }, + { + title: "App development", + description: "Start here to use an SDK to build your apps", + icon: , + }, + ]; + + return ( +
+ + + +
+ ); + }; + + const title = "Build your database"; + return ( +
+
+ {title} +
+ {getSplashScreenButtons()} +
+ Need help?{" "} + + Learn more Learn more + +
+
+ ); +}; diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx index b0c6514ef..e8013bcbd 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx @@ -2,6 +2,7 @@ import { FeedResponse, ItemDefinition, Resource } from "@azure/cosmos"; import { waitFor } from "@testing-library/react"; import { deleteDocuments } from "Common/dataAccess/deleteDocument"; import { Platform, updateConfigContext } from "ConfigContext"; +import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract"; import { useDialog } from "Explorer/Controls/Dialog"; import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact"; import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog"; @@ -71,7 +72,7 @@ jest.mock("Explorer/Controls/Editor/EditorReact", () => ({ const mockDialogState = { showOkCancelModalDialog: jest.fn((title: string, subText: string, okLabel: string, onOk: () => void) => onOk()), - showOkModalDialog: () => {}, + showOkModalDialog: () => { }, }; jest.mock("Explorer/Controls/Dialog", () => ({ @@ -342,7 +343,9 @@ describe("Documents tab (noSql API)", () => { updateUserContext({ fabricContext: { connectionId: "test", - databaseConnectionInfo: undefined, + mirroredConnectionInfo: undefined, + nativeConnectionInfo: undefined, + artifactType: CosmosDbArtifactType.MIRRORED, isReadOnly: true, isVisible: true, }, diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index 3314416a6..cbb768088 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -6,6 +6,7 @@ import { CollectionTabKind } from "Contracts/ViewModels"; import Explorer from "Explorer/Explorer"; import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab"; +import { FabricHomeScreen } from "Explorer/SplashScreen/FabricHome"; import { SplashScreen } from "Explorer/SplashScreen/SplashScreen"; import { ConnectTab } from "Explorer/Tabs/ConnectTab"; import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab"; @@ -14,6 +15,7 @@ import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab"; import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab"; import { LayoutConstants } from "Explorer/Theme/ThemeUtil"; import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; +import { isFabricNative } from "Platform/Fabric/FabricUtil"; import { userContext } from "UserContext"; import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils"; import { useTeachingBubble } from "hooks/useTeachingBubble"; @@ -301,7 +303,11 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J ); case ReactTabKind.Home: - return ; + if (isFabricNative()) { + return ; + } else { + return ; + } case ReactTabKind.Quickstart: return userContext.apiType === "VCoreMongo" ? ( diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 2e08562e4..c692d0747 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -1,6 +1,7 @@ import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos"; import { useNotebook } from "Explorer/Notebook/useNotebook"; import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; +import { isFabricMirrored } from "Platform/Fabric/FabricUtil"; import * as ko from "knockout"; import * as _ from "underscore"; import * as Constants from "../../Common/Constants"; @@ -34,7 +35,6 @@ import QueryTablesTab from "../Tabs/QueryTablesTab"; import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; -import { Platform, configContext } from "./../../ConfigContext"; import ConflictId from "./ConflictId"; import DocumentId from "./DocumentId"; import StoredProcedure from "./StoredProcedure"; @@ -210,7 +210,7 @@ export default class Collection implements ViewModels.Collection { }); const showScriptsMenus: boolean = - configContext.platform != Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin"); + !isFabricMirrored() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin"); this.showStoredProcedures = ko.observable(showScriptsMenus); this.showTriggers = ko.observable(showScriptsMenus); this.showUserDefinedFunctions = ko.observable(showScriptsMenus); diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index 71ccd82bb..10b8316c0 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -1,7 +1,6 @@ import { Tree, TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from "@fluentui/react-components"; import { Home16Regular } from "@fluentui/react-icons"; import { AuthType } from "AuthType"; -import { Platform, configContext } from "ConfigContext"; import { useTreeStyles } from "Explorer/Controls/TreeComponent/Styles"; import { TreeNode, TreeNodeComponent } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { @@ -11,6 +10,7 @@ import { } from "Explorer/Tree/treeNodeUtil"; import { useDatabases } from "Explorer/useDatabases"; import { useSelectedNode } from "Explorer/useSelectedNode"; +import { isFabricMirrored } from "Platform/Fabric/FabricUtil"; import { userContext } from "UserContext"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import { ReactTabKind, useTabs } from "hooks/useTabs"; @@ -76,23 +76,22 @@ export const ResourceTree: React.FC = ({ explorer }: Resource : []; }, [isSampleDataEnabled, sampleDataResourceTokenCollection]); - const headerNodes: TreeNode[] = - configContext.platform === Platform.Fabric - ? [] - : [ - { - id: "home", - iconSrc: , - label: "Home", - isSelected: () => - useSelectedNode.getState().selectedNode === undefined && - useTabs.getState().activeReactTab === ReactTabKind.Home, - onClick: () => { - useSelectedNode.getState().setSelectedNode(undefined); - useTabs.getState().openAndActivateReactTab(ReactTabKind.Home); - }, + const headerNodes: TreeNode[] = isFabricMirrored() + ? [] + : [ + { + id: "home", + iconSrc: , + label: "Home", + isSelected: () => + useSelectedNode.getState().selectedNode === undefined && + useTabs.getState().activeReactTab === ReactTabKind.Home, + onClick: () => { + useSelectedNode.getState().setSelectedNode(undefined); + useTabs.getState().openAndActivateReactTab(ReactTabKind.Home); }, - ]; + }, + ]; const rootNodes: TreeNode[] = useMemo(() => { if (sampleDataNodes.length > 0) { diff --git a/src/Explorer/Tree/treeNodeUtil.tsx b/src/Explorer/Tree/treeNodeUtil.tsx index 8e6c94559..60fe9079b 100644 --- a/src/Explorer/Tree/treeNodeUtil.tsx +++ b/src/Explorer/Tree/treeNodeUtil.tsx @@ -6,6 +6,7 @@ import StoredProcedure from "Explorer/Tree/StoredProcedure"; import Trigger from "Explorer/Tree/Trigger"; import UserDefinedFunction from "Explorer/Tree/UserDefinedFunction"; import { useDatabases } from "Explorer/useDatabases"; +import { isFabricMirrored } from "Platform/Fabric/FabricUtil"; import { getItemName } from "Utils/APITypeUtils"; import { isServerlessAccount } from "Utils/CapabilityUtils"; import { useTabs } from "hooks/useTabs"; @@ -22,9 +23,7 @@ import { useNotebook } from "../Notebook/useNotebook"; import { useSelectedNode } from "../useSelectedNode"; export const shouldShowScriptNodes = (): boolean => { - return ( - configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") - ); + return !isFabricMirrored() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin"); }; const TreeDatabaseIcon = ; diff --git a/src/Platform/Fabric/FabricUtil.ts b/src/Platform/Fabric/FabricUtil.ts index 26ba859ff..f73e4aeb5 100644 --- a/src/Platform/Fabric/FabricUtil.ts +++ b/src/Platform/Fabric/FabricUtil.ts @@ -1,6 +1,7 @@ import { sendCachedDataMessage } from "Common/MessageHandler"; +import { configContext, Platform } from "ConfigContext"; import { FabricMessageTypes } from "Contracts/FabricMessageTypes"; -import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract"; +import { CosmosDbArtifactType, FabricMirroredDatabaseConnectionInfo } from "Contracts/FabricMessagesContract"; import { updateUserContext, userContext } from "UserContext"; import { logConsoleError } from "Utils/NotificationConsoleUtils"; @@ -18,7 +19,7 @@ const requestDatabaseResourceTokens = async (): Promise => { lastRequestTimestamp = Date.now(); try { - const fabricDatabaseConnectionInfo = await sendCachedDataMessage( + const fabricDatabaseConnectionInfo = await sendCachedDataMessage( FabricMessageTypes.GetAllResourceTokens, [], userContext.fabricContext.connectionId, @@ -31,7 +32,7 @@ const requestDatabaseResourceTokens = async (): Promise => { updateUserContext({ fabricContext: { ...userContext.fabricContext, - databaseConnectionInfo: fabricDatabaseConnectionInfo, + mirroredConnectionInfo: fabricDatabaseConnectionInfo, isReadOnly: true, }, databaseAccount: { ...userContext.databaseAccount }, @@ -71,3 +72,10 @@ export const checkDatabaseResourceTokensValidity = (tokenTimestamp: number): voi scheduleRefreshDatabaseResourceToken(true); } }; + +export const isFabricMirrored = (): boolean => + configContext.platform === Platform.Fabric && + userContext.fabricContext?.artifactType === CosmosDbArtifactType.MIRRORED; + +export const isFabricNative = (): boolean => + configContext.platform === Platform.Fabric && userContext.fabricContext?.artifactType === CosmosDbArtifactType.NATIVE; diff --git a/src/UserContext.ts b/src/UserContext.ts index 955452d3a..e1a918ca4 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -1,4 +1,8 @@ -import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract"; +import { + CosmosDbArtifactType, + FabricMirroredDatabaseConnectionInfo, + FabricNativeDatabaseConnectionInfo, +} from "Contracts/FabricMessagesContract"; import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils"; import { Action } from "Shared/Telemetry/TelemetryConstants"; import { traceOpen } from "Shared/Telemetry/TelemetryProcessor"; @@ -49,9 +53,11 @@ export interface VCoreMongoConnectionParams { interface FabricContext { connectionId: string; - databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined; isReadOnly: boolean; isVisible: boolean; + artifactType: CosmosDbArtifactType; + mirroredConnectionInfo: FabricMirroredDatabaseConnectionInfo | undefined; + nativeConnectionInfo: FabricNativeDatabaseConnectionInfo | undefined; } export type AdminFeedbackControlPolicy = diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 0656c8a9b..5e061920a 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -2,7 +2,12 @@ import * as Constants from "Common/Constants"; import { createUri } from "Common/UrlUtility"; import { DATA_EXPLORER_RPC_VERSION } from "Contracts/DataExplorerMessagesContract"; import { FabricMessageTypes } from "Contracts/FabricMessageTypes"; -import { FABRIC_RPC_VERSION, FabricMessageV2 } from "Contracts/FabricMessagesContract"; +import { + CosmosDbArtifactType, + FABRIC_RPC_VERSION, + FabricMessageV2, + FabricNativeDatabaseConnectionInfo, +} from "Contracts/FabricMessagesContract"; import Explorer from "Explorer/Explorer"; import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane"; import { useSelectedNode } from "Explorer/useSelectedNode"; @@ -23,7 +28,7 @@ import { AccountKind, Flights } from "../Common/Constants"; import { normalizeArmEndpoint } from "../Common/EnvironmentUtility"; import * as Logger from "../Common/Logger"; import { handleCachedDataMessage, sendMessage, sendReadyMessage } from "../Common/MessageHandler"; -import { Platform, configContext, updateConfigContext } from "../ConfigContext"; +import { configContext, Platform, updateConfigContext } from "../ConfigContext"; import { ActionType, DataExplorerAction, TabKind } from "../Contracts/ActionContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts"; import { DataExplorerInputsFrame } from "../Contracts/ViewModels"; @@ -85,9 +90,7 @@ export function useKnockoutExplorer(platform: Platform): Explorer { await updateContextForSampleData(explorer); } - if (userContext.features.restoreTabs) { - restoreOpenTabs(); - } + restoreOpenTabs(); setExplorer(explorer); } @@ -138,12 +141,17 @@ async function configureFabric(): Promise { } explorer = createExplorerFabric(data.message); - await scheduleRefreshDatabaseResourceToken(true); + + if (data.message.artifactType === CosmosDbArtifactType.MIRRORED) { + await scheduleRefreshDatabaseResourceToken(true); + } + resolve(explorer); await explorer.refreshAllDatabases(); - if (userContext.fabricContext.isVisible) { + + if (userContext.fabricContext.isVisible && userContext.fabricContext.mirroredConnectionInfo?.databaseId) { firstContainerOpened = true; - openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId); + openFirstContainer(explorer, userContext.fabricContext.mirroredConnectionInfo.databaseId); } break; } @@ -160,10 +168,10 @@ async function configureFabric(): Promise { if ( userContext.fabricContext.isVisible && !firstContainerOpened && - userContext?.fabricContext?.databaseConnectionInfo?.databaseId !== undefined + userContext?.fabricContext?.mirroredConnectionInfo?.databaseId !== undefined ) { firstContainerOpened = true; - openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId); + openFirstContainer(explorer, userContext.fabricContext.mirroredConnectionInfo.databaseId); } break; } @@ -418,30 +426,62 @@ function configureHostedWithResourceToken(config: ResourceToken): Explorer { return explorer; } -function createExplorerFabric(params: { connectionId: string; isVisible: boolean }): Explorer { +const createExplorerFabric = (params: { + connectionId: string; + isVisible: boolean; + isReadOnly?: boolean; + artifactType?: CosmosDbArtifactType; + nativeConnectionInfo?: FabricNativeDatabaseConnectionInfo; +}): Explorer => { + const artifactType = params.artifactType ?? CosmosDbArtifactType.MIRRORED; + updateUserContext({ fabricContext: { connectionId: params.connectionId, - databaseConnectionInfo: undefined, - isReadOnly: true, + mirroredConnectionInfo: undefined, + isReadOnly: params.isReadOnly ?? true, isVisible: params.isVisible ?? true, - }, - authType: AuthType.ConnectionString, - databaseAccount: { - id: "", - location: "", - type: "", - name: "Mounted", - kind: AccountKind.Default, - properties: { - documentEndpoint: undefined, - }, + artifactType, + nativeConnectionInfo: params.nativeConnectionInfo, }, }); - useTabs.getState().closeAllTabs(); + + if (artifactType === CosmosDbArtifactType.MIRRORED) { + updateUserContext({ + authType: AuthType.ConnectionString, // TODO: will need its own type and Mirroring could be using AAD + databaseAccount: { + id: "", + location: "", + type: "", + name: "Mounted", + kind: AccountKind.Default, + properties: { + documentEndpoint: undefined, + }, + }, + }); + } else if (artifactType === CosmosDbArtifactType.NATIVE) { + updateUserContext({ + databaseAccount: { + id: "", + location: "", + type: "", + name: params.nativeConnectionInfo.accountName, + kind: AccountKind.Default, + properties: { + documentEndpoint: params.nativeConnectionInfo.connectionString, // TODO: verify that .sql.cosmos.fabric.microsoft.com is passed to the client as account endpoint + }, + }, + // For legacy reasons lots of code expects a connection string login to look and act like an encrypted token login + authType: AuthType.EncryptedToken, + accessToken: params.nativeConnectionInfo.accessToken, + masterKey: undefined, + }); + } + const explorer = new Explorer(); return explorer; -} +}; function configureWithEncryptedToken(config: EncryptedToken): Explorer { const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind); diff --git a/src/hooks/useTabs.ts b/src/hooks/useTabs.ts index f29f34f72..97e6736cc 100644 --- a/src/hooks/useTabs.ts +++ b/src/hooks/useTabs.ts @@ -1,6 +1,8 @@ import { clamp } from "@fluentui/react"; +import { Platform } from "ConfigContext"; import { OpenTab } from "Contracts/ActionContracts"; import { useSelectedNode } from "Explorer/useSelectedNode"; +import { isFabricMirrored } from "Platform/Fabric/FabricUtil"; import { AppStateComponentNames, OPEN_TABS_SUBCOMPONENT_NAME, @@ -11,7 +13,6 @@ import * as ViewModels from "../Contracts/ViewModels"; import { CollectionTabKind } from "../Contracts/ViewModels"; import NotebookTabV2 from "../Explorer/Tabs/NotebookV2Tab"; import TabsBase from "../Explorer/Tabs/TabsBase"; -import { Platform, configContext } from "./../ConfigContext"; export interface TabsState { openedTabs: TabsBase[]; @@ -122,7 +123,7 @@ export const useTabs: UseStore = create((set, get) => ({ } return true; }); - if (updatedTabs.length === 0 && configContext.platform !== Platform.Fabric) { + if (updatedTabs.length === 0 && !isFabricMirrored()) { set({ activeTab: undefined, activeReactTab: undefined }); } @@ -162,7 +163,7 @@ export const useTabs: UseStore = create((set, get) => ({ } }); - if (get().openedTabs.length === 0 && configContext.platform !== Platform.Fabric) { + if (get().openedTabs.length === 0 && !isFabricMirrored()) { set({ activeTab: undefined, activeReactTab: undefined }); } }