diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.html index a53ede221c..60c8410f85 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.html @@ -2,7 +2,7 @@ -
+
; constructor( @@ -78,6 +85,7 @@ export class OverviewComponent implements OnInit, OnDestroy { private serviceAccountService: ServiceAccountService, private dialogService: DialogService, private organizationService: OrganizationService, + private stateService: StateService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService ) {} @@ -97,37 +105,47 @@ export class OverviewComponent implements OnInit, OnDestroy { this.organizationId = org.id; this.organizationName = org.name; this.userIsAdmin = org.isAdmin; + this.loading = true; }); const projects$ = combineLatest([ orgId$, this.projectService.project$.pipe(startWith(null)), - ]).pipe(switchMap(([orgId]) => this.projectService.getProjects(orgId))); + ]).pipe( + switchMap(([orgId]) => this.projectService.getProjects(orgId)), + share() + ); const secrets$ = combineLatest([orgId$, this.secretService.secret$.pipe(startWith(null))]).pipe( - switchMap(([orgId]) => this.secretService.getSecrets(orgId)) + switchMap(([orgId]) => this.secretService.getSecrets(orgId)), + share() ); const serviceAccounts$ = combineLatest([ orgId$, this.serviceAccountService.serviceAccount$.pipe(startWith(null)), - ]).pipe(switchMap(([orgId]) => this.serviceAccountService.getServiceAccounts(orgId))); + ]).pipe( + switchMap(([orgId]) => this.serviceAccountService.getServiceAccounts(orgId)), + share() + ); - this.view$ = combineLatest([projects$, secrets$, serviceAccounts$]).pipe( - map(([projects, secrets, serviceAccounts]) => { - return { - latestProjects: this.getRecentItems(projects, this.tableSize), - latestSecrets: this.getRecentItems(secrets, this.tableSize), - allProjects: projects, - allSecrets: secrets, - tasks: { - importSecrets: secrets.length > 0, - createSecret: secrets.length > 0, - createProject: projects.length > 0, - createServiceAccount: serviceAccounts.length > 0, - }, - }; - }) + this.view$ = orgId$.pipe( + switchMap((orgId) => + combineLatest([projects$, secrets$, serviceAccounts$]).pipe( + switchMap(async ([projects, secrets, serviceAccounts]) => ({ + latestProjects: this.getRecentItems(projects, this.tableSize), + latestSecrets: this.getRecentItems(secrets, this.tableSize), + allProjects: projects, + allSecrets: secrets, + tasks: await this.saveCompletedTasks(orgId, { + importSecrets: secrets.length > 0, + createSecret: secrets.length > 0, + createProject: projects.length > 0, + createServiceAccount: serviceAccounts.length > 0, + }), + })) + ) + ) ); // Refresh onboarding status when orgId changes by fetching the first value from view$. @@ -138,6 +156,7 @@ export class OverviewComponent implements OnInit, OnDestroy { ) .subscribe((view) => { this.showOnboarding = Object.values(view.tasks).includes(false); + this.loading = false; }); } @@ -154,6 +173,29 @@ export class OverviewComponent implements OnInit, OnDestroy { .slice(0, length) as T; } + private async saveCompletedTasks( + organizationId: string, + orgTasks: OrganizationTasks + ): Promise { + const prevTasks = ((await this.stateService.getSMOnboardingTasks()) || {}) as Tasks; + const newlyCompletedOrgTasks = Object.fromEntries( + Object.entries(orgTasks).filter(([_k, v]) => v === true) + ); + const nextOrgTasks = { + importSecrets: false, + createSecret: false, + createProject: false, + createServiceAccount: false, + ...prevTasks[organizationId], + ...newlyCompletedOrgTasks, + }; + this.stateService.setSMOnboardingTasks({ + ...prevTasks, + [organizationId]: nextOrgTasks, + }); + return nextOrgTasks as OrganizationTasks; + } + // Projects --- openEditProject(projectId: string) { diff --git a/libs/common/src/abstractions/state.service.ts b/libs/common/src/abstractions/state.service.ts index 51b939893d..c244c34ae0 100644 --- a/libs/common/src/abstractions/state.service.ts +++ b/libs/common/src/abstractions/state.service.ts @@ -357,4 +357,12 @@ export abstract class StateService { getAvatarColor: (options?: StorageOptions) => Promise; setAvatarColor: (value: string, options?: StorageOptions) => Promise; + + getSMOnboardingTasks: ( + options?: StorageOptions + ) => Promise>>; + setSMOnboardingTasks: ( + value: Record>, + options?: StorageOptions + ) => Promise; } diff --git a/libs/common/src/models/domain/account.ts b/libs/common/src/models/domain/account.ts index 821800e701..175eeaa3b4 100644 --- a/libs/common/src/models/domain/account.ts +++ b/libs/common/src/models/domain/account.ts @@ -238,6 +238,7 @@ export class AccountSettings { serverConfig?: ServerConfigData; approveLoginRequests?: boolean; avatarColor?: string; + smOnboardingTasks?: Record>; static fromJSON(obj: Jsonify): AccountSettings { if (obj == null) { diff --git a/libs/common/src/services/state.service.ts b/libs/common/src/services/state.service.ts index 11aab653db..48d83e841d 100644 --- a/libs/common/src/services/state.service.ts +++ b/libs/common/src/services/state.service.ts @@ -2364,6 +2364,28 @@ export class StateService< ); } + async getSMOnboardingTasks( + options?: StorageOptions + ): Promise>> { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) + )?.settings?.smOnboardingTasks; + } + + async setSMOnboardingTasks( + value: Record>, + options?: StorageOptions + ): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + account.settings.smOnboardingTasks = value; + return await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + } + protected async getGlobals(options: StorageOptions): Promise { let globals: TGlobalState; if (this.useMemory(options.storageLocation)) {