From ceed162491005a7155dfd9198a23e341071056c8 Mon Sep 17 00:00:00 2001 From: Armando Trejo Oliver Date: Thu, 29 Jun 2023 08:47:46 -0700 Subject: [PATCH] Add Sample Data to Resource Tree (#1499) * Add Sample Data to Resource Tree * Format * Fix strict build * Fix lint * Fixed implementation to show Sample data container * Udated logic based on TokenCollection * Re-configure copilot flag --------- Co-authored-by: Predrag Klepic --- src/Common/SampleDataClient.ts | 26 ++++++ src/Common/dataAccess/readCollection.ts | 30 ++++++- src/Explorer/Explorer.tsx | 35 ++++++-- src/Explorer/Tree/ResourceTree.tsx | 79 ++++++++++++++----- src/Explorer/Tree/SampleDataTree.tsx | 41 ++++++++++ src/Explorer/useDatabases.ts | 2 + .../Hosted/Helpers/ResourceTokenUtils.ts | 21 ++--- src/UserContext.ts | 4 +- src/hooks/useKnockoutExplorer.ts | 43 ++++++++-- 9 files changed, 234 insertions(+), 47 deletions(-) create mode 100644 src/Common/SampleDataClient.ts create mode 100644 src/Explorer/Tree/SampleDataTree.tsx diff --git a/src/Common/SampleDataClient.ts b/src/Common/SampleDataClient.ts new file mode 100644 index 000000000..9ec271255 --- /dev/null +++ b/src/Common/SampleDataClient.ts @@ -0,0 +1,26 @@ +import * as Cosmos from "@azure/cosmos"; +import { userContext } from "UserContext"; + +let _sampleDataclient: Cosmos.CosmosClient; + +export function sampleDataClient(): Cosmos.CosmosClient { + if (_sampleDataclient) { + return _sampleDataclient; + } + + const sampleDataConnectionInfo = userContext.sampleDataConnectionInfo; + const options: Cosmos.CosmosClientOptions = { + endpoint: sampleDataConnectionInfo.accountEndpoint, + tokenProvider: async () => { + const sampleDataConnectionInfo = userContext.sampleDataConnectionInfo; + return Promise.resolve(sampleDataConnectionInfo.resourceToken); + }, + connectionPolicy: { + enableEndpointDiscovery: false, + }, + userAgentSuffix: "Azure Portal", + }; + + _sampleDataclient = new Cosmos.CosmosClient(options); + return _sampleDataclient; +} diff --git a/src/Common/dataAccess/readCollection.ts b/src/Common/dataAccess/readCollection.ts index 6df44a932..9377edaa2 100644 --- a/src/Common/dataAccess/readCollection.ts +++ b/src/Common/dataAccess/readCollection.ts @@ -1,13 +1,39 @@ +import { CosmosClient } from "@azure/cosmos"; +import { sampleDataClient } from "Common/SampleDataClient"; +import { userContext } from "UserContext"; import * as DataModels from "../../Contracts/DataModels"; +import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; -import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; export async function readCollection(databaseId: string, collectionId: string): Promise { + const cosmosClient = client(); + return await readCollectionInternal(cosmosClient, databaseId, collectionId); +} + +export async function readSampleCollection(): Promise { + const cosmosClient = sampleDataClient(); + + const sampleDataConnectionInfo = userContext.sampleDataConnectionInfo; + const databaseId = sampleDataConnectionInfo?.databaseId; + const collectionId = sampleDataConnectionInfo?.collectionId; + + if (!databaseId || !collectionId) { + return undefined; + } + + return await readCollectionInternal(cosmosClient, databaseId, collectionId); +} + +export async function readCollectionInternal( + cosmosClient: CosmosClient, + databaseId: string, + collectionId: string +): Promise { let collection: DataModels.Collection; const clearMessage = logConsoleProgress(`Querying container ${collectionId}`); try { - const response = await client().database(databaseId).container(collectionId).read(); + const response = await cosmosClient.database(databaseId).container(collectionId).read(); collection = response.resource as DataModels.Collection; } catch (error) { handleError(error, "ReadCollection", `Error while querying container ${collectionId}`); diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index f8ecb6923..4f74ec1db 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -1,39 +1,39 @@ import { Link } from "@fluentui/react/lib/Link"; import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; import { IGalleryItem } from "Juno/JunoClient"; +import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation"; import * as ko from "knockout"; import React from "react"; import _ from "underscore"; -import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation"; import shallow from "zustand/shallow"; import { AuthType } from "../AuthType"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import * as Constants from "../Common/Constants"; import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook, PoolIdType } from "../Common/Constants"; -import { readCollection } from "../Common/dataAccess/readCollection"; -import { readDatabases } from "../Common/dataAccess/readDatabases"; import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils"; import * as Logger from "../Common/Logger"; import { QueriesClient } from "../Common/QueriesClient"; +import { readCollection, readSampleCollection } from "../Common/dataAccess/readCollection"; +import { readDatabases } from "../Common/dataAccess/readDatabases"; import * as DataModels from "../Contracts/DataModels"; import { ContainerConnectionInfo, IPhoenixServiceInfo, IProvisionData, IResponse } from "../Contracts/DataModels"; import * as ViewModels from "../Contracts/ViewModels"; import { GitHubOAuthService } from "../GitHub/GitHubOAuthService"; -import { useSidePanel } from "../hooks/useSidePanel"; -import { useTabs } from "../hooks/useTabs"; import { PhoenixClient } from "../Phoenix/PhoenixClient"; import * as ExplorerSettings from "../Shared/ExplorerSettings"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../UserContext"; import { getCollectionName, getUploadName } from "../Utils/APITypeUtils"; -import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; -import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces"; import { stringToBlob } from "../Utils/BlobUtils"; import { isCapabilityEnabled } from "../Utils/CapabilityUtils"; import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; +import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; +import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces"; +import { useSidePanel } from "../hooks/useSidePanel"; +import { useTabs } from "../hooks/useTabs"; import "./ComponentRegisterer"; import { DialogProps, useDialog } from "./Controls/Dialog"; import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent"; @@ -1272,5 +1272,26 @@ export default class Explorer { if (useNotebook.getState().isPhoenixNotebooks) { await this.initNotebooks(userContext.databaseAccount); } + + await this.refreshSampleData(); + } + + public async refreshSampleData(): Promise { + if (!userContext.sampleDataConnectionInfo) { + return; + } + + const collection: DataModels.Collection = await readSampleCollection(); + if (!collection) { + return; + } + + const databaseId = userContext.sampleDataConnectionInfo?.databaseId; + if (!databaseId) { + return; + } + + const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection); + useDatabases.setState({ sampleDataResourceTokenCollection }); } } diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index afb5a118e..6a25f742b 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -1,14 +1,15 @@ import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; -import * as React from "react"; +import { SampleDataTree } from "Explorer/Tree/SampleDataTree"; import { getItemName } from "Utils/APITypeUtils"; +import * as React from "react"; import shallow from "zustand/shallow"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; -import DeleteIcon from "../../../images/delete.svg"; import GalleryIcon from "../../../images/GalleryIcon.svg"; -import FileIcon from "../../../images/notebook/file-cosmos.svg"; +import DeleteIcon from "../../../images/delete.svg"; import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; +import FileIcon from "../../../images/notebook/file-cosmos.svg"; import PublishIcon from "../../../images/notebook/publish_content.svg"; import RefreshIcon from "../../../images/refresh-cosmos.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; @@ -16,14 +17,14 @@ import { Areas, ConnectionStatusType, Notebook } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; -import { useSidePanel } from "../../hooks/useSidePanel"; -import { useTabs } from "../../hooks/useTabs"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils"; +import { useSidePanel } from "../../hooks/useSidePanel"; +import { useTabs } from "../../hooks/useTabs"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; import { useDialog } from "../Controls/Dialog"; @@ -764,23 +765,59 @@ export const ResourceTree: React.FC = ({ container }: Resourc }; const dataRootNode = buildDataTree(); + const isSampleDataEnabled = userContext.sampleDataConnectionInfo && userContext.apiType === "SQL"; + const sampleDataResourceTokenCollection = useDatabases((state) => state.sampleDataResourceTokenCollection); - if (isNotebookEnabled) { - return ( - <> - - - - - - - - + return ( + <> + {!isNotebookEnabled && !isSampleDataEnabled && ( + + )} + {isNotebookEnabled && !isSampleDataEnabled && ( + <> + + + + + + + + - {buildGalleryCallout()} - - ); - } + {buildGalleryCallout()} + + )} + {!isNotebookEnabled && isSampleDataEnabled && ( + <> + + + + + + + + - return ; + {buildGalleryCallout()} + + )} + {isNotebookEnabled && isSampleDataEnabled && ( + <> + + + + + + + + + + + + + {buildGalleryCallout()} + + )} + + ); }; diff --git a/src/Explorer/Tree/SampleDataTree.tsx b/src/Explorer/Tree/SampleDataTree.tsx new file mode 100644 index 000000000..1b4ad7f5f --- /dev/null +++ b/src/Explorer/Tree/SampleDataTree.tsx @@ -0,0 +1,41 @@ +import React, { useEffect, useState } from "react"; +import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; +import CollectionIcon from "../../../images/tree-collection.svg"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent"; + +export const SampleDataTree = ({ + sampleDataResourceTokenCollection, +}: { + sampleDataResourceTokenCollection: ViewModels.CollectionBase; +}): JSX.Element => { + const [root, setRoot] = useState(undefined); + + useEffect(() => { + if (sampleDataResourceTokenCollection) { + const updatedSampleTree: TreeNode = { + label: sampleDataResourceTokenCollection.databaseId, + isExpanded: true, + iconSrc: CosmosDBIcon, + children: [ + { + label: sampleDataResourceTokenCollection.id(), + iconSrc: CollectionIcon, + isExpanded: true, + className: "collectionHeader", + children: [ + { + label: "Items", + }, + ], + }, + ], + }; + setRoot(updatedSampleTree); + } + }, [sampleDataResourceTokenCollection]); + + return ( + + ); +}; diff --git a/src/Explorer/useDatabases.ts b/src/Explorer/useDatabases.ts index 7d1760af3..fd76557c2 100644 --- a/src/Explorer/useDatabases.ts +++ b/src/Explorer/useDatabases.ts @@ -8,6 +8,7 @@ import { useSelectedNode } from "./useSelectedNode"; interface DatabasesState { databases: ViewModels.Database[]; resourceTokenCollection: ViewModels.CollectionBase; + sampleDataResourceTokenCollection: ViewModels.CollectionBase; updateDatabase: (database: ViewModels.Database) => void; addDatabases: (databases: ViewModels.Database[]) => void; deleteDatabase: (database: ViewModels.Database) => void; @@ -28,6 +29,7 @@ interface DatabasesState { export const useDatabases: UseStore = create((set, get) => ({ databases: [], resourceTokenCollection: undefined, + sampleDataResourceTokenCollection: undefined, updateDatabase: (updatedDatabase: ViewModels.Database) => set((state) => { const updatedDatabases = state.databases.map((database: ViewModels.Database) => { diff --git a/src/Platform/Hosted/Helpers/ResourceTokenUtils.ts b/src/Platform/Hosted/Helpers/ResourceTokenUtils.ts index b55014c23..28c6522b1 100644 --- a/src/Platform/Hosted/Helpers/ResourceTokenUtils.ts +++ b/src/Platform/Hosted/Helpers/ResourceTokenUtils.ts @@ -1,17 +1,18 @@ export interface ParsedResourceTokenConnectionString { - accountEndpoint: string; - collectionId: string; - databaseId: string; + accountEndpoint?: string; + collectionId?: string; + databaseId?: string; partitionKey?: string; - resourceToken: string; + resourceToken?: string; } export function parseResourceTokenConnectionString(connectionString: string): ParsedResourceTokenConnectionString { - let accountEndpoint: string; - let collectionId: string; - let databaseId: string; - let partitionKey: string; - let resourceToken: string; + let accountEndpoint: string | undefined; + let collectionId: string | undefined; + let databaseId: string | undefined; + let partitionKey: string | undefined; + let resourceToken: string | undefined; + const connectionStringParts = connectionString.split(";"); connectionStringParts.forEach((part: string) => { if (part.startsWith("type=resource")) { @@ -39,5 +40,5 @@ export function parseResourceTokenConnectionString(connectionString: string): Pa } export function isResourceTokenConnectionString(connectionString: string): boolean { - return connectionString && connectionString.includes("type=resource"); + return !!connectionString && connectionString.includes("type=resource"); } diff --git a/src/UserContext.ts b/src/UserContext.ts index 90aff4e2f..90195f845 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -1,5 +1,6 @@ import { useCarousel } from "hooks/useCarousel"; import { usePostgres } from "hooks/usePostgres"; +import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils"; import { Action } from "Shared/Telemetry/TelemetryConstants"; import { traceOpen } from "Shared/Telemetry/TelemetryProcessor"; import { AuthType } from "./AuthType"; @@ -69,6 +70,7 @@ interface UserContext { readonly postgresConnectionStrParams?: PostgresConnectionStrParams; readonly isReplica?: boolean; collectionCreationDefaults: CollectionCreationDefaults; + sampleDataConnectionInfo?: ParsedResourceTokenConnectionString; } export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres"; @@ -157,4 +159,4 @@ function apiType(account: DatabaseAccount | undefined): ApiType { return "SQL"; } -export { userContext, updateUserContext }; +export { updateUserContext, userContext }; diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index b9d088ee0..a87f61cba 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -1,13 +1,13 @@ +import { createUri } from "Common/UrlUtility"; import Explorer from "Explorer/Explorer"; +import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility"; import { ReactTabKind, useTabs } from "hooks/useTabs"; import { useEffect, useState } from "react"; -import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility"; -import { applyExplorerBindings } from "../applyExplorerBindings"; import { AuthType } from "../AuthType"; import { AccountKind, Flights } from "../Common/Constants"; import { normalizeArmEndpoint } from "../Common/EnvironmentUtility"; import { sendMessage, sendReadyMessage } from "../Common/MessageHandler"; -import { configContext, Platform, updateConfigContext } from "../ConfigContext"; +import { Platform, configContext, updateConfigContext } from "../ConfigContext"; import { ActionType, DataExplorerAction } from "../Contracts/ActionContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts"; import { DataExplorerInputsFrame } from "../Contracts/ViewModels"; @@ -21,19 +21,20 @@ import { ResourceToken, } from "../HostedExplorerChildFrame"; import { emulatorAccount } from "../Platform/Emulator/emulatorAccount"; -import { extractFeatures } from "../Platform/Hosted/extractFeatures"; import { parseResourceTokenConnectionString } from "../Platform/Hosted/Helpers/ResourceTokenUtils"; import { getDatabaseAccountKindFromExperience, getDatabaseAccountPropertiesFromMetadata, } from "../Platform/Hosted/HostedUtils"; +import { extractFeatures } from "../Platform/Hosted/extractFeatures"; import { CollectionCreation } from "../Shared/Constants"; import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext"; +import { getAuthorizationHeader, getMsalInstance } from "../Utils/AuthorizationUtils"; +import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/MessageValidation"; import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; import { DatabaseAccountListKeysResult } from "../Utils/arm/generatedClients/cosmos/types"; -import { getMsalInstance } from "../Utils/AuthorizationUtils"; -import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/MessageValidation"; +import { applyExplorerBindings } from "../applyExplorerBindings"; // This hook will create a new instance of Explorer.ts and bind it to the DOM // This hook has a LOT of magic, but ideally we can delete it once we have removed KO and switched entirely to React @@ -61,6 +62,10 @@ export function useKnockoutExplorer(platform: Platform): Explorer { setExplorer(explorer); } } + + if (userContext.features.enableCopilot) { + await updateContextForSampleData(); + } }; effect(); }, [platform]); @@ -409,3 +414,29 @@ interface PortalMessage { type?: MessageTypes; inputs?: DataExplorerInputsFrame; } + +async function updateContextForSampleData(): Promise { + if (!userContext.features.enableCopilot) { + return; + } + + const url = createUri(`${configContext.BACKEND_ENDPOINT}`, `/api/tokens/sampledataconnection`); + const authorizationHeader = getAuthorizationHeader(); + const headers = { [authorizationHeader.header]: authorizationHeader.token }; + + const response = await window.fetch(url, { + headers, + }); + + if (!response.ok) { + return undefined; + } + + const data: SampledataconnectionResponse = await response.json(); + const sampleDataConnectionInfo = parseResourceTokenConnectionString(data.connectionString); + updateUserContext({ sampleDataConnectionInfo }); +} + +interface SampledataconnectionResponse { + connectionString: string; +}