Skip to content

Commit 09b4d64

Browse files
voommen-livefrontBryanCunningham
authored andcommitted
[PM-34724] Access Intelligence Carousel for first time admins (#20734)
1 parent 2253576 commit 09b4d64

21 files changed

Lines changed: 991 additions & 140 deletions

apps/web/src/locales/en/messages.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,33 @@
214214
"takeAQuickTourOfAccessIntelligence": {
215215
"message": "Take a quick tour of Access Intelligence and see exactly how to turn your org's data into action."
216216
},
217+
"yourEntireOrgsSecurityInOneView": {
218+
"message": "Your entire org's security, in one view"
219+
},
220+
"accessIntelligenceGivesYouSinglePlace": {
221+
"message": "Access Intelligence gives you a single place to see every app, password, and member that's at risk."
222+
},
223+
"youSetThePrioritiesWeSurfaceTheRisks": {
224+
"message": "You set the priorities. We surface the risks."
225+
},
226+
"youMarkWhichAppsAreMostCritical": {
227+
"message": "You mark which apps are most critical, and Access Intelligence automatically identifies the at-risk passwords that need attention first."
228+
},
229+
"trackImprovementsAcrossYourTeam": {
230+
"message": "Track improvements across your team"
231+
},
232+
"membersAreAutomaticallyNotified": {
233+
"message": "Members are automatically notified of at-risk passwords in their Bitwarden extension. No chasing required."
234+
},
235+
"importYourOrgDataToGetStarted": {
236+
"message": "Import your org data to get started"
237+
},
238+
"onceItHasTheVaultData": {
239+
"message": "Once it has the vault data, Access Intelligence will have everything it needs to report on your org's password health."
240+
},
241+
"accessIntelligenceWelcomeTour": {
242+
"message": "Access Intelligence welcome tour"
243+
},
217244
"startTour": {
218245
"message": "Start tour"
219246
},
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<bit-dialog dialogSize="large">
2+
<div bitDialogContent class="tw-text-center">
3+
<vault-carousel
4+
[hideArrows]="true"
5+
[label]="'accessIntelligenceWelcomeTour' | i18n"
6+
(slideChange)="onSlideChange($event)"
7+
#carousel
8+
>
9+
<!-- Slide 1 -->
10+
<vault-carousel-slide [label]="'yourEntireOrgsSecurityInOneView' | i18n" noFocusableChildren>
11+
<div class="tw-flex tw-flex-col tw-items-center tw-gap-4 tw-text-center">
12+
<img
13+
src="/images/access-intelligence/data-is-in.svg"
14+
class="tw-w-full tw-mx-auto"
15+
alt=""
16+
aria-hidden="true"
17+
/>
18+
<h2 bitTypography="h2">{{ "yourEntireOrgsSecurityInOneView" | i18n }}</h2>
19+
<p bitTypography="body1">{{ "accessIntelligenceGivesYouSinglePlace" | i18n }}</p>
20+
</div>
21+
</vault-carousel-slide>
22+
23+
<!-- Slide 2 -->
24+
<vault-carousel-slide
25+
[label]="'youSetThePrioritiesWeSurfaceTheRisks' | i18n"
26+
noFocusableChildren
27+
>
28+
<div class="tw-flex tw-flex-col tw-items-center tw-gap-4 tw-text-center">
29+
<img
30+
src="/images/access-intelligence/data-is-in.svg"
31+
class="tw-w-full tw-mx-auto"
32+
alt=""
33+
aria-hidden="true"
34+
/>
35+
<h2 bitTypography="h2">{{ "youSetThePrioritiesWeSurfaceTheRisks" | i18n }}</h2>
36+
<p bitTypography="body1">{{ "youMarkWhichAppsAreMostCritical" | i18n }}</p>
37+
</div>
38+
</vault-carousel-slide>
39+
40+
<!-- Slide 3 -->
41+
<vault-carousel-slide [label]="'trackImprovementsAcrossYourTeam' | i18n" noFocusableChildren>
42+
<div class="tw-flex tw-flex-col tw-items-center tw-gap-4 tw-text-center">
43+
<img
44+
src="/images/access-intelligence/data-is-in.svg"
45+
class="tw-w-full tw-mx-auto"
46+
alt=""
47+
aria-hidden="true"
48+
/>
49+
<h2 bitTypography="h2">{{ "trackImprovementsAcrossYourTeam" | i18n }}</h2>
50+
<p bitTypography="body1">{{ "membersAreAutomaticallyNotified" | i18n }}</p>
51+
</div>
52+
</vault-carousel-slide>
53+
54+
<!-- Slide 4 -->
55+
<vault-carousel-slide [label]="'importYourOrgDataToGetStarted' | i18n" noFocusableChildren>
56+
<div class="tw-flex tw-flex-col tw-items-center tw-gap-4 tw-text-center">
57+
<img
58+
src="/images/access-intelligence/data-is-in.svg"
59+
class="tw-w-full tw-mx-auto"
60+
alt=""
61+
aria-hidden="true"
62+
/>
63+
<h2 bitTypography="h2">{{ "importYourOrgDataToGetStarted" | i18n }}</h2>
64+
<p bitTypography="body1">{{ "onceItHasTheVaultData" | i18n }}</p>
65+
</div>
66+
</vault-carousel-slide>
67+
68+
<!-- Action buttons projected below dots via carouselActions slot -->
69+
<div carouselActions class="tw-flex tw-justify-center tw-gap-4 tw-mt-4">
70+
@if (isFirstSlide()) {
71+
<button
72+
type="button"
73+
bitButton
74+
buttonType="secondary"
75+
data-testid="skip-btn"
76+
(click)="onSkip()"
77+
>
78+
{{ "skip" | i18n }}
79+
</button>
80+
} @else {
81+
<button
82+
type="button"
83+
bitButton
84+
buttonType="secondary"
85+
data-testid="back-btn"
86+
(click)="carousel.prevSlide()"
87+
>
88+
{{ "back" | i18n }}
89+
</button>
90+
}
91+
92+
@if (isLastSlide()) {
93+
<button
94+
type="button"
95+
bitButton
96+
buttonType="primary"
97+
data-testid="import-btn"
98+
(click)="onImportData()"
99+
>
100+
{{ "importData" | i18n }}
101+
</button>
102+
} @else {
103+
<button
104+
type="button"
105+
bitButton
106+
buttonType="primary"
107+
data-testid="next-btn"
108+
(click)="carousel.nextSlide()"
109+
>
110+
{{ "next" | i18n }}
111+
</button>
112+
}
113+
</div>
114+
</vault-carousel>
115+
</div>
116+
</bit-dialog>
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { ComponentFixture, TestBed } from "@angular/core/testing";
2+
import { Router } from "@angular/router";
3+
import { of } from "rxjs";
4+
5+
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
6+
import { OrganizationId } from "@bitwarden/common/types/guid";
7+
import { ButtonModule, DialogModule, DialogRef, DIALOG_DATA } from "@bitwarden/components";
8+
import { VaultCarouselModule } from "@bitwarden/vault";
9+
10+
import { NewAdminWelcomeDialogComponent } from "./new-admin-welcome-dialog.component";
11+
import { OnboardingService } from "./services/onboarding.service";
12+
13+
const mockOrganizationId = "test-org-id" as OrganizationId;
14+
15+
const mockDialogRef = {
16+
close: jest.fn(),
17+
afterClosed: jest.fn().mockReturnValue(of(undefined)),
18+
closed: of(undefined),
19+
} as unknown as DialogRef<any, any>;
20+
21+
const mockOnboardingService = {
22+
setNewAdminWelcomeDialogAcknowledged: jest.fn().mockResolvedValue(undefined),
23+
};
24+
25+
const mockRouter = {
26+
navigate: jest.fn().mockResolvedValue(true),
27+
};
28+
29+
describe("NewAdminWelcomeDialogComponent", () => {
30+
let component: NewAdminWelcomeDialogComponent;
31+
let fixture: ComponentFixture<NewAdminWelcomeDialogComponent>;
32+
33+
beforeEach(async () => {
34+
jest.clearAllMocks();
35+
36+
await TestBed.configureTestingModule({
37+
imports: [NewAdminWelcomeDialogComponent, VaultCarouselModule, DialogModule, ButtonModule],
38+
providers: [
39+
{ provide: I18nService, useValue: { t: (key: string) => key } },
40+
{ provide: DialogRef, useValue: mockDialogRef },
41+
{ provide: OnboardingService, useValue: mockOnboardingService },
42+
{ provide: Router, useValue: mockRouter },
43+
{ provide: DIALOG_DATA, useValue: { organizationId: mockOrganizationId } },
44+
],
45+
}).compileComponents();
46+
47+
fixture = TestBed.createComponent(NewAdminWelcomeDialogComponent);
48+
component = fixture.componentInstance;
49+
fixture.detectChanges();
50+
});
51+
52+
it("should create", () => {
53+
expect(component).toBeTruthy();
54+
});
55+
56+
it("starts on the first slide", () => {
57+
expect(component["currentSlide"]()).toBe(0);
58+
expect(component["isFirstSlide"]()).toBe(true);
59+
expect(component["isLastSlide"]()).toBe(false);
60+
});
61+
62+
it("onSlideChange updates the currentSlide signal", () => {
63+
component["onSlideChange"](2);
64+
expect(component["currentSlide"]()).toBe(2);
65+
});
66+
67+
it("isLastSlide is true when on slide 3 (index 3)", () => {
68+
component["onSlideChange"](3);
69+
expect(component["isLastSlide"]()).toBe(true);
70+
expect(component["isFirstSlide"]()).toBe(false);
71+
});
72+
73+
describe("onSkip", () => {
74+
it("calls setNewAdminWelcomeDialogAcknowledged", async () => {
75+
await component["onSkip"]();
76+
expect(mockOnboardingService.setNewAdminWelcomeDialogAcknowledged).toHaveBeenCalledTimes(1);
77+
});
78+
79+
it("closes the dialog", async () => {
80+
await component["onSkip"]();
81+
expect(mockDialogRef.close).toHaveBeenCalledTimes(1);
82+
});
83+
});
84+
85+
describe("onImportData", () => {
86+
it("calls setNewAdminWelcomeDialogAcknowledged", async () => {
87+
await component["onImportData"]();
88+
expect(mockOnboardingService.setNewAdminWelcomeDialogAcknowledged).toHaveBeenCalledTimes(1);
89+
});
90+
91+
it("navigates to the org import page", async () => {
92+
await component["onImportData"]();
93+
expect(mockRouter.navigate).toHaveBeenCalledWith(
94+
["/organizations", mockOrganizationId, "settings", "tools", "import"],
95+
{ queryParams: { returnTo: "access-intelligence" } },
96+
);
97+
});
98+
99+
it("closes the dialog", async () => {
100+
await component["onImportData"]();
101+
expect(mockDialogRef.close).toHaveBeenCalledTimes(1);
102+
});
103+
104+
it("closes the dialog before navigating", async () => {
105+
const callOrder: string[] = [];
106+
(mockDialogRef.close as jest.Mock).mockImplementation(() => callOrder.push("close"));
107+
mockRouter.navigate.mockImplementation(async () => {
108+
callOrder.push("navigate");
109+
return true;
110+
});
111+
112+
await component["onImportData"]();
113+
114+
expect(callOrder).toEqual(["close", "navigate"]);
115+
});
116+
});
117+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { Router } from "@angular/router";
2+
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
3+
4+
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
5+
import { OrganizationId } from "@bitwarden/common/types/guid";
6+
import {
7+
ButtonModule,
8+
DialogModule,
9+
DialogRef,
10+
DIALOG_DATA,
11+
TypographyModule,
12+
I18nMockService,
13+
} from "@bitwarden/components";
14+
import { VaultCarouselModule } from "@bitwarden/vault";
15+
16+
import { NewAdminWelcomeDialogComponent } from "./new-admin-welcome-dialog.component";
17+
import { OnboardingService } from "./services/onboarding.service";
18+
19+
const mockDialogRef = { close: async () => {} };
20+
const mockOnboardingService = {
21+
setPostImportDialogAcknowledged: async () => {},
22+
setNewAdminWelcomeDialogAcknowledged: async () => {},
23+
};
24+
const mockOrganizationId = "story-org-id" as OrganizationId;
25+
26+
const mockI18nService = new I18nMockService({
27+
accessIntelligenceWelcomeTour: "Welcome to Access Intelligence!",
28+
yourEntireOrgsSecurityInOneView: "Your entire org's security in one view",
29+
accessIntelligenceGivesYouSinglePlace:
30+
"Access Intelligence gives you a single place to view and manage your organization's security posture, so you can spend less time on security administration and more time on strategic initiatives.",
31+
youSetThePrioritiesWeSurfaceTheRisks: "You set the priorities, we surface the risks.",
32+
youMarkWhichAppsAreMostCritical:
33+
"You mark which apps are most critical to your org, and Access Intelligence surfaces the riskiest accounts and weakest links in those apps, so you can focus on what matters most.",
34+
trackImprovementsAcrossYourTeam: "Track improvements across your team.",
35+
membersAreAutomaticallyNotified:
36+
"Members are automatically notified of security risks and can take action to resolve them, making it easier than ever to maintain a strong security posture across your organization.",
37+
importYourOrgDataToGetStarted: "Import your org data to get started",
38+
onceItHasTheVaultData:
39+
"Once it has the vault data, Access Intelligence can start surfacing insights and recommendations to help you improve your organization's security.",
40+
skip: "Skip",
41+
back: "Back",
42+
next: "Next",
43+
importData: "Import Data",
44+
});
45+
46+
export default {
47+
title: "Access Intelligence/NewAdminWelcomeDialog",
48+
component: NewAdminWelcomeDialogComponent,
49+
decorators: [
50+
moduleMetadata({
51+
imports: [VaultCarouselModule, DialogModule, ButtonModule, TypographyModule],
52+
providers: [
53+
{ provide: I18nService, useValue: mockI18nService },
54+
{ provide: DialogRef, useValue: mockDialogRef },
55+
{ provide: OnboardingService, useValue: mockOnboardingService },
56+
{ provide: DIALOG_DATA, useValue: { organizationId: mockOrganizationId } },
57+
{ provide: Router, useValue: { navigate: async () => {} } },
58+
],
59+
}),
60+
],
61+
} as Meta;
62+
63+
type Story = StoryObj<NewAdminWelcomeDialogComponent>;
64+
65+
export const Default: Story = {};

0 commit comments

Comments
 (0)