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
This commit is contained in:
sunghyunkang1111 2024-01-09 13:32:20 -06:00 committed by GitHub
parent c4cceceafc
commit 5d13463bdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 135 additions and 33 deletions

View File

@ -2897,9 +2897,21 @@ a:link {
padding-left: 8px; padding-left: 8px;
} }
.settingsSectionInlineCheckbox {
display: flex;
flex-direction: row-reverse;
justify-content: flex-end;
align-items: center;
gap: 5px;
}
.settingsSectionLabel { .settingsSectionLabel {
margin-bottom: @DefaultSpace; margin-bottom: @DefaultSpace;
margin-right: 5px; margin-right: 5px;
.panelInfoIcon {
margin-left: 5px;
}
} }
.pageOptionsPart { .pageOptionsPart {

View File

@ -3,9 +3,10 @@ import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { sendMessage } from "Common/MessageHandler"; import { sendMessage } from "Common/MessageHandler";
import { Platform, configContext } from "ConfigContext"; import { Platform, configContext } from "ConfigContext";
import { MessageTypes } from "Contracts/ExplorerContracts"; 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 { IGalleryItem } from "Juno/JunoClient";
import { requestDatabaseResourceTokens } from "Platform/Fabric/FabricUtil"; import { requestDatabaseResourceTokens } from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation"; import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as ko from "knockout"; import * as ko from "knockout";
@ -1389,9 +1390,18 @@ export default class Explorer {
if (userContext.apiType !== "SQL" || !userContext.subscriptionId) { if (userContext.apiType !== "SQL" || !userContext.subscriptionId) {
return; return;
} }
const copilotEnabled = await getCopilotEnabled(); const copilotEnabledPromise = getCopilotEnabled();
useQueryCopilot.getState().setCopilotEnabled(copilotEnabled); const copilotUserDBEnabledPromise = isCopilotFeatureRegistered(userContext.subscriptionId);
useQueryCopilot.getState().setCopilotUserDBEnabled(copilotEnabled); 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<void> { public async refreshSampleData(): Promise<void> {

View File

@ -200,7 +200,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
{ {
iconSrc: SettingsIcon, iconSrc: SettingsIcon,
iconAlt: "Settings", iconAlt: "Settings",
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane />), onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
commandButtonLabel: undefined, commandButtonLabel: undefined,
ariaLabel: "Settings", ariaLabel: "Settings",
tooltipText: "Settings", tooltipText: "Settings",

View File

@ -154,7 +154,7 @@ function openPane(action: ActionContracts.OpenPane, explorer: Explorer) {
action.paneKind === ActionContracts.PaneKind.GlobalSettings || action.paneKind === ActionContracts.PaneKind.GlobalSettings ||
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings] action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings]
) { ) {
useSidePanel.getState().openSidePanel("Settings", <SettingsPane />); useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={explorer} />);
} }
} }

View File

@ -6,7 +6,7 @@ import { SettingsPane } from "./SettingsPane";
describe("Settings Pane", () => { describe("Settings Pane", () => {
it("should render Default properly", () => { it("should render Default properly", () => {
const wrapper = shallow(<SettingsPane />); const wrapper = shallow(<SettingsPane explorer={null} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
@ -18,7 +18,7 @@ describe("Settings Pane", () => {
}, },
} as DatabaseAccount, } as DatabaseAccount,
}); });
const wrapper = shallow(<SettingsPane />); const wrapper = shallow(<SettingsPane explorer={null} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
}); });

View File

