Skip to content
11 changes: 10 additions & 1 deletion packages/compass-assistant/src/compass-assistant-drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ const assistantTitleTextStyles = css({
* it's within an AssistantProvider.
*/
export const CompassAssistantDrawer: React.FunctionComponent<{
appName: string;
autoOpen?: boolean;
hasNonGenuineConnections?: boolean;
}> = ({ autoOpen, hasNonGenuineConnections = false }) => {
}> = ({ appName, autoOpen, hasNonGenuineConnections = false }) => {
const chat = useContext(AssistantContext);
const { clearChat } = useContext(AssistantActionsContext);

Expand Down Expand Up @@ -92,6 +93,14 @@ export const CompassAssistantDrawer: React.FunctionComponent<{
label="MongoDB Assistant"
glyph="Sparkle"
autoOpen={autoOpen}
guideCue={{
cueId: 'assistant-drawer',
title: 'Introducing MongoDB Assistant',
description: `AI-powered assistant to intelligently guide you through your database tasks. Get expert MongoDB help and streamline your workflow directly within ${appName}.`,
buttonText: 'Got it',
tooltipAlign: 'left',
tooltipJustify: 'start',
}}
>
<AssistantChat
chat={chat}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ const TestComponent: React.FunctionComponent<{
<DrawerAnchor>
<div data-testid="provider-children">Provider children</div>
<CompassAssistantDrawer
appName="Compass"
autoOpen={autoOpen}
hasNonGenuineConnections={hasNonGenuineConnections}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,40 @@ describe('DrawerSection', function () {
expect(screen.queryByText('This is the controlled section')).not.to.exist;
});
});

it('renders guide cue when passed in props', async function () {
localStorage.compass_guide_cues = '[]';
function TestDrawer() {
return (
<DrawerContentProvider>
<DrawerAnchor>
<DrawerSection
id="test-section"
label="Test section"
title={`Test section`}
glyph="Trash"
guideCue={{
cueId: 'test-drawer',
title: 'Introducing this new test drawer',
description: 'Does all the things',
buttonText: 'ok',
tooltipAlign: 'bottom',
tooltipJustify: 'end',
}}
autoOpen
>
This is a test section
</DrawerSection>
</DrawerAnchor>
</DrawerContentProvider>
);
}

render(<TestDrawer></TestDrawer>);

await waitFor(() => {
expect(screen.getByText('Introducing this new test drawer')).to.be
.visible;
});
});
});
86 changes: 72 additions & 14 deletions packages/compass-components/src/components/drawer-portal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { css, cx } from '@leafygreen-ui/emotion';
import { isEqual } from 'lodash';
import { rafraf } from '../utils/rafraf';
import { GuideCue, type GuideCueProps } from './guide-cue/guide-cue';
import { BaseFontSize, fontWeights } from '@leafygreen-ui/tokens';

type ToolbarData = Required<DrawerLayoutProps>['toolbarData'];
Expand All @@ -37,6 +38,7 @@ type DrawerSectionProps = Omit<SectionData, 'content' | 'onClick'> & {
* provided will stay unordered at the bottom of the list
*/
order?: number;
guideCue?: GuideCueProps<HTMLButtonElement>;
};

type DrawerOpenStateContextValue = boolean;
Expand Down Expand Up @@ -271,21 +273,77 @@ export const DrawerAnchor: React.FunctionComponent = ({ children }) => {
return orderB < orderA ? 1 : orderB > orderA ? -1 : 0;
});
}, [drawerSectionItems]);

const [assistantNodes, setAssistantNodes] = useState<
Record<string, HTMLButtonElement | undefined>
>({});

const [failedLookupCount, setFailedLookupCount] = useState(0);

useEffect(
function () {
const nodes: Record<string, HTMLButtonElement | undefined> = {};
for (const [index, item] of toolbarData.entries()) {
if (!item.guideCue) {
continue;
}

const button = document.querySelector<HTMLButtonElement>(
`[data-testid="lg-drawer-toolbar-icon_button-${index}"]`
);
if (button) {
nodes[item.id] = button;
} else {
// we don't re-render enough times for unit tests to pass and this
// forces it to keep re-trying until the node is found
if (failedLookupCount < 10) {
setFailedLookupCount((c) => c + 1);
}
}
}

setAssistantNodes((oldNodes) => {
// account for removed nodes by checking all keys of both old and new
for (const id of Object.keys({ ...oldNodes, ...nodes })) {
if (nodes[id] !== oldNodes[id]) {
return nodes;
}
}
return oldNodes;
});
},
[toolbarData, failedLookupCount]
);

