1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-02 18:17:46 +01:00

[PM-8368] AnonLayout Footer Updates (#9397)

* add hostname to footer via env service

* add logic for showing/hiding environment

* add docs

* add web env-selector

* refactor to use one slot for env-selector

* add storybook docs

* add env hostname to stories

* remove sample route
This commit is contained in:
rr-bw 2024-05-29 13:31:04 -07:00 committed by GitHub
parent 05f5d632aa
commit 828e26f93c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 155 additions and 47 deletions

View File

@ -581,6 +581,9 @@
"accessLevel": { "accessLevel": {
"message": "Access level" "message": "Access level"
}, },
"accessing": {
"message": "Accessing"
},
"loggedOut": { "loggedOut": {
"message": "Logged out" "message": "Logged out"
}, },

View File

@ -1,4 +1,10 @@
<auth-anon-layout [title]="pageTitle" [subtitle]="pageSubtitle" [icon]="pageIcon"> <auth-anon-layout
[title]="pageTitle"
[subtitle]="pageSubtitle"
[icon]="pageIcon"
[showReadonlyHostname]="showReadonlyHostname"
>
<router-outlet></router-outlet> <router-outlet></router-outlet>
<router-outlet slot="secondary" name="secondary"></router-outlet> <router-outlet slot="secondary" name="secondary"></router-outlet>
<router-outlet slot="environment-selector" name="environment-selector"></router-outlet>
</auth-anon-layout> </auth-anon-layout>

View File

@ -9,6 +9,7 @@ export interface AnonLayoutWrapperData {
pageTitle?: string; pageTitle?: string;
pageSubtitle?: string; pageSubtitle?: string;
pageIcon?: Icon; pageIcon?: Icon;
showReadonlyHostname?: boolean;
} }
@Component({ @Component({
@ -20,6 +21,7 @@ export class AnonLayoutWrapperComponent {
protected pageTitle: string; protected pageTitle: string;
protected pageSubtitle: string; protected pageSubtitle: string;
protected pageIcon: Icon; protected pageIcon: Icon;
protected showReadonlyHostname: boolean;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@ -27,6 +29,7 @@ export class AnonLayoutWrapperComponent {
) { ) {
this.pageTitle = this.i18nService.t(this.route.snapshot.firstChild.data["pageTitle"]); this.pageTitle = this.i18nService.t(this.route.snapshot.firstChild.data["pageTitle"]);
this.pageSubtitle = this.i18nService.t(this.route.snapshot.firstChild.data["pageSubtitle"]); this.pageSubtitle = this.i18nService.t(this.route.snapshot.firstChild.data["pageSubtitle"]);
this.pageIcon = this.route.snapshot.firstChild.data["pageIcon"]; // don't translate this.pageIcon = this.route.snapshot.firstChild.data["pageIcon"];
this.showReadonlyHostname = this.route.snapshot.firstChild.data["showReadonlyHostname"];
} }
} }

View File

@ -22,7 +22,13 @@
<ng-content select="[slot=secondary]"></ng-content> <ng-content select="[slot=secondary]"></ng-content>
</div> </div>
<footer class="tw-text-center"> <footer class="tw-text-center">
<div *ngIf="showReadonlyHostname">{{ "accessing" | i18n }} {{ hostname }}</div>
<ng-container *ngIf="!showReadonlyHostname">
<ng-content select="[slot=environment-selector]"></ng-content>
</ng-container>
<ng-container *ngIf="showYearAndVersion">
<div>&copy; {{ year }} Bitwarden Inc.</div> <div>&copy; {{ year }} Bitwarden Inc.</div>
<div>{{ version }}</div> <div>{{ version }}</div>
</ng-container>
</footer> </footer>
</main> </main>

View File

@ -1,9 +1,13 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core"; import { Component, Input } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { ClientType } from "@bitwarden/common/enums";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { IconModule, Icon } from "../../../../components/src/icon"; import { IconModule, Icon } from "../../../../components/src/icon";
import { SharedModule } from "../../../../components/src/shared";
import { TypographyModule } from "../../../../components/src/typography"; import { TypographyModule } from "../../../../components/src/typography";
import { BitwardenLogo } from "../icons/bitwarden-logo.icon"; import { BitwardenLogo } from "../icons/bitwarden-logo.icon";
@ -11,21 +15,34 @@ import { BitwardenLogo } from "../icons/bitwarden-logo.icon";
standalone: true, standalone: true,
selector: "auth-anon-layout", selector: "auth-anon-layout",
templateUrl: "./anon-layout.component.html", templateUrl: "./anon-layout.component.html",
imports: [IconModule, CommonModule, TypographyModule], imports: [IconModule, CommonModule, TypographyModule, SharedModule],
}) })
export class AnonLayoutComponent { export class AnonLayoutComponent {
@Input() title: string; @Input() title: string;
@Input() subtitle: string; @Input() subtitle: string;
@Input() icon: Icon; @Input() icon: Icon;
@Input() showReadonlyHostname: boolean;
protected logo = BitwardenLogo; protected logo = BitwardenLogo;
protected version: string;
protected year = "2024";
constructor(private platformUtilsService: PlatformUtilsService) {} protected year = "2024";
protected clientType: ClientType;
protected hostname: string;
protected version: string;
protected showYearAndVersion = true;
constructor(
private environmentService: EnvironmentService,
private platformUtilsService: PlatformUtilsService,
) {
this.year = new Date().getFullYear().toString();
this.clientType = this.platformUtilsService.getClientType();
this.showYearAndVersion = this.clientType === ClientType.Web;
}
async ngOnInit() { async ngOnInit() {
this.year = new Date().getFullYear().toString(); this.hostname = (await firstValueFrom(this.environmentService.environment$)).getHostname();
this.version = await this.platformUtilsService.getApplicationVersion(); this.version = await this.platformUtilsService.getApplicationVersion();
} }
} }

