diff --git a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx index 1c55645717c..7040b30c8ec 100644 --- a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx +++ b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx @@ -42,6 +42,7 @@ describe('CollectionHeaderActions [Component]', function () { return renderWithActiveConnection( diff --git a/packages/compass-collection/src/components/collection-header/collection-header.spec.tsx b/packages/compass-collection/src/components/collection-header/collection-header.spec.tsx index 42ff1aa3e87..764c3210937 100644 --- a/packages/compass-collection/src/components/collection-header/collection-header.spec.tsx +++ b/packages/compass-collection/src/components/collection-header/collection-header.spec.tsx @@ -394,6 +394,7 @@ describe('CollectionHeader [Component]', function () { return renderWithActiveConnection( diff --git a/packages/compass-components/src/components/atlas-skills-banner.spec.tsx b/packages/compass-components/src/components/atlas-skills-banner.spec.tsx new file mode 100644 index 00000000000..f1b8a22e85b --- /dev/null +++ b/packages/compass-components/src/components/atlas-skills-banner.spec.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { render, screen, userEvent } from '@mongodb-js/testing-library-compass'; +import { expect } from 'chai'; +import sinon from 'sinon'; + +import { AtlasSkillsBanner } from './atlas-skills-banner'; + +describe('AtlasSkillsBanner Component', function () { + const defaultProps = { + ctaText: + 'New to MongoDB? Document modeling skills will accelerate your progress.', + skillsUrl: 'https://www.mongodb.com/skills', + onCloseSkillsBanner: sinon.spy(), + showBanner: true, + }; + + it('should render the banner with correct text', function () { + render(); + + expect( + screen.getByText( + 'New to MongoDB? Document modeling skills will accelerate your progress.' + ) + ).to.be.visible; + }); + + it('should render the badge with award icon', function () { + render(); + + // Check for the award icon + const awardIcon = screen.getByLabelText('Award Icon'); + expect(awardIcon).to.be.visible; + }); + + it('should render the "Go to Skills" button with correct href', function () { + render(); + + const goToSkillsButton = screen.getByRole('link', { + name: /go to skills/i, + }); + expect(goToSkillsButton).to.be.visible; + expect(goToSkillsButton.getAttribute('href')).to.equal( + 'https://www.mongodb.com/skills' + ); + expect(goToSkillsButton.getAttribute('target')).to.equal('_blank'); + }); + + it('should call onCtaClick when "Go to Skills" button is clicked', function () { + const onCtaClick = sinon.spy(); + render(); + + const goToSkillsButton = screen.getByRole('link', { + name: /go to skills/i, + }); + + userEvent.click(goToSkillsButton); + expect(onCtaClick).to.have.been.calledOnce; + }); + + it('should render the close button and call onCloseSkillsBanner when clicked', function () { + const onCloseSkillsBanner = sinon.spy(); + render( + + ); + + const closeButton = screen.getByRole('button', { + name: 'Dismiss Skills Banner', + }); + expect(closeButton).to.be.visible; + expect(closeButton.getAttribute('title')).to.equal('Dismiss Skills Banner'); + + userEvent.click(closeButton); + expect(onCloseSkillsBanner).to.have.been.calledOnce; + }); + + it('should not render when showBanner is false', function () { + render(); + + // Banner should not be visible + expect( + screen.queryByText( + 'New to MongoDB? Document modeling skills will accelerate your progress.' + ) + ).to.not.exist; + expect(screen.queryByLabelText('Award Icon')).to.not.exist; + expect(screen.queryByRole('link', { name: /go to skills/i })).to.not.exist; + }); +}); diff --git a/packages/compass-components/src/components/atlas-skills-banner.tsx b/packages/compass-components/src/components/atlas-skills-banner.tsx new file mode 100644 index 00000000000..c8ce1f7abde --- /dev/null +++ b/packages/compass-components/src/components/atlas-skills-banner.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { css } from '@leafygreen-ui/emotion'; +import IconButton from '@leafygreen-ui/icon-button'; +import Badge, { Variant as BadgeVariant } from '@leafygreen-ui/badge'; +import Icon from '@leafygreen-ui/icon'; +import { spacing } from '@leafygreen-ui/tokens'; +import Button from '@leafygreen-ui/button'; +import { palette } from '@leafygreen-ui/palette'; + +const skillsCTAContent = css({ + border: `1px ${palette.gray.light1} solid`, + borderRadius: spacing[300], + padding: spacing[300], + paddingLeft: spacing[400], + display: 'flex', + width: '100%', + alignItems: 'center', +}); + +const skillsCTAText = css({ + display: 'flex', + alignSelf: 'center', + paddingLeft: spacing[200], +}); + +const badgeStyles = css({ + padding: '0 10px', + minHeight: spacing[600], +}); + +const learnMoreBtnStyles = css({ + marginLeft: spacing[200], +}); + +const closeButtonStyles = css({ + marginLeft: 'auto', +}); + +// @experiment Skills in Atlas | Jira Epic: CLOUDP-346311 +export const AtlasSkillsBanner: React.FunctionComponent<{ + ctaText: string; + onCloseSkillsBanner: () => void; + onCtaClick?: () => void; + skillsUrl: string; + showBanner: boolean; +}> = ({ ctaText, skillsUrl, onCloseSkillsBanner, onCtaClick, showBanner }) => { + return showBanner ? ( +
+ + + +
{ctaText}
+ + + + + +
+ ) : null; +}; diff --git a/packages/compass-components/src/index.ts b/packages/compass-components/src/index.ts index 855acf5ba6b..420e9aec119 100644 --- a/packages/compass-components/src/index.ts +++ b/packages/compass-components/src/index.ts @@ -246,3 +246,5 @@ export type { NodeField, NodeGlyph, } from '@mongodb-js/diagramming'; +// @experiment Skills in Atlas | Jira Epic: CLOUDP-346311 +export { AtlasSkillsBanner } from './components/atlas-skills-banner'; diff --git a/packages/compass-telemetry/src/atlas-skills.ts b/packages/compass-telemetry/src/atlas-skills.ts new file mode 100644 index 00000000000..6f6dbc1ebb8 --- /dev/null +++ b/packages/compass-telemetry/src/atlas-skills.ts @@ -0,0 +1,30 @@ +import { ExperimentTestGroup, ExperimentTestName } from './growth-experiments'; +import { useAssignment, useTrackInSample } from './experimentation-provider'; + +export enum SkillsBannerContextEnum { + Documents = 'documents', + Aggregation = 'aggregation', + Indexes = 'indexes', + Schema = 'schema', +} + +// @experiment Skills in Atlas | Jira Epic: CLOUDP-346311 +export const useAtlasSkillsBanner = (context: SkillsBannerContextEnum) => { + const atlasSkillsAssignment = useAssignment( + ExperimentTestName.atlasSkills, + false + ); + + const isInSkillsVariant = + atlasSkillsAssignment?.assignment?.assignmentData?.variant === + ExperimentTestGroup.atlasSkillsVariant; + + // Track users who are assigned to the skills experiment (variant or control) + useTrackInSample(ExperimentTestName.atlasSkills, !!atlasSkillsAssignment, { + screen: context, + }); + + return { + shouldShowAtlasSkillsBanner: isInSkillsVariant, + }; +}; diff --git a/packages/compass-telemetry/src/experimentation-provider.tsx b/packages/compass-telemetry/src/experimentation-provider.tsx index 6bdbda73647..ae74459d727 100644 --- a/packages/compass-telemetry/src/experimentation-provider.tsx +++ b/packages/compass-telemetry/src/experimentation-provider.tsx @@ -20,10 +20,18 @@ type GetAssignmentFn = ( options?: types.GetAssignmentOptions ) => Promise | null>; +type UseTrackInSampleHook = ( + experimentName: ExperimentTestName, + shouldFireEvent?: boolean, + customProperties?: types.TypeData['experimentViewedProps'], + team?: types.TypeData['loggerTeam'] +) => typesReact.BasicHookResponse; + interface CompassExperimentationProviderContextValue { useAssignment: UseAssignmentHook; assignExperiment: AssignExperimentFn; getAssignment: GetAssignmentFn; + useTrackInSample: UseTrackInSampleHook; } const initialContext: CompassExperimentationProviderContextValue = { @@ -43,6 +51,15 @@ const initialContext: CompassExperimentationProviderContextValue = { getAssignment() { return Promise.resolve(null); }, + useTrackInSample() { + return { + asyncStatus: null, + error: null, + isLoading: false, + isError: false, + isSuccess: true, + }; + }, }; export const ExperimentationContext = @@ -54,17 +71,26 @@ export const CompassExperimentationProvider: React.FC<{ useAssignment: UseAssignmentHook; assignExperiment: AssignExperimentFn; getAssignment: GetAssignmentFn; -}> = ({ children, useAssignment, assignExperiment, getAssignment }) => { + useTrackInSample: UseTrackInSampleHook; +}> = ({ + children, + useAssignment, + assignExperiment, + getAssignment, + useTrackInSample, +}) => { // Use useRef to keep the functions up-to-date; Use mutation pattern to maintain the // same object reference to prevent unnecessary re-renders of consuming components const { current: contextValue } = useRef({ useAssignment, assignExperiment, getAssignment, + useTrackInSample, }); contextValue.useAssignment = useAssignment; contextValue.assignExperiment = assignExperiment; contextValue.getAssignment = getAssignment; + contextValue.useTrackInSample = useTrackInSample; return ( @@ -77,3 +103,8 @@ export const CompassExperimentationProvider: React.FC<{ export const useAssignment = (...args: Parameters) => { return useContext(ExperimentationContext).useAssignment(...args); }; + +// Hook for components to access experiment assignment +export const useTrackInSample = (...args: Parameters) => { + return useContext(ExperimentationContext).useTrackInSample(...args); +}; diff --git a/packages/compass-telemetry/src/growth-experiments.ts b/packages/compass-telemetry/src/growth-experiments.ts index 6b242a921fd..f7b079e5531 100644 --- a/packages/compass-telemetry/src/growth-experiments.ts +++ b/packages/compass-telemetry/src/growth-experiments.ts @@ -1,9 +1,12 @@ export enum ExperimentTestName { earlyJourneyIndexesGuidance = 'EARLY_JOURNEY_INDEXES_GUIDANCE_20250328', mockDataGenerator = 'MOCK_DATA_GENERATOR_20251001', + atlasSkills = 'ATLAS_SKILLS_EXPERIMENT_20251007', } export enum ExperimentTestGroup { mockDataGeneratorVariant = 'mockDataGeneratorVariant', mockDataGeneratorControl = 'mockDataGeneratorControl', + atlasSkillsVariant = 'atlasSkillsVariant', + atlasSkillsControl = 'atlasSkillsControl', } diff --git a/packages/compass-telemetry/src/index.ts b/packages/compass-telemetry/src/index.ts index 3fdacb2c506..299103f2563 100644 --- a/packages/compass-telemetry/src/index.ts +++ b/packages/compass-telemetry/src/index.ts @@ -8,3 +8,6 @@ export type { export { CompassExperimentationProvider } from './experimentation-provider'; export { ExperimentTestName, ExperimentTestGroup } from './growth-experiments'; + +// @experiment Skills in Atlas | Jira Epic: CLOUDP-346311 +export { SkillsBannerContextEnum, useAtlasSkillsBanner } from './atlas-skills'; diff --git a/packages/compass-telemetry/src/provider.tsx b/packages/compass-telemetry/src/provider.tsx index 4446dbbfb03..d481433e40b 100644 --- a/packages/compass-telemetry/src/provider.tsx +++ b/packages/compass-telemetry/src/provider.tsx @@ -162,4 +162,7 @@ export const useFireExperimentViewed = ({ export type { TrackFunction }; export { ExperimentTestName, ExperimentTestGroup } from './growth-experiments'; -export { useAssignment } from './experimentation-provider'; +export { useAssignment, useTrackInSample } from './experimentation-provider'; + +// @experiment Skills in Atlas | Jira Epic: CLOUDP-346311 +export { SkillsBannerContextEnum, useAtlasSkillsBanner } from './atlas-skills';