Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const CompassAssistantDrawer: React.FunctionComponent<{
'data-testid': 'assistant-confirm-clear-chat-modal',
});
if (confirmed) {
clearChat();
clearChat?.();
}
}, [clearChat]);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import {
render,
renderHook,
screen,
userEvent,
waitFor,
Expand All @@ -10,6 +11,7 @@ import {
import {
AssistantProvider,
CompassAssistantProvider,
useAssistantActions,
type AssistantMessage,
} from './compass-assistant-provider';
import { expect } from 'chai';
Expand Down Expand Up @@ -41,6 +43,94 @@ const TestComponent: React.FunctionComponent<{
);
};

describe('useAssistantActions', function () {
const createWrapper = (chat: Chat<AssistantMessage>) => {
function TestWrapper({ children }: { children: React.ReactNode }) {
return (
<DrawerContentProvider>
<AssistantProvider chat={chat}>{children}</AssistantProvider>
</DrawerContentProvider>
);
}
return TestWrapper;
};

it('returns empty object when AI features are disabled via isAIFeatureEnabled', function () {
const { result } = renderHook(() => useAssistantActions(), {
wrapper: createWrapper(createMockChat({ messages: [] })),
preferences: {
enableAIAssistant: true,
// These control isAIFeatureEnabled
enableGenAIFeatures: false,
enableGenAIFeaturesAtlasOrg: true,
cloudFeatureRolloutAccess: { GEN_AI_COMPASS: true },
},
});

expect(result.current).to.deep.equal({});
});

it('returns empty object when enableGenAIFeaturesAtlasOrg is disabled', function () {
const { result } = renderHook(() => useAssistantActions(), {
wrapper: createWrapper(createMockChat({ messages: [] })),
preferences: {
enableAIAssistant: true,
enableGenAIFeatures: true,
enableGenAIFeaturesAtlasOrg: false,
cloudFeatureRolloutAccess: { GEN_AI_COMPASS: true },
},
});

expect(result.current).to.deep.equal({});
});

it('returns empty object when cloudFeatureRolloutAccess is disabled', function () {
const { result } = renderHook(() => useAssistantActions(), {
wrapper: createWrapper(createMockChat({ messages: [] })),
preferences: {
enableAIAssistant: true,
enableGenAIFeatures: true,
enableGenAIFeaturesAtlasOrg: true,
cloudFeatureRolloutAccess: { GEN_AI_COMPASS: false },
},
});

expect(result.current).to.deep.equal({});
});

it('returns empty object when enableAIAssistant preference is disabled', function () {
const { result } = renderHook(() => useAssistantActions(), {
wrapper: createWrapper(createMockChat({ messages: [] })),
preferences: {
enableAIAssistant: false,
enableGenAIFeatures: true,
enableGenAIFeaturesAtlasOrg: true,
cloudFeatureRolloutAccess: { GEN_AI_COMPASS: true },
},
});

expect(result.current).to.deep.equal({});
});

it('returns actions when both AI features and assistant flag are enabled', function () {
const { result } = renderHook(() => useAssistantActions(), {
wrapper: createWrapper(createMockChat({ messages: [] })),
preferences: {
enableAIAssistant: true,
enableGenAIFeatures: true,
enableGenAIFeaturesAtlasOrg: true,
cloudFeatureRolloutAccess: { GEN_AI_COMPASS: true },
},
});

expect(Object.keys(result.current)).to.have.length.greaterThan(0);
expect(result.current.interpretExplainPlan).to.be.a('function');
expect(result.current.interpretConnectionError).to.be.a('function');
expect(result.current.tellMoreAboutInsight).to.be.a('function');
expect(result.current.clearChat).to.be.a('function');
});
});

describe('AssistantProvider', function () {
const mockMessages: AssistantMessage[] = [
{
Expand Down
43 changes: 19 additions & 24 deletions packages/compass-assistant/src/compass-assistant-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import {
type EntryPointMessage,
type ProactiveInsightsContext,
} from './prompts';
import { usePreference } from 'compass-preferences-model/provider';
import {
useIsAIFeatureEnabled,
usePreference,
} from 'compass-preferences-model/provider';
import { createLoggerLocator } from '@mongodb-js/compass-logging/provider';
import type { ConnectionInfo } from '@mongodb-js/connection-info';
import { useTelemetry } from '@mongodb-js/compass-telemetry/provider';
Expand All @@ -37,22 +40,22 @@ export const AssistantContext = createContext<AssistantContextType | null>(
);

type AssistantActionsContextType = {
interpretExplainPlan: ({
interpretExplainPlan?: ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about making these optional and using the existence of the callbacks to be able to tell if the thing should be enabled or not, but was worried that it kinda disabled some type checking benefits - you can't tell between the feature being enabled/disabled vs a mock/stub just not containing it, for example.

But I was probably overthinking it.

Lower down like in the connection error entry point it indeed makes sense to have just the optional callback rather than a boolean and the optional callback. This high up.. I don't know. It is probably fine.

I don't have strong feelings about it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the alternative tends to make us be attentive in also including the isEnabled returned from the hook in our conditions for showing the button which can be easy to oversee so I find this to be more type checked for this purpose. Open to revisiting it later if we run into problems.

namespace,
explainPlan,
}: {
namespace: string;
explainPlan: string;
}) => void;
interpretConnectionError: ({
interpretConnectionError?: ({
connectionInfo,
error,
}: {
connectionInfo: ConnectionInfo;
error: Error;
}) => void;
clearChat: () => void;
tellMoreAboutInsight: (context: ProactiveInsightsContext) => void;
clearChat?: () => void;
tellMoreAboutInsight?: (context: ProactiveInsightsContext) => void;
};
export const AssistantActionsContext =
createContext<AssistantActionsContextType>({
Expand All @@ -62,29 +65,21 @@ export const AssistantActionsContext =
clearChat: () => {},
});

export function useAssistantActions(): AssistantActionsContextType & {
isAssistantEnabled: boolean;
} {
const isAssistantEnabled = usePreference('enableAIAssistant');
export function useAssistantActions(): AssistantActionsContextType {
const isAIFeatureEnabled = useIsAIFeatureEnabled();
const isAssistantFlagEnabled = usePreference('enableAIAssistant');
const actions = useContext(AssistantActionsContext);
if (!isAIFeatureEnabled || !isAssistantFlagEnabled) {
return {};
}

return {
...useContext(AssistantActionsContext),
isAssistantEnabled,
};
return actions;
}

export const compassAssistantServiceLocator = createServiceLocator(function () {
const { isAssistantEnabled, ...actions } = useAssistantActions();
const actions = useAssistantActions();

const assistantEnabledRef = useRef(isAssistantEnabled);
assistantEnabledRef.current = isAssistantEnabled;

return {
...actions,
getIsAssistantEnabled() {
return assistantEnabledRef.current;
},
};
return actions;
}, 'compassAssistantLocator');

export type CompassAssistantService = ReturnType<
Expand Down Expand Up @@ -160,7 +155,7 @@ export const CompassAssistantProvider = registerCompassPlugin(
transport: new DocsProviderTransport({
baseUrl: atlasService.assistantApiEndpoint(),
}),
onError: (err) => {
onError: (err: Error) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I swear this gets either added or removed every other PR 😆

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am rebasing so much everything feels like a deja-vu 😆

logger.log.error(
logger.mongoLogId(1_001_000_370),
'Assistant',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,8 @@ export function getConnectingStatusText(connectionInfo: ConnectionInfo) {
type ConnectionErrorToastBodyProps = {
info?: ConnectionInfo | null;
error: Error;
showReviewButton: boolean;
showDebugButton: boolean;
onReview: () => void;
onDebug: () => void;
onReview?: () => void;
onDebug?: () => void;
};

const connectionErrorToastStyles = css({
Expand Down Expand Up @@ -94,8 +92,6 @@ const debugActionStyles = css({
function ConnectionErrorToastBody({
info,
error,
showReviewButton,
showDebugButton,
onReview,
onDebug,
}: ConnectionErrorToastBodyProps): React.ReactElement {
Expand All @@ -111,7 +107,7 @@ function ConnectionErrorToastBody({
<span data-testid="connection-error-text">{error.message}</span>
</span>
<span className={connectionErrorActionsStyles}>
{info && showReviewButton && (
{info && onReview && (
<span>
<Button
onClick={onReview}
Expand All @@ -122,7 +118,7 @@ function ConnectionErrorToastBody({
</Button>
</span>
)}
{info && showDebugButton && (
{info && onDebug && (
<span className={debugActionStyles}>
<Icon glyph="Sparkle" size="small"></Icon>
<Link
Expand Down Expand Up @@ -182,8 +178,6 @@ const openConnectionSucceededToast = (connectionInfo: ConnectionInfo) => {
const openConnectionFailedToast = ({
connectionInfo,
error,
showReviewButton,
showDebugButton,
onReviewClick,
onDebugClick,
}: {
Expand All @@ -192,10 +186,8 @@ const openConnectionFailedToast = ({
// can happen is autoconnect flow
connectionInfo: ConnectionInfo | null | undefined;
error: Error;
showReviewButton: boolean;
showDebugButton: boolean;
onReviewClick: () => void;
onDebugClick: () => void;
onReviewClick?: () => void;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

personal preference but I think since showing and the handler are inherently tied it's good to combine the two. It's a pattern in LG too so works out pretty well usually

onDebugClick?: () => void;
}) => {
const failedToastId = connectionInfo?.id ?? 'failed';

Expand All @@ -206,19 +198,19 @@ const openConnectionFailedToast = ({
<ConnectionErrorToastBody
info={connectionInfo}
error={error}
showReviewButton={showReviewButton}
showDebugButton={showDebugButton}
onReview={() => {
if (!showDebugButton) {
// don't close the toast if there are two actions so that the user
// can still use the other one
closeToast(`connection-status--${failedToastId}`);
}
onReviewClick();
}}
onDebug={() => {
onDebugClick();
}}
onReview={
onReviewClick
? () => {
if (!onDebugClick) {
// don't close the toast if there are two actions so that the user
// can still use the other one
closeToast(`connection-status--${failedToastId}`);
}
onReviewClick();
}
: undefined
}
onDebug={onDebugClick}
/>
),
variant: 'warning',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1272,27 +1272,26 @@ const connectionAttemptError = (
) => {
const { openConnectionFailedToast } = getNotificationTriggers();

const isAssistanceEnabled = compassAssistant.getIsAssistantEnabled();

const showReviewButton = !!connectionInfo && !connectionInfo.atlasMetadata;
openConnectionFailedToast({
connectionInfo,
error: err,
showReviewButton,
showDebugButton: isAssistanceEnabled,
onReviewClick() {
if (connectionInfo) {
dispatch(editConnection(connectionInfo.id));
}
},
onDebugClick() {
if (connectionInfo) {
compassAssistant.interpretConnectionError({
connectionInfo,
error: err,
});
}
},
onReviewClick: showReviewButton
? () => {
if (connectionInfo) {
dispatch(editConnection(connectionInfo.id));
}
}
: undefined,
onDebugClick:
compassAssistant.interpretConnectionError && connectionInfo
? () => {
compassAssistant.interpretConnectionError?.({
connectionInfo,
error: err,
});
}
: undefined,
});

track(
Expand Down
12 changes: 7 additions & 5 deletions packages/compass-crud/src/components/document-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -558,11 +558,13 @@ const DocumentList: React.FunctionComponent<DocumentListProps> = (props) => {
isSearchIndexesSupported,
onCreateIndex: store.openCreateIndexModal.bind(store),
onCreateSearchIndex: store.openCreateSearchIndexModal.bind(store),
onAssistantButtonClick: () =>
tellMoreAboutInsight({
id: 'query-executed-without-index',
query: JSON.stringify(query),
}),
onAssistantButtonClick: tellMoreAboutInsight
? () =>
tellMoreAboutInsight({
id: 'query-executed-without-index',
query: JSON.stringify(query),
})
: undefined,
})}
docsPerPage={docsPerPage}
updateMaxDocumentsPerPage={handleMaxDocsPerPageChanged}
Expand Down
2 changes: 1 addition & 1 deletion packages/compass-crud/src/utils/toolbar-signal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const getToolbarSignal = ({
isSearchIndexesSupported: boolean;
onCreateIndex: () => void;
onCreateSearchIndex: () => void;
onAssistantButtonClick: () => void;
onAssistantButtonClick?: () => void;
}): Signal | undefined => {
if (!isCollectionScan) {
return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export const ExplainPlanModal: React.FunctionComponent<
error,
onModalClose,
}) => {
const { interpretExplainPlan, isAssistantEnabled } = useAssistantActions();
const { interpretExplainPlan } = useAssistantActions();

return (
<Modal
Expand Down Expand Up @@ -132,7 +132,7 @@ export const ExplainPlanModal: React.FunctionComponent<
}
/>
</div>
{isAssistantEnabled && explainPlan && (
{explainPlan && interpretExplainPlan && (
<div className={headerButtonSectionStyles}>
<Button
size="small"
Expand Down