View File

@ -44,9 +44,15 @@ The AnonLayout**Wrapper**Component embeds the AnonLayoutComponent along with the
```html ```html
<!-- File: anon-layout-wrapper.component.html --> <!-- File: anon-layout-wrapper.component.html -->
<auth-anon-layout> <auth-anon-layout
[title]="pageTitle"
[subtitle]="pageSubtitle"
[icon]="pageIcon"
[showReadonlyHostname]="showReadonlyHostname"
>
<router-outlet></router-outlet> <router-outlet></router-outlet>
<router-outlet slot="secondary" name="secondary"></router-outlet> <router-outlet slot="secondary" name="secondary"></router-outlet>
<router-outlet slot="environment-selector" name="environment-selector"></router-outlet>
</auth-anon-layout> </auth-anon-layout>
``` ```
@ -54,8 +60,9 @@ To implement, the developer does not need to work with the base AnonLayoutCompon
devoloper simply uses the AnonLayout**Wrapper**Component in `oss-routing.module.ts` (for Web, for devoloper simply uses the AnonLayout**Wrapper**Component in `oss-routing.module.ts` (for Web, for
example) to construct the page via routable composition: example) to construct the page via routable composition:
```javascript ```typescript
// File: oss-routing.module.ts // File: oss-routing.module.ts
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, LockIcon } from "@bitwarden/auth/angular";
{ {
path: "", path: "",
@ -81,26 +88,32 @@ example) to construct the page via routable composition:
} satisfies AnonLayoutWrapperData, } satisfies AnonLayoutWrapperData,
}, },
], ],
}, },
``` ```
And if the AnonLayout**Wrapper**Component is already being used in your client's routing module, (Notice that you can optionally add an `outlet: "secondary"` if you want to project secondary
then your work will be as simple as just adding another child route under the `children` array. content below the primary content).
If the AnonLayout**Wrapper**Component is already being used in your client's routing module, then
your work will be as simple as just adding another child route under the `children` array.
<br />
### Data Properties ### Data Properties
In the `oss-routing.module.ts` example above, notice the data properties being passed in: Routes that use the AnonLayou**tWrapper**Component can take several unique data properties defined
in the `AnonLayoutWrapperData` interface:
- For the `pageTitle` and `pageSubtitle` - pass in a translation key from `messages.json`. - For the `pageTitle` and `pageSubtitle` - pass in a translation key from `messages.json`.
- For the `pageIcon` - import an icon (of type `Icon`) into the router file and use the icon - For the `pageIcon` - import an icon (of type `Icon`) into the router file and use the icon
directly. directly.
- `showReadonlyHostname` - set to `true` if you want to show the hostname in the footer (ex:
"Accessing bitwarden.com")
All 3 of these properties are optional. All of these properties are optional.
```javascript ```typescript
import { AnonLayoutWrapperData, LockIcon } from "@bitwarden/auth/angular"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, LockIcon } from "@bitwarden/auth/angular";
// ...
{ {
// ... // ...
@ -108,10 +121,45 @@ import { AnonLayoutWrapperData, LockIcon } from "@bitwarden/auth/angular";
pageTitle: "logIn", pageTitle: "logIn",
pageSubtitle: "loginWithMasterPassword", pageSubtitle: "loginWithMasterPassword",
pageIcon: LockIcon, pageIcon: LockIcon,
showReadonlyHostname: true,
} satisfies AnonLayoutWrapperData, } satisfies AnonLayoutWrapperData,
} }
``` ```
### Environment Selector
For some routes, you may want to display the environment selector in the footer of the
AnonLayoutComponent. To do so, add the relevant environment selector (Web or Libs version, depending
on your client) as a component with `outlet: "environment-selector"`.
```javascript
// File: oss-routing.module.ts
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, LockIcon } from "@bitwarden/auth/angular";
import { EnvironmentSelectorComponent } from "./components/environment-selector/environment-selector.component";
{
path: "",
component: AnonLayoutWrapperComponent,
children: [
{
path: "sample-route",
children: [
{
path: "",
component: MyPrimaryComponent,
},
{
path: "",
component: EnvironmentSelectorComponent, // use Web or Libs component depending on your client
outlet: "environment-selector",
},
],
// ...
},
],
},
```
--- ---
<Story of={stories.WithSecondaryContent} /> <Story of={stories.WithSecondaryContent} />

