From 0ff3c679c999aa6a6567597d61016b837a77e8cf Mon Sep 17 00:00:00 2001 From: Will Martin Date: Tue, 7 Mar 2023 14:03:51 -0500 Subject: [PATCH] [SM-537] add local storage persistence for onboarding tasks (#4880) * add local storage check for tasks * associate saved tasks with organization ID * remove redundant parenthesis * revert last commit * add falsy check * use distinctUntilChanged * remove extra observable * apply code review --- .../overview/overview.component.html | 2 +- .../overview/overview.component.ts | 80 ++++++++++++++----- libs/common/src/abstractions/state.service.ts | 8 ++ libs/common/src/models/domain/account.ts | 1 + libs/common/src/services/state.service.ts | 22 +++++ 5 files changed, 93 insertions(+), 20 deletions(-) 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 a53ede221c0..60c8410f854 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 51b939893db..c244c34ae0e 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 821800e7017..175eeaa3b4d 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 11aab653db2..48d83e841de 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)) {