From 5d13463bdbcd5e53b1135737fe26fd1de7d16a87 Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Tue, 9 Jan 2024 13:32:20 -0600 Subject: [PATCH] Copilot settings (#1719) * gating copilot with AFEC * Add enable sample DB in settings * Add enable sample DB in settings * Add enable sample DB in settings * PR comments --- less/documentDB.less | 12 +++++ src/Explorer/Explorer.tsx | 18 +++++-- .../CommandBarComponentButtonFactory.tsx | 2 +- src/Explorer/OpenActions/OpenActions.tsx | 2 +- .../Panes/SettingsPane/SettingsPane.test.tsx | 4 +- .../Panes/SettingsPane/SettingsPane.tsx | 51 +++++++++++++++++-- .../__snapshots__/SettingsPane.test.tsx.snap | 6 +-- .../QueryCopilot/Shared/QueryCopilotClient.ts | 29 ++++++++++- src/Explorer/SplashScreen/SplashScreen.tsx | 34 +++++++------ src/Explorer/Tree/ResourceTree.tsx | 5 +- src/Shared/StorageUtility.ts | 1 + src/hooks/useQueryCopilot.ts | 4 ++ 12 files changed, 135 insertions(+), 33 deletions(-) diff --git a/less/documentDB.less b/less/documentDB.less index bdefc2dbd..d8ae159db 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -2897,9 +2897,21 @@ a:link { padding-left: 8px; } + .settingsSectionInlineCheckbox { + display: flex; + flex-direction: row-reverse; + justify-content: flex-end; + align-items: center; + gap: 5px; + } + .settingsSectionLabel { margin-bottom: @DefaultSpace; margin-right: 5px; + + .panelInfoIcon { + margin-left: 5px; + } } .pageOptionsPart { diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index f159c6ac1..fb4a08147 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -3,9 +3,10 @@ import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; import { sendMessage } from "Common/MessageHandler"; import { Platform, configContext } from "ConfigContext"; import { MessageTypes } from "Contracts/ExplorerContracts"; -import { getCopilotEnabled } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; +import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { IGalleryItem } from "Juno/JunoClient"; import { requestDatabaseResourceTokens } from "Platform/Fabric/FabricUtil"; +import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import * as ko from "knockout"; @@ -1389,9 +1390,18 @@ export default class Explorer { if (userContext.apiType !== "SQL" || !userContext.subscriptionId) { return; } - const copilotEnabled = await getCopilotEnabled(); - useQueryCopilot.getState().setCopilotEnabled(copilotEnabled); - useQueryCopilot.getState().setCopilotUserDBEnabled(copilotEnabled); + const copilotEnabledPromise = getCopilotEnabled(); + const copilotUserDBEnabledPromise = isCopilotFeatureRegistered(userContext.subscriptionId); + const [copilotEnabled, copilotUserDBEnabled] = await Promise.all([ + copilotEnabledPromise, + copilotUserDBEnabledPromise, + ]); + const copilotSampleDBEnabled = LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true"; + useQueryCopilot.getState().setCopilotEnabled(copilotEnabled && copilotUserDBEnabled); + useQueryCopilot.getState().setCopilotUserDBEnabled(copilotUserDBEnabled); + useQueryCopilot + .getState() + .setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled); } public async refreshSampleData(): Promise { diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 5e9de9cde..78810e9db 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -200,7 +200,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt { iconSrc: SettingsIcon, iconAlt: "Settings", - onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", ), + onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", ), commandButtonLabel: undefined, ariaLabel: "Settings", tooltipText: "Settings", diff --git a/src/Explorer/OpenActions/OpenActions.tsx b/src/Explorer/OpenActions/OpenActions.tsx index d1813ab98..876f8e391 100644 --- a/src/Explorer/OpenActions/OpenActions.tsx +++ b/src/Explorer/OpenActions/OpenActions.tsx @@ -154,7 +154,7 @@ function openPane(action: ActionContracts.OpenPane, explorer: Explorer) { action.paneKind === ActionContracts.PaneKind.GlobalSettings || action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings] ) { - useSidePanel.getState().openSidePanel("Settings", ); + useSidePanel.getState().openSidePanel("Settings", ); } } diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.test.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.test.tsx index 4838ca858..25b0167f5 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.test.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.test.tsx @@ -6,7 +6,7 @@ import { SettingsPane } from "./SettingsPane"; describe("Settings Pane", () => { it("should render Default properly", () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); @@ -18,7 +18,7 @@ describe("Settings Pane", () => { }, } as DatabaseAccount, }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index 4f7373fbb..ef25a7d73 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -16,13 +16,20 @@ import * as StringUtility from "Shared/StringUtility"; import { userContext } from "UserContext"; import { logConsoleInfo } from "Utils/NotificationConsoleUtils"; import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils"; +import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useSidePanel } from "hooks/useSidePanel"; import React, { FunctionComponent, useState } from "react"; +import Explorer from "../../Explorer"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; -export const SettingsPane: FunctionComponent = () => { +export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ + explorer, +}: { + explorer: Explorer; +}): JSX.Element => { const closeSidePanel = useSidePanel((state) => state.closeSidePanel); const [isExecuting, setIsExecuting] = useState(false); + const [refreshExplorer, setRefreshExplorer] = useState(false); const [pageOption, setPageOption] = useState( LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage) === Constants.Queries.unlimitedItemsPerPage ? Constants.Queries.UnlimitedPageOption @@ -78,13 +85,17 @@ export const SettingsPane: FunctionComponent = () => { ? LocalStorageUtility.getEntryString(StorageKey.PriorityLevel) : Constants.PriorityLevel.Default, ); + const [copilotSampleDBEnabled, setCopilotSampleDBEnabled] = useState( + LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true", + ); const explorerVersion = configContext.gitSha; const shouldShowQueryPageOptions = userContext.apiType === "SQL"; const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin"; const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin"; const shouldShowParallelismOption = userContext.apiType !== "Gremlin"; const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled(); - const handlerOnSubmit = () => { + const shouldShowCopilotSampleDBOption = userContext.apiType === "SQL" && useQueryCopilot.getState().copilotEnabled; + const handlerOnSubmit = async () => { setIsExecuting(true); LocalStorageUtility.setEntryNumber( @@ -100,6 +111,7 @@ export const SettingsPane: FunctionComponent = () => { LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString()); LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism); LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString()); + LocalStorageUtility.setEntryString(StorageKey.CopilotSampleDBEnabled, copilotSampleDBEnabled.toString()); if (shouldShowGraphAutoVizOption) { LocalStorageUtility.setEntryBoolean( @@ -139,6 +151,7 @@ export const SettingsPane: FunctionComponent = () => { logConsoleInfo( `Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`, ); + refreshExplorer && (await explorer.refreshExplorer()); closeSidePanel(); }; @@ -218,6 +231,12 @@ export const SettingsPane: FunctionComponent = () => { } }; + const handleSampleDatabaseChange = async (ev: React.MouseEvent, checked?: boolean): Promise => { + setCopilotSampleDBEnabled(checked); + useQueryCopilot.getState().setCopilotSampleDBEnabled(checked); + setRefreshExplorer(!refreshExplorer); + }; + const choiceButtonStyles = { root: { clear: "both", @@ -434,7 +453,7 @@ export const SettingsPane: FunctionComponent = () => {
-
+
Enable container pagination @@ -454,7 +473,7 @@ export const SettingsPane: FunctionComponent = () => {
{shouldShowCrossPartitionOption && (
-
+
Enable cross-partition query @@ -545,6 +564,30 @@ export const SettingsPane: FunctionComponent = () => {
)} + {shouldShowCopilotSampleDBOption && ( +
+
+
+ Enable sample database + + This is a sample database and collection with synthetic product data you can use to explore using + NoSQL queries and Copilot. This will appear as another database in the Data Explorer UI, and is + created by, and maintained by Microsoft at no cost to you. + +
+ + +
+
+ )}
Explorer Version
diff --git a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap index 8898bae23..f4de6deab 100644 --- a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap +++ b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap @@ -274,7 +274,7 @@ exports[`Settings Pane should render Default properly 1`] = ` className="settingsSection" >
=> { + const api_version = "2021-07-01"; + const url = `${configContext.ARM_ENDPOINT}/subscriptions/${subscriptionId}/providers/Microsoft.Features/featureProviders/Microsoft.DocumentDB/subscriptionFeatureRegistrations/MicrosoftCopilotForAzureInCDB?api-version=${api_version}`; + const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader(); + const headers = { [authorizationHeader.header]: authorizationHeader.token }; + + let response; + + try { + response = await fetchWithTimeout(url, headers); + } catch (error) { + return false; + } + + if (!response?.ok) { + return false; + } + + const featureRegistration = (await response?.json()) as FeatureRegistration; + return featureRegistration?.properties?.state === "Registered"; +}; + export const getCopilotEnabled = async (): Promise => { const url = `${configContext.BACKEND_ENDPOINT}/api/portalsettings/querycopilot`; const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader(); diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index cffb2b264..ce5385329 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -148,23 +148,25 @@ export class SplashScreen extends React.Component { /> - { - const copilotVersion = userContext.features.copilotVersion; - if (copilotVersion === "v1.0") { - useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot); - } else if (copilotVersion === "v2.0") { - const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection; - sampleCollection.onNewQueryClick(sampleCollection, undefined); + {useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotSampleDBEnabled && ( + + onClick={() => { + const copilotVersion = userContext.features.copilotVersion; + if (copilotVersion === "v1.0") { + useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot); + } else if (copilotVersion === "v2.0") { + const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection; + sampleCollection.onNewQueryClick(sampleCollection, undefined); + } + traceOpen(Action.OpenQueryCopilotFromSplashScreen, { apiType: userContext.apiType }); + }} + /> + )} = ({ container }: Resourc const dataRootNode = buildDataTree(); const isSampleDataEnabled = - useQueryCopilot().copilotEnabled && userContext.sampleDataConnectionInfo && userContext.apiType === "SQL"; + useQueryCopilot().copilotEnabled && + useQueryCopilot().copilotSampleDBEnabled && + userContext.sampleDataConnectionInfo && + userContext.apiType === "SQL"; const sampleDataResourceTokenCollection = useDatabases((state) => state.sampleDataResourceTokenCollection); return ( diff --git a/src/Shared/StorageUtility.ts b/src/Shared/StorageUtility.ts index d3aaa6f13..ca7bc81af 100644 --- a/src/Shared/StorageUtility.ts +++ b/src/Shared/StorageUtility.ts @@ -11,6 +11,7 @@ export enum StorageKey { MaxWaitTimeInSeconds, AutomaticallyCancelQueryAfterTimeout, ContainerPaginationEnabled, + CopilotSampleDBEnabled, CustomItemPerPage, DatabaseAccountId, EncryptedKeyToken, diff --git a/src/hooks/useQueryCopilot.ts b/src/hooks/useQueryCopilot.ts index f0b5edd74..1d9685b6f 100644 --- a/src/hooks/useQueryCopilot.ts +++ b/src/hooks/useQueryCopilot.ts @@ -10,6 +10,7 @@ import { ContainerInfo } from "../Contracts/DataModels"; export interface QueryCopilotState { copilotEnabled: boolean; copilotUserDBEnabled: boolean; + copilotSampleDBEnabled: boolean; generatedQuery: string; likeQuery: boolean; userPrompt: string; @@ -50,6 +51,7 @@ export interface QueryCopilotState { setCopilotEnabled: (copilotEnabled: boolean) => void; setCopilotUserDBEnabled: (copilotUserDBEnabled: boolean) => void; + setCopilotSampleDBEnabled: (copilotSampleDBEnabled: boolean) => void; openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) => void; closeFeedbackModal: () => void; setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) => void; @@ -96,6 +98,7 @@ type QueryCopilotStore = UseStore; export const useQueryCopilot: QueryCopilotStore = create((set) => ({ copilotEnabled: false, copilotUserDBEnabled: false, + copilotSampleDBEnabled: false, generatedQuery: "", likeQuery: false, userPrompt: "", @@ -145,6 +148,7 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({ setCopilotEnabled: (copilotEnabled: boolean) => set({ copilotEnabled }), setCopilotUserDBEnabled: (copilotUserDBEnabled: boolean) => set({ copilotUserDBEnabled }), + setCopilotSampleDBEnabled: (copilotSampleDBEnabled: boolean) => set({ copilotSampleDBEnabled }), openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) => set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }), closeFeedbackModal: () => set({ showFeedbackModal: false }),