diff --git a/packages/atlas-service/src/main.ts b/packages/atlas-service/src/main.ts index cbef345e325..bf6c4ca4c49 100644 --- a/packages/atlas-service/src/main.ts +++ b/packages/atlas-service/src/main.ts @@ -41,7 +41,7 @@ import preferences from 'compass-preferences-model'; import { SecretStore, SECRET_STORE_KEY } from './secret-store'; import { AtlasUserConfigStore } from './user-config-store'; import { OidcPluginLogger } from './oidc-plugin-logger'; -import { getActiveUser } from 'compass-preferences-model'; +import { getActiveUser, isAIFeatureEnabled } from 'compass-preferences-model'; import { spawn } from 'child_process'; const { log, track } = createLoggerAndTelemetry('COMPASS-ATLAS-SERVICE'); @@ -92,11 +92,7 @@ export async function throwIfNotOk( } function throwIfAINotEnabled(atlasService: typeof AtlasService) { - if ( - (!preferences.getPreferences().cloudFeatureRolloutAccess?.GEN_AI_COMPASS && - !preferences.getPreferences().enableAIWithoutRolloutAccess) || - !preferences.getPreferences().enableAIFeatures - ) { + if (!isAIFeatureEnabled()) { throw new Error( "Compass' AI functionality is not currently enabled. Please try again later." ); diff --git a/packages/compass-e2e-tests/tests/collection-ai-query.test.ts b/packages/compass-e2e-tests/tests/collection-ai-query.test.ts index 59f95b53dab..72abd16cbbd 100644 --- a/packages/compass-e2e-tests/tests/collection-ai-query.test.ts +++ b/packages/compass-e2e-tests/tests/collection-ai-query.test.ts @@ -44,7 +44,7 @@ describe('Collection ai query', function () { telemetry = await startTelemetryServer(); compass = await beforeTests({ - extraSpawnArgs: ['--enableAIExperience'], + extraSpawnArgs: ['--enableGenAIExperience'], }); browser = compass.browser; }); diff --git a/packages/compass-preferences-model/src/feature-flags.ts b/packages/compass-preferences-model/src/feature-flags.ts index 06a4591e6ca..0ceb5352315 100644 --- a/packages/compass-preferences-model/src/feature-flags.ts +++ b/packages/compass-preferences-model/src/feature-flags.ts @@ -14,7 +14,7 @@ export type FeatureFlagDefinition = { }; export type FeatureFlags = { - enableAIExperience: boolean; + enableGenAIExperience: boolean; enableAIWithoutRolloutAccess: boolean; enableLgDarkmode: boolean; enableOidc: boolean; // Not capitalized "OIDC" for spawn arg casing. @@ -31,7 +31,7 @@ export const featureFlags: Required<{ * Feature flag for enabling the natural text input on the query bar. * Epic: COMPASS-6866 */ - enableAIExperience: { + enableGenAIExperience: { stage: 'released', description: { short: 'Compass AI Features', diff --git a/packages/compass-preferences-model/src/index.ts b/packages/compass-preferences-model/src/index.ts index 9c7dc753777..dd2e9358798 100644 --- a/packages/compass-preferences-model/src/index.ts +++ b/packages/compass-preferences-model/src/index.ts @@ -29,6 +29,7 @@ export { setupPreferencesAndUser, getActiveUser, useIsAIFeatureEnabled, + isAIFeatureEnabled, } from './utils'; export type { User } from './storage'; diff --git a/packages/compass-preferences-model/src/preferences.spec.ts b/packages/compass-preferences-model/src/preferences.spec.ts index 214f64fecbf..75ab3885ea2 100644 --- a/packages/compass-preferences-model/src/preferences.spec.ts +++ b/packages/compass-preferences-model/src/preferences.spec.ts @@ -151,7 +151,7 @@ describe('Preferences class', function () { enableDevTools: 'set-global', networkTraffic: 'set-global', trackUsageStatistics: 'set-global', - enableAIFeatures: 'set-global', + enableGenAIFeatures: 'set-global', enableMaps: 'set-cli', enableShell: 'set-cli', readOnly: 'set-global', @@ -215,7 +215,7 @@ describe('Preferences class', function () { }, { networkTraffic: false, - enableAIFeatures: false, + enableGenAIFeatures: false, enableMaps: false, enableFeedbackPanel: false, trackUsageStatistics: false, @@ -248,7 +248,7 @@ describe('Preferences class', function () { const states = preferences.getPreferenceStates(); expect(states).to.deep.equal({ - enableAIFeatures: 'hardcoded', + enableGenAIFeatures: '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 a5178c3c8e5..b8b475c17f0 100644 --- a/packages/compass-preferences-model/src/preferences.ts +++ b/packages/compass-preferences-model/src/preferences.ts @@ -26,7 +26,7 @@ export type UserConfigurablePreferences = PermanentFeatureFlags & FeatureFlags & { // User-facing preferences autoUpdates: boolean; - enableAIFeatures: boolean; + enableGenAIFeatures: boolean; enableMaps: boolean; trackUsageStatistics: boolean; enableFeedbackPanel: boolean; @@ -439,7 +439,7 @@ export const storedUserPreferencesProps: Required<{ validator: z.boolean().default(false), type: 'boolean', }, - enableAIFeatures: { + enableGenAIFeatures: { ui: true, cli: true, global: true, @@ -447,7 +447,7 @@ export const storedUserPreferencesProps: Required<{ 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'), + deriveValue: deriveNetworkTrafficOptionState('enableGenAIFeatures'), validator: z.boolean().default(true), type: 'boolean', }, @@ -1064,7 +1064,7 @@ export class Preferences { if (!showedNetworkOptIn) { await this.savePreferences({ autoUpdates: true, - enableAIFeatures: true, + enableGenAIFeatures: 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 028caed8b32..c0f938193bc 100644 --- a/packages/compass-preferences-model/src/utils.ts +++ b/packages/compass-preferences-model/src/utils.ts @@ -1,5 +1,5 @@ import preferences, { preferencesAccess, usePreference } from '.'; -import type { ParsedGlobalPreferencesResult } from '.'; +import type { AllPreferences, ParsedGlobalPreferencesResult } from '.'; import { setupPreferences } from './setup-preferences'; import { UserStorage } from './storage'; import type { ReactHooks } from './react'; @@ -41,21 +41,58 @@ export function capMaxTimeMSAtPreferenceLimit(value: T): T | number { return value; } +/** + * Helper method to check whether or not AI feature is enabled in Compass. The + * feature is considered enabled if: + * - AI feature flag is enabled + * - config preference that controls AI is enabled + * - either mms backend rollout enabled feature for the compass user or special + * option to bypass the check is passed + */ +export function isAIFeatureEnabled( + preferences: Pick< + AllPreferences, + | 'enableGenAIFeatures' + | 'enableGenAIExperience' + | 'cloudFeatureRolloutAccess' + | 'enableAIWithoutRolloutAccess' + > = preferencesAccess.getPreferences() +) { + const { + // a "kill switch" property from configuration file to be able to disable + // feature in global config + enableGenAIFeatures, + // feature flag + enableGenAIExperience, + // based on mms backend rollout response + cloudFeatureRolloutAccess, + // feature flag to bypass rollout access check + enableAIWithoutRolloutAccess, + } = preferences; + return ( + enableGenAIFeatures && + enableGenAIExperience && + (!!cloudFeatureRolloutAccess?.GEN_AI_COMPASS || + enableAIWithoutRolloutAccess) + ); +} + export function useIsAIFeatureEnabled(React: ReactHooks) { + const enableGenAIFeatures = usePreference('enableGenAIFeatures', React); + const enableGenAIExperience = usePreference('enableGenAIExperience', React); + const cloudFeatureRolloutAccess = usePreference( + 'cloudFeatureRolloutAccess', + React + ); const enableAIWithoutRolloutAccess = usePreference( 'enableAIWithoutRolloutAccess', React ); - const enableAIExperience = usePreference('enableAIExperience', React); - const isAIFeatureEnabled = usePreference( - 'cloudFeatureRolloutAccess', - React - )?.GEN_AI_COMPASS; - const enableAIFeatures = usePreference('enableAIFeatures', React); - return ( - enableAIExperience && - (enableAIWithoutRolloutAccess || isAIFeatureEnabled) && - enableAIFeatures - ); + return isAIFeatureEnabled({ + enableGenAIFeatures, + enableGenAIExperience, + cloudFeatureRolloutAccess, + enableAIWithoutRolloutAccess, + }); } 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 8ab43c263a4..3d15a29076c 100644 --- a/packages/compass-query-bar/src/components/query-bar.spec.tsx +++ b/packages/compass-query-bar/src/components/query-bar.spec.tsx @@ -125,8 +125,8 @@ describe('QueryBar Component', function () { beforeEach(function () { sandbox = sinon.createSandbox(); sandbox.stub(preferencesAccess, 'getPreferences').returns({ - enableAIExperience: true, - enableAIFeatures: true, + enableGenAIExperience: true, + enableGenAIFeatures: true, cloudFeatureRolloutAccess: { GEN_AI_COMPASS: true, }, @@ -172,14 +172,14 @@ describe('QueryBar Component', function () { }); }); - describe('with enableAIExperience ai disabled', function () { + describe('with enableGenAIExperience ai disabled', function () { let sandbox: sinon.SinonSandbox; beforeEach(function () { sandbox = sinon.createSandbox(); sandbox.stub(preferencesAccess, 'getPreferences').returns({ - enableAIExperience: false, - enableAIFeatures: true, + enableGenAIExperience: false, + enableGenAIFeatures: true, cloudFeatureRolloutAccess: { GEN_AI_COMPASS: true, }, @@ -198,14 +198,14 @@ describe('QueryBar Component', function () { }); }); - describe('with enableAIFeatures ai disabled', function () { + describe('with enableGenAIFeatures ai disabled', function () { let sandbox: sinon.SinonSandbox; beforeEach(function () { sandbox = sinon.createSandbox(); sandbox.stub(preferencesAccess, 'getPreferences').returns({ - enableAIExperience: true, - enableAIFeatures: false, + enableGenAIExperience: true, + enableGenAIFeatures: false, cloudFeatureRolloutAccess: { GEN_AI_COMPASS: true, }, diff --git a/packages/compass-settings/src/components/modal.tsx b/packages/compass-settings/src/components/modal.tsx index 35fbafb530b..16ce7573a31 100644 --- a/packages/compass-settings/src/components/modal.tsx +++ b/packages/compass-settings/src/components/modal.tsx @@ -19,6 +19,7 @@ import Sidebar from './sidebar'; import { saveSettings, closeModal } from '../stores/settings'; import type { RootState } from '../stores'; import { getUserInfo } from '../stores/atlas-login'; +import { useIsAIFeatureEnabled } from 'compass-preferences-model'; type Settings = { name: string; @@ -27,7 +28,7 @@ type Settings = { type SettingsModalProps = { isOpen: boolean; - isOIDCTabEnabled: boolean; + isOIDCEnabled: boolean; onMount?: () => void; onClose: () => void; onSave: () => void; @@ -59,9 +60,10 @@ export const SettingsModal: React.FunctionComponent = ({ onMount, onClose, onSave, - isOIDCTabEnabled, + isOIDCEnabled, hasChangedSettings, }) => { + const aiFeatureEnabled = useIsAIFeatureEnabled(React); const onMountRef = useRef(onMount); useEffect(() => { @@ -74,7 +76,11 @@ export const SettingsModal: React.FunctionComponent = ({ { name: 'Privacy', component: PrivacySettings }, ]; - if (isOIDCTabEnabled) { + if ( + isOIDCEnabled || + // because oidc options overlap with atlas login used for ai feature + aiFeatureEnabled + ) { settings.push({ name: 'OIDC (Preview)', component: OIDCSettings, @@ -133,12 +139,7 @@ export default connect( return { isOpen: state.settings.isModalOpen && state.settings.loadingState === 'ready', - isOIDCTabEnabled: - !!state.settings.settings.enableOidc || - // because oidc options overlap with atlas login used for ai feature - !!state.settings.settings.enableAIWithoutRolloutAccess || - !!state.settings.settings.enableAIExperience || - !!state.settings.settings.enableAIFeatures, + isOIDCEnabled: !!state.settings.settings.enableOidc, hasChangedSettings: state.settings.updatedFields.length > 0, }; }, diff --git a/packages/compass-settings/src/components/settings/feature-preview.tsx b/packages/compass-settings/src/components/settings/feature-preview.tsx index 0de3cdaa265..1e8adb28241 100644 --- a/packages/compass-settings/src/components/settings/feature-preview.tsx +++ b/packages/compass-settings/src/components/settings/feature-preview.tsx @@ -3,11 +3,8 @@ import SettingsList from './settings-list'; import { usePreference, featureFlags, - withPreferences, + useIsAIFeatureEnabled, } from 'compass-preferences-model'; -import type { UserPreferences } from 'compass-preferences-model'; -import { connect } from 'react-redux'; -import type { RootState } from '../../stores'; import { ConnectedAtlasLoginSettings } from './atlas-login'; import { css, spacing } from '@mongodb-js/compass-components'; @@ -36,33 +33,24 @@ function useShouldShowPreviewFeatures(): boolean { export function useShouldShowFeaturePreviewSettings(): boolean { // We want show the feature preview settings tab if: + // - AI feature flag is enabled // - there are feature flags in preview stage // - or if: // - we are in a development environment or 'showDevFeatureFlags' is explicitly enabled // - and there are feature flags in 'development' stage. - // - AI feature flag is enabled - const enableAIWithoutRolloutAccess = usePreference( - 'enableAIWithoutRolloutAccess', - React - ); - const enableAIExperience = usePreference('enableAIExperience', React); + const aiFeatureEnabled = useIsAIFeatureEnabled(React); const showDevFeatures = useShouldShowDevFeatures(); const showPreviewFeatures = useShouldShowPreviewFeatures(); - return ( - enableAIWithoutRolloutAccess || - enableAIExperience || - showPreviewFeatures || - showDevFeatures - ); + + return aiFeatureEnabled || showPreviewFeatures || showDevFeatures; } const atlasSettingsContainerStyles = css({ marginTop: spacing[3], }); -export const FeaturePreviewSettings: React.FunctionComponent<{ - showAtlasLoginSettings?: boolean; -}> = ({ showAtlasLoginSettings }) => { +export const FeaturePreviewSettings: React.FunctionComponent = () => { + const aiFeatureEnabled = useIsAIFeatureEnabled(React); const showPreviewFeatures = useShouldShowPreviewFeatures(); const showDevFeatures = useShouldShowDevFeatures(); @@ -73,7 +61,7 @@ export const FeaturePreviewSettings: React.FunctionComponent<{ your own risk! - {showAtlasLoginSettings && ( + {aiFeatureEnabled && (
@@ -92,30 +80,4 @@ export const FeaturePreviewSettings: React.FunctionComponent<{ ); }; -export default withPreferences( - connect( - ( - state: RootState, - ownProps: { - enableAIExperience?: boolean; - enableAIWithoutRolloutAccess?: boolean; - cloudFeatureRolloutAccess?: UserPreferences['cloudFeatureRolloutAccess']; - } - ) => { - return { - showAtlasLoginSettings: - state.settings.settings.enableAIExperience || - ['authenticated', 'in-progress'].includes(state.atlasLogin.status) || - ownProps.enableAIExperience || - ownProps.enableAIWithoutRolloutAccess || - ownProps.cloudFeatureRolloutAccess?.GEN_AI_COMPASS, - }; - } - )(FeaturePreviewSettings), - [ - 'enableAIExperience', - 'cloudFeatureRolloutAccess', - 'enableAIWithoutRolloutAccess', - ], - React -); +export default FeaturePreviewSettings; diff --git a/packages/compass-settings/src/components/settings/privacy.spec.tsx b/packages/compass-settings/src/components/settings/privacy.spec.tsx index d88a661d6da..ab97991ff99 100644 --- a/packages/compass-settings/src/components/settings/privacy.spec.tsx +++ b/packages/compass-settings/src/components/settings/privacy.spec.tsx @@ -62,17 +62,17 @@ describe('PrivacySettings', function () { }); }); - it('does not render enableAIFeatures when isAIFeatureRolledOutToUser is false', function () { + it('does not render enableGenAIFeatures when isAIFeatureRolledOutToUser is false', function () { container = renderPrivacySettings(store, { isAIFeatureRolledOutToUser: false, }); - expect(within(container).queryByTestId('enableAIFeatures')).to.not.exist; + expect(within(container).queryByTestId('enableGenAIFeatures')).to.not.exist; }); - it('renders enableAIFeatures when GisAIFeatureRolledOutToUser is true', function () { + it('renders enableGenAIFeatures when GisAIFeatureRolledOutToUser is true', function () { container = renderPrivacySettings(store, { isAIFeatureRolledOutToUser: true, }); - expect(within(container).getByTestId('enableAIFeatures')).to.be.visible; + expect(within(container).getByTestId('enableGenAIFeatures')).to.be.visible; }); }); diff --git a/packages/compass-settings/src/components/settings/privacy.tsx b/packages/compass-settings/src/components/settings/privacy.tsx index bd7b1927824..2d8e01c2240 100644 --- a/packages/compass-settings/src/components/settings/privacy.tsx +++ b/packages/compass-settings/src/components/settings/privacy.tsx @@ -10,7 +10,7 @@ import type { RootState } from '../../stores'; const privacyFields = [ 'autoUpdates', 'enableMaps', - 'enableAIFeatures', + 'enableGenAIFeatures', 'trackUsageStatistics', 'enableFeedbackPanel', ] as const; @@ -21,7 +21,7 @@ export const PrivacySettings: React.FunctionComponent<{ const privacyFieldsShown = useMemo(() => { return isAIFeatureRolledOutToUser ? privacyFields - : privacyFields.filter((field) => field !== 'enableAIFeatures'); + : privacyFields.filter((field) => field !== 'enableGenAIFeatures'); }, [isAIFeatureRolledOutToUser]); return (