1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-07 04:03:29 +00:00

refactor: update web header to use bit-header component

- Refactor web-header to use new bit-header component from libs/components
  - Extract account menu to separate component
  - Update header module to import HeaderComponent
This commit is contained in:
William Martin
2025-11-25 12:42:12 -05:00
parent fdf3576685
commit 73f31fd828
5 changed files with 158 additions and 170 deletions

View File

@@ -0,0 +1,62 @@
@if (account()) {
<button
type="button"
[bitMenuTriggerFor]="accountMenu"
class="tw-border-0 tw-bg-transparent tw-p-0 tw-shrink-0"
>
<dynamic-avatar [id]="account().id" [text]="account() | userName"></dynamic-avatar>
</button>
<bit-menu #accountMenu>
<div class="tw-flex tw-min-w-52 tw-max-w-72 tw-flex-col">
<div
class="tw-flex tw-items-center tw-px-4 tw-py-1 tw-leading-tight tw-text-muted"
appStopProp
>
<dynamic-avatar [id]="account().id" [text]="account() | userName"></dynamic-avatar>
<div class="tw-ml-2 tw-block tw-overflow-hidden tw-whitespace-nowrap">
<span>{{ "loggedInAs" | i18n }}</span>
<small class="tw-block tw-overflow-hidden tw-whitespace-nowrap">
{{ account() | userName }}
</small>
</div>
</div>
@if (selfHosted) {
<bit-menu-divider></bit-menu-divider>
<span class="tw-break-all tw-px-4 tw-py-1 tw-text-left tw-text-muted">
{{ hostname }}
</span>
}
<bit-menu-divider></bit-menu-divider>
<a bitMenuItem routerLink="/settings/account">
<i class="bwi bwi-fw bwi-user" aria-hidden="true"></i>
{{ "accountSettings" | i18n }}
</a>
<a bitMenuItem href="https://bitwarden.com/help/" target="_blank" rel="noreferrer">
<i class="bwi bwi-fw bwi-question-circle" aria-hidden="true"></i>
{{ "getHelp" | i18n }}
</a>
<a bitMenuItem href="https://bitwarden.com/download/" target="_blank" rel="noreferrer">
<i class="bwi bwi-fw bwi-download" aria-hidden="true"></i>
{{ "getApps" | i18n }}
</a>
<bit-menu-divider></bit-menu-divider>
@if (canLock$ | async) {
<button bitMenuItem type="button" (click)="lock()">
<i class="bwi bwi-fw bwi-lock" aria-hidden="true"></i>
{{ "lockNow" | i18n }}
</button>
}
<button bitMenuItem type="button" (click)="logout()">
<i class="bwi bwi-fw bwi-sign-out" aria-hidden="true"></i>
{{ "logOut" | i18n }}
</button>
</div>
</bit-menu>
}

View File