return (
<DrawerLayout
displayMode={DrawerDisplayMode.Embedded}
resizable
toolbarData={toolbarData}
className={cx(
drawerLayoutFixesStyles,
toolbarData.length === 0 && emptyDrawerLayoutFixesStyles,
// classname is the only property leafygreen passes over to the drawer
// wrapper component that would allow us to target it
'compass-drawer-anchor'
)}
>
<DrawerContextGrabber>{children}</DrawerContextGrabber>
</DrawerLayout>
<>
{toolbarData.map((item) => {
return (
assistantNodes[item.id] &&
item.guideCue && (
<GuideCue<HTMLButtonElement>
key={item.id}
{...item.guideCue}
triggerNode={assistantNodes[item.id]}
/>
)
);
})}
<DrawerLayout
displayMode={DrawerDisplayMode.Embedded}
resizable
toolbarData={toolbarData}
className={cx(
drawerLayoutFixesStyles,
toolbarData.length === 0 && emptyDrawerLayoutFixesStyles,
// classname is the only property leafygreen passes over to the drawer
// wrapper component that would allow us to target it
'compass-drawer-anchor'
)}
>
<DrawerContextGrabber>{children}</DrawerContextGrabber>
</DrawerLayout>
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,15 @@ export type GuideCueProps<T> = Omit<
GroupAndStep & {
cueId: string;
description: React.ReactChild;
trigger: ({ ref }: { ref: React.Ref<T> }) => React.ReactElement;
triggerNode?: T;
trigger?: ({ ref }: { ref: React.Ref<T> }) => React.ReactElement;
onOpenChange?: (isOpen: boolean) => void;
};

export const GuideCue = <T extends HTMLElement>({
description,
trigger,
triggerNode,
cueId,
groupId,
step,
Expand All @@ -106,7 +108,7 @@ export const GuideCue = <T extends HTMLElement>({
}: GuideCueProps<T>) => {
const [isCueOpen, setIsCueOpen] = useState(false);
const [isIntersecting, setIsIntersecting] = useState(true);
const refEl = useRef<T>(null);
const refEl = useRef<T>(triggerNode ?? null);
const [readyToRender, setReadyToRender] = useState(false);
const context = useContext(GuideCueContext);

Expand Down Expand Up @@ -276,7 +278,7 @@ export const GuideCue = <T extends HTMLElement>({
{description}
</LGGuideCue>
)}
{trigger({ ref: refEl })}
{trigger?.({ ref: refEl })}
</>
);
};
7 changes: 6 additions & 1 deletion packages/compass-web/src/compass-assistant-drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import { CompassAssistantDrawer } from '@mongodb-js/compass-assistant';
// TODO(COMPASS-7830): This is a temporary solution to pass the
// hasNonGenuineConnections prop to the CompassAssistantDrawer as otherwise
// we end up with a circular dependency.
export function CompassAssistantDrawerWithConnections() {
export function CompassAssistantDrawerWithConnections({
appName,
}: {
appName: string;
}) {
// Check for non-genuine connections
const activeConnectionIds = useConnectionIds(
(conn) =>
Expand All @@ -15,6 +19,7 @@ export function CompassAssistantDrawerWithConnections() {
);
return (
<CompassAssistantDrawer
appName={appName}
hasNonGenuineConnections={activeConnectionIds.length > 0}
/>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/compass-web/src/entrypoint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ function CompassWorkspace({
<CreateNamespacePlugin></CreateNamespacePlugin>
<DropNamespacePlugin></DropNamespacePlugin>
<RenameCollectionPlugin></RenameCollectionPlugin>
<CompassAssistantDrawerWithConnections />
<CompassAssistantDrawerWithConnections appName="Data Explorer" />
</>
);
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import { CompassAssistantDrawer } from '@mongodb-js/compass-assistant';
// TODO(COMPASS-7830): This is a temporary solution to pass the
// hasNonGenuineConnections prop to the CompassAssistantDrawer as otherwise
// we end up with a circular dependency.
export function CompassAssistantDrawerWithConnections() {
export function CompassAssistantDrawerWithConnections({
appName,
}: {
appName: string;
}) {
// Check for non-genuine connections
const activeConnectionIds = useConnectionIds(
(conn) =>
Expand All @@ -15,6 +19,7 @@ export function CompassAssistantDrawerWithConnections() {
);
return (
<CompassAssistantDrawer
appName={appName}
hasNonGenuineConnections={activeConnectionIds.length > 0}
/>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/compass/src/app/components/workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export default function Workspace({
<CreateNamespacePlugin></CreateNamespacePlugin>
<DropNamespacePlugin></DropNamespacePlugin>
<RenameCollectionPlugin></RenameCollectionPlugin>
<CompassAssistantDrawerWithConnections />
<CompassAssistantDrawerWithConnections appName="Compass" />
</>
)}
></WorkspacesPlugin>
Expand Down
Loading