View File

@ -1,14 +1,20 @@
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { BehaviorSubject } from "rxjs";
import { ClientType } from "@bitwarden/common/enums";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ButtonModule } from "../../../../components/src/button"; import { ButtonModule } from "../../../../components/src/button";
import { I18nMockService } from "../../../../components/src/utils/i18n-mock.service";
import { LockIcon } from "../icons"; import { LockIcon } from "../icons";
import { AnonLayoutComponent } from "./anon-layout.component"; import { AnonLayoutComponent } from "./anon-layout.component";
class MockPlatformUtilsService implements Partial<PlatformUtilsService> { class MockPlatformUtilsService implements Partial<PlatformUtilsService> {
getApplicationVersion = () => Promise.resolve("Version 2024.1.1"); getApplicationVersion = () => Promise.resolve("Version 2024.1.1");
getClientType = () => ClientType.Web;
} }
export default { export default {
@ -22,12 +28,31 @@ export default {
provide: PlatformUtilsService, provide: PlatformUtilsService,
useClass: MockPlatformUtilsService, useClass: MockPlatformUtilsService,
}, },
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
accessing: "Accessing",
});
},
},
{
provide: EnvironmentService,
useValue: {
environment$: new BehaviorSubject({
getHostname() {
return "bitwarden.com";
},
}).asObservable(),
},
},
], ],
}), }),
], ],
args: { args: {
title: "The Page Title", title: "The Page Title",
subtitle: "The subtitle (optional)", subtitle: "The subtitle (optional)",
showReadonlyHostname: true,
icon: LockIcon, icon: LockIcon,
}, },
} as Meta; } as Meta;
@ -40,7 +65,7 @@ export const WithPrimaryContent: Story = {
template: template:
// Projected content (the <div>) and styling is just a sample and can be replaced with any content/styling. // Projected content (the <div>) and styling is just a sample and can be replaced with any content/styling.
` `
<auth-anon-layout [title]="title" [subtitle]="subtitle"> <auth-anon-layout [title]="title" [subtitle]="subtitle" [showReadonlyHostname]="showReadonlyHostname">
<div> <div>
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div> <div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div> <div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
@ -57,7 +82,7 @@ export const WithSecondaryContent: Story = {
// Projected content (the <div>'s) and styling is just a sample and can be replaced with any content/styling. // Projected content (the <div>'s) and styling is just a sample and can be replaced with any content/styling.
// Notice that slot="secondary" is requred to project any secondary content. // Notice that slot="secondary" is requred to project any secondary content.
` `
<auth-anon-layout [title]="title" [subtitle]="subtitle"> <auth-anon-layout [title]="title" [subtitle]="subtitle" [showReadonlyHostname]="showReadonlyHostname">
<div> <div>
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div> <div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div> <div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
@ -78,7 +103,7 @@ export const WithLongContent: Story = {
template: template:
// Projected content (the <div>'s) and styling is just a sample and can be replaced with any content/styling. // Projected content (the <div>'s) and styling is just a sample and can be replaced with any content/styling.
` `
<auth-anon-layout title="Page Title lorem ipsum dolor consectetur sit amet expedita quod est" subtitle="Subtitle here Lorem ipsum dolor sit amet consectetur adipisicing elit. Expedita, quod est?"> <auth-anon-layout title="Page Title lorem ipsum dolor consectetur sit amet expedita quod est" subtitle="Subtitle here Lorem ipsum dolor sit amet consectetur adipisicing elit. Expedita, quod est?" [showReadonlyHostname]="showReadonlyHostname">
<div> <div>
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div> <div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam? Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit.</div> <div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam? Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit.</div>
@ -100,7 +125,7 @@ export const WithIcon: Story = {
template: template:
// Projected content (the <div>) and styling is just a sample and can be replaced with any content/styling. // Projected content (the <div>) and styling is just a sample and can be replaced with any content/styling.
` `
<auth-anon-layout [title]="title" [subtitle]="subtitle" [icon]="icon"> <auth-anon-layout [title]="title" [subtitle]="subtitle" [icon]="icon" [showReadonlyHostname]="showReadonlyHostname">
<div> <div>
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div> <div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div> <div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>