@@ -0,0 +1,50 @@
import { ChangeDetectionStrategy, Component, inject } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import { map, Observable } from "rxjs";
import { LockService, LogoutService } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import {
VaultTimeoutAction,
VaultTimeoutSettingsService,
} from "@bitwarden/common/key-management/vault-timeout";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DynamicAvatarComponent } from "../../components/dynamic-avatar.component";
import { SharedModule } from "../../shared";
@Component({
selector: "app-account-menu",
templateUrl: "./account-menu.component.html",
imports: [SharedModule, DynamicAvatarComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccountMenuComponent {
private readonly platformUtilsService = inject(PlatformUtilsService);
private readonly vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService);
private readonly accountService = inject(AccountService);
private readonly logoutService = inject(LogoutService);
private readonly lockService = inject(LockService);
protected readonly account = toSignal(this.accountService.activeAccount$);
protected readonly canLock$: Observable<boolean> = this.vaultTimeoutSettingsService
.availableVaultTimeoutActions$()
.pipe(map((actions) => actions.includes(VaultTimeoutAction.Lock)));
protected readonly selfHosted = this.platformUtilsService.isSelfHost();
protected readonly hostname = globalThis.location.hostname;
protected async lock() {
const userId = this.account()?.id;
if (userId) {
await this.lockService.lock(userId);
}
}
protected async logout() {
const userId = this.account()?.id;
if (userId) {
await this.logoutService.logout(userId);
}
}
}

View File

@@ -1,16 +1,9 @@
import { NgModule } from "@angular/core";
import { BannerModule } from "@bitwarden/components";
import { DynamicAvatarComponent } from "../../components/dynamic-avatar.component";
import { SharedModule } from "../../shared";
import { ProductSwitcherModule } from "../product-switcher/product-switcher.module";
import { WebHeaderComponent } from "./web-header.component";
@NgModule({
imports: [SharedModule, DynamicAvatarComponent, ProductSwitcherModule, BannerModule],
declarations: [WebHeaderComponent],
imports: [WebHeaderComponent],
exports: [WebHeaderComponent],
})
export class HeaderModule {}

View File

@@ -1,110 +1,24 @@
<header
*ngIf="routeData$ | async as routeData"
class="-tw-mt-6 -tw-mx-8 tw-mb-3 tw-flex tw-flex-col tw-py-6 tw-px-8"
[ngClass]="{
'tw-border-0 tw-border-b tw-border-solid tw-border-secondary-100 tw-bg-background-alt tw-pb-0':
tabsContainer.childElementCount !== 0,
}"
>
<div class="tw-flex">
<div class="tw-flex tw-min-w-0 tw-flex-1 tw-flex-col tw-gap-2">
@let routeData = routeData$ | async;
@if (routeData) {
<bit-header [title]="title() || (routeData.titleId | i18n)" [icon]="icon()">
<ng-container slot="breadcrumbs">
<ng-content select="[slot=breadcrumbs]"></ng-content>
<h1
bitTypography="h1"
noMargin
class="tw-m-0 tw-mr-2 tw-leading-10 tw-flex tw-gap-1"
[title]="title || (routeData.titleId | i18n)"
>
<div class="tw-truncate">
<i *ngIf="icon" class="bwi {{ icon }}" aria-hidden="true"></i>
{{ title || (routeData.titleId | i18n) }}
</div>
<div><ng-content select="[slot=title-suffix]"></ng-content></div>
</h1>
</div>
<div class="tw-ml-auto tw-flex tw-flex-col tw-gap-4">
<div class="tw-flex tw-min-w-max tw-items-center tw-justify-end tw-gap-2">
<ng-content></ng-content>
<product-switcher></product-switcher>
<ng-container *ngIf="account$ | async as account">
<button
type="button"
[bitMenuTriggerFor]="accountMenu"
class="tw-border-0 tw-bg-transparent tw-p-0 tw-shrink-0"
>
<dynamic-avatar [id]="account.id" [text]="account | userName"></dynamic-avatar>
</button>
</ng-container>
<bit-menu #accountMenu>
<div class="tw-flex tw-min-w-52 tw-max-w-72 tw-flex-col">
<div
class="tw-flex tw-items-center tw-px-4 tw-py-1 tw-leading-tight tw-text-muted"
appStopProp
>
<dynamic-avatar [id]="account.id" [text]="account | userName"></dynamic-avatar>
<div class="tw-ml-2 tw-block tw-overflow-hidden tw-whitespace-nowrap">
<span>{{ "loggedInAs" | i18n }}</span>
<small class="tw-block tw-overflow-hidden tw-whitespace-nowrap">
{{ account | userName }}
</small>
</div>
</div>
<ng-content></ng-content>
<product-switcher></product-switcher>
<app-account-menu></app-account-menu>
<ng-container *ngIf="selfHosted">
<bit-menu-divider></bit-menu-divider>
<span class="tw-break-all tw-px-4 tw-py-1 tw-text-left tw-text-muted">
{{ hostname }}
</span>
</ng-container>
<ng-container slot="title-suffix">
<ng-content select="[slot=title-suffix]"></ng-content>
</ng-container>
<bit-menu-divider></bit-menu-divider>
<ng-container slot="secondary">
<ng-content select="[slot=secondary]"></ng-content>
</ng-container>
<a bitMenuItem routerLink="/settings/account">
<i class="bwi bwi-fw bwi-user" aria-hidden="true"></i>
{{ "accountSettings" | i18n }}
</a>
<a bitMenuItem href="https://bitwarden.com/help/" target="_blank" rel="noreferrer">
<i class="bwi bwi-fw bwi-question-circle" aria-hidden="true"></i>
{{ "getHelp" | i18n }}
</a>
<a
bitMenuItem
href="https://bitwarden.com/download/"
target="_blank"
rel="noreferrer"
>
<i class="bwi bwi-fw bwi-download" aria-hidden="true"></i>
{{ "getApps" | i18n }}
</a>
<bit-menu-divider></bit-menu-divider>
<button *ngIf="canLock$ | async" bitMenuItem type="button" (click)="lock()">
<i class="bwi bwi-fw bwi-lock" aria-hidden="true"></i>
{{ "lockNow" | i18n }}
</button>
<button bitMenuItem type="button" (click)="logout()">
<i class="bwi bwi-fw bwi-sign-out" aria-hidden="true"></i>
{{ "logOut" | i18n }}
</button>
</div>
</bit-menu>
</ng-container>
</div>
<div
class="tw-ml-auto"
#contentContainer
[ngClass]="{ 'tw-hidden': contentContainer.childElementCount === 0 }"
>
<ng-content select="[slot=secondary]"></ng-content>
</div>
</div>
</div>
<div
#tabsContainer
class="-tw-mx-4 -tw-mb-px"
[ngClass]="{ 'tw-hidden': tabsContainer.childElementCount === 0 }"
>
<ng-content select="[slot=tabs]"></ng-content>
</div>
</header>
<ng-container slot="tabs">
<ng-content select="[slot=tabs]"></ng-content>
</ng-container>
</bit-header>
}

View File

@@ -1,75 +1,44 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, Input } from "@angular/core";
import { ChangeDetectionStrategy, Component, inject, input } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { map, Observable } from "rxjs";
import { User } from "@bitwarden/angular/pipes/user-name.pipe";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import {
VaultTimeoutAction,
VaultTimeoutSettingsService,
} from "@bitwarden/common/key-management/vault-timeout";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { UserId } from "@bitwarden/common/types/guid";
import { BannerModule, HeaderComponent } from "@bitwarden/components";
import { SharedModule } from "../../shared";
import { ProductSwitcherModule } from "../product-switcher/product-switcher.module";
import { AccountMenuComponent } from "./account-menu.component";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "app-header",
templateUrl: "./web-header.component.html",
standalone: false,
imports: [
SharedModule,
ProductSwitcherModule,
BannerModule,
HeaderComponent,
AccountMenuComponent,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WebHeaderComponent {
private route = inject(ActivatedRoute);
/**
* Custom title that overrides the route data `titleId`
*/
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() title: string;
readonly title = input<string>();
/**
* Icon to show before the title
*/
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() icon: string;
readonly icon = input<string>();
protected routeData$: Observable<{ titleId: string }>;
protected account$: Observable<User & { id: UserId }>;
protected canLock$: Observable<boolean>;
protected selfHosted: boolean;
protected hostname = location.hostname;
constructor(
private route: ActivatedRoute,
private platformUtilsService: PlatformUtilsService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private messagingService: MessagingService,
private accountService: AccountService,
) {
this.routeData$ = this.route.data.pipe(
map((params) => {
return {
titleId: params.titleId,
};
}),
);
this.selfHosted = this.platformUtilsService.isSelfHost();
this.account$ = this.accountService.activeAccount$;
this.canLock$ = this.vaultTimeoutSettingsService
.availableVaultTimeoutActions$()
.pipe(map((actions) => actions.includes(VaultTimeoutAction.Lock)));
}
protected lock() {
this.messagingService.send("lockVault");
}
protected logout() {
this.messagingService.send("logout");
}
protected routeData$: Observable<{ titleId: string }> = this.route.data.pipe(
map((params) => {
return {
titleId: params.titleId,
};
}),
);
}