[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
This commit is contained in:
parent
e153105774
commit
0ff3c679c9
|
@ -2,7 +2,7 @@
|
||||||
<sm-new-menu></sm-new-menu>
|
<sm-new-menu></sm-new-menu>
|
||||||
</sm-header>
|
</sm-header>
|
||||||
|
|
||||||
<div *ngIf="view$ | async as view; else spinner">
|
<div *ngIf="!loading && view$ | async as view; else spinner">
|
||||||
<sm-onboarding [title]="'getStarted' | i18n" *ngIf="showOnboarding" (dismiss)="hideOnboarding()">
|
<sm-onboarding [title]="'getStarted' | i18n" *ngIf="showOnboarding" (dismiss)="hideOnboarding()">
|
||||||
<sm-onboarding-task
|
<sm-onboarding-task
|
||||||
[title]="'createServiceAccount' | i18n"
|
[title]="'createServiceAccount' | i18n"
|
||||||
|
|
|
@ -10,11 +10,13 @@ import {
|
||||||
startWith,
|
startWith,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
take,
|
take,
|
||||||
|
share,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
|
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { ProjectListView } from "../models/view/project-list.view";
|
import { ProjectListView } from "../models/view/project-list.view";
|
||||||
|
@ -45,6 +47,10 @@ import {
|
||||||
import { ServiceAccountService } from "../service-accounts/service-account.service";
|
import { ServiceAccountService } from "../service-accounts/service-account.service";
|
||||||
|
|
||||||
type Tasks = {
|
type Tasks = {
|
||||||
|
[organizationId: string]: OrganizationTasks;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OrganizationTasks = {
|
||||||
importSecrets: boolean;
|
importSecrets: boolean;
|
||||||
createSecret: boolean;
|
createSecret: boolean;
|
||||||
createProject: boolean;
|
createProject: boolean;
|
||||||
|
@ -62,13 +68,14 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
||||||
protected organizationName: string;
|
protected organizationName: string;
|
||||||
protected userIsAdmin: boolean;
|
protected userIsAdmin: boolean;
|
||||||
protected showOnboarding = false;
|
protected showOnboarding = false;
|
||||||
|
protected loading = true;
|
||||||
|
|
||||||
protected view$: Observable<{
|
protected view$: Observable<{
|
||||||
allProjects: ProjectListView[];
|
allProjects: ProjectListView[];
|
||||||
allSecrets: SecretListView[];
|
allSecrets: SecretListView[];
|
||||||
latestProjects: ProjectListView[];
|
latestProjects: ProjectListView[];
|
||||||
latestSecrets: SecretListView[];
|
latestSecrets: SecretListView[];
|
||||||
tasks: Tasks;
|
tasks: OrganizationTasks;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -78,6 +85,7 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
||||||
private serviceAccountService: ServiceAccountService,
|
private serviceAccountService: ServiceAccountService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
|
private stateService: StateService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private i18nService: I18nService
|
private i18nService: I18nService
|
||||||
) {}
|
) {}
|
||||||
|
@ -97,37 +105,47 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
||||||
this.organizationId = org.id;
|
this.organizationId = org.id;
|
||||||
this.organizationName = org.name;
|
this.organizationName = org.name;
|
||||||
this.userIsAdmin = org.isAdmin;
|
this.userIsAdmin = org.isAdmin;
|
||||||
|
this.loading = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const projects$ = combineLatest([
|
const projects$ = combineLatest([
|
||||||
orgId$,
|
orgId$,
|
||||||
this.projectService.project$.pipe(startWith(null)),
|
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(
|
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([
|
const serviceAccounts$ = combineLatest([
|
||||||
orgId$,
|
orgId$,
|
||||||
this.serviceAccountService.serviceAccount$.pipe(startWith(null)),
|
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(
|
this.view$ = orgId$.pipe(
|
||||||
map(([projects, secrets, serviceAccounts]) => {
|
switchMap((orgId) =>
|
||||||
return {
|
combineLatest([projects$, secrets$, serviceAccounts$]).pipe(
|
||||||
latestProjects: this.getRecentItems(projects, this.tableSize),
|
switchMap(async ([projects, secrets, serviceAccounts]) => ({
|
||||||
latestSecrets: this.getRecentItems(secrets, this.tableSize),
|
latestProjects: this.getRecentItems(projects, this.tableSize),
|
||||||
allProjects: projects,
|
latestSecrets: this.getRecentItems(secrets, this.tableSize),
|
||||||
allSecrets: secrets,
|
allProjects: projects,
|
||||||
tasks: {
|
allSecrets: secrets,
|
||||||
importSecrets: secrets.length > 0,
|
tasks: await this.saveCompletedTasks(orgId, {
|
||||||
createSecret: secrets.length > 0,
|
importSecrets: secrets.length > 0,
|
||||||
createProject: projects.length > 0,
|
createSecret: secrets.length > 0,
|
||||||
createServiceAccount: serviceAccounts.length > 0,
|
createProject: projects.length > 0,
|
||||||
},
|
createServiceAccount: serviceAccounts.length > 0,
|
||||||
};
|
}),
|
||||||
})
|
}))
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Refresh onboarding status when orgId changes by fetching the first value from view$.
|
// 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) => {
|
.subscribe((view) => {
|
||||||
this.showOnboarding = Object.values(view.tasks).includes(false);
|
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;
|
.slice(0, length) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async saveCompletedTasks(
|
||||||
|
organizationId: string,
|
||||||
|
orgTasks: OrganizationTasks
|
||||||
|
): Promise<OrganizationTasks> {
|
||||||
|
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 ---
|
// Projects ---
|
||||||
|
|
||||||
openEditProject(projectId: string) {
|
openEditProject(projectId: string) {
|
||||||
|
|
|
@ -357,4 +357,12 @@ export abstract class StateService<T extends Account = Account> {
|
||||||
|
|
||||||
getAvatarColor: (options?: StorageOptions) => Promise<string | null | undefined>;
|
getAvatarColor: (options?: StorageOptions) => Promise<string | null | undefined>;
|
||||||
setAvatarColor: (value: string, options?: StorageOptions) => Promise<void>;
|
setAvatarColor: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
|
|
||||||
|
getSMOnboardingTasks: (
|
||||||
|
options?: StorageOptions
|
||||||
|
) => Promise<Record<string, Record<string, boolean>>>;
|
||||||
|
setSMOnboardingTasks: (
|
||||||
|
value: Record<string, Record<string, boolean>>,
|
||||||
|
options?: StorageOptions
|
||||||
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -238,6 +238,7 @@ export class AccountSettings {
|
||||||
serverConfig?: ServerConfigData;
|
serverConfig?: ServerConfigData;
|
||||||
approveLoginRequests?: boolean;
|
approveLoginRequests?: boolean;
|
||||||
avatarColor?: string;
|
avatarColor?: string;
|
||||||
|
smOnboardingTasks?: Record<string, Record<string, boolean>>;
|
||||||
|
|
||||||
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {
|
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {
|
||||||
if (obj == null) {
|
if (obj == null) {
|
||||||
|
|
|
@ -2364,6 +2364,28 @@ export class StateService<
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSMOnboardingTasks(
|
||||||
|
options?: StorageOptions
|
||||||
|
): Promise<Record<string, Record<string, boolean>>> {
|
||||||
|
return (
|
||||||
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||||
|
)?.settings?.smOnboardingTasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setSMOnboardingTasks(
|
||||||
|
value: Record<string, Record<string, boolean>>,
|
||||||
|
options?: StorageOptions
|
||||||
|
): Promise<void> {
|
||||||
|
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<TGlobalState> {
|
protected async getGlobals(options: StorageOptions): Promise<TGlobalState> {
|
||||||
let globals: TGlobalState;
|
let globals: TGlobalState;
|
||||||
if (this.useMemory(options.storageLocation)) {
|
if (this.useMemory(options.storageLocation)) {
|
||||||
|
|
Loading…
Reference in New Issue