@ -16,13 +16,20 @@ import * as StringUtility from "Shared/StringUtility";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { logConsoleInfo } from "Utils/NotificationConsoleUtils"; import { logConsoleInfo } from "Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils"; import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import Explorer from "../../Explorer";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; 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 closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [isExecuting, setIsExecuting] = useState<boolean>(false); const [isExecuting, setIsExecuting] = useState<boolean>(false);
const [refreshExplorer, setRefreshExplorer] = useState<boolean>(false);
const [pageOption, setPageOption] = useState<string>( const [pageOption, setPageOption] = useState<string>(
LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage) === Constants.Queries.unlimitedItemsPerPage LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage) === Constants.Queries.unlimitedItemsPerPage
? Constants.Queries.UnlimitedPageOption ? Constants.Queries.UnlimitedPageOption
@ -78,13 +85,17 @@ export const SettingsPane: FunctionComponent = () => {
? LocalStorageUtility.getEntryString(StorageKey.PriorityLevel) ? LocalStorageUtility.getEntryString(StorageKey.PriorityLevel)
: Constants.PriorityLevel.Default, : Constants.PriorityLevel.Default,
); );
const [copilotSampleDBEnabled, setCopilotSampleDBEnabled] = useState<boolean>(
LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true",
);
const explorerVersion = configContext.gitSha; const explorerVersion = configContext.gitSha;
const shouldShowQueryPageOptions = userContext.apiType === "SQL"; const shouldShowQueryPageOptions = userContext.apiType === "SQL";
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin"; const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin";
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin"; const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
const shouldShowParallelismOption = userContext.apiType !== "Gremlin"; const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled(); const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
const handlerOnSubmit = () => { const shouldShowCopilotSampleDBOption = userContext.apiType === "SQL" && useQueryCopilot.getState().copilotEnabled;
const handlerOnSubmit = async () => {
setIsExecuting(true); setIsExecuting(true);
LocalStorageUtility.setEntryNumber( LocalStorageUtility.setEntryNumber(
@ -100,6 +111,7 @@ export const SettingsPane: FunctionComponent = () => {
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString()); LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism); LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString()); LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString());
LocalStorageUtility.setEntryString(StorageKey.CopilotSampleDBEnabled, copilotSampleDBEnabled.toString());
if (shouldShowGraphAutoVizOption) { if (shouldShowGraphAutoVizOption) {
LocalStorageUtility.setEntryBoolean( LocalStorageUtility.setEntryBoolean(
@ -139,6 +151,7 @@ export const SettingsPane: FunctionComponent = () => {
logConsoleInfo( logConsoleInfo(
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`, `Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`,
); );
refreshExplorer && (await explorer.refreshExplorer());
closeSidePanel(); closeSidePanel();
}; };
@ -218,6 +231,12 @@ export const SettingsPane: FunctionComponent = () => {
} }
}; };
const handleSampleDatabaseChange = async (ev: React.MouseEvent<HTMLElement>, checked?: boolean): Promise<void> => {
setCopilotSampleDBEnabled(checked);
useQueryCopilot.getState().setCopilotSampleDBEnabled(checked);
setRefreshExplorer(!refreshExplorer);
};
const choiceButtonStyles = { const choiceButtonStyles = {
root: { root: {
clear: "both", clear: "both",
@ -434,7 +453,7 @@ export const SettingsPane: FunctionComponent = () => {
</div> </div>
</div> </div>
<div className="settingsSection"> <div className="settingsSection">
<div className="settingsSectionPart"> <div className="settingsSectionPart settingsSectionInlineCheckbox">
<div className="settingsSectionLabel"> <div className="settingsSectionLabel">
Enable container pagination Enable container pagination
<InfoTooltip> <InfoTooltip>
@ -454,7 +473,7 @@ export const SettingsPane: FunctionComponent = () => {
</div> </div>
{shouldShowCrossPartitionOption && ( {shouldShowCrossPartitionOption && (
<div className="settingsSection"> <div className="settingsSection">
<div className="settingsSectionPart"> <div className="settingsSectionPart settingsSectionInlineCheckbox">
<div className="settingsSectionLabel"> <div className="settingsSectionLabel">
Enable cross-partition query Enable cross-partition query
<InfoTooltip> <InfoTooltip>
@ -545,6 +564,30 @@ export const SettingsPane: FunctionComponent = () => {
</div> </div>
</div> </div>
)} )}
{shouldShowCopilotSampleDBOption && (
<div className="settingsSection">
<div className="settingsSectionPart settingsSectionInlineCheckbox">
<div className="settingsSectionLabel">
Enable sample database
<InfoTooltip>
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.
</InfoTooltip>
</div>
<Checkbox
styles={{
label: { padding: 0 },
}}
className="padding"
ariaLabel="Enable sample db for Copilot"
checked={copilotSampleDBEnabled}
onChange={handleSampleDatabaseChange}
/>
</div>
</div>
)}
<div className="settingsSection"> <div className="settingsSection">
<div className="settingsSectionPart"> <div className="settingsSectionPart">
<div className="settingsSectionLabel">Explorer Version</div> <div className="settingsSectionLabel">Explorer Version</div>

View File

@ -274,7 +274,7 @@ exports[`Settings Pane should render Default properly 1`] = `
className="settingsSection" className="settingsSection"
> >
<div <div
className="settingsSectionPart" className="settingsSectionPart settingsSectionInlineCheckbox"
> >
<div <div
className="settingsSectionLabel" className="settingsSectionLabel"
@ -303,7 +303,7 @@ exports[`Settings Pane should render Default properly 1`] = `
className="settingsSection" className="settingsSection"
> >
<div <div
className="settingsSectionPart" className="settingsSectionPart settingsSectionInlineCheckbox"
> >
<div <div
className="settingsSectionLabel" className="settingsSectionLabel"
@ -521,7 +521,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
className="settingsSection" className="settingsSection"
> >
<div <div
className="settingsSectionPart" className="settingsSectionPart settingsSectionInlineCheckbox"
> >
<div <div
className="settingsSectionLabel" className="settingsSectionLabel"

View File

@ -15,7 +15,12 @@ import { MinimalQueryIterator } from "Common/IteratorUtilities";
import { createUri } from "Common/UrlUtility"; import { createUri } from "Common/UrlUtility";
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage"; import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
import { configContext } from "ConfigContext"; import { configContext } from "ConfigContext";
import { ContainerConnectionInfo, CopilotEnabledConfiguration, IProvisionData } from "Contracts/DataModels"; import {
ContainerConnectionInfo,
CopilotEnabledConfiguration,
FeatureRegistration,
IProvisionData,
} from "Contracts/DataModels";
import { AuthorizationTokenHeaderMetadata, QueryResults } from "Contracts/ViewModels"; import { AuthorizationTokenHeaderMetadata, QueryResults } from "Contracts/ViewModels";
import { useDialog } from "Explorer/Controls/Dialog"; import { useDialog } from "Explorer/Controls/Dialog";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
@ -52,6 +57,28 @@ async function fetchWithTimeout(
return response; return response;
} }
export const isCopilotFeatureRegistered = async (subscriptionId: string): Promise<boolean> => {
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<boolean> => { export const getCopilotEnabled = async (): Promise<boolean> => {
const url = `${configContext.BACKEND_ENDPOINT}/api/portalsettings/querycopilot`; const url = `${configContext.BACKEND_ENDPOINT}/api/portalsettings/querycopilot`;
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader(); const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();

View File

@ -148,23 +148,25 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
/> />
</Stack> </Stack>
<Stack horizontal tokens={{ childrenGap: 16 }}> <Stack horizontal tokens={{ childrenGap: 16 }}>
<SplashScreenButton {useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotSampleDBEnabled && (
imgSrc={CopilotIcon} <SplashScreenButton
title={"Query faster with Copilot"} imgSrc={CopilotIcon}
description={ title={"Query faster with Copilot"}
"Copilot is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!" description={
} "Copilot is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
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 }); 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 });
}}
/>
)}
<SplashScreenButton <SplashScreenButton
imgSrc={ConnectIcon} imgSrc={ConnectIcon}
title={"Connect"} title={"Connect"}

View File

@ -769,7 +769,10 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
const dataRootNode = buildDataTree(); const dataRootNode = buildDataTree();
const isSampleDataEnabled = const isSampleDataEnabled =
useQueryCopilot().copilotEnabled && userContext.sampleDataConnectionInfo && userContext.apiType === "SQL"; useQueryCopilot().copilotEnabled &&
useQueryCopilot().copilotSampleDBEnabled &&
userContext.sampleDataConnectionInfo &&
userContext.apiType === "SQL";
const sampleDataResourceTokenCollection = useDatabases((state) => state.sampleDataResourceTokenCollection); const sampleDataResourceTokenCollection = useDatabases((state) => state.sampleDataResourceTokenCollection);
return ( return (

View File

@ -11,6 +11,7 @@ export enum StorageKey {
MaxWaitTimeInSeconds, MaxWaitTimeInSeconds,
AutomaticallyCancelQueryAfterTimeout, AutomaticallyCancelQueryAfterTimeout,
ContainerPaginationEnabled, ContainerPaginationEnabled,
CopilotSampleDBEnabled,
CustomItemPerPage, CustomItemPerPage,
DatabaseAccountId, DatabaseAccountId,
EncryptedKeyToken, EncryptedKeyToken,

View File

@ -10,6 +10,7 @@ import { ContainerInfo } from "../Contracts/DataModels";
export interface QueryCopilotState { export interface QueryCopilotState {
copilotEnabled: boolean; copilotEnabled: boolean;
copilotUserDBEnabled: boolean; copilotUserDBEnabled: boolean;
copilotSampleDBEnabled: boolean;
generatedQuery: string; generatedQuery: string;
likeQuery: boolean; likeQuery: boolean;
userPrompt: string; userPrompt: string;
@ -50,6 +51,7 @@ export interface QueryCopilotState {
setCopilotEnabled: (copilotEnabled: boolean) => void; setCopilotEnabled: (copilotEnabled: boolean) => void;
setCopilotUserDBEnabled: (copilotUserDBEnabled: boolean) => void; setCopilotUserDBEnabled: (copilotUserDBEnabled: boolean) => void;
setCopilotSampleDBEnabled: (copilotSampleDBEnabled: boolean) => void;
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) => void; openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) => void;
closeFeedbackModal: () => void; closeFeedbackModal: () => void;
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) => void; setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) => void;
@ -96,6 +98,7 @@ type QueryCopilotStore = UseStore<QueryCopilotState>;
export const useQueryCopilot: QueryCopilotStore = create((set) => ({ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
copilotEnabled: false, copilotEnabled: false,
copilotUserDBEnabled: false, copilotUserDBEnabled: false,
copilotSampleDBEnabled: false,
generatedQuery: "", generatedQuery: "",
likeQuery: false, likeQuery: false,
userPrompt: "", userPrompt: "",
@ -145,6 +148,7 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
setCopilotEnabled: (copilotEnabled: boolean) => set({ copilotEnabled }), setCopilotEnabled: (copilotEnabled: boolean) => set({ copilotEnabled }),
setCopilotUserDBEnabled: (copilotUserDBEnabled: boolean) => set({ copilotUserDBEnabled }), setCopilotUserDBEnabled: (copilotUserDBEnabled: boolean) => set({ copilotUserDBEnabled }),
setCopilotSampleDBEnabled: (copilotSampleDBEnabled: boolean) => set({ copilotSampleDBEnabled }),
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) => openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }), set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
closeFeedbackModal: () => set({ showFeedbackModal: false }), closeFeedbackModal: () => set({ showFeedbackModal: false }),