From a48aee7028eac4ddad01cf70093d26c6da07569c Mon Sep 17 00:00:00 2001 From: Rhys Date: Mon, 18 Sep 2023 14:37:18 -0400 Subject: [PATCH] feat(compass-preferences-model): add preference for enabling ai features COMPASS-7147 (#4845) --- packages/atlas-service/src/main.ts | 5 +- .../src/preferences.spec.ts | 3 + .../src/preferences.ts | 15 +++- .../compass-preferences-model/src/utils.ts | 3 +- .../src/components/query-bar.spec.tsx | 30 +++++++- .../src/components/settings/privacy.spec.tsx | 68 +++++++++++++------ .../src/components/settings/privacy.tsx | 38 +++++++++-- 7 files changed, 133 insertions(+), 29 deletions(-) diff --git a/packages/atlas-service/src/main.ts b/packages/atlas-service/src/main.ts index 6ebca18a8bc..d8a33cffe04 100644 --- a/packages/atlas-service/src/main.ts +++ b/packages/atlas-service/src/main.ts @@ -93,7 +93,10 @@ export async function throwIfNotOk( } function throwIfAINotEnabled(atlasService: typeof AtlasService) { - if (!preferences.getPreferences().cloudFeatureRolloutAccess?.GEN_AI_COMPASS) { + if ( + !preferences.getPreferences().cloudFeatureRolloutAccess?.GEN_AI_COMPASS || + !preferences.getPreferences().enableAIFeatures + ) { throw new Error( "Compass' AI functionality is not currently enabled. Please try again later." ); diff --git a/packages/compass-preferences-model/src/preferences.spec.ts b/packages/compass-preferences-model/src/preferences.spec.ts index adb1703d311..214f64fecbf 100644 --- a/packages/compass-preferences-model/src/preferences.spec.ts +++ b/packages/compass-preferences-model/src/preferences.spec.ts @@ -151,6 +151,7 @@ describe('Preferences class', function () { enableDevTools: 'set-global', networkTraffic: 'set-global', trackUsageStatistics: 'set-global', + enableAIFeatures: 'set-global', enableMaps: 'set-cli', enableShell: 'set-cli', readOnly: 'set-global', @@ -214,6 +215,7 @@ describe('Preferences class', function () { }, { networkTraffic: false, + enableAIFeatures: false, enableMaps: false, enableFeedbackPanel: false, trackUsageStatistics: false, @@ -246,6 +248,7 @@ describe('Preferences class', function () { const states = preferences.getPreferenceStates(); expect(states).to.deep.equal({ + enableAIFeatures: 'hardcoded', enableDevTools: 'set-global', enableMaps: 'set-cli', enableFeedbackPanel: 'hardcoded', diff --git a/packages/compass-preferences-model/src/preferences.ts b/packages/compass-preferences-model/src/preferences.ts index b5b2c47c82f..d247873484e 100644 --- a/packages/compass-preferences-model/src/preferences.ts +++ b/packages/compass-preferences-model/src/preferences.ts @@ -26,6 +26,7 @@ export type UserConfigurablePreferences = PermanentFeatureFlags & FeatureFlags & { // User-facing preferences autoUpdates: boolean; + enableAIFeatures: boolean; enableMaps: boolean; trackUsageStatistics: boolean; enableFeedbackPanel: boolean; @@ -429,6 +430,18 @@ export const storedUserPreferencesProps: Required<{ validator: z.boolean().default(false), type: 'boolean', }, + enableAIFeatures: { + ui: true, + cli: true, + global: true, + description: { + short: 'Enable AI Features', + long: 'Allow the use of AI features in Compass which make requests to 3rd party services. These features are currently experimental and offered as a preview to only a limited number of users.', + }, + deriveValue: deriveNetworkTrafficOptionState('enableAIFeatures'), + validator: z.boolean().default(true), + type: 'boolean', + }, /** * Switch to enable/disable Intercom panel (renamed from `intercom`). */ @@ -617,7 +630,6 @@ export const storedUserPreferencesProps: Required<{ validator: z.boolean().default(false), type: 'boolean', }, - /** * Chooses atlas service backend configuration from preset * - compas-dev: locally running compass kanopy backend (localhost) @@ -1009,6 +1021,7 @@ export class Preferences { if (!showedNetworkOptIn) { await this.savePreferences({ autoUpdates: true, + enableAIFeatures: true, enableMaps: true, trackUsageStatistics: true, enableFeedbackPanel: true, diff --git a/packages/compass-preferences-model/src/utils.ts b/packages/compass-preferences-model/src/utils.ts index 6edc649bc86..cd6a8d3cce7 100644 --- a/packages/compass-preferences-model/src/utils.ts +++ b/packages/compass-preferences-model/src/utils.ts @@ -47,6 +47,7 @@ export function useIsAIFeatureEnabled(React: ReactHooks) { 'cloudFeatureRolloutAccess', React )?.GEN_AI_COMPASS; + const enableAIFeatures = usePreference('enableAIFeatures', React); - return enableAIExperience && isAIFeatureEnabled; + return enableAIExperience && isAIFeatureEnabled && enableAIFeatures; } diff --git a/packages/compass-query-bar/src/components/query-bar.spec.tsx b/packages/compass-query-bar/src/components/query-bar.spec.tsx index b07afb11e2e..8ab43c263a4 100644 --- a/packages/compass-query-bar/src/components/query-bar.spec.tsx +++ b/packages/compass-query-bar/src/components/query-bar.spec.tsx @@ -126,6 +126,7 @@ describe('QueryBar Component', function () { sandbox = sinon.createSandbox(); sandbox.stub(preferencesAccess, 'getPreferences').returns({ enableAIExperience: true, + enableAIFeatures: true, cloudFeatureRolloutAccess: { GEN_AI_COMPASS: true, }, @@ -171,13 +172,40 @@ describe('QueryBar Component', function () { }); }); - describe('with ai disabled', function () { + describe('with enableAIExperience ai disabled', function () { let sandbox: sinon.SinonSandbox; beforeEach(function () { sandbox = sinon.createSandbox(); sandbox.stub(preferencesAccess, 'getPreferences').returns({ enableAIExperience: false, + enableAIFeatures: true, + cloudFeatureRolloutAccess: { + GEN_AI_COMPASS: true, + }, + } as any); + renderQueryBar({ + queryOptionsLayout: ['filter'], + }); + }); + + afterEach(function () { + return sandbox.restore(); + }); + + it('does not render the ask ai button', function () { + expect(screen.queryByText('Ask AI')).to.not.exist; + }); + }); + + describe('with enableAIFeatures ai disabled', function () { + let sandbox: sinon.SinonSandbox; + + beforeEach(function () { + sandbox = sinon.createSandbox(); + sandbox.stub(preferencesAccess, 'getPreferences').returns({ + enableAIExperience: true, + enableAIFeatures: false, cloudFeatureRolloutAccess: { GEN_AI_COMPASS: true, }, diff --git a/packages/compass-settings/src/components/settings/privacy.spec.tsx b/packages/compass-settings/src/components/settings/privacy.spec.tsx index 8750700bfc7..d88a661d6da 100644 --- a/packages/compass-settings/src/components/settings/privacy.spec.tsx +++ b/packages/compass-settings/src/components/settings/privacy.spec.tsx @@ -7,6 +7,19 @@ import { PrivacySettings } from './privacy'; import { configureStore } from '../../stores'; import { fetchSettings } from '../../stores/settings'; +function renderPrivacySettings( + store, + props: Partial> = {} +) { + const component = () => ( + + + + ); + render(component()); + return screen.getByTestId('privacy-settings'); +} + describe('PrivacySettings', function () { let container: HTMLElement; let store: ReturnType; @@ -18,35 +31,48 @@ describe('PrivacySettings', function () { beforeEach(async function () { store = configureStore(); await store.dispatch(fetchSettings()); - const component = () => ( - - - - ); - render(component()); - container = screen.getByTestId('privacy-settings'); }); afterEach(function () { cleanup(); }); - [ - 'autoUpdates', - 'enableMaps', - 'trackUsageStatistics', - 'enableFeedbackPanel', - ].forEach((option) => { - it(`renders ${option}`, function () { - expect(within(container).getByTestId(option)).to.exist; + describe('when rendered', function () { + beforeEach(function () { + container = renderPrivacySettings(store); }); - it(`changes ${option} value when option is clicked`, function () { - const checkbox = within(container).getByTestId(option); - const initialValue = getSettings()[option]; - userEvent.click(checkbox, undefined, { - skipPointerEventsCheck: true, + + [ + 'autoUpdates', + 'enableMaps', + 'trackUsageStatistics', + 'enableFeedbackPanel', + ].forEach((option) => { + it(`renders ${option}`, function () { + expect(within(container).getByTestId(option)).to.exist; + }); + it(`changes ${option} value when option is clicked`, function () { + const checkbox = within(container).getByTestId(option); + const initialValue = getSettings()[option]; + userEvent.click(checkbox, undefined, { + skipPointerEventsCheck: true, + }); + expect(getSettings()).to.have.property(option, !initialValue); }); - expect(getSettings()).to.have.property(option, !initialValue); }); }); + + it('does not render enableAIFeatures when isAIFeatureRolledOutToUser is false', function () { + container = renderPrivacySettings(store, { + isAIFeatureRolledOutToUser: false, + }); + expect(within(container).queryByTestId('enableAIFeatures')).to.not.exist; + }); + + it('renders enableAIFeatures when GisAIFeatureRolledOutToUser is true', function () { + container = renderPrivacySettings(store, { + isAIFeatureRolledOutToUser: true, + }); + expect(within(container).getByTestId('enableAIFeatures')).to.be.visible; + }); }); diff --git a/packages/compass-settings/src/components/settings/privacy.tsx b/packages/compass-settings/src/components/settings/privacy.tsx index 495963d9643..bd7b1927824 100644 --- a/packages/compass-settings/src/components/settings/privacy.tsx +++ b/packages/compass-settings/src/components/settings/privacy.tsx @@ -1,15 +1,29 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { Link } from '@mongodb-js/compass-components'; +import { connect } from 'react-redux'; +import type { UserPreferences } from 'compass-preferences-model'; +import { withPreferences } from 'compass-preferences-model'; + import SettingsList from './settings-list'; +import type { RootState } from '../../stores'; const privacyFields = [ 'autoUpdates', 'enableMaps', + 'enableAIFeatures', 'trackUsageStatistics', 'enableFeedbackPanel', ] as const; -export const PrivacySettings: React.FunctionComponent = () => { +export const PrivacySettings: React.FunctionComponent<{ + isAIFeatureRolledOutToUser?: boolean; +}> = ({ isAIFeatureRolledOutToUser }) => { + const privacyFieldsShown = useMemo(() => { + return isAIFeatureRolledOutToUser + ? privacyFields + : privacyFields.filter((field) => field !== 'enableAIFeatures'); + }, [isAIFeatureRolledOutToUser]); + return (
@@ -17,7 +31,7 @@ export const PrivacySettings: React.FunctionComponent = () => { services, which requires external network requests. Please choose from the settings below:
- +
With any of these options, none of your personal information or stored data will be submitted. @@ -31,4 +45,20 @@ export const PrivacySettings: React.FunctionComponent = () => { ); }; -export default PrivacySettings; +export default withPreferences( + connect( + ( + state: RootState, + ownProps: { + cloudFeatureRolloutAccess?: UserPreferences['cloudFeatureRolloutAccess']; + } + ) => { + return { + isAIFeatureRolledOutToUser: + ownProps.cloudFeatureRolloutAccess?.GEN_AI_COMPASS, + }; + } + )(PrivacySettings), + ['cloudFeatureRolloutAccess'], + React +);