mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 05:30:01 +00:00
even more cleanup
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { BehaviorSubject, merge, of, Subject } from "rxjs";
|
||||
import { merge, of, Subject } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
@@ -111,9 +111,6 @@ import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/imp
|
||||
import { PrimarySecondaryStorageService } from "@bitwarden/common/platform/storage/primary-secondary-storage.service";
|
||||
import { WindowStorageService } from "@bitwarden/common/platform/storage/window-storage.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { AchievementHub } from "@bitwarden/common/tools/achievements/achievement-hub";
|
||||
import { TotallyAttachedValidator, TotallyAttachedAchievement } from "@bitwarden/common/tools/achievements/examples/example-validators";
|
||||
import {AchievementEvent, AchievementValidator, UserActionEvent } from "@bitwarden/common/tools/achievements/types";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -195,8 +192,6 @@ const DISK_BACKUP_LOCAL_STORAGE = new SafeInjectionToken<
|
||||
AbstractStorageService & ObservableStorageService
|
||||
>("DISK_BACKUP_LOCAL_STORAGE");
|
||||
|
||||
const events$ = new Subject<UserActionEvent>();
|
||||
|
||||
/**
|
||||
* Provider definitions used in the ngModule.
|
||||
* Add your provider definition here using the safeProvider function as a wrapper. This will give you type safety.
|
||||
@@ -667,18 +662,6 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultSshImportPromptService,
|
||||
deps: [DialogService, ToastService, PlatformUtilsService, I18nServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AchievementHub,
|
||||
useFactory: (
|
||||
) => {
|
||||
const validators$ = new BehaviorSubject<AchievementValidator[]>([TotallyAttachedValidator]);
|
||||
const achievements$ = new Subject<AchievementEvent>();
|
||||
const hub = new AchievementHub(validators$, events$, achievements$);
|
||||
return hub;
|
||||
},
|
||||
deps: [
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Component } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { Subject, combineLatestWith, filter, map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AchievementsListComponent } from "@bitwarden/angular/tools/achievements/achievements-list.component";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { EventInfo, UserEventLogProvider } from "@bitwarden/common/tools/log/logger";
|
||||
import { UserEventCollector } from "@bitwarden/common/tools/log/user-event-collector";
|
||||
import { EventInfo } from "@bitwarden/common/tools/log/user-event-monitor";
|
||||
import { ButtonModule, IconModule } from "@bitwarden/components";
|
||||
|
||||
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||
@@ -29,16 +30,16 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
|
||||
AchievementsListComponent,
|
||||
],
|
||||
})
|
||||
export class AchievementsComponent implements OnInit {
|
||||
export class AchievementsComponent {
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private readonly eventLogs: UserEventLogProvider,
|
||||
private readonly collector: UserEventCollector,
|
||||
) {
|
||||
// FIXME: add a subscription to this service and feed the data somewhere
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
filter((account): account is Account => !!account),
|
||||
map((account) => this.eventLogs.capture(account)),
|
||||
map((account) => this.collector.monitor(account)),
|
||||
combineLatestWith(this._addEvent),
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
@@ -47,8 +48,6 @@ export class AchievementsComponent implements OnInit {
|
||||
|
||||
private _addEvent = new Subject<EventInfo>();
|
||||
|
||||
async ngOnInit() {}
|
||||
|
||||
addEvent() {
|
||||
this._addEvent.next({
|
||||
action: "vault-item-added",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<bit-container>
|
||||
<achievements-list></achievements-list>
|
||||
<button bitButton type="button" bitFormButton buttonType="primary" (click)="testAchievement()">
|
||||
Test achievements
|
||||
<button bitButton type="button" bitFormButton buttonType="primary" (click)="addEvent()">
|
||||
Send synthetic add vault item event
|
||||
</button>
|
||||
</bit-container>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { Component } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { Subject, combineLatestWith, filter, map } from "rxjs";
|
||||
|
||||
import { AchievementNotifierService } from "@bitwarden/angular/tools/achievements/achievement-notifier.abstraction";
|
||||
import { AchievementsListComponent } from "@bitwarden/angular/tools/achievements/achievements-list.component";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { EventStoreAbstraction } from "@bitwarden/common/tools/achievements/event-store.abstraction.service";
|
||||
import { VaultItems_10_Added_Achievement } from "@bitwarden/common/tools/achievements/examples/achievements";
|
||||
import { AchievementEarnedEvent, AchievementId } from "@bitwarden/common/tools/achievements/types";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserEventCollector } from "@bitwarden/common/tools/log/user-event-collector";
|
||||
import { EventInfo } from "@bitwarden/common/tools/log/user-event-monitor";
|
||||
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../shared";
|
||||
@@ -17,40 +15,29 @@ import { SharedModule } from "../../shared";
|
||||
standalone: true,
|
||||
imports: [SharedModule, HeaderModule, AchievementsListComponent],
|
||||
})
|
||||
export class AchievementsComponent implements OnInit {
|
||||
private currentUserId: UserId;
|
||||
|
||||
export class AchievementsComponent {
|
||||
constructor(
|
||||
private eventStore: EventStoreAbstraction,
|
||||
private achievementNotifierService: AchievementNotifierService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.achievementNotifierService.init();
|
||||
this.currentUserId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||
private readonly collector: UserEventCollector,
|
||||
) {
|
||||
// FIXME: add a subscription to this service and feed the data somewhere
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
filter((account): account is Account => !!account),
|
||||
map((account) => this.collector.monitor(account)),
|
||||
combineLatestWith(this._addEvent),
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe(([capture, event]) => capture.info(event));
|
||||
}
|
||||
|
||||
testAchievement() {
|
||||
const earnedAchievement: AchievementEarnedEvent = {
|
||||
"@timestamp": Date.now(),
|
||||
event: {
|
||||
kind: "alert",
|
||||
category: "session",
|
||||
},
|
||||
service: {
|
||||
name: "web",
|
||||
type: "client",
|
||||
node: {
|
||||
name: "an-installation-identifier-for-this-client-instance",
|
||||
},
|
||||
environment: "local",
|
||||
version: "2025.3.1-innovation-sprint",
|
||||
},
|
||||
user: { id: this.currentUserId },
|
||||
achievement: { type: "earned", name: VaultItems_10_Added_Achievement.name as AchievementId },
|
||||
};
|
||||
private _addEvent = new Subject<EventInfo>();
|
||||
|
||||
this.eventStore.addEvent(earnedAchievement);
|
||||
addEvent() {
|
||||
this._addEvent.next({
|
||||
action: "vault-item-added",
|
||||
labels: { "vault-item-type": "login", "vault-item-uri-quantity": 1 },
|
||||
tags: ["with-attachment"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ import { EventCollectionService } from "@bitwarden/common/services/event/event-c
|
||||
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
||||
import { SearchService } from "@bitwarden/common/services/search.service";
|
||||
import { AchievementService } from "@bitwarden/common/tools/achievements/achievement.service.abstraction";
|
||||
import { NextAchievementService } from "@bitwarden/common/tools/achievements/next-achievement.service";
|
||||
import { DefaultAchievementService } from "@bitwarden/common/tools/achievements/next-achievement.service";
|
||||
import { DefaultUserEventCollector } from "@bitwarden/common/tools/log/default-user-event-collector";
|
||||
import { UserEventCollector } from "@bitwarden/common/tools/log/user-event-collector";
|
||||
import {
|
||||
@@ -1503,7 +1503,7 @@ const safeProviders: SafeProvider[] = [
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AchievementService,
|
||||
useClass: NextAchievementService,
|
||||
useClass: DefaultAchievementService,
|
||||
deps: [UserEventCollector],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<h2 class="tw-font-bold" bitTypography="h6">
|
||||
{{ "achievements" | i18n }}
|
||||
</h2>
|
||||
<span bitTypography="body1" slot="end">{{ allAchievementCards.length }}</span>
|
||||
<span bitTypography="body1" slot="end">{{ achievements.length }}</span>
|
||||
</bit-section-header>
|
||||
|
||||
<bit-item-group>
|
||||
@for (achievement of achievements; track achievement.name) {
|
||||
<achievement-item
|
||||
[icon]="lookupIcon(achievement)"
|
||||
[icon]="icon(achievement)"
|
||||
[title]="achievement.name"
|
||||
[description]="achievement.description"
|
||||
[earned]="isEarned(achievement)"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, NgZone } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { filter, switchMap } from "rxjs";
|
||||
|
||||
@@ -51,6 +51,7 @@ export class AchievementsListComponent {
|
||||
constructor(
|
||||
private achievementService: AchievementService,
|
||||
private accountService: AccountService,
|
||||
zone: NgZone,
|
||||
) {
|
||||
this.achievements = achievementService.achievementMap();
|
||||
|
||||
@@ -60,7 +61,7 @@ export class AchievementsListComponent {
|
||||
switchMap((account) => this.achievementService.earnedMap$(account)),
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe((earned) => (this._earned = earned));
|
||||
.subscribe((earned) => zone.run(() => (this._earned = earned)));
|
||||
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
@@ -68,7 +69,7 @@ export class AchievementsListComponent {
|
||||
switchMap((account) => this.achievementService.metricsMap$(account)),
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe((progress) => (this._progress = progress));
|
||||
.subscribe((progress) => zone.run(() => (this._progress = progress)));
|
||||
}
|
||||
|
||||
protected isEarned(achievement: Achievement) {
|
||||
@@ -87,7 +88,7 @@ export class AchievementsListComponent {
|
||||
return this._progress.get(achievement.active.metric)?.achievement?.value ?? -1;
|
||||
}
|
||||
|
||||
protected lookupIcon(achievement: Achievement): Icon {
|
||||
protected icon(achievement: Achievement): Icon {
|
||||
return (iconMap[achievement.achievement] as Icon) ?? AchievementIcon;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { UserId } from "../../types/guid";
|
||||
|
||||
import { AchievementEarnedEvent, AchievementId, AchievementProgressEvent, MetricId } from "./types";
|
||||
|
||||
// FIXME: see <./types.ts> AchievementValidator
|
||||
export function progressEvent(name: MetricId, value: number = 1): AchievementProgressEvent {
|
||||
return {
|
||||
"@timestamp": Date.now(),
|
||||
@@ -25,6 +26,7 @@ export function progressEvent(name: MetricId, value: number = 1): AchievementPro
|
||||
};
|
||||
}
|
||||
|
||||
// FIXME: see <./types.ts> AchievementValidator
|
||||
export function earnedEvent(name: AchievementId): AchievementEarnedEvent {
|
||||
return {
|
||||
"@timestamp": Date.now(),
|
||||
|
||||
@@ -97,9 +97,4 @@ export class AchievementHub {
|
||||
startWith(new Map<MetricId, AchievementProgressEvent>()),
|
||||
);
|
||||
}
|
||||
|
||||
//Test methods
|
||||
addEvent(event: AchievementEvent) {
|
||||
this.achievementLog.next(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { SendItemCreatedCountConfig } from "./validators/config/send-created-cou
|
||||
import { SendItemCreatedCountValidator } from "./validators/send-item-created-count-validator";
|
||||
import { VaultItemCreatedCountValidator } from "./validators/vault-item-created-count-validator";
|
||||
|
||||
export class NextAchievementService implements AchievementService {
|
||||
export class DefaultAchievementService implements AchievementService {
|
||||
constructor(private readonly collector: UserEventCollector) {}
|
||||
|
||||
private hubs = new Map<string, AchievementHub>();
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { AchievementEarnedEvent, AchievementProgressEvent, UserActionEvent } from "./types";
|
||||
|
||||
export abstract class EventStoreAbstraction {
|
||||
abstract events$: Observable<UserActionEvent | AchievementProgressEvent | AchievementEarnedEvent>;
|
||||
abstract addEvent(
|
||||
event: UserActionEvent | AchievementProgressEvent | AchievementEarnedEvent,
|
||||
): boolean;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Observable, Subject } from "rxjs";
|
||||
|
||||
import { EventStoreAbstraction } from "./event-store.abstraction.service";
|
||||
import { AchievementEarnedEvent, AchievementProgressEvent } from "./types";
|
||||
|
||||
// Will be replaced by the achievementHub
|
||||
export class EventStore implements EventStoreAbstraction {
|
||||
private _events = new Subject<AchievementProgressEvent | AchievementEarnedEvent>();
|
||||
|
||||
events$: Observable<AchievementProgressEvent | AchievementEarnedEvent> =
|
||||
this._events.asObservable();
|
||||
|
||||
constructor() {}
|
||||
|
||||
addEvent(event: AchievementProgressEvent | AchievementEarnedEvent): boolean {
|
||||
// FIXME Collapse existing of same metric/higher count AchievementProgressEvents
|
||||
//eslint-disable-next-line no-console
|
||||
console.log("EventStore.addEvent", event);
|
||||
this._events.next(event);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { Achievement, AchievementEvent, AchievementValidator, UserActionEvent } from "./types";
|
||||
|
||||
// sync data from the server (consumed by event store)
|
||||
const replicationIn$ = new Subject<AchievementEvent>();
|
||||
|
||||
// data incoming from the UI (consumed by validator)
|
||||
const userActionIn$ = new Subject<UserActionEvent>();
|
||||
|
||||
// what to look for (consumed by validator)
|
||||
const achievementMonitors$ = new Subject<AchievementValidator[]>();
|
||||
|
||||
// data stored in local state (consumed by validator and achievement list)
|
||||
const achievementsLocal$ = new Subject<AchievementEvent[]>();
|
||||
|
||||
// metadata (consumed by achievement list)
|
||||
const achievementMetadata$ = new Subject<Achievement>();
|
||||
|
||||
export {
|
||||
replicationIn$,
|
||||
userActionIn$,
|
||||
achievementsLocal$,
|
||||
achievementMonitors$,
|
||||
achievementMetadata$,
|
||||
};
|
||||
@@ -21,49 +21,67 @@ export type AchievementEarnedEvent = EventFormat &
|
||||
export type AchievementEvent = AchievementProgressEvent | AchievementEarnedEvent;
|
||||
|
||||
type MetricCriteria = {
|
||||
// the metric observed by low/high triggers
|
||||
/** the metric observed by low/high triggers */
|
||||
metric: MetricId;
|
||||
} & RequireAtLeastOne<{
|
||||
// criteria fail when the metric is less than or equal to `low`
|
||||
/** criteria fail when the metric is less than or equal to `low` */
|
||||
low: number;
|
||||
// criteria fail when the metric is greater than `high`
|
||||
/** criteria fail when the metric is greater than `high` */
|
||||
high: number;
|
||||
}>;
|
||||
type ActiveCriteria = "until-earned" | MetricCriteria;
|
||||
|
||||
// consumed by validator and achievement list (should this include a "toast-alerter"?)
|
||||
/** consumed by validator and achievement list (should this include a "toast-alerter"?) */
|
||||
export type Achievement = {
|
||||
// identifies the achievement being monitored
|
||||
/** identifies the achievement being monitored */
|
||||
achievement: AchievementId;
|
||||
|
||||
// human-readable name of the achievement
|
||||
/** human-readable name of the achievement */
|
||||
name: string;
|
||||
|
||||
// human-readable description of the achievement
|
||||
/** human-readable description of the achievement */
|
||||
description?: string;
|
||||
|
||||
// conditions that determine when the achievement validator should be loaded
|
||||
// by the processor
|
||||
/* conditions that determine when the achievement validator should be loaded
|
||||
* by the processor
|
||||
*/
|
||||
active: ActiveCriteria;
|
||||
|
||||
// identifies the validator containing filter/measure/earn methods
|
||||
/** identifies the validator containing filter/measure/earn methods */
|
||||
validator: ValidatorId;
|
||||
|
||||
// whether or not the achievement is hidden until it is earned
|
||||
/** whether or not the achievement is hidden until it is earned */
|
||||
hidden: boolean;
|
||||
};
|
||||
|
||||
// consumed by validator
|
||||
/** An achievement completion monitor */
|
||||
//
|
||||
// FIXME:
|
||||
// * inject a monitor/capture interface into measure and rewards
|
||||
// * this interface contains methods from <./achievement-events.ts>
|
||||
// * it constructs context-specific events, filling in device/time/etc
|
||||
export type AchievementValidator = Achievement & {
|
||||
// when the watch triggers on incoming user events
|
||||
trigger: (item: UserActionEvent) => boolean;
|
||||
/** when the watch triggers on incoming user events
|
||||
* @param event a monitored user action event
|
||||
* @returns true when the validator should process the event, otherwise false.
|
||||
*/
|
||||
trigger: (event: UserActionEvent) => boolean;
|
||||
|
||||
// observe data from the event stream and produces measurements
|
||||
measure?: (item: UserActionEvent, metrics: Map<MetricId, number>) => AchievementProgressEvent[];
|
||||
/** observes data from the event stream and produces measurements;
|
||||
* this runs after the event is triggered.
|
||||
* @param event a monitored user action event
|
||||
* @returns a collection of measurements extracted from the event (may be empty)
|
||||
*/
|
||||
measure?: (event: UserActionEvent, metrics: Map<MetricId, number>) => AchievementProgressEvent[];
|
||||
|
||||
// monitors achievement progress and emits earned achievements
|
||||
/** monitors achievement progress and emits earned achievements;
|
||||
* this runs after all measurements are taken.
|
||||
* @param progress events emitted by `measure`. If `measure` is undefined, this is an empty array.
|
||||
* @param metrics last-recorded progress value for all achievement metrics
|
||||
* @returns a collection of achievements awarded by the event.
|
||||
*/
|
||||
award?: (
|
||||
events: AchievementProgressEvent[],
|
||||
progress: AchievementProgressEvent[],
|
||||
metrics: Map<MetricId, number>,
|
||||
) => AchievementEarnedEvent[];
|
||||
};
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { ItemCreatedProgressEvent } from "./examples/achievement-events";
|
||||
import { ItemCreatedProgress } from "./examples/example-validators";
|
||||
import { mapProgressByName } from "./util";
|
||||
|
||||
describe("mapProgressByName", () => {
|
||||
it("creates a map containing a progress value", () => {
|
||||
const result = mapProgressByName([ItemCreatedProgressEvent]);
|
||||
|
||||
expect(result.get(ItemCreatedProgress)).toEqual(ItemCreatedProgressEvent.achievement.value);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
import { isProgressEvent } from "./meta";
|
||||
import { AchievementEvent } from "./types";
|
||||
|
||||
export function mapProgressByName(status: AchievementEvent[]) {
|
||||
return new Map(
|
||||
status.filter(isProgressEvent).map((e) => [e.achievement.name, e.achievement.value] as